feat(net): real IPC client + working remote shutdown

DaemonHandle's shutdown_tx (oneshot) is replaced with a CancellationToken
shared between the daemon loop and the IPC server. The token is the
single source of truth for "should we shut down" — `DaemonHandle::shutdown`
cancels it, and an IPC `Stop` request also cancels it.

- daemon/state: store the CancellationToken on DaemonHandle and clone it
  on Clone (so cached IPC handles can still trigger shutdown).
- daemon/ipc: IpcServer takes a daemon_shutdown token; `Stop` now cancels
  it instead of returning Ok and doing nothing. Add IpcClient with
  `request`, `status`, and `stop` methods so the CLI can drive a
  backgrounded daemon over the Unix socket.
- daemon/lifecycle: thread the token through run_daemon_loop and
  run_session, pass a clone to IpcServer::new.
- lib.rs: re-export IpcClient/IpcCommand/IpcResponse so callers don't
  have to reach into the daemon module.
- src/vpn_cmds.rs: `sunbeam disconnect` now actually talks to the daemon
  via IpcClient::stop, and `sunbeam vpn status` queries IpcClient::status
  and prints addresses + peer count + DERP home.
This commit is contained in:
2026-04-07 14:46:47 +01:00
parent a57246fd9f
commit 7019937f6f
6 changed files with 174 additions and 42 deletions

View File

@@ -108,33 +108,49 @@ pub async fn cmd_connect() -> Result<()> {
/// Run `sunbeam disconnect` — signal a running daemon via its IPC socket.
pub async fn cmd_disconnect() -> Result<()> {
let state_dir = vpn_state_dir()?;
let socket = state_dir.join("daemon.sock");
if !socket.exists() {
let socket = vpn_state_dir()?.join("daemon.sock");
let client = sunbeam_net::IpcClient::new(&socket);
if !client.socket_exists() {
return Err(SunbeamError::Other(
"no running VPN daemon (control socket missing)".into(),
));
}
// The daemon's IPC server lives in sunbeam_net::daemon::ipc, but it's
// not currently exported as a client. Until that lands, the canonical
// way to disconnect is to ^C the foreground `sunbeam connect` process.
Err(SunbeamError::Other(
"background daemon control not yet implemented — Ctrl-C the running `sunbeam connect`"
.into(),
))
step("Asking VPN daemon to stop...");
client
.stop()
.await
.map_err(|e| SunbeamError::Other(format!("IPC stop: {e}")))?;
ok("Daemon acknowledged shutdown.");
Ok(())
}
/// Run `sunbeam vpn status` — query a running daemon's status via IPC.
pub async fn cmd_vpn_status() -> Result<()> {
let state_dir = vpn_state_dir()?;
let socket = state_dir.join("daemon.sock");
if !socket.exists() {
let socket = vpn_state_dir()?.join("daemon.sock");
let client = sunbeam_net::IpcClient::new(&socket);
if !client.socket_exists() {
println!("VPN: not running");
return Ok(());
}
println!("VPN: running (control socket at {})", socket.display());
// TODO: actually query the IPC socket once the IPC client API is
// exposed from sunbeam-net.
match client.status().await {
Ok(sunbeam_net::DaemonStatus::Running { addresses, peer_count, derp_home }) => {
let addrs: Vec<String> = addresses.iter().map(|a| a.to_string()).collect();
println!("VPN: running");
println!(" addresses: {}", addrs.join(", "));
println!(" peers: {peer_count}");
if let Some(region) = derp_home {
println!(" derp home: region {region}");
}
}
Ok(other) => {
println!("VPN: {other}");
}
Err(e) => {
// Socket exists but daemon isn't actually responding — common
// when the daemon crashed and left a stale socket file behind.
println!("VPN: stale socket at {} ({e})", socket.display());
}
}
Ok(())
}