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
39 changes: 9 additions & 30 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions crates/relay/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
] }
26 changes: 20 additions & 6 deletions crates/relay/src/cli/launch.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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));
}
}
}

Expand Down
22 changes: 13 additions & 9 deletions crates/relay/src/cli/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down
36 changes: 27 additions & 9 deletions crates/relay/src/cli/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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();
Expand Down
1 change: 1 addition & 0 deletions crates/relay/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod cli;
mod control;
mod db;
mod mcp;
mod proc;
mod protocol;
mod spawn;
mod state;
Expand Down
65 changes: 65 additions & 0 deletions crates/relay/src/proc.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
4 changes: 1 addition & 3 deletions crates/relay/src/spawn/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading