Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 36 additions & 13 deletions frontend/src-tauri/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CommandEvent>) {
Expand Down Expand Up @@ -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<Self> {
let resource_root = app
.path()
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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;
}
_ => {}
}
}
Expand Down
64 changes: 61 additions & 3 deletions python/valuecell/server/main.py
Original file line number Diff line number Diff line change
@@ -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

import uvicorn
from loguru import logger

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__":
Expand Down