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:
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user