From b7c7bedb24f6efc790c8b944bfefeb50798d4e0c Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Thu, 27 Nov 2025 16:48:07 +0800 Subject: [PATCH 1/2] feat(backend): implement graceful shutdown for backend processes --- frontend/src-tauri/src/backend.rs | 49 ++++++++++++++++------- python/valuecell/server/main.py | 64 +++++++++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 16 deletions(-) diff --git a/frontend/src-tauri/src/backend.rs b/frontend/src-tauri/src/backend.rs index 1d54d9e45..4e5752f87 100644 --- a/frontend/src-tauri/src/backend.rs +++ b/frontend/src-tauri/src/backend.rs @@ -22,6 +22,8 @@ pub struct BackendManager { } const MAIN_MODULE: &str = "valuecell.server.main"; +const EXIT_COMMAND: &[u8] = b"__EXIT__\n"; +const GRACEFUL_TIMEOUT_SECS: u64 = 3; impl BackendManager { fn wait_until_terminated(mut rx: Receiver) { @@ -108,6 +110,31 @@ impl BackendManager { .context("Failed to spawn backend process") } + fn request_graceful_then_kill(&self, mut process: CommandChild) { + let pid = process.pid(); + log::info!("Requesting graceful shutdown for process {}", pid); + + if let Err(err) = process.write(EXIT_COMMAND) { + log::warn!( + "Failed to send shutdown command to process {}: {}", + pid, err + ); + } else { + log::info!("Exit command written to process {}", pid); + } + + std::thread::sleep(Duration::from_secs(GRACEFUL_TIMEOUT_SECS)); + + log::info!("Sending forceful shutdown to process {}", pid); + self.kill_descendants_best_effort(pid); + + if let Err(err) = process.kill() { + log::error!("Failed to kill process {}: {}", pid, err); + } else { + log::info!("Force kill signal sent to process {}", pid); + } + } + pub fn new(app: AppHandle) -> Result { let resource_root = app .path() @@ -175,18 +202,7 @@ impl BackendManager { pub fn stop_all(&self) { let mut processes = self.processes.lock().unwrap(); for process in processes.drain(..) { - let pid = process.pid(); - log::info!("Terminating process {}", pid); - - // Attempt to terminate any descendants spawned under this process BEFORE killing the parent - self.kill_descendants_best_effort(pid); - - // Use CommandChild's kill method - if let Err(e) = process.kill() { - log::error!("Failed to kill process {}: {}", pid, e); - } else { - log::info!("Process {} terminated", pid); - } + self.request_graceful_then_kill(process); } } @@ -217,7 +233,14 @@ impl BackendManager { log::error!("Backend process error: {}", err); break; } - CommandEvent::Terminated(_) => break, + CommandEvent::Terminated(payload) => { + log::info!( + "Backend process terminated (code: {:?}, signal: {:?})", + payload.code, + payload.signal + ); + break; + } _ => {} } } diff --git a/python/valuecell/server/main.py b/python/valuecell/server/main.py index 54f4b802c..ae06be910 100644 --- a/python/valuecell/server/main.py +++ b/python/valuecell/server/main.py @@ -1,28 +1,86 @@ """Main entry point for ValueCell Server Backend.""" +from __future__ import annotations + import io import sys +import threading +from typing import Callable, Optional, TextIO +from loguru import logger import uvicorn from valuecell.server.api.app import create_app from valuecell.server.config.settings import get_settings +EXIT_COMMAND: str = "__EXIT__" + # Set stdout encoding to utf-8 sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") + # Create app instance for uvicorn app = create_app() -def main(): - """Start the server.""" +def control_loop( + request_stop: Callable[[], None], + command_stream: Optional[TextIO] = None, +) -> None: + """Listen for control commands on stdin and request shutdown when needed.""" + + stream = command_stream if command_stream is not None else sys.stdin + for raw_line in stream: + command = raw_line.strip() + if command == EXIT_COMMAND: + logger.info("Received shutdown request via control channel") + request_stop() + return + if command: + logger.debug("Ignoring unknown control command: {}", command) + + logger.debug("Control channel closed; requesting shutdown") + request_stop() + + +def main() -> None: + """Start the server and coordinate graceful shutdown via stdin control.""" + settings = get_settings() - uvicorn.run( + config = uvicorn.Config( app, host=settings.API_HOST, port=settings.API_PORT, + log_level="debug" if settings.API_DEBUG else "info", ) + server = uvicorn.Server(config) + server.install_signal_handlers = False + + stop_event = threading.Event() + + def request_stop() -> None: + if stop_event.is_set(): + return + + stop_event.set() + server.should_exit = True + logger.info("Shutdown signal propagated to uvicorn") + + control_thread = threading.Thread( + target=control_loop, + name="stdin-control", + args=(request_stop,), + daemon=True, + ) + control_thread.start() + + try: + server.run() + except KeyboardInterrupt: + logger.info("KeyboardInterrupt caught; requesting shutdown") + request_stop() + finally: + request_stop() if __name__ == "__main__": From 22dda70c3da8de0342e2a533c2e57f509c9cd299 Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Thu, 27 Nov 2025 17:10:04 +0800 Subject: [PATCH 2/2] make format --- python/valuecell/server/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/valuecell/server/main.py b/python/valuecell/server/main.py index ae06be910..c009f275f 100644 --- a/python/valuecell/server/main.py +++ b/python/valuecell/server/main.py @@ -7,8 +7,8 @@ import threading from typing import Callable, Optional, TextIO -from loguru import logger import uvicorn +from loguru import logger from valuecell.server.api.app import create_app from valuecell.server.config.settings import get_settings