diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index a7a444e..eb7d859 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,15 +1,11 @@ name: Windows Build -# Phase 0 Windows de-risk spike. The point of this job is information, not a -# shippable artifact: it proves (or kills) the make-or-break dependency — gpui -# at our pinned zed rev, plus the guise component library on top of it — -# compiling on Windows, and confirms the platform-independent crates build -# cleanly. A final informational step compiles the whole workspace to log the -# remaining Windows porting surface (pty/terminal/app are still Unix-only) -# without failing the gate. +# Windows build gate. The whole workspace compiles on Windows: the terminal +# app (ConPTY backend) and the relay sidecar included. The gpui/guise probes +# run first as distinct steps so a failure in the make-or-break UI dependency +# is obvious at a glance, then the full workspace build gates every crate. # -# Every probe carries `if: always()` so one failure never hides the others: a -# single run collects gpui, guise, and full-workspace logs together. +# Each step carries `if: always()` so one failure never hides the others. on: workflow_dispatch: {} pull_request: @@ -45,25 +41,8 @@ jobs: if: always() run: cargo build -p guise-ui - # Platform-independent crates: pure logic, no OS calls. These must build. - - name: Build portable crates + # The whole workspace now builds on Windows — the terminal app (ConPTY + # backend) and the relay sidecar included. This gates every crate. + - name: Build workspace if: always() - run: > - cargo build - -p vt -p input -p workspace -p theme -p config - -p macros -p mcp -p cast -p plugin - - # The terminal application and its backend: pty (ConPTY), terminal, notes, - # and app itself all build on Windows. - - name: Build terminal app - if: always() - run: cargo build -p pty -p terminal -p notes -p app - - # Informational only: compile the whole workspace so the log captures the - # remaining Windows surface. `relay` (the standalone sidecar) is still - # Unix-only; `--keep-going` enumerates every failing crate in one run. - # Expected to fail on relay today; never gates. - - name: Full workspace surface (informational) - if: always() - continue-on-error: true - run: cargo build --workspace --keep-going + run: cargo build --workspace diff --git a/Cargo.lock b/Cargo.lock index 9626ab1..f3e263b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6903,6 +6903,7 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", + "windows 0.58.0", ] [[package]] diff --git a/crates/relay/Cargo.toml b/crates/relay/Cargo.toml index fbcc3b2..77b1e58 100644 --- a/crates/relay/Cargo.toml +++ b/crates/relay/Cargo.toml @@ -22,4 +22,12 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } anyhow = "1" clap = { version = "4", features = ["derive"] } + +[target.'cfg(unix)'.dependencies] libc = "0.2" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.58", features = [ + "Win32_Foundation", + "Win32_System_Threading", +] } diff --git a/crates/relay/src/cli/launch.rs b/crates/relay/src/cli/launch.rs index 338e2c0..a7a15fe 100644 --- a/crates/relay/src/cli/launch.rs +++ b/crates/relay/src/cli/launch.rs @@ -1,7 +1,6 @@ use super::{agent, http, paths, role, LaunchArgs}; use anyhow::{anyhow, Result}; use std::io::Write; -use std::os::unix::process::CommandExt; use std::process::Command; /// Launch an agent wired to the bus. Foreground takes over this terminal; @@ -101,11 +100,26 @@ pub async fn launch(a: LaunchArgs) -> Result<()> { } else { let label = if a.cmd.is_some() { "custom" } else { agent_name.as_str() }; println!("launching {label} as '{name}' on {endpoint} …"); - let err = Command::new(&built.program) - .args(&built.args) - .current_dir(&cwd) - .exec(); - Err(anyhow!("failed to exec {}: {err}", built.program)) + // Foreground: replace this process on Unix; on Windows, run it to + // completion and exit with its status (there is no exec()). + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + let err = Command::new(&built.program) + .args(&built.args) + .current_dir(&cwd) + .exec(); + Err(anyhow!("failed to exec {}: {err}", built.program)) + } + #[cfg(windows)] + { + let status = Command::new(&built.program) + .args(&built.args) + .current_dir(&cwd) + .status() + .map_err(|e| anyhow!("failed to run {}: {e}", built.program))?; + std::process::exit(status.code().unwrap_or(1)); + } } } diff --git a/crates/relay/src/cli/paths.rs b/crates/relay/src/cli/paths.rs index ba17c3f..9ee9fb2 100644 --- a/crates/relay/src/cli/paths.rs +++ b/crates/relay/src/cli/paths.rs @@ -44,15 +44,21 @@ pub fn abs_dir() -> PathBuf { pub fn ensure_dir() -> Result<()> { let d = dir(); std::fs::create_dir_all(&d)?; - use std::os::unix::fs::PermissionsExt; - let _ = std::fs::set_permissions(&d, std::fs::Permissions::from_mode(0o700)); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let _ = std::fs::set_permissions(&d, std::fs::Permissions::from_mode(0o700)); + } Ok(()) } /// Restrict a freshly written file in the state dir to owner read/write. -pub fn lock_file(path: &std::path::Path) { - use std::os::unix::fs::PermissionsExt; - let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)); +pub fn lock_file(_path: &std::path::Path) { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let _ = std::fs::set_permissions(_path, std::fs::Permissions::from_mode(0o600)); + } } pub fn info_path() -> PathBuf { @@ -108,7 +114,7 @@ pub fn endpoint(addr: &str) -> String { /// True if a process with this pid is alive. pub fn alive(pid: u32) -> bool { - unsafe { libc::kill(pid as i32, 0) == 0 } + crate::proc::alive(pid) } /// Pids of spawned workers, persisted so a fresh daemon can reap children left @@ -169,9 +175,7 @@ pub fn reap_stray_workers() { let me = std::process::id(); for pid in read_pids() { if pid != 0 && pid != me && alive(pid) { - unsafe { - libc::kill(pid as i32, libc::SIGKILL); - } + crate::proc::kill(pid); } } let _ = std::fs::remove_file(workers_pids_path()); diff --git a/crates/relay/src/cli/server.rs b/crates/relay/src/cli/server.rs index 00a1622..9a9e497 100644 --- a/crates/relay/src/cli/server.rs +++ b/crates/relay/src/cli/server.rs @@ -11,7 +11,6 @@ use axum::http::{header::AUTHORIZATION, StatusCode}; use axum::response::{IntoResponse, Response}; use axum::routing::{get, post}; use axum::Router; -use std::os::unix::process::CommandExt; use std::process::{Command, Stdio}; use std::time::Duration; @@ -145,11 +144,19 @@ fn constant_time_eq(a: &str, b: &str) -> bool { } async fn shutdown(app: App) { - use tokio::signal::unix::{signal, SignalKind}; - let mut term = signal(SignalKind::terminate()).expect("sigterm"); - tokio::select! { - _ = tokio::signal::ctrl_c() => {} - _ = term.recv() => {} + #[cfg(unix)] + { + use tokio::signal::unix::{signal, SignalKind}; + let mut term = signal(SignalKind::terminate()).expect("sigterm"); + tokio::select! { + _ = tokio::signal::ctrl_c() => {} + _ = term.recv() => {} + } + } + #[cfg(windows)] + { + // No SIGTERM on Windows; Ctrl-C (or a hard terminate) ends the daemon. + let _ = tokio::signal::ctrl_c().await; } tracing::info!("shutting down; stopping workers"); spawn::stop_all(&app).await; @@ -183,12 +190,25 @@ pub fn start(args: ServeArgs) -> Result<()> { .stdin(Stdio::null()) .stdout(Stdio::from(log)) .stderr(Stdio::from(errlog)); + // Detach into a new session/group so the daemon survives the launching + // shell exiting. + #[cfg(unix)] unsafe { + use std::os::unix::process::CommandExt; cmd.pre_exec(|| { libc::setsid(); Ok(()) }); } + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + // DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW. + const DETACHED_PROCESS: u32 = 0x0000_0008; + const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW); + } cmd.spawn()?; for _ in 0..40 { @@ -211,9 +231,7 @@ pub fn stop() -> Result<()> { println!("relay was not running (cleaned stale record)"); return Ok(()); } - unsafe { - libc::kill(info.pid as i32, libc::SIGTERM); - } + crate::proc::terminate(info.pid); for _ in 0..40 { if !paths::alive(info.pid) { paths::clear_info(); diff --git a/crates/relay/src/main.rs b/crates/relay/src/main.rs index f10e28e..f6d6c5e 100644 --- a/crates/relay/src/main.rs +++ b/crates/relay/src/main.rs @@ -3,6 +3,7 @@ mod cli; mod control; mod db; mod mcp; +mod proc; mod protocol; mod spawn; mod state; diff --git a/crates/relay/src/proc.rs b/crates/relay/src/proc.rs new file mode 100644 index 0000000..cae915d --- /dev/null +++ b/crates/relay/src/proc.rs @@ -0,0 +1,65 @@ +//! Cross-platform process control for the daemon manager: liveness checks and +//! termination by pid. Unix uses signals; Windows uses the process APIs — there +//! is no SIGTERM for a detached console daemon, so `terminate` is a hard +//! `TerminateProcess` there, same as `kill`. + +/// True if a process with this pid exists. +#[cfg(unix)] +pub fn alive(pid: u32) -> bool { + unsafe { libc::kill(pid as i32, 0) == 0 } +} + +/// Ask the process to stop (SIGTERM). +#[cfg(unix)] +pub fn terminate(pid: u32) { + unsafe { + libc::kill(pid as i32, libc::SIGTERM); + } +} + +/// Force-kill the process (SIGKILL). +#[cfg(unix)] +pub fn kill(pid: u32) { + unsafe { + libc::kill(pid as i32, libc::SIGKILL); + } +} + +#[cfg(windows)] +use windows::Win32::Foundation::CloseHandle; +#[cfg(windows)] +use windows::Win32::System::Threading::{ + OpenProcess, TerminateProcess, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_TERMINATE, +}; + +/// True if a handle can be opened for this pid (the process exists). +#[cfg(windows)] +pub fn alive(pid: u32) -> bool { + unsafe { + match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) { + Ok(h) => { + let _ = CloseHandle(h); + true + } + Err(_) => false, + } + } +} + +/// Terminate the process. No SIGTERM equivalent for a detached console daemon, +/// so this hard-terminates (same as [`kill`]). +#[cfg(windows)] +pub fn terminate(pid: u32) { + kill(pid); +} + +/// Force-kill the process. +#[cfg(windows)] +pub fn kill(pid: u32) { + unsafe { + if let Ok(h) = OpenProcess(PROCESS_TERMINATE, false, pid) { + let _ = TerminateProcess(h, 1); + let _ = CloseHandle(h); + } + } +} diff --git a/crates/relay/src/spawn/mod.rs b/crates/relay/src/spawn/mod.rs index dceaef3..dccac28 100644 --- a/crates/relay/src/spawn/mod.rs +++ b/crates/relay/src/spawn/mod.rs @@ -211,9 +211,7 @@ pub async fn stop(app: &App, name: &str) -> bool { w.stop.store(true, Ordering::SeqCst); let pid = w.pid.load(Ordering::SeqCst); if pid != 0 { - unsafe { - libc::kill(pid as i32, libc::SIGTERM); - } + crate::proc::terminate(pid); } true }