switch from runner to ipc and long-running sidecar process (#21)

This commit is contained in:
Josh Thomas 2024-12-11 16:16:40 -06:00 committed by GitHub
parent 4c10afb602
commit 235bb4419d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 556 additions and 281 deletions

View file

@ -5,6 +5,7 @@ edition = "2021"
[dependencies] [dependencies]
djls-django = { workspace = true } djls-django = { workspace = true }
djls-ipc = { workspace = true }
djls-server = { workspace = true } djls-server = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }

View file

@ -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)] #[derive(Debug, Parser)]
struct Cli { struct Cli {
@ -6,10 +8,35 @@ struct Cli {
command: Commands, 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<Duration> {
if self.no_health_check {
None
} else {
Some(Duration::from_secs(self.health_interval))
}
}
}
#[derive(Debug, Subcommand)] #[derive(Debug, Subcommand)]
enum Commands { enum Commands {
/// Start the LSP server /// Start the LSP server
Serve, Serve(CommonOpts),
/// Get Python environment information
Info(CommonOpts),
/// Print the version
Version(CommonOpts),
} }
#[tokio::main] #[tokio::main]
@ -17,7 +44,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse(); let cli = Cli::parse();
match cli.command { 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(()) Ok(())

View file

@ -4,6 +4,7 @@ version = "0.0.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
djls-ipc = { workspace = true }
djls-python = { workspace = true } djls-python = { workspace = true }
serde = { workspace = true } serde = { workspace = true }

View file

@ -1,9 +1,7 @@
use djls_python::{Python, RunnerError, ScriptRunner}; use djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError};
use serde::Deserialize; use serde::Deserialize;
use std::fmt; use std::fmt;
use crate::scripts;
#[derive(Debug)] #[derive(Debug)]
pub struct App(String); pub struct App(String);
@ -27,8 +25,16 @@ struct InstalledAppsCheck {
has_app: bool, has_app: bool,
} }
impl ScriptRunner for InstalledAppsCheck { impl TryFrom<JsonResponse> for InstalledAppsCheck {
const SCRIPT: &'static str = scripts::INSTALLED_APPS_CHECK; type Error = TransportError;
fn try_from(response: JsonResponse) -> Result<Self, Self::Error> {
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 { impl Apps {
@ -48,8 +54,10 @@ impl Apps {
self.apps().iter() self.apps().iter()
} }
pub fn check_installed(py: &Python, app: &str) -> Result<bool, RunnerError> { pub fn check_installed(python: &mut PythonProcess, app: &str) -> Result<bool, TransportError> {
let result = InstalledAppsCheck::run_with_py_args(py, app)?; 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) Ok(result.has_app)
} }
} }

View file

@ -1,14 +1,15 @@
use crate::apps::Apps; use crate::apps::Apps;
use crate::gis::{check_gis_setup, GISError}; use crate::gis::{check_gis_setup, GISError};
use crate::scripts;
use crate::templates::TemplateTags; 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 serde::Deserialize;
use std::fmt; use std::fmt;
#[derive(Debug)] #[derive(Debug)]
pub struct DjangoProject { pub struct DjangoProject {
py: Python, py: Python,
python: PythonProcess,
settings_module: String, settings_module: String,
installed_apps: Apps, installed_apps: Apps,
templatetags: TemplateTags, templatetags: TemplateTags,
@ -20,54 +21,67 @@ struct DjangoSetup {
templatetags: TemplateTags, templatetags: TemplateTags,
} }
impl ScriptRunner for DjangoSetup { impl DjangoSetup {
const SCRIPT: &'static str = scripts::DJANGO_SETUP; pub fn setup(python: &mut PythonProcess) -> Result<JsonResponse, ProjectError> {
let response = python.send("django_setup", None)?;
let response = parse_json_response(response)?;
Ok(response)
}
} }
impl DjangoProject { impl DjangoProject {
fn new( fn new(
py: Python, py: Python,
python: PythonProcess,
settings_module: String, settings_module: String,
installed_apps: Apps, installed_apps: Apps,
templatetags: TemplateTags, templatetags: TemplateTags,
) -> Self { ) -> Self {
Self { Self {
py, py,
python,
settings_module, settings_module,
installed_apps, installed_apps,
templatetags, templatetags,
} }
} }
pub fn setup() -> Result<Self, ProjectError> { pub fn setup(mut python: PythonProcess) -> Result<Self, ProjectError> {
let settings_module = let settings_module =
std::env::var("DJANGO_SETTINGS_MODULE").expect("DJANGO_SETTINGS_MODULE must be set"); 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 { if !has_django {
return Err(ProjectError::DjangoNotFound); 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!("Warning: GeoDjango detected but GDAL is not available.");
eprintln!("Django initialization will be skipped. Some features may be limited."); eprintln!("Django initialization will be skipped. Some features may be limited.");
eprintln!("To enable full functionality, please install GDAL and other GeoDjango prerequisites."); eprintln!("To enable full functionality, please install GDAL and other GeoDjango prerequisites.");
return Ok(Self { return Ok(Self {
py, py,
python,
settings_module, settings_module,
installed_apps: Apps::default(), installed_apps: Apps::default(),
templatetags: TemplateTags::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( Ok(Self::new(
py, py,
python,
settings_module, settings_module,
Apps::from_strings(setup.installed_apps.to_vec()), Apps::from_strings(setup.installed_apps.to_vec()),
setup.templatetags, setup.templatetags,
@ -110,8 +124,11 @@ pub enum ProjectError {
Json(#[from] serde_json::Error), Json(#[from] serde_json::Error),
#[error(transparent)] #[error(transparent)]
Python(#[from] djls_python::PythonError), Packaging(#[from] djls_python::PackagingError),
#[error(transparent)] #[error(transparent)]
Runner(#[from] RunnerError), Python(#[from] djls_python::PythonError),
#[error("Transport error: {0}")]
Transport(#[from] TransportError),
} }

View file

@ -1,9 +1,9 @@
use crate::apps::Apps; use crate::apps::Apps;
use djls_python::{Python, RunnerError}; use djls_ipc::{PythonProcess, TransportError};
use std::process::Command; use std::process::Command;
pub fn check_gis_setup(py: &Python) -> Result<bool, GISError> { pub fn check_gis_setup(python: &mut PythonProcess) -> Result<bool, GISError> {
let has_geodjango = Apps::check_installed(py, "django.contrib.gis")?; let has_geodjango = Apps::check_installed(python, "django.contrib.gis")?;
let gdal_is_installed = Command::new("gdalinfo") let gdal_is_installed = Command::new("gdalinfo")
.arg("--version") .arg("--version")
.output() .output()
@ -21,6 +21,6 @@ pub enum GISError {
#[error("JSON parsing error: {0}")] #[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error), Json(#[from] serde_json::Error),
#[error(transparent)] #[error("Transport error: {0}")]
Runner(#[from] RunnerError), Transport(#[from] TransportError),
} }

View file

@ -1,7 +1,6 @@
mod apps; mod apps;
mod django; mod django;
mod gis; mod gis;
mod scripts;
mod templates; mod templates;
pub use django::DjangoProject; pub use django::DjangoProject;

View file

@ -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");

View file

@ -8,6 +8,7 @@ anyhow = { workspace = true }
async-trait = { workspace = true } async-trait = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tempfile = "3.14.0" tempfile = "3.14.0"

View file

@ -1,5 +1,13 @@
mod client; mod client;
mod process;
mod server; mod server;
mod transport;
pub use client::Client; pub use client::Client;
pub use process::PythonProcess;
pub use server::Server; 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;

View file

@ -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<Mutex<Box<dyn TransportProtocol>>>,
_child: Child,
healthy: Arc<AtomicBool>,
}
impl PythonProcess {
pub fn new(
module: &str,
transport: Transport,
health_check_interval: Option<Duration>,
) -> Result<Self, TransportError> {
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<Vec<String>>,
) -> Result<String, TransportError> {
let mut transport = self.transport.lock().unwrap();
transport.send(message, args)
}
}

View file

@ -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<Box<dyn TransportProtocol>, 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<Self, TransportError>
where
Self: Sized;
fn health_check(&mut self) -> Result<(), TransportError>;
fn clone_box(&self) -> Box<dyn TransportProtocol>;
fn send_impl(
&mut self,
message: &str,
args: Option<Vec<String>>,
) -> Result<String, TransportError>;
fn send(&mut self, message: &str, args: Option<Vec<String>>) -> Result<String, TransportError> {
self.health_check()?;
self.send_impl(message, args)
}
}
impl Clone for Box<dyn TransportProtocol> {
fn clone(&self) -> Self {
self.clone_box()
}
}
#[derive(Debug)]
pub struct RawTransport {
reader: Arc<Mutex<BufReader<ChildStdout>>>,
writer: Arc<Mutex<BufWriter<ChildStdin>>>,
}
impl TransportProtocol for RawTransport {
fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result<Self, TransportError> {
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<dyn TransportProtocol> {
Box::new(RawTransport {
reader: self.reader.clone(),
writer: self.writer.clone(),
})
}
fn send_impl(
&mut self,
message: &str,
args: Option<Vec<String>>,
) -> Result<String, TransportError> {
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<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JsonResponse {
status: String,
data: Option<Value>,
error: Option<String>,
}
impl JsonResponse {
pub fn data(&self) -> &Option<Value> {
&self.data
}
}
#[derive(Debug)]
pub struct JsonTransport {
reader: Arc<Mutex<BufReader<ChildStdout>>>,
writer: Arc<Mutex<BufWriter<ChildStdin>>>,
}
impl TransportProtocol for JsonTransport {
fn new(stdin: ChildStdin, stdout: ChildStdout) -> Result<Self, TransportError> {
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<dyn TransportProtocol> {
Box::new(JsonTransport {
reader: self.reader.clone(),
writer: self.writer.clone(),
})
}
fn send_impl(
&mut self,
message: &str,
args: Option<Vec<String>>,
) -> Result<String, TransportError> {
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<String, TransportError> {
Ok(response)
}
pub fn parse_json_response(response: String) -> Result<JsonResponse, TransportError> {
serde_json::from_str(&response).map_err(TransportError::Json)
}

View file

@ -4,6 +4,8 @@ version = "0.0.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
djls-ipc = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }

View file

@ -1,11 +1,7 @@
mod packaging; mod packaging;
mod python; mod python;
mod runner;
mod scripts;
pub use crate::packaging::ImportCheck; pub use crate::packaging::ImportCheck;
pub use crate::packaging::PackagingError;
pub use crate::python::Python; pub use crate::python::Python;
pub use crate::python::PythonError; pub use crate::python::PythonError;
pub use crate::runner::Runner;
pub use crate::runner::RunnerError;
pub use crate::runner::ScriptRunner;

View file

@ -1,6 +1,4 @@
use crate::python::Python; use djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError};
use crate::runner::{RunnerError, ScriptRunner};
use crate::scripts;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::fmt;
@ -59,8 +57,16 @@ pub struct ImportCheck {
can_import: bool, can_import: bool,
} }
impl ScriptRunner for ImportCheck { impl TryFrom<JsonResponse> for ImportCheck {
const SCRIPT: &'static str = scripts::HAS_IMPORT; type Error = TransportError;
fn try_from(response: JsonResponse) -> Result<Self, Self::Error> {
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 { impl ImportCheck {
@ -68,9 +74,14 @@ impl ImportCheck {
self.can_import self.can_import
} }
pub fn check(py: &Python, module: &str) -> Result<bool, RunnerError> { pub fn check(
let result = ImportCheck::run_with_py_args(py, module)?; python: &mut PythonProcess,
Ok(result.can_import) modules: Option<Vec<String>>,
) -> Result<bool, PackagingError> {
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}")] #[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error), Json(#[from] serde_json::Error),
#[error(transparent)] #[error("Transport error: {0}")]
Runner(#[from] Box<RunnerError>), Transport(#[from] TransportError),
#[error("UTF-8 conversion error: {0}")] #[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::string::FromUtf8Error), Utf8(#[from] std::string::FromUtf8Error),
} }
impl From<RunnerError> for PackagingError {
fn from(err: RunnerError) -> Self {
PackagingError::Runner(Box::new(err))
}
}

View file

@ -1,10 +1,8 @@
use crate::packaging::{Packages, PackagingError}; use crate::packaging::{Packages, PackagingError};
use crate::runner::{Runner, RunnerError, ScriptRunner}; use djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError};
use crate::scripts;
use serde::Deserialize; use serde::Deserialize;
use std::fmt; use std::fmt;
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use which::which;
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct VersionInfo { pub struct VersionInfo {
@ -60,29 +58,23 @@ pub struct Python {
packages: Packages, packages: Packages,
} }
#[derive(Debug, Deserialize)] impl TryFrom<JsonResponse> for Python {
pub struct PythonSetup(Python); type Error = TransportError;
impl ScriptRunner for PythonSetup { fn try_from(response: JsonResponse) -> Result<Self, Self::Error> {
const SCRIPT: &'static str = scripts::PYTHON_SETUP; response
} .data()
.clone()
impl From<PythonSetup> for Python { .ok_or_else(|| TransportError::Process("No data in response".to_string()))
fn from(setup: PythonSetup) -> Self { .and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))
setup.0
} }
} }
impl Python { impl Python {
pub fn initialize() -> Result<Self, PythonError> { pub fn setup(python: &mut PythonProcess) -> Result<Self, PythonError> {
let executable = which("python")?; let response = python.send("python_setup", None)?;
Ok(PythonSetup::run_with_path(&executable)?.into()) let response = parse_json_response(response)?;
} Ok(Self::try_from(response)?)
}
impl Runner for Python {
fn get_executable(&self) -> &Path {
&self.sys_executable
} }
} }
@ -123,15 +115,9 @@ pub enum PythonError {
#[error("Failed to locate Python executable: {0}")] #[error("Failed to locate Python executable: {0}")]
PythonNotFound(#[from] which::Error), PythonNotFound(#[from] which::Error),
#[error(transparent)] #[error("Transport error: {0}")]
Runner(#[from] Box<RunnerError>), Transport(#[from] TransportError),
#[error("UTF-8 conversion error: {0}")] #[error("UTF-8 conversion error: {0}")]
Utf8(#[from] std::string::FromUtf8Error), Utf8(#[from] std::string::FromUtf8Error),
} }
impl From<RunnerError> for PythonError {
fn from(err: RunnerError) -> Self {
PythonError::Runner(Box::new(err))
}
}

View file

@ -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<String> {
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<String> {
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<String> {
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<String> {
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<T>(&self, script: &str) -> Result<T, serde_json::Error>
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<T>(&self, script: &str, args: &str) -> Result<T, serde_json::Error>
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<R: Runner>(runner: &R) -> Result<Self, RunnerError>
where
Self: for<'de> Deserialize<'de>,
{
let result = runner.run_script(Self::SCRIPT).map_err(RunnerError::from)?;
Ok(result)
}
fn run_with_exe_args<R: Runner>(runner: &R, args: &str) -> Result<Self, RunnerError>
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<Self, RunnerError>
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<Self, RunnerError>
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<Self, RunnerError>
where
Self: for<'de> Deserialize<'de>,
{
Self::run_with_exe(python)
}
fn run_with_py_args(python: &Python, args: &str) -> Result<Self, RunnerError>
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),
}

View file

@ -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"];

View file

@ -6,6 +6,7 @@ edition = "2021"
[dependencies] [dependencies]
djls-ast = { workspace = true } djls-ast = { workspace = true }
djls-django = { workspace = true } djls-django = { workspace = true }
djls-ipc = { workspace = true }
djls-python = { workspace = true } djls-python = { workspace = true }
djls-worker = { workspace = true } djls-worker = { workspace = true }

View file

@ -7,6 +7,7 @@ use crate::notifier::TowerLspNotifier;
use crate::server::{DjangoLanguageServer, LspNotification, LspRequest}; use crate::server::{DjangoLanguageServer, LspNotification, LspRequest};
use anyhow::Result; use anyhow::Result;
use djls_django::DjangoProject; use djls_django::DjangoProject;
use djls_ipc::PythonProcess;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tower_lsp::jsonrpc::Result as LspResult; use tower_lsp::jsonrpc::Result as LspResult;
@ -80,8 +81,8 @@ impl LanguageServer for TowerLspBackend {
} }
} }
pub async fn serve() -> Result<()> { pub async fn serve(python: PythonProcess) -> Result<()> {
let django = DjangoProject::setup()?; let django = DjangoProject::setup(python)?;
let stdin = tokio::io::stdin(); let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout(); let stdout = tokio::io::stdout();

View file

@ -16,7 +16,7 @@ dependencies = [
] ]
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
include = ["python/djls"] sources = ["python"]
[dependency-groups] [dependency-groups]
dev = [ dev = [

104
python/djls/lsp.py Normal file
View file

@ -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()

View file

@ -2,11 +2,11 @@ from __future__ import annotations
import json import json
def get_django_setup_info():
from django.conf import settings from django.conf import settings
from django.template.engine import Engine from django.template.engine import Engine
def get_django_setup_info():
return { return {
"installed_apps": list(settings.INSTALLED_APPS), "installed_apps": list(settings.INSTALLED_APPS),
"templatetags": [ "templatetags": [