swap from IPC architecture to PyO3 library (#45)

This commit is contained in:
Josh Thomas 2024-12-23 10:12:10 -06:00 committed by GitHub
parent df30aafde5
commit a73e912e0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 136 additions and 2224 deletions

View file

@ -1,12 +0,0 @@
[package]
name = "djls-django"
version = "0.0.0"
edition = "2021"
[dependencies]
djls-ipc = { workspace = true }
djls-python = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }

View file

@ -1,46 +0,0 @@
use std::fmt;
#[derive(Debug)]
pub struct App(String);
impl App {
pub fn name(&self) -> &str {
&self.0
}
}
impl fmt::Display for App {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Default)]
pub struct Apps(Vec<App>);
impl Apps {
pub fn from_strings(apps: Vec<String>) -> Self {
Self(apps.into_iter().map(App).collect())
}
pub fn apps(&self) -> &[App] {
&self.0
}
pub fn has_app(&self, name: &str) -> bool {
self.apps().iter().any(|app| app.0 == name)
}
pub fn iter(&self) -> impl Iterator<Item = &App> {
self.apps().iter()
}
}
impl fmt::Display for Apps {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for app in &self.0 {
writeln!(f, " {}", app)?;
}
Ok(())
}
}

View file

@ -1,97 +0,0 @@
use djls_ipc::v1::*;
use djls_ipc::IpcCommand;
use djls_ipc::{ProcessError, PythonProcess, TransportError};
use djls_python::Python;
use std::fmt;
#[derive(Debug)]
pub struct DjangoProject {
py: Python,
python: PythonProcess,
version: String,
}
impl DjangoProject {
fn new(py: Python, python: PythonProcess, version: String) -> Self {
Self {
py,
python,
version,
}
}
pub fn setup(mut python: PythonProcess) -> Result<Self, ProjectError> {
let py = Python::setup(&mut python)?;
match commands::check::GeoDjangoPrereqsRequest::execute(&mut python)?.result {
Some(messages::response::Result::CheckGeodjangoPrereqs(response)) => {
if !response.passed {
eprintln!("Warning: GeoDjango detected but GDAL is not available.");
eprintln!(
"Django initialization will be skipped. Some features may be limited."
);
eprintln!("To enable full functionality, please install GDAL and other GeoDjango prerequisites.");
return Ok(Self {
py,
python,
version: String::new(),
});
}
}
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message))?,
_ => Err(ProcessError::Response)?,
}
let response = commands::django::GetProjectInfoRequest::execute(&mut python)?;
let version = match response.result {
Some(messages::response::Result::DjangoGetProjectInfo(response)) => {
response.project.unwrap().version
}
_ => {
return Err(ProjectError::Process(ProcessError::Response));
}
};
Ok(Self {
py,
python,
version,
})
}
pub fn py(&self) -> &Python {
&self.py
}
fn version(&self) -> &String {
&self.version
}
}
impl fmt::Display for DjangoProject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Django Project")?;
writeln!(f, "Version: {}", self.version)?;
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum ProjectError {
#[error("Django is not installed or cannot be imported")]
DjangoNotFound,
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error),
#[error(transparent)]
Packaging(#[from] djls_python::PackagingError),
#[error("Process error: {0}")]
Process(#[from] ProcessError),
#[error(transparent)]
Python(#[from] djls_python::PythonError),
#[error("Transport error: {0}")]
Transport(#[from] TransportError),
}

View file

@ -1,5 +0,0 @@
mod apps;
mod django;
mod templates;
pub use django::DjangoProject;

View file

@ -1,63 +0,0 @@
use serde::Deserialize;
use std::fmt;
#[derive(Clone, Debug, Deserialize)]
pub struct TemplateTag {
name: String,
library: String,
doc: Option<String>,
}
impl fmt::Display for TemplateTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let library = if self.library.is_empty() {
"builtins"
} else {
&self.library
};
write!(f, "{} ({})", self.name, library)?;
writeln!(f)?;
if let Some(doc) = &self.doc {
for line in doc.trim_end().split("\n") {
writeln!(f, "{}", line)?;
}
}
Ok(())
}
}
#[derive(Debug, Default, Deserialize)]
pub struct TemplateTags(Vec<TemplateTag>);
impl TemplateTags {
pub fn tags(&self) -> &Vec<TemplateTag> {
&self.0
}
fn iter(&self) -> impl Iterator<Item = &TemplateTag> {
self.tags().iter()
}
pub fn filter_by_prefix<'a>(
&'a self,
prefix: &'a str,
) -> impl Iterator<Item = &'a TemplateTag> {
self.iter().filter(move |tag| tag.name.starts_with(prefix))
}
pub fn get_by_name(&self, name: &str) -> Option<&TemplateTag> {
self.iter().find(|tag| tag.name == name)
}
}
impl fmt::Display for TemplateTags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for tag in &self.0 {
writeln!(f, " {}", tag)?;
}
Ok(())
}
}

View file

@ -1,19 +0,0 @@
[package]
name = "djls-ipc"
version = "0.0.0"
edition = "2021"
[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
bytes = "1.9"
prost = "0.13"
tempfile = "3.14"
[build-dependencies]
prost-build = "0.13"

View file

@ -1,38 +0,0 @@
use std::fs;
use std::path::{Path, PathBuf};
struct Version(&'static str);
impl Version {
fn collect_protos(&self, proto_root: &Path) -> Vec<PathBuf> {
fs::read_dir(proto_root.join(self.0))
.unwrap()
.filter_map(Result::ok)
.filter(|entry| entry.path().extension().and_then(|s| s.to_str()) == Some("proto"))
.map(|entry| entry.path())
.collect()
}
}
const VERSIONS: &[Version] = &[Version("v1")];
fn main() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace_root = manifest_dir.parent().unwrap().parent().unwrap();
let proto_dir = workspace_root.join("proto");
let mut protos = Vec::new();
for version in VERSIONS {
protos.extend(version.collect_protos(&proto_dir));
}
prost_build::Config::new()
.compile_protos(
&protos
.iter()
.map(|p| p.to_str().unwrap())
.collect::<Vec<_>>(),
&[proto_dir],
)
.unwrap();
}

View file

@ -1,78 +0,0 @@
use crate::proto::v1::{self, messages};
use crate::{ProcessError, PythonProcess};
pub trait IpcCommand: Default {
fn into_request(&self) -> messages::Request;
fn from_response(response: messages::Response) -> Result<messages::Response, ProcessError>;
fn execute(process: &mut PythonProcess) -> Result<messages::Response, ProcessError> {
let cmd = Self::default();
let request = cmd.into_request();
let response = process.send(request).map_err(ProcessError::Transport)?;
Self::from_response(response)
}
}
impl IpcCommand for v1::commands::check::HealthRequest {
fn into_request(&self) -> messages::Request {
messages::Request {
command: Some(messages::request::Command::CheckHealth(*self)),
}
}
fn from_response(response: messages::Response) -> Result<messages::Response, ProcessError> {
match response.result {
Some(messages::response::Result::CheckHealth(_)) => Ok(response),
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message)),
_ => Err(ProcessError::Response),
}
}
}
impl IpcCommand for v1::commands::check::GeoDjangoPrereqsRequest {
fn into_request(&self) -> messages::Request {
messages::Request {
command: Some(messages::request::Command::CheckGeodjangoPrereqs(*self)),
}
}
fn from_response(response: messages::Response) -> Result<messages::Response, ProcessError> {
match response.result {
Some(messages::response::Result::CheckGeodjangoPrereqs(_)) => Ok(response),
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message)),
_ => Err(ProcessError::Response),
}
}
}
impl IpcCommand for v1::commands::python::GetEnvironmentRequest {
fn into_request(&self) -> messages::Request {
messages::Request {
command: Some(messages::request::Command::PythonGetEnvironment(*self)),
}
}
fn from_response(response: messages::Response) -> Result<messages::Response, ProcessError> {
match response.result {
Some(messages::response::Result::PythonGetEnvironment(_)) => Ok(response),
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message)),
_ => Err(ProcessError::Response),
}
}
}
impl IpcCommand for v1::commands::django::GetProjectInfoRequest {
fn into_request(&self) -> messages::Request {
messages::Request {
command: Some(messages::request::Command::DjangoGetProjectInfo(*self)),
}
}
fn from_response(response: messages::Response) -> Result<messages::Response, ProcessError> {
match response.result {
Some(messages::response::Result::DjangoGetProjectInfo(_)) => Ok(response),
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message)),
_ => Err(ProcessError::Response),
}
}
}

View file

@ -1,11 +0,0 @@
mod commands;
mod process;
mod proto;
mod transport;
pub use commands::IpcCommand;
pub use process::ProcessError;
pub use process::PythonProcess;
pub use proto::v1;
pub use transport::Transport;
pub use transport::TransportError;

View file

@ -1,138 +0,0 @@
use crate::proto::v1::*;
use crate::transport::{Transport, TransportError};
use std::ffi::OsStr;
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::time;
#[derive(Debug)]
pub struct PythonProcess {
transport: Arc<Mutex<Transport>>,
_child: Child,
healthy: Arc<AtomicBool>,
}
impl PythonProcess {
pub fn new<I, S>(
module: &str,
args: Option<I>,
health_check_interval: Option<Duration>,
) -> Result<Self, ProcessError>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut command = Command::new("python");
command.arg("-m").arg(module);
if let Some(args) = args {
command.args(args);
}
command.stdin(Stdio::piped()).stdout(Stdio::piped());
let mut child = command.spawn().map_err(TransportError::Io)?;
let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
let transport = Transport::new(stdin, stdout)?;
let process = Self {
transport: Arc::new(Mutex::new(transport)),
_child: child,
healthy: Arc::new(AtomicBool::new(true)),
};
if let Some(interval) = health_check_interval {
let transport = process.transport.clone();
let healthy = process.healthy.clone();
tokio::spawn(async move {
let mut interval = time::interval(interval);
loop {
interval.tick().await;
let _ = PythonProcess::check_health(transport.clone(), healthy.clone()).await;
}
});
}
Ok(process)
}
pub fn is_healthy(&self) -> bool {
self.healthy.load(Ordering::SeqCst)
}
pub fn send(
&mut self,
request: messages::Request,
) -> Result<messages::Response, TransportError> {
let mut transport = self.transport.lock().unwrap();
transport.send(request)
}
async fn check_health(
transport: Arc<Mutex<Transport>>,
healthy: Arc<AtomicBool>,
) -> Result<(), ProcessError> {
let request = messages::Request {
command: Some(messages::request::Command::CheckHealth(
commands::check::HealthRequest {},
)),
};
let response = tokio::time::timeout(
Duration::from_secs(5),
tokio::task::spawn_blocking(move || {
let mut transport = transport.lock().unwrap();
transport.send(request)
}),
)
.await
.map_err(|_| ProcessError::Timeout(5))?
.map_err(TransportError::Task)?
.map_err(ProcessError::Transport)?;
let result = match response.result {
Some(messages::response::Result::CheckHealth(health)) => {
if !health.passed {
let error_msg = health.error.unwrap_or_else(|| "Unknown error".to_string());
Err(ProcessError::Health(error_msg))
} else {
Ok(())
}
}
Some(messages::response::Result::Error(e)) => Err(ProcessError::Health(e.message)),
_ => Err(ProcessError::Response),
};
healthy.store(result.is_ok(), Ordering::SeqCst);
result
}
}
impl Drop for PythonProcess {
fn drop(&mut self) {
if let Ok(()) = self._child.kill() {
let _ = self._child.wait();
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum ProcessError {
#[error("Health check failed: {0}")]
Health(String),
#[error("Operation timed out after {0} seconds")]
Timeout(u64),
#[error("Unexpected response type")]
Response,
#[error("Failed to acquire lock: {0}")]
Lock(String),
#[error("Process not ready: {0}")]
Ready(String),
#[error("Transport error: {0}")]
Transport(#[from] TransportError),
}

View file

@ -1,17 +0,0 @@
pub mod v1 {
pub mod commands {
include!(concat!(env!("OUT_DIR"), "/djls.v1.commands.rs"));
}
pub mod django {
include!(concat!(env!("OUT_DIR"), "/djls.v1.django.rs"));
}
pub mod messages {
include!(concat!(env!("OUT_DIR"), "/djls.v1.messages.rs"));
}
pub mod python {
include!(concat!(env!("OUT_DIR"), "/djls.v1.python.rs"));
}
}

View file

@ -1,81 +0,0 @@
use crate::process::ProcessError;
use crate::proto::v1::*;
use prost::Message;
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
use std::process::{ChildStdin, ChildStdout};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone)]
pub struct Transport {
reader: Arc<Mutex<BufReader<ChildStdout>>>,
writer: Arc<Mutex<BufWriter<ChildStdin>>>,
}
impl Transport {
pub fn new(mut stdin: ChildStdin, mut stdout: ChildStdout) -> Result<Self, ProcessError> {
stdin.flush().map_err(TransportError::Io)?;
let mut ready_line = String::new();
BufReader::new(&mut stdout)
.read_line(&mut ready_line)
.map_err(TransportError::Io)?;
if ready_line.trim() != "ready" {
return Err(ProcessError::Ready("Python process not ready".to_string()));
}
Ok(Self {
reader: Arc::new(Mutex::new(BufReader::new(stdout))),
writer: Arc::new(Mutex::new(BufWriter::new(stdin))),
})
}
pub fn send(
&mut self,
message: messages::Request,
) -> Result<messages::Response, TransportError> {
let buf = message.encode_to_vec();
let mut writer = self.writer.lock().map_err(|_| {
TransportError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to acquire writer lock",
))
})?;
writer
.write_all(&(buf.len() as u32).to_be_bytes())
.map_err(TransportError::Io)?;
writer.write_all(&buf).map_err(TransportError::Io)?;
writer.flush().map_err(TransportError::Io)?;
let mut reader = self.reader.lock().map_err(|_| {
TransportError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to acquire reader lock",
))
})?;
let mut length_bytes = [0u8; 4];
reader
.read_exact(&mut length_bytes)
.map_err(TransportError::Io)?;
let length = u32::from_be_bytes(length_bytes);
let mut message_bytes = vec![0u8; length as usize];
reader
.read_exact(&mut message_bytes)
.map_err(TransportError::Io)?;
messages::Response::decode(message_bytes.as_slice())
.map_err(|e| TransportError::Decode(e.to_string()))
}
}
#[derive(thiserror::Error, Debug)]
pub enum TransportError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Task error: {0}")]
Task(#[from] tokio::task::JoinError),
#[error("Failed to decode message: {0}")]
Decode(String),
}

View file

@ -1,47 +0,0 @@
from __future__ import annotations
import argparse
import asyncio
import json
from pathlib import Path
async def handle_client(reader, writer):
while True:
try:
data = await reader.readline()
if not data:
break
# Parse the incoming message
message = json.loads(data)
# Echo back with same ID but just echo the content
response = {"id": message["id"], "content": message["content"]}
writer.write(json.dumps(response).encode() + b"\n")
await writer.drain()
except Exception:
break
writer.close()
await writer.wait_closed()
async def main(ipc_path):
try:
Path(ipc_path).unlink()
except FileNotFoundError:
pass
server = await asyncio.start_unix_server(
handle_client,
path=ipc_path,
)
async with server:
await server.serve_forever()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--ipc-path", required=True)
args = parser.parse_args()
asyncio.run(main(args.ipc_path))

View file

@ -1,13 +0,0 @@
[package]
name = "djls-python"
version = "0.0.0"
edition = "2021"
[dependencies]
djls-ipc = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
which = "7.0"

View file

@ -1,6 +0,0 @@
mod packaging;
mod python;
pub use crate::packaging::PackagingError;
pub use crate::python::Python;
pub use crate::python::PythonError;

View file

@ -1,87 +0,0 @@
use djls_ipc::v1::*;
use djls_ipc::{ProcessError, TransportError};
use serde::Deserialize;
use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf;
#[derive(Clone, Debug, Deserialize)]
pub struct Package {
dist_name: String,
dist_version: String,
dist_location: Option<PathBuf>,
}
impl From<python::Package> for Package {
fn from(p: python::Package) -> Self {
Package {
dist_name: p.dist_name,
dist_version: p.dist_version,
dist_location: p.dist_location.map(PathBuf::from),
}
}
}
impl fmt::Display for Package {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.dist_name, self.dist_version)?;
if let Some(location) = &self.dist_location {
write!(f, " ({})", location.display())?;
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct Packages(HashMap<String, Package>);
impl Packages {
pub fn packages(&self) -> Vec<&Package> {
self.0.values().collect()
}
}
impl From<HashMap<String, python::Package>> for Packages {
fn from(packages: HashMap<String, python::Package>) -> Self {
Packages(packages.into_iter().map(|(k, v)| (k, v.into())).collect())
}
}
impl FromIterator<(String, Package)> for Packages {
fn from_iter<T: IntoIterator<Item = (String, Package)>>(iter: T) -> Self {
Self(HashMap::from_iter(iter))
}
}
impl fmt::Display for Packages {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut packages: Vec<_> = self.packages();
packages.sort_by(|a, b| a.dist_name.cmp(&b.dist_name));
if packages.is_empty() {
writeln!(f, " (no packages installed)")?;
} else {
for package in packages {
writeln!(f, "{}", package)?;
}
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum PackagingError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error),
#[error("Transport error: {0}")]
Transport(#[from] TransportError),
#[error("Process error: {0}")]
Process(#[from] ProcessError),
#[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
}

View file

@ -1,169 +0,0 @@
use crate::packaging::{Packages, PackagingError};
use djls_ipc::v1::*;
use djls_ipc::IpcCommand;
use djls_ipc::{ProcessError, PythonProcess, TransportError};
use serde::Deserialize;
use std::fmt;
use std::path::PathBuf;
#[derive(Clone, Debug, Deserialize)]
pub struct VersionInfo {
major: u8,
minor: u8,
micro: u8,
releaselevel: ReleaseLevel,
serial: Option<String>,
}
impl From<python::VersionInfo> for VersionInfo {
fn from(v: python::VersionInfo) -> Self {
Self {
major: v.major as u8,
minor: v.minor as u8,
micro: v.micro as u8,
releaselevel: v.releaselevel().into(),
serial: Some(v.serial.to_string()),
}
}
}
#[derive(Clone, Debug, Deserialize)]
pub enum ReleaseLevel {
Alpha,
Beta,
Candidate,
Final,
}
impl From<python::ReleaseLevel> for ReleaseLevel {
fn from(level: python::ReleaseLevel) -> Self {
match level {
python::ReleaseLevel::Alpha => ReleaseLevel::Alpha,
python::ReleaseLevel::Beta => ReleaseLevel::Beta,
python::ReleaseLevel::Candidate => ReleaseLevel::Candidate,
python::ReleaseLevel::Final => ReleaseLevel::Final,
}
}
}
impl fmt::Display for VersionInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.micro)?;
Ok(())
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct SysconfigPaths {
data: PathBuf,
include: PathBuf,
platinclude: PathBuf,
platlib: PathBuf,
platstdlib: PathBuf,
purelib: PathBuf,
scripts: PathBuf,
stdlib: PathBuf,
}
impl fmt::Display for SysconfigPaths {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "data: {}", self.data.display())?;
writeln!(f, "include: {}", self.include.display())?;
writeln!(f, "platinclude: {}", self.platinclude.display())?;
writeln!(f, "platlib: {}", self.platlib.display())?;
writeln!(f, "platstdlib: {}", self.platstdlib.display())?;
writeln!(f, "purelib: {}", self.purelib.display())?;
writeln!(f, "scripts: {}", self.scripts.display())?;
write!(f, "stdlib: {}", self.stdlib.display())
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct Python {
version_info: VersionInfo,
sysconfig_paths: SysconfigPaths,
sys_prefix: PathBuf,
sys_base_prefix: PathBuf,
sys_executable: PathBuf,
sys_path: Vec<PathBuf>,
packages: Packages,
}
impl Python {
pub fn setup(python: &mut PythonProcess) -> Result<Self, PythonError> {
let response = commands::python::GetEnvironmentRequest::execute(python)?;
match response.result {
Some(messages::response::Result::PythonGetEnvironment(response)) => response
.python
.ok_or_else(|| PythonError::Process(ProcessError::Response))
.map(Into::into),
_ => Err(PythonError::Process(ProcessError::Response)),
}
}
}
impl From<python::Python> for Python {
fn from(p: python::Python) -> Self {
let sys = p.sys.unwrap();
let sysconfig = p.sysconfig.unwrap();
let site = p.site.unwrap();
Self {
version_info: sys.version_info.unwrap_or_default().into(),
sysconfig_paths: SysconfigPaths {
data: PathBuf::from(sysconfig.data),
include: PathBuf::from(sysconfig.include),
platinclude: PathBuf::from(sysconfig.platinclude),
platlib: PathBuf::from(sysconfig.platlib),
platstdlib: PathBuf::from(sysconfig.platstdlib),
purelib: PathBuf::from(sysconfig.purelib),
scripts: PathBuf::from(sysconfig.scripts),
stdlib: PathBuf::from(sysconfig.stdlib),
},
sys_prefix: PathBuf::from(sys.prefix),
sys_base_prefix: PathBuf::from(sys.base_prefix),
sys_executable: PathBuf::from(sys.executable),
sys_path: sys.path.into_iter().map(PathBuf::from).collect(),
packages: site.packages.into(),
}
}
}
impl fmt::Display for Python {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Version: {}", self.version_info)?;
writeln!(f, "Executable: {}", self.sys_executable.display())?;
writeln!(f, "Prefix: {}", self.sys_prefix.display())?;
writeln!(f, "Base Prefix: {}", self.sys_base_prefix.display())?;
writeln!(f, "Paths:")?;
for path in &self.sys_path {
writeln!(f, "{}", path.display())?;
}
writeln!(f, "Sysconfig Paths:")?;
write!(f, "{}", self.sysconfig_paths)?;
writeln!(f, "\nInstalled Packages:")?;
write!(f, "{}", self.packages)
}
}
#[derive(Debug, thiserror::Error)]
pub enum PythonError {
#[error("Python execution failed: {0}")]
Execution(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error),
#[error("Packaging error: {0}")]
Packaging(#[from] PackagingError),
#[error("Integer parsing error: {0}")]
Parse(#[from] std::num::ParseIntError),
#[error("Process error: {0}")]
Process(#[from] ProcessError),
#[error("Failed to locate Python executable: {0}")]
PythonNotFound(#[from] which::Error),
#[error("Transport error: {0}")]
Transport(#[from] TransportError),
#[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
}

View file

@ -4,9 +4,6 @@ version = "0.1.0"
edition = "2021"
[dependencies]
djls-django = { workspace = true }
djls-ipc = { workspace = true }
djls-python = { workspace = true }
djls-template-ast = { workspace = true }
djls-worker = { workspace = true }

View file

@ -6,8 +6,6 @@ mod tasks;
use crate::notifier::TowerLspNotifier;
use crate::server::{DjangoLanguageServer, LspNotification, LspRequest};
use anyhow::Result;
use djls_django::DjangoProject;
use djls_ipc::PythonProcess;
use std::sync::Arc;
use tokio::sync::RwLock;
use tower_lsp::jsonrpc::Result as LspResult;
@ -81,15 +79,13 @@ impl LanguageServer for TowerLspBackend {
}
}
pub async fn serve(python: PythonProcess) -> Result<()> {
let django = DjangoProject::setup(python)?;
pub async fn serve() -> Result<()> {
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
let (service, socket) = LspService::build(|client| {
let notifier = Box::new(TowerLspNotifier::new(client.clone()));
let server = DjangoLanguageServer::new(django, notifier);
let server = DjangoLanguageServer::new(notifier);
TowerLspBackend {
server: Arc::new(RwLock::new(server)),
}

View file

@ -2,7 +2,6 @@ use crate::documents::Store;
use crate::notifier::Notifier;
use crate::tasks::DebugTask;
use anyhow::Result;
use djls_django::DjangoProject;
use djls_worker::Worker;
use std::sync::Arc;
use std::time::Duration;
@ -24,18 +23,16 @@ pub enum LspNotification {
}
pub struct DjangoLanguageServer {
django: DjangoProject,
notifier: Arc<Box<dyn Notifier>>,
documents: Store,
worker: Worker,
}
impl DjangoLanguageServer {
pub fn new(django: DjangoProject, notifier: Box<dyn Notifier>) -> Self {
pub fn new(notifier: Box<dyn Notifier>) -> Self {
let notifier = Arc::new(notifier);
Self {
django,
notifier,
documents: Store::new(),
worker: Worker::new(),
@ -125,10 +122,6 @@ impl DjangoLanguageServer {
LspNotification::Initialized(_) => {
self.notifier
.log_message(MessageType::INFO, "server initialized!")?;
self.notifier
.log_message(MessageType::INFO, &format!("\n{}", self.django.py()))?;
self.notifier
.log_message(MessageType::INFO, &format!("\n{}", self.django))?;
Ok(())
}
LspNotification::Shutdown => Ok(()),

View file

@ -3,12 +3,16 @@ name = "djls"
version = "5.1.0-alpha.0"
edition = "2021"
[lib]
name = "djls"
crate-type = ["cdylib"]
[dependencies]
djls-django = { workspace = true }
djls-ipc = { workspace = true }
djls-server = { workspace = true }
anyhow = { workspace = true }
pyo3 = { workspace = true, features = ["extension-module"] }
pyo3-async-runtimes = { workspace = true, features = ["tokio-runtime"] }
serde_json = { workspace = true }
tokio = { workspace = true }

113
crates/djls/src/lib.rs Normal file
View file

@ -0,0 +1,113 @@
mod commands;
use crate::commands::Serve;
use anyhow::Result;
use clap::{Parser, Subcommand};
use pyo3::prelude::*;
use std::env;
use std::process::ExitCode;
#[derive(Parser)]
#[command(name = "djls")]
#[command(version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
command: Command,
#[command(flatten)]
args: Args,
}
#[derive(Debug, Subcommand)]
enum Command {
/// Start the LSP server
Serve(Serve),
}
#[derive(Parser)]
pub struct Args {
#[command(flatten)]
global: GlobalArgs,
}
#[derive(Parser, Debug, Clone)]
struct GlobalArgs {
/// Do not print any output.
#[arg(global = true, long, short, conflicts_with = "verbose")]
pub quiet: bool,
/// Use verbose output.
#[arg(global = true, action = clap::ArgAction::Count, long, short, conflicts_with = "quiet")]
pub verbose: u8,
}
#[pyfunction]
fn cli_entrypoint(_py: Python) -> PyResult<()> {
// Skip python interpreter and script path, add command name
let args: Vec<String> = std::iter::once("djls".to_string())
.chain(env::args().skip(2))
.collect();
let runtime = tokio::runtime::Runtime::new().unwrap();
let local = tokio::task::LocalSet::new();
local.block_on(&runtime, async move {
tokio::select! {
// The main CLI program
result = cli_main(args) => {
match result {
Ok(code) => {
if code != ExitCode::SUCCESS {
std::process::exit(1);
}
Ok::<(), PyErr>(())
}
Err(e) => {
eprintln!("Error: {}", e);
if let Some(source) = e.source() {
eprintln!("Caused by: {}", source);
}
std::process::exit(1);
}
}
}
// Ctrl+C handling
_ = tokio::signal::ctrl_c() => {
println!("\nReceived Ctrl+C, shutting down...");
// Cleanup code here if needed
std::process::exit(130); // Standard Ctrl+C exit code
}
// SIGTERM handling (Unix only)
_ = async {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
let mut term = signal(SignalKind::terminate()).unwrap();
term.recv().await;
}
} => {
println!("\nReceived termination signal, shutting down...");
std::process::exit(143); // Standard SIGTERM exit code
}
}
})?;
Ok(())
}
async fn cli_main(args: Vec<String>) -> Result<ExitCode> {
let cli = Cli::try_parse_from(args).unwrap_or_else(|e| {
e.exit();
});
match cli.command {
Command::Serve(_serve) => djls_server::serve().await?,
}
Ok(ExitCode::SUCCESS)
}
#[pymodule]
fn djls(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(cli_entrypoint, m)?)?;
Ok(())
}

View file

@ -1,54 +0,0 @@
mod commands;
use crate::commands::Serve;
use anyhow::Result;
use clap::{Parser, Subcommand};
use djls_ipc::PythonProcess;
use std::ffi::OsStr;
use std::process::ExitCode;
#[derive(Parser)]
#[command(name = "djls")]
#[command(version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
command: Command,
#[command(flatten)]
args: Args,
}
#[derive(Debug, Subcommand)]
enum Command {
/// Start the LSP server
Serve(Serve),
}
#[derive(Parser)]
pub struct Args {
#[command(flatten)]
global: GlobalArgs,
}
#[derive(Parser, Debug, Clone)]
struct GlobalArgs {
/// Do not print any output.
#[arg(global = true, long, short, conflicts_with = "verbose")]
pub quiet: bool,
/// Use verbose output.
#[arg(global = true, action = clap::ArgAction::Count, long, short, conflicts_with = "quiet")]
pub verbose: u8,
}
#[tokio::main]
async fn main() -> Result<ExitCode> {
let cli = Cli::parse();
match cli.command {
Command::Serve(_serve) => {
let python = PythonProcess::new::<Vec<&OsStr>, &OsStr>("djls_agent", None, None)?;
djls_server::serve(python).await?
}
}
Ok(ExitCode::SUCCESS)
}