diff --git a/crates/djlc-cli/Cargo.toml b/crates/djlc-cli/Cargo.toml index 938b625..bc2d0bb 100644 --- a/crates/djlc-cli/Cargo.toml +++ b/crates/djlc-cli/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] djls-django = { workspace = true } +djls-ipc = { workspace = true } djls-server = { workspace = true } anyhow = { workspace = true } diff --git a/crates/djlc-cli/src/main.rs b/crates/djlc-cli/src/main.rs index bd19be2..c645b53 100644 --- a/crates/djlc-cli/src/main.rs +++ b/crates/djlc-cli/src/main.rs @@ -1,4 +1,6 @@ -use clap::{Parser, Subcommand}; +use clap::{Args, Parser, Subcommand}; +use djls_ipc::{PythonProcess, Transport}; +use std::time::Duration; #[derive(Debug, Parser)] struct Cli { @@ -6,10 +8,35 @@ struct Cli { command: Commands, } +#[derive(Debug, Args)] +struct CommonOpts { + /// Disable periodic health checks + #[arg(long)] + no_health_check: bool, + + /// Health check interval in seconds + #[arg(long, default_value = "30")] + health_interval: u64, +} + +impl CommonOpts { + fn health_check_interval(&self) -> Option { + if self.no_health_check { + None + } else { + Some(Duration::from_secs(self.health_interval)) + } + } +} + #[derive(Debug, Subcommand)] enum Commands { /// Start the LSP server - Serve, + Serve(CommonOpts), + /// Get Python environment information + Info(CommonOpts), + /// Print the version + Version(CommonOpts), } #[tokio::main] @@ -17,7 +44,27 @@ async fn main() -> Result<(), Box> { let cli = Cli::parse(); match cli.command { - Commands::Serve => djls_server::serve().await?, + Commands::Serve(opts) => { + let python = + PythonProcess::new("djls.lsp", Transport::Json, opts.health_check_interval())?; + djls_server::serve(python).await? + } + Commands::Info(opts) => { + let mut python = + PythonProcess::new("djls.lsp", Transport::Json, opts.health_check_interval())?; + match python.send("python_setup", None) { + Ok(info) => println!("{}", info), + Err(e) => eprintln!("Failed to get info: {}", e), + } + } + Commands::Version(opts) => { + let mut python = + PythonProcess::new("djls.lsp", Transport::Json, opts.health_check_interval())?; + match python.send("version", None) { + Ok(version) => println!("Python module version: {}", version), + Err(e) => eprintln!("Failed to get version: {}", e), + } + } } Ok(()) diff --git a/crates/djls-django/Cargo.toml b/crates/djls-django/Cargo.toml index 40c30e7..beb0360 100644 --- a/crates/djls-django/Cargo.toml +++ b/crates/djls-django/Cargo.toml @@ -4,6 +4,7 @@ version = "0.0.0" edition = "2021" [dependencies] +djls-ipc = { workspace = true } djls-python = { workspace = true } serde = { workspace = true } diff --git a/crates/djls-django/src/apps.rs b/crates/djls-django/src/apps.rs index 5a5d5a6..975dd82 100644 --- a/crates/djls-django/src/apps.rs +++ b/crates/djls-django/src/apps.rs @@ -1,9 +1,7 @@ -use djls_python::{Python, RunnerError, ScriptRunner}; +use djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError}; use serde::Deserialize; use std::fmt; -use crate::scripts; - #[derive(Debug)] pub struct App(String); @@ -27,8 +25,16 @@ struct InstalledAppsCheck { has_app: bool, } -impl ScriptRunner for InstalledAppsCheck { - const SCRIPT: &'static str = scripts::INSTALLED_APPS_CHECK; +impl TryFrom for InstalledAppsCheck { + type Error = TransportError; + + fn try_from(response: JsonResponse) -> Result { + response + .data() + .clone() + .ok_or_else(|| TransportError::Process("No data in response".to_string())) + .and_then(|data| serde_json::from_value(data).map_err(TransportError::Json)) + } } impl Apps { @@ -48,8 +54,10 @@ impl Apps { self.apps().iter() } - pub fn check_installed(py: &Python, app: &str) -> Result { - let result = InstalledAppsCheck::run_with_py_args(py, app)?; + pub fn check_installed(python: &mut PythonProcess, app: &str) -> Result { + let response = python.send("installed_apps_check", Some(vec![app.to_string()]))?; + let response = parse_json_response(response)?; + let result = InstalledAppsCheck::try_from(response)?; Ok(result.has_app) } } diff --git a/crates/djls-django/src/django.rs b/crates/djls-django/src/django.rs index d52eb11..0ea1c41 100644 --- a/crates/djls-django/src/django.rs +++ b/crates/djls-django/src/django.rs @@ -1,14 +1,15 @@ use crate::apps::Apps; use crate::gis::{check_gis_setup, GISError}; -use crate::scripts; use crate::templates::TemplateTags; -use djls_python::{ImportCheck, Python, RunnerError, ScriptRunner}; +use djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError}; +use djls_python::{ImportCheck, Python}; use serde::Deserialize; use std::fmt; #[derive(Debug)] pub struct DjangoProject { py: Python, + python: PythonProcess, settings_module: String, installed_apps: Apps, templatetags: TemplateTags, @@ -20,54 +21,67 @@ struct DjangoSetup { templatetags: TemplateTags, } -impl ScriptRunner for DjangoSetup { - const SCRIPT: &'static str = scripts::DJANGO_SETUP; +impl DjangoSetup { + pub fn setup(python: &mut PythonProcess) -> Result { + let response = python.send("django_setup", None)?; + let response = parse_json_response(response)?; + Ok(response) + } } impl DjangoProject { fn new( py: Python, + python: PythonProcess, settings_module: String, installed_apps: Apps, templatetags: TemplateTags, ) -> Self { Self { py, + python, settings_module, installed_apps, templatetags, } } - pub fn setup() -> Result { + pub fn setup(mut python: PythonProcess) -> Result { let settings_module = std::env::var("DJANGO_SETTINGS_MODULE").expect("DJANGO_SETTINGS_MODULE must be set"); - let py = Python::initialize()?; + let py = Python::setup(&mut python)?; - let has_django = ImportCheck::check(&py, "django")?; + let has_django = ImportCheck::check(&mut python, Some(vec!["django".to_string()]))?; if !has_django { return Err(ProjectError::DjangoNotFound); } - if !check_gis_setup(&py)? { + if !check_gis_setup(&mut python)? { 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, settings_module, installed_apps: Apps::default(), templatetags: TemplateTags::default(), }); } - let setup = DjangoSetup::run_with_py(&py)?; + let response = DjangoSetup::setup(&mut python)?; + let setup: DjangoSetup = response + .data() + .clone() + .ok_or_else(|| TransportError::Process("No data in response".to_string())) + .and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))?; Ok(Self::new( py, + python, settings_module, Apps::from_strings(setup.installed_apps.to_vec()), setup.templatetags, @@ -110,8 +124,11 @@ pub enum ProjectError { Json(#[from] serde_json::Error), #[error(transparent)] - Python(#[from] djls_python::PythonError), + Packaging(#[from] djls_python::PackagingError), #[error(transparent)] - Runner(#[from] RunnerError), + Python(#[from] djls_python::PythonError), + + #[error("Transport error: {0}")] + Transport(#[from] TransportError), } diff --git a/crates/djls-django/src/gis.rs b/crates/djls-django/src/gis.rs index 3e38cb0..d5702bb 100644 --- a/crates/djls-django/src/gis.rs +++ b/crates/djls-django/src/gis.rs @@ -1,9 +1,9 @@ use crate::apps::Apps; -use djls_python::{Python, RunnerError}; +use djls_ipc::{PythonProcess, TransportError}; use std::process::Command; -pub fn check_gis_setup(py: &Python) -> Result { - let has_geodjango = Apps::check_installed(py, "django.contrib.gis")?; +pub fn check_gis_setup(python: &mut PythonProcess) -> Result { + let has_geodjango = Apps::check_installed(python, "django.contrib.gis")?; let gdal_is_installed = Command::new("gdalinfo") .arg("--version") .output() @@ -21,6 +21,6 @@ pub enum GISError { #[error("JSON parsing error: {0}")] Json(#[from] serde_json::Error), - #[error(transparent)] - Runner(#[from] RunnerError), + #[error("Transport error: {0}")] + Transport(#[from] TransportError), } diff --git a/crates/djls-django/src/lib.rs b/crates/djls-django/src/lib.rs index 690f385..6b6bf8f 100644 --- a/crates/djls-django/src/lib.rs +++ b/crates/djls-django/src/lib.rs @@ -1,7 +1,6 @@ mod apps; mod django; mod gis; -mod scripts; mod templates; pub use django::DjangoProject; diff --git a/crates/djls-django/src/scripts.rs b/crates/djls-django/src/scripts.rs deleted file mode 100644 index 742398c..0000000 --- a/crates/djls-django/src/scripts.rs +++ /dev/null @@ -1,4 +0,0 @@ -use djls_python::include_script; - -pub const DJANGO_SETUP: &str = include_script!("django_setup"); -pub const INSTALLED_APPS_CHECK: &str = include_script!("installed_apps_check"); diff --git a/crates/djls-ipc/Cargo.toml b/crates/djls-ipc/Cargo.toml index d9bbeda..43c7fbd 100644 --- a/crates/djls-ipc/Cargo.toml +++ b/crates/djls-ipc/Cargo.toml @@ -8,6 +8,7 @@ anyhow = { workspace = true } async-trait = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +thiserror = { workspace = true } tokio = { workspace = true } tempfile = "3.14.0" diff --git a/crates/djls-ipc/src/lib.rs b/crates/djls-ipc/src/lib.rs index 9aa5b70..ebd6429 100644 --- a/crates/djls-ipc/src/lib.rs +++ b/crates/djls-ipc/src/lib.rs @@ -1,5 +1,13 @@ mod client; +mod process; mod server; +mod transport; pub use client::Client; +pub use process::PythonProcess; pub use server::Server; +pub use transport::parse_json_response; +pub use transport::parse_raw_response; +pub use transport::JsonResponse; +pub use transport::Transport; +pub use transport::TransportError; diff --git a/crates/djls-ipc/src/process.rs b/crates/djls-ipc/src/process.rs new file mode 100644 index 0000000..fa3989b --- /dev/null +++ b/crates/djls-ipc/src/process.rs @@ -0,0 +1,81 @@ +use crate::transport::{Transport, TransportError, TransportProtocol}; +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>>, + _child: Child, + healthy: Arc, +} + +impl PythonProcess { + pub fn new( + module: &str, + transport: Transport, + health_check_interval: Option, + ) -> Result { + let mut child = Command::new("python") + .arg("-m") + .arg(module) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + + let stdin = child.stdin.take().unwrap(); + let stdout = child.stdout.take().unwrap(); + + let process = Self { + transport: Arc::new(Mutex::new(transport.create(stdin, stdout)?)), + _child: child, + healthy: Arc::new(AtomicBool::new(true)), + }; + + if let Some(interval) = health_check_interval { + process.start_health_check_task(interval)?; + } + + Ok(process) + } + + fn start_health_check_task(&self, interval: Duration) -> Result<(), TransportError> { + let healthy = self.healthy.clone(); + let transport = self.transport.clone(); + + tokio::spawn(async move { + let mut interval = time::interval(interval); + loop { + interval.tick().await; + + if let Ok(mut transport) = transport.lock() { + match transport.health_check() { + Ok(()) => { + healthy.store(true, Ordering::SeqCst); + } + Err(_) => { + healthy.store(false, Ordering::SeqCst); + } + } + } + } + }); + + Ok(()) + } + + pub fn is_healthy(&self) -> bool { + self.healthy.load(Ordering::SeqCst) + } + + pub fn send( + &mut self, + message: &str, + args: Option>, + ) -> Result { + let mut transport = self.transport.lock().unwrap(); + transport.send(message, args) + } +} diff --git a/crates/djls-ipc/src/transport.rs b/crates/djls-ipc/src/transport.rs new file mode 100644 index 0000000..f2093d3 --- /dev/null +++ b/crates/djls-ipc/src/transport.rs @@ -0,0 +1,214 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::fmt::Debug; +use std::io::{BufRead, BufReader, BufWriter, Write}; +use std::process::{ChildStdin, ChildStdout}; +use std::sync::{Arc, Mutex}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum TransportError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), + #[error("Process error: {0}")] + Process(String), +} + +pub enum Transport { + Raw, + Json, +} + +impl Transport { + pub fn create( + &self, + mut stdin: ChildStdin, + mut stdout: ChildStdout, + ) -> Result, TransportError> { + let transport_type = match self { + Transport::Raw => "raw", + Transport::Json => "json", + }; + + writeln!(stdin, "{}", transport_type).map_err(TransportError::Io)?; + 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(TransportError::Process( + "Python process not ready".to_string(), + )); + } + + match self { + Transport::Raw => Ok(Box::new(RawTransport::new(stdin, stdout)?)), + Transport::Json => Ok(Box::new(JsonTransport::new(stdin, stdout)?)), + } + } +} + +pub trait TransportProtocol: Debug + Send { + fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result + where + Self: Sized; + fn health_check(&mut self) -> Result<(), TransportError>; + fn clone_box(&self) -> Box; + fn send_impl( + &mut self, + message: &str, + args: Option>, + ) -> Result; + + fn send(&mut self, message: &str, args: Option>) -> Result { + self.health_check()?; + self.send_impl(message, args) + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.clone_box() + } +} + +#[derive(Debug)] +pub struct RawTransport { + reader: Arc>>, + writer: Arc>>, +} + +impl TransportProtocol for RawTransport { + fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result { + Ok(Self { + reader: Arc::new(Mutex::new(BufReader::new(stdout))), + writer: Arc::new(Mutex::new(BufWriter::new(stdin))), + }) + } + + fn health_check(&mut self) -> Result<(), TransportError> { + self.send_impl("health", None) + .and_then(|response| match response.as_str() { + "ok" => Ok(()), + other => Err(TransportError::Process(format!( + "Health check failed: {}", + other + ))), + }) + } + + fn clone_box(&self) -> Box { + Box::new(RawTransport { + reader: self.reader.clone(), + writer: self.writer.clone(), + }) + } + + fn send_impl( + &mut self, + message: &str, + args: Option>, + ) -> Result { + let mut writer = self.writer.lock().unwrap(); + + if let Some(args) = args { + // Join command and args with spaces + writeln!(writer, "{} {}", message, args.join(" ")).map_err(TransportError::Io)?; + } else { + writeln!(writer, "{}", message).map_err(TransportError::Io)?; + } + + writer.flush().map_err(TransportError::Io)?; + + let mut reader = self.reader.lock().unwrap(); + let mut line = String::new(); + reader.read_line(&mut line).map_err(TransportError::Io)?; + Ok(line.trim().to_string()) + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct JsonCommand { + command: String, + args: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JsonResponse { + status: String, + data: Option, + error: Option, +} + +impl JsonResponse { + pub fn data(&self) -> &Option { + &self.data + } +} + +#[derive(Debug)] +pub struct JsonTransport { + reader: Arc>>, + writer: Arc>>, +} + +impl TransportProtocol for JsonTransport { + fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result { + Ok(Self { + reader: Arc::new(Mutex::new(BufReader::new(stdout))), + writer: Arc::new(Mutex::new(BufWriter::new(stdin))), + }) + } + + fn health_check(&mut self) -> Result<(), TransportError> { + self.send_impl("health", None).and_then(|response| { + let json: JsonResponse = serde_json::from_str(&response)?; + match json.status.as_str() { + "ok" => Ok(()), + _ => Err(TransportError::Process( + json.error.unwrap_or_else(|| "Unknown error".to_string()), + )), + } + }) + } + + fn clone_box(&self) -> Box { + Box::new(JsonTransport { + reader: self.reader.clone(), + writer: self.writer.clone(), + }) + } + + fn send_impl( + &mut self, + message: &str, + args: Option>, + ) -> Result { + let command = JsonCommand { + command: message.to_string(), + args, + }; + + let mut writer = self.writer.lock().unwrap(); + serde_json::to_writer(&mut *writer, &command)?; + writeln!(writer).map_err(TransportError::Io)?; + writer.flush().map_err(TransportError::Io)?; + + let mut reader = self.reader.lock().unwrap(); + let mut line = String::new(); + reader.read_line(&mut line).map_err(TransportError::Io)?; + Ok(line.trim().to_string()) + } +} + +pub fn parse_raw_response(response: String) -> Result { + Ok(response) +} + +pub fn parse_json_response(response: String) -> Result { + serde_json::from_str(&response).map_err(TransportError::Json) +} diff --git a/crates/djls-python/Cargo.toml b/crates/djls-python/Cargo.toml index 90756a3..99ced11 100644 --- a/crates/djls-python/Cargo.toml +++ b/crates/djls-python/Cargo.toml @@ -4,6 +4,8 @@ version = "0.0.0" edition = "2021" [dependencies] +djls-ipc = { workspace = true } + serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/crates/djls-python/src/lib.rs b/crates/djls-python/src/lib.rs index d61a386..ae22bff 100644 --- a/crates/djls-python/src/lib.rs +++ b/crates/djls-python/src/lib.rs @@ -1,11 +1,7 @@ mod packaging; mod python; -mod runner; -mod scripts; pub use crate::packaging::ImportCheck; +pub use crate::packaging::PackagingError; pub use crate::python::Python; pub use crate::python::PythonError; -pub use crate::runner::Runner; -pub use crate::runner::RunnerError; -pub use crate::runner::ScriptRunner; diff --git a/crates/djls-python/src/packaging.rs b/crates/djls-python/src/packaging.rs index 1d006f6..08a563c 100644 --- a/crates/djls-python/src/packaging.rs +++ b/crates/djls-python/src/packaging.rs @@ -1,6 +1,4 @@ -use crate::python::Python; -use crate::runner::{RunnerError, ScriptRunner}; -use crate::scripts; +use djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError}; use serde::Deserialize; use std::collections::HashMap; use std::fmt; @@ -59,8 +57,16 @@ pub struct ImportCheck { can_import: bool, } -impl ScriptRunner for ImportCheck { - const SCRIPT: &'static str = scripts::HAS_IMPORT; +impl TryFrom for ImportCheck { + type Error = TransportError; + + fn try_from(response: JsonResponse) -> Result { + response + .data() + .clone() + .ok_or_else(|| TransportError::Process("No data in response".to_string())) + .and_then(|data| serde_json::from_value(data).map_err(TransportError::Json)) + } } impl ImportCheck { @@ -68,9 +74,14 @@ impl ImportCheck { self.can_import } - pub fn check(py: &Python, module: &str) -> Result { - let result = ImportCheck::run_with_py_args(py, module)?; - Ok(result.can_import) + pub fn check( + python: &mut PythonProcess, + modules: Option>, + ) -> Result { + let response = python.send("has_import", modules)?; + let response = parse_json_response(response)?; + let check = Self::try_from(response)?; + Ok(check.can_import) } } @@ -82,15 +93,9 @@ pub enum PackagingError { #[error("JSON parsing error: {0}")] Json(#[from] serde_json::Error), - #[error(transparent)] - Runner(#[from] Box), + #[error("Transport error: {0}")] + Transport(#[from] TransportError), #[error("UTF-8 conversion error: {0}")] Utf8(#[from] std::string::FromUtf8Error), } - -impl From for PackagingError { - fn from(err: RunnerError) -> Self { - PackagingError::Runner(Box::new(err)) - } -} diff --git a/crates/djls-python/src/python.rs b/crates/djls-python/src/python.rs index 1530c6c..052bb10 100644 --- a/crates/djls-python/src/python.rs +++ b/crates/djls-python/src/python.rs @@ -1,10 +1,8 @@ use crate::packaging::{Packages, PackagingError}; -use crate::runner::{Runner, RunnerError, ScriptRunner}; -use crate::scripts; +use djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError}; use serde::Deserialize; use std::fmt; -use std::path::{Path, PathBuf}; -use which::which; +use std::path::PathBuf; #[derive(Clone, Debug, Deserialize)] pub struct VersionInfo { @@ -60,29 +58,23 @@ pub struct Python { packages: Packages, } -#[derive(Debug, Deserialize)] -pub struct PythonSetup(Python); +impl TryFrom for Python { + type Error = TransportError; -impl ScriptRunner for PythonSetup { - const SCRIPT: &'static str = scripts::PYTHON_SETUP; -} - -impl From for Python { - fn from(setup: PythonSetup) -> Self { - setup.0 + fn try_from(response: JsonResponse) -> Result { + response + .data() + .clone() + .ok_or_else(|| TransportError::Process("No data in response".to_string())) + .and_then(|data| serde_json::from_value(data).map_err(TransportError::Json)) } } impl Python { - pub fn initialize() -> Result { - let executable = which("python")?; - Ok(PythonSetup::run_with_path(&executable)?.into()) - } -} - -impl Runner for Python { - fn get_executable(&self) -> &Path { - &self.sys_executable + pub fn setup(python: &mut PythonProcess) -> Result { + let response = python.send("python_setup", None)?; + let response = parse_json_response(response)?; + Ok(Self::try_from(response)?) } } @@ -123,15 +115,9 @@ pub enum PythonError { #[error("Failed to locate Python executable: {0}")] PythonNotFound(#[from] which::Error), - #[error(transparent)] - Runner(#[from] Box), + #[error("Transport error: {0}")] + Transport(#[from] TransportError), #[error("UTF-8 conversion error: {0}")] Utf8(#[from] std::string::FromUtf8Error), } - -impl From for PythonError { - fn from(err: RunnerError) -> Self { - PythonError::Runner(Box::new(err)) - } -} diff --git a/crates/djls-python/src/runner.rs b/crates/djls-python/src/runner.rs deleted file mode 100644 index dd71b2c..0000000 --- a/crates/djls-python/src/runner.rs +++ /dev/null @@ -1,179 +0,0 @@ -use crate::python::{Python, PythonError}; -use serde::ser::Error; -use serde::Deserialize; -use std::path::{Path, PathBuf}; -use std::process::Command; - -pub trait Runner { - fn get_executable(&self) -> &Path; - - fn run_module(&self, command: &str) -> std::io::Result { - let output = Command::new(self.get_executable()) - .arg("-m") - .arg(command) - .output()?; - - if !output.status.success() { - let error = String::from_utf8_lossy(&output.stderr); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("Command failed: {}", error), - )); - } - - Ok(String::from_utf8_lossy(&output.stdout).into_owned()) - } - - fn run_module_with_args(&self, command: &str, args: &[&str]) -> std::io::Result { - let output = Command::new(self.get_executable()) - .arg("-m") - .arg(command) - .args(args) - .output()?; - - if !output.status.success() { - let error = String::from_utf8_lossy(&output.stderr); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("Command failed: {}", error), - )); - } - - Ok(String::from_utf8_lossy(&output.stdout).into_owned()) - } - - fn run_python_code(&self, code: &str) -> std::io::Result { - let output = Command::new(self.get_executable()) - .args(["-c", code]) - .output()?; - - if !output.status.success() { - let error = String::from_utf8_lossy(&output.stderr); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("Python execution failed: {}", error), - )); - } - - Ok(String::from_utf8_lossy(&output.stdout).into_owned()) - } - - fn run_python_code_with_args(&self, code: &str, args: &str) -> std::io::Result { - let output = Command::new(self.get_executable()) - .args(["-c", code, args]) - .output()?; - - if !output.status.success() { - let error = String::from_utf8_lossy(&output.stderr); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("Python execution failed: {}", error), - )); - } - - Ok(String::from_utf8_lossy(&output.stdout).into_owned()) - } - - fn run_script(&self, script: &str) -> Result - where - T: for<'de> Deserialize<'de>, - { - let result = self - .run_python_code(script) - .map_err(|e| serde_json::Error::custom(e.to_string()))?; - serde_json::from_str(&result) - } - - fn run_script_with_args(&self, script: &str, args: &str) -> Result - where - T: for<'de> Deserialize<'de>, - { - let result = self - .run_python_code_with_args(script, args) - .map_err(|e| serde_json::Error::custom(e.to_string()))?; - serde_json::from_str(&result) - } -} - -pub struct SimpleRunner { - executable: PathBuf, -} - -impl SimpleRunner { - pub fn new(executable: PathBuf) -> Self { - Self { executable } - } -} - -impl Runner for SimpleRunner { - fn get_executable(&self) -> &Path { - &self.executable - } -} - -pub trait ScriptRunner: Sized { - const SCRIPT: &'static str; - - fn run_with_exe(runner: &R) -> Result - where - Self: for<'de> Deserialize<'de>, - { - let result = runner.run_script(Self::SCRIPT).map_err(RunnerError::from)?; - Ok(result) - } - - fn run_with_exe_args(runner: &R, args: &str) -> Result - where - Self: for<'de> Deserialize<'de>, - { - let result = runner - .run_script_with_args(Self::SCRIPT, args) - .map_err(RunnerError::from)?; - Ok(result) - } - - fn run_with_path(executable: &Path) -> Result - where - Self: for<'de> Deserialize<'de>, - { - let runner = &SimpleRunner::new(executable.to_path_buf()); - Self::run_with_exe(runner) - } - - fn run_with_path_args(executable: &Path, args: &str) -> Result - where - Self: for<'de> Deserialize<'de>, - { - let runner = &SimpleRunner::new(executable.to_path_buf()); - Self::run_with_exe_args(runner, args) - } - - fn run_with_py(python: &Python) -> Result - where - Self: for<'de> Deserialize<'de>, - { - Self::run_with_exe(python) - } - - fn run_with_py_args(python: &Python, args: &str) -> Result - where - Self: for<'de> Deserialize<'de>, - { - Self::run_with_exe_args(python, args) - } -} - -#[derive(Debug, thiserror::Error)] -pub enum RunnerError { - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("JSON parsing error: {0}")] - Json(#[from] serde_json::Error), - - #[error(transparent)] - Python(#[from] PythonError), - - #[error("UTF-8 conversion error: {0}")] - Utf8(#[from] std::string::FromUtf8Error), -} diff --git a/crates/djls-python/src/scripts.rs b/crates/djls-python/src/scripts.rs deleted file mode 100644 index bd7e2a0..0000000 --- a/crates/djls-python/src/scripts.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[macro_export] -macro_rules! include_script { - ($name:expr) => { - include_str!(concat!( - env!("CARGO_WORKSPACE_DIR"), - "python/djls/scripts/", - $name, - ".py" - )) - }; -} - -pub const HAS_IMPORT: &str = include_script!("has_import"); -pub const PYTHON_SETUP: &str = include_script!["python_setup"]; diff --git a/crates/djls-server/Cargo.toml b/crates/djls-server/Cargo.toml index 7a9250f..9afdae3 100644 --- a/crates/djls-server/Cargo.toml +++ b/crates/djls-server/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] djls-ast = { workspace = true } djls-django = { workspace = true } +djls-ipc = { workspace = true } djls-python = { workspace = true } djls-worker = { workspace = true } diff --git a/crates/djls-server/src/lib.rs b/crates/djls-server/src/lib.rs index a8ac7d7..dbaeee9 100644 --- a/crates/djls-server/src/lib.rs +++ b/crates/djls-server/src/lib.rs @@ -7,6 +7,7 @@ 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; @@ -80,8 +81,8 @@ impl LanguageServer for TowerLspBackend { } } -pub async fn serve() -> Result<()> { - let django = DjangoProject::setup()?; +pub async fn serve(python: PythonProcess) -> Result<()> { + let django = DjangoProject::setup(python)?; let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); diff --git a/pyproject.toml b/pyproject.toml index 6a4214b..b5f0e85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ ] [tool.hatch.build.targets.wheel] -include = ["python/djls"] +sources = ["python"] [dependency-groups] dev = [ diff --git a/python/djls/lsp.py b/python/djls/lsp.py new file mode 100644 index 0000000..fad874f --- /dev/null +++ b/python/djls/lsp.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import json +import sys +from typing import Any + +from .scripts import django_setup +from .scripts import has_import +from .scripts import python_setup + + +def handle_command(command: str) -> str: + parts = command.strip().split() + command = parts[0] + args = parts[1:] if len(parts) > 1 else [] + + if command == "django_setup": + return json.dumps(django_setup.get_django_setup_info()) + if command == "has_import": + if not args: + return "error: Missing module name argument" + return json.dumps({"can_import": has_import.check_import(args[0])}) + if command == "health": + return "ok" + if command == "installed_apps_check": + import django + from django.conf import settings + + django.setup() + if not args: + return "error: Missing module name argument" + return json.dumps({"has_app": args[0] in settings.INSTALLED_APPS}) + if command == "python_setup": + return json.dumps(python_setup.get_python_info()) + if command == "version": + return "0.1.0" + return f"Unknown command: {command}" + + +def handle_json_command(data: dict[str, Any]) -> dict[str, Any]: + command = data["command"] + args = data.get("args", []) # Get args if they exist + + if command == "django_setup": + import django + + django.setup() + return {"status": "ok", "data": django_setup.get_django_setup_info()} + if command == "has_import": + if not args: + return {"status": "error", "error": "Missing module name argument"} + return { + "status": "ok", + "data": {"can_import": has_import.check_import(args[0])}, + } + if command == "health": + return {"status": "ok"} + if command == "installed_apps_check": + import django + from django.conf import settings + + django.setup() + if not args: + return {"status": "error", "error": "Missing module name argument"} + return { + "status": "ok", + "data": {"has_app": args[0] in settings.INSTALLED_APPS}, + } + if command == "python_setup": + return {"status": "ok", "data": python_setup.get_python_info()} + if command == "version": + return {"status": "ok", "data": "0.1.0"} + + return {"status": "error", "error": f"Unknown command: {command}"} + + +def main(): + transport_type = sys.stdin.readline().strip() + print("ready", flush=True) + + while True: + try: + line = sys.stdin.readline() + if not line: + break + + if transport_type == "json": + data = json.loads(line) + response = handle_json_command(data) + print(json.dumps(response), flush=True) + else: + command = line.strip() + response = handle_command(command) + print(response, flush=True) + + except Exception as e: + if transport_type == "json": + print(json.dumps({"status": "error", "error": str(e)}), flush=True) + else: + print(f"error: {str(e)}", flush=True) + + +if __name__ == "__main__": + main() diff --git a/python/djls/scripts/django_setup.py b/python/djls/scripts/django_setup.py index 145bc77..fef95e3 100644 --- a/python/djls/scripts/django_setup.py +++ b/python/djls/scripts/django_setup.py @@ -2,11 +2,11 @@ from __future__ import annotations import json -from django.conf import settings -from django.template.engine import Engine - def get_django_setup_info(): + from django.conf import settings + from django.template.engine import Engine + return { "installed_apps": list(settings.INSTALLED_APPS), "templatetags": [