Skip to content

Commit 26cd151

Browse files
move LSP serve method to main cli crate and fix shutdown handling (#143)
1 parent d55ca65 commit 26cd151

File tree

8 files changed

+178
-49
lines changed

8 files changed

+178
-49
lines changed

crates/djls-server/src/lib.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,4 @@ mod queue;
33
mod server;
44
mod workspace;
55

6-
use crate::server::DjangoLanguageServer;
7-
use anyhow::Result;
8-
use tower_lsp_server::{LspService, Server};
9-
10-
pub async fn serve() -> Result<()> {
11-
let stdin = tokio::io::stdin();
12-
let stdout = tokio::io::stdout();
13-
14-
let (service, socket) = LspService::build(DjangoLanguageServer::new).finish();
15-
16-
Server::new(stdin, stdout, socket).serve(service).await;
17-
18-
Ok(())
19-
}
6+
pub use crate::server::DjangoLanguageServer;

crates/djls-server/src/queue.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ impl Queue {
105105
}
106106
}
107107
}
108-
eprintln!("Queue worker task shutting down.");
108+
eprintln!("Queue worker task shutting down");
109109
});
110110

111111
Self {
@@ -179,7 +179,7 @@ impl Drop for QueueInner {
179179
// `.ok()` ignores the result, as the receiver might have already
180180
// terminated if the channel closed naturally or panicked.
181181
sender.send(()).ok();
182-
eprintln!("Sent shutdown signal to queue worker.");
182+
eprintln!("Sent shutdown signal to queue worker");
183183
}
184184
}
185185
}

crates/djls/src/cli.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use crate::args::Args;
22
use crate::commands::{Command, DjlsCommand};
3+
use crate::exit::Exit;
34
use anyhow::Result;
45
use clap::Parser;
5-
use std::process::ExitCode;
66

77
/// Main CLI structure that defines the command-line interface
88
#[derive(Parser)]
@@ -16,13 +16,24 @@ pub struct Cli {
1616
pub args: Args,
1717
}
1818

19-
/// Parse CLI arguments and execute the chosen command
20-
pub async fn run(args: Vec<String>) -> Result<ExitCode> {
19+
/// Parse CLI arguments, execute the chosen command, and handle results
20+
pub fn run(args: Vec<String>) -> Result<()> {
2121
let cli = Cli::try_parse_from(args).unwrap_or_else(|e| {
2222
e.exit();
2323
});
2424

25-
match &cli.command {
26-
DjlsCommand::Serve(cmd) => cmd.execute(&cli.args).await,
25+
let result = match &cli.command {
26+
DjlsCommand::Serve(cmd) => cmd.execute(&cli.args),
27+
};
28+
29+
match result {
30+
Ok(exit) => exit.process_exit(),
31+
Err(e) => {
32+
let mut msg = e.to_string();
33+
if let Some(source) = e.source() {
34+
msg += &format!(", caused by {}", source);
35+
}
36+
Exit::error().with_message(msg).process_exit()
37+
}
2738
}
2839
}

crates/djls/src/commands.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
mod serve;
22

33
use crate::args::Args;
4+
use crate::exit::Exit;
45
use anyhow::Result;
56
use clap::Subcommand;
6-
use std::process::ExitCode;
77

88
pub trait Command {
9-
async fn execute(&self, args: &Args) -> Result<ExitCode>;
9+
fn execute(&self, args: &Args) -> Result<Exit>;
1010
}
1111

1212
#[derive(Debug, Subcommand)]

crates/djls/src/commands/serve.rs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use crate::args::Args;
22
use crate::commands::Command;
3+
use crate::exit::Exit;
34
use anyhow::Result;
45
use clap::{Parser, ValueEnum};
5-
use std::process::ExitCode;
6+
use djls_server::DjangoLanguageServer;
7+
use tower_lsp_server::{LspService, Server};
68

79
#[derive(Debug, Parser)]
810
pub struct Serve {
@@ -17,8 +19,29 @@ enum ConnectionType {
1719
}
1820

1921
impl Command for Serve {
20-
async fn execute(&self, _args: &Args) -> Result<ExitCode> {
21-
djls_server::serve().await?;
22-
Ok(ExitCode::SUCCESS)
22+
fn execute(&self, _args: &Args) -> Result<Exit> {
23+
let runtime = tokio::runtime::Builder::new_multi_thread()
24+
.enable_all()
25+
.build()
26+
.unwrap();
27+
28+
let exit_status = runtime.block_on(async {
29+
let stdin = tokio::io::stdin();
30+
let stdout = tokio::io::stdout();
31+
32+
let (service, socket) = LspService::build(DjangoLanguageServer::new).finish();
33+
34+
Server::new(stdin, stdout, socket).serve(service).await;
35+
36+
// Exit here instead of returning control to the `Cli`, for ... reasons?
37+
// If we don't exit here, ~~~ something ~~~ goes on with PyO3 (I assume)
38+
// or the Python entrypoint wrapper to indefinitely hang the CLI and keep
39+
// the process running
40+
Exit::success()
41+
.with_message("Server completed successfully")
42+
.process_exit()
43+
});
44+
45+
Ok(exit_status)
2346
}
2447
}

crates/djls/src/exit.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use anyhow::Result;
2+
use std::error::Error;
3+
use std::fmt;
4+
5+
type ExitMessage = Option<String>;
6+
7+
#[derive(Debug)]
8+
pub enum ExitStatus {
9+
Success,
10+
Error,
11+
}
12+
13+
impl ExitStatus {
14+
pub fn as_raw(&self) -> i32 {
15+
match self {
16+
ExitStatus::Success => 0,
17+
ExitStatus::Error => 1,
18+
}
19+
}
20+
21+
pub fn as_str(&self) -> &str {
22+
match self {
23+
ExitStatus::Success => "Command succeeded",
24+
ExitStatus::Error => "Command error",
25+
}
26+
}
27+
}
28+
29+
impl From<ExitStatus> for i32 {
30+
fn from(status: ExitStatus) -> Self {
31+
status.as_raw()
32+
}
33+
}
34+
35+
impl fmt::Display for ExitStatus {
36+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37+
let msg = self.as_str();
38+
write!(f, "{}", msg)
39+
}
40+
}
41+
42+
#[derive(Debug)]
43+
pub struct Exit {
44+
status: ExitStatus,
45+
message: ExitMessage,
46+
}
47+
48+
impl Exit {
49+
fn new(status: ExitStatus) -> Self {
50+
Self {
51+
status,
52+
message: None,
53+
}
54+
}
55+
56+
pub fn success() -> Self {
57+
Self::new(ExitStatus::Success)
58+
}
59+
60+
pub fn error() -> Self {
61+
Self::new(ExitStatus::Error)
62+
}
63+
64+
pub fn with_message<S: Into<String>>(mut self, message: S) -> Self {
65+
self.message = Some(message.into());
66+
self
67+
}
68+
69+
pub fn process_exit(self) -> ! {
70+
if let Some(message) = self.message {
71+
println!("{}", message)
72+
}
73+
std::process::exit(self.status.as_raw())
74+
}
75+
76+
#[allow(dead_code)]
77+
pub fn ok(self) -> Result<()> {
78+
match self.status {
79+
ExitStatus::Success => Ok(()),
80+
_ => Err(self.into()),
81+
}
82+
}
83+
84+
#[allow(dead_code)]
85+
pub fn as_raw(&self) -> i32 {
86+
self.status.as_raw()
87+
}
88+
}
89+
90+
impl fmt::Display for Exit {
91+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92+
let status_str = self.status.as_str();
93+
match &self.message {
94+
Some(msg) => write!(f, "{}: {}", status_str, msg),
95+
None => write!(f, "{}", status_str),
96+
}
97+
}
98+
}
99+
100+
impl Error for Exit {}

crates/djls/src/lib.rs

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,29 @@
1+
/// PyO3 entrypoint for the Django Language Server CLI.
2+
///
3+
/// This module provides a Python interface using PyO3 to solve Python runtime
4+
/// interpreter linking issues. The PyO3 approach avoids complexities with
5+
/// static/dynamic linking when building binaries that interact with Python.
16
mod args;
27
mod cli;
38
mod commands;
9+
mod exit;
410

511
use pyo3::prelude::*;
612
use std::env;
7-
use std::process::ExitCode;
813

914
#[pyfunction]
15+
/// Entry point called by Python when the CLI is invoked.
16+
/// This function handles argument parsing from Python and routes to the Rust CLI logic.
1017
fn entrypoint(_py: Python) -> PyResult<()> {
1118
// Skip python interpreter and script path, add command name
1219
let args: Vec<String> = std::iter::once("djls".to_string())
1320
.chain(env::args().skip(2))
1421
.collect();
1522

16-
let runtime = tokio::runtime::Builder::new_multi_thread()
17-
.enable_all()
18-
.build()
19-
.unwrap();
23+
// Run the CLI with the adjusted args
24+
cli::run(args).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
2025

21-
let result = runtime.block_on(cli::run(args));
22-
23-
match result {
24-
Ok(code) => {
25-
if code != ExitCode::SUCCESS {
26-
std::process::exit(1);
27-
}
28-
Ok(())
29-
}
30-
Err(e) => {
31-
eprintln!("Error: {}", e);
32-
if let Some(source) = e.source() {
33-
eprintln!("Caused by: {}", source);
34-
}
35-
std::process::exit(1);
36-
}
37-
}
26+
Ok(())
3827
}
3928

4029
#[pymodule]

crates/djls/src/main.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// Binary interface for local development only.
2+
///
3+
/// This binary exists for development and testing with `cargo run`.
4+
/// The production CLI is distributed through the PyO3 interface in lib.rs.
5+
mod args;
6+
mod cli;
7+
mod commands;
8+
mod exit;
9+
10+
use anyhow::Result;
11+
12+
/// Process CLI args and run the appropriate command.
13+
fn main() -> Result<()> {
14+
// Get command line arguments
15+
let args: Vec<String> = std::env::args().collect();
16+
17+
// Call the unified CLI run function
18+
cli::run(args)
19+
}

0 commit comments

Comments
 (0)