Skip to content

Commit 235bb44

Browse files
switch from runner to ipc and long-running sidecar process (#21)
1 parent 4c10afb commit 235bb44

File tree

23 files changed

+556
-281
lines changed

23 files changed

+556
-281
lines changed

crates/djlc-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ edition = "2021"
55

66
[dependencies]
77
djls-django = { workspace = true }
8+
djls-ipc = { workspace = true }
89
djls-server = { workspace = true }
910

1011
anyhow = { workspace = true }

crates/djlc-cli/src/main.rs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,70 @@
1-
use clap::{Parser, Subcommand};
1+
use clap::{Args, Parser, Subcommand};
2+
use djls_ipc::{PythonProcess, Transport};
3+
use std::time::Duration;
24

35
#[derive(Debug, Parser)]
46
struct Cli {
57
#[command(subcommand)]
68
command: Commands,
79
}
810

11+
#[derive(Debug, Args)]
12+
struct CommonOpts {
13+
/// Disable periodic health checks
14+
#[arg(long)]
15+
no_health_check: bool,
16+
17+
/// Health check interval in seconds
18+
#[arg(long, default_value = "30")]
19+
health_interval: u64,
20+
}
21+
22+
impl CommonOpts {
23+
fn health_check_interval(&self) -> Option<Duration> {
24+
if self.no_health_check {
25+
None
26+
} else {
27+
Some(Duration::from_secs(self.health_interval))
28+
}
29+
}
30+
}
31+
932
#[derive(Debug, Subcommand)]
1033
enum Commands {
1134
/// Start the LSP server
12-
Serve,
35+
Serve(CommonOpts),
36+
/// Get Python environment information
37+
Info(CommonOpts),
38+
/// Print the version
39+
Version(CommonOpts),
1340
}
1441

1542
#[tokio::main]
1643
async fn main() -> Result<(), Box<dyn std::error::Error>> {
1744
let cli = Cli::parse();
1845

1946
match cli.command {
20-
Commands::Serve => djls_server::serve().await?,
47+
Commands::Serve(opts) => {
48+
let python =
49+
PythonProcess::new("djls.lsp", Transport::Json, opts.health_check_interval())?;
50+
djls_server::serve(python).await?
51+
}
52+
Commands::Info(opts) => {
53+
let mut python =
54+
PythonProcess::new("djls.lsp", Transport::Json, opts.health_check_interval())?;
55+
match python.send("python_setup", None) {
56+
Ok(info) => println!("{}", info),
57+
Err(e) => eprintln!("Failed to get info: {}", e),
58+
}
59+
}
60+
Commands::Version(opts) => {
61+
let mut python =
62+
PythonProcess::new("djls.lsp", Transport::Json, opts.health_check_interval())?;
63+
match python.send("version", None) {
64+
Ok(version) => println!("Python module version: {}", version),
65+
Err(e) => eprintln!("Failed to get version: {}", e),
66+
}
67+
}
2168
}
2269

2370
Ok(())

crates/djls-django/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ version = "0.0.0"
44
edition = "2021"
55

66
[dependencies]
7+
djls-ipc = { workspace = true }
78
djls-python = { workspace = true }
89

910
serde = { workspace = true }

crates/djls-django/src/apps.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
use djls_python::{Python, RunnerError, ScriptRunner};
1+
use djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError};
22
use serde::Deserialize;
33
use std::fmt;
44

5-
use crate::scripts;
6-
75
#[derive(Debug)]
86
pub struct App(String);
97

@@ -27,8 +25,16 @@ struct InstalledAppsCheck {
2725
has_app: bool,
2826
}
2927

30-
impl ScriptRunner for InstalledAppsCheck {
31-
const SCRIPT: &'static str = scripts::INSTALLED_APPS_CHECK;
28+
impl TryFrom<JsonResponse> for InstalledAppsCheck {
29+
type Error = TransportError;
30+
31+
fn try_from(response: JsonResponse) -> Result<Self, Self::Error> {
32+
response
33+
.data()
34+
.clone()
35+
.ok_or_else(|| TransportError::Process("No data in response".to_string()))
36+
.and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))
37+
}
3238
}
3339

3440
impl Apps {
@@ -48,8 +54,10 @@ impl Apps {
4854
self.apps().iter()
4955
}
5056

51-
pub fn check_installed(py: &Python, app: &str) -> Result<bool, RunnerError> {
52-
let result = InstalledAppsCheck::run_with_py_args(py, app)?;
57+
pub fn check_installed(python: &mut PythonProcess, app: &str) -> Result<bool, TransportError> {
58+
let response = python.send("installed_apps_check", Some(vec![app.to_string()]))?;
59+
let response = parse_json_response(response)?;
60+
let result = InstalledAppsCheck::try_from(response)?;
5361
Ok(result.has_app)
5462
}
5563
}

crates/djls-django/src/django.rs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
use crate::apps::Apps;
22
use crate::gis::{check_gis_setup, GISError};
3-
use crate::scripts;
43
use crate::templates::TemplateTags;
5-
use djls_python::{ImportCheck, Python, RunnerError, ScriptRunner};
4+
use djls_ipc::{parse_json_response, JsonResponse, PythonProcess, TransportError};
5+
use djls_python::{ImportCheck, Python};
66
use serde::Deserialize;
77
use std::fmt;
88

99
#[derive(Debug)]
1010
pub struct DjangoProject {
1111
py: Python,
12+
python: PythonProcess,
1213
settings_module: String,
1314
installed_apps: Apps,
1415
templatetags: TemplateTags,
@@ -20,54 +21,67 @@ struct DjangoSetup {
2021
templatetags: TemplateTags,
2122
}
2223

23-
impl ScriptRunner for DjangoSetup {
24-
const SCRIPT: &'static str = scripts::DJANGO_SETUP;
24+
impl DjangoSetup {
25+
pub fn setup(python: &mut PythonProcess) -> Result<JsonResponse, ProjectError> {
26+
let response = python.send("django_setup", None)?;
27+
let response = parse_json_response(response)?;
28+
Ok(response)
29+
}
2530
}
2631

2732
impl DjangoProject {
2833
fn new(
2934
py: Python,
35+
python: PythonProcess,
3036
settings_module: String,
3137
installed_apps: Apps,
3238
templatetags: TemplateTags,
3339
) -> Self {
3440
Self {
3541
py,
42+
python,
3643
settings_module,
3744
installed_apps,
3845
templatetags,
3946
}
4047
}
4148

42-
pub fn setup() -> Result<Self, ProjectError> {
49+
pub fn setup(mut python: PythonProcess) -> Result<Self, ProjectError> {
4350
let settings_module =
4451
std::env::var("DJANGO_SETTINGS_MODULE").expect("DJANGO_SETTINGS_MODULE must be set");
4552

46-
let py = Python::initialize()?;
53+
let py = Python::setup(&mut python)?;
4754

48-
let has_django = ImportCheck::check(&py, "django")?;
55+
let has_django = ImportCheck::check(&mut python, Some(vec!["django".to_string()]))?;
4956

5057
if !has_django {
5158
return Err(ProjectError::DjangoNotFound);
5259
}
5360

54-
if !check_gis_setup(&py)? {
61+
if !check_gis_setup(&mut python)? {
5562
eprintln!("Warning: GeoDjango detected but GDAL is not available.");
5663
eprintln!("Django initialization will be skipped. Some features may be limited.");
5764
eprintln!("To enable full functionality, please install GDAL and other GeoDjango prerequisites.");
5865

5966
return Ok(Self {
6067
py,
68+
python,
6169
settings_module,
6270
installed_apps: Apps::default(),
6371
templatetags: TemplateTags::default(),
6472
});
6573
}
6674

67-
let setup = DjangoSetup::run_with_py(&py)?;
75+
let response = DjangoSetup::setup(&mut python)?;
76+
let setup: DjangoSetup = response
77+
.data()
78+
.clone()
79+
.ok_or_else(|| TransportError::Process("No data in response".to_string()))
80+
.and_then(|data| serde_json::from_value(data).map_err(TransportError::Json))?;
6881

6982
Ok(Self::new(
7083
py,
84+
python,
7185
settings_module,
7286
Apps::from_strings(setup.installed_apps.to_vec()),
7387
setup.templatetags,
@@ -110,8 +124,11 @@ pub enum ProjectError {
110124
Json(#[from] serde_json::Error),
111125

112126
#[error(transparent)]
113-
Python(#[from] djls_python::PythonError),
127+
Packaging(#[from] djls_python::PackagingError),
114128

115129
#[error(transparent)]
116-
Runner(#[from] RunnerError),
130+
Python(#[from] djls_python::PythonError),
131+
132+
#[error("Transport error: {0}")]
133+
Transport(#[from] TransportError),
117134
}

crates/djls-django/src/gis.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use crate::apps::Apps;
2-
use djls_python::{Python, RunnerError};
2+
use djls_ipc::{PythonProcess, TransportError};
33
use std::process::Command;
44

5-
pub fn check_gis_setup(py: &Python) -> Result<bool, GISError> {
6-
let has_geodjango = Apps::check_installed(py, "django.contrib.gis")?;
5+
pub fn check_gis_setup(python: &mut PythonProcess) -> Result<bool, GISError> {
6+
let has_geodjango = Apps::check_installed(python, "django.contrib.gis")?;
77
let gdal_is_installed = Command::new("gdalinfo")
88
.arg("--version")
99
.output()
@@ -21,6 +21,6 @@ pub enum GISError {
2121
#[error("JSON parsing error: {0}")]
2222
Json(#[from] serde_json::Error),
2323

24-
#[error(transparent)]
25-
Runner(#[from] RunnerError),
24+
#[error("Transport error: {0}")]
25+
Transport(#[from] TransportError),
2626
}

crates/djls-django/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
mod apps;
22
mod django;
33
mod gis;
4-
mod scripts;
54
mod templates;
65

76
pub use django::DjangoProject;

crates/djls-django/src/scripts.rs

Lines changed: 0 additions & 4 deletions
This file was deleted.

crates/djls-ipc/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ anyhow = { workspace = true }
88
async-trait = { workspace = true }
99
serde = { workspace = true }
1010
serde_json = { workspace = true }
11+
thiserror = { workspace = true }
1112
tokio = { workspace = true }
1213

1314
tempfile = "3.14.0"

crates/djls-ipc/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
mod client;
2+
mod process;
23
mod server;
4+
mod transport;
35

46
pub use client::Client;
7+
pub use process::PythonProcess;
58
pub use server::Server;
9+
pub use transport::parse_json_response;
10+
pub use transport::parse_raw_response;
11+
pub use transport::JsonResponse;
12+
pub use transport::Transport;
13+
pub use transport::TransportError;

0 commit comments

Comments
 (0)