deskctl/src/cli/connection.rs
Harivansh Rathi e392ba1055 Rewrite X11 backend to drop xcap
Use x11rb directly for screenshot capture and window metadata so the Linux build no longer drags in Wayland build dependencies.

Co-authored-by: Codex <noreply@openai.com>
2026-03-25 12:28:23 -04:00

162 lines
4.5 KiB
Rust

use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;
use anyhow::{bail, Context, Result};
use crate::cli::GlobalOpts;
use crate::core::protocol::{Request, Response};
fn socket_dir() -> PathBuf {
if let Ok(dir) = std::env::var("DESKCTL_SOCKET_DIR") {
return PathBuf::from(dir);
}
if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") {
return PathBuf::from(runtime).join("deskctl");
}
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join(".deskctl")
}
fn socket_path(opts: &GlobalOpts) -> PathBuf {
if let Some(ref path) = opts.socket {
return path.clone();
}
socket_dir().join(format!("{}.sock", opts.session))
}
fn pid_path(opts: &GlobalOpts) -> PathBuf {
socket_dir().join(format!("{}.pid", opts.session))
}
fn try_connect(opts: &GlobalOpts) -> Option<UnixStream> {
UnixStream::connect(socket_path(opts)).ok()
}
fn spawn_daemon(opts: &GlobalOpts) -> Result<()> {
let exe = std::env::current_exe().context("Failed to determine executable path")?;
let sock_dir = socket_dir();
std::fs::create_dir_all(&sock_dir).context("Failed to create socket directory")?;
let mut cmd = Command::new(exe);
cmd.env("DESKCTL_DAEMON", "1")
.env("DESKCTL_SESSION", &opts.session)
.env("DESKCTL_SOCKET_PATH", socket_path(opts))
.env("DESKCTL_PID_PATH", pid_path(opts))
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped());
// Detach the daemon process on Unix
unsafe {
cmd.pre_exec(|| {
libc::setsid();
Ok(())
});
}
cmd.spawn().context("Failed to spawn daemon")?;
Ok(())
}
fn ensure_daemon(opts: &GlobalOpts) -> Result<UnixStream> {
// Try connecting first
if let Some(stream) = try_connect(opts) {
return Ok(stream);
}
// Spawn daemon
spawn_daemon(opts)?;
// Retry with backoff
let max_retries = 20;
let base_delay = Duration::from_millis(50);
for i in 0..max_retries {
thread::sleep(base_delay * (i + 1).min(4));
if let Some(stream) = try_connect(opts) {
return Ok(stream);
}
}
bail!(
"Failed to connect to daemon after {} retries.\n\
Socket path: {}",
max_retries,
socket_path(opts).display()
);
}
pub fn send_command(opts: &GlobalOpts, request: &Request) -> Result<Response> {
let mut stream = ensure_daemon(opts)?;
stream.set_read_timeout(Some(Duration::from_secs(30)))?;
stream.set_write_timeout(Some(Duration::from_secs(5)))?;
// Send NDJSON request
let json = serde_json::to_string(request)?;
writeln!(stream, "{json}")?;
stream.flush()?;
// Read NDJSON response
let mut reader = BufReader::new(&stream);
let mut line = String::new();
reader.read_line(&mut line)?;
let response: Response =
serde_json::from_str(line.trim()).context("Failed to parse daemon response")?;
Ok(response)
}
pub fn start_daemon(opts: &GlobalOpts) -> Result<()> {
if try_connect(opts).is_some() {
println!("Daemon already running ({})", socket_path(opts).display());
return Ok(());
}
spawn_daemon(opts)?;
// Wait briefly and verify
thread::sleep(Duration::from_millis(200));
if try_connect(opts).is_some() {
println!("Daemon started ({})", socket_path(opts).display());
} else {
bail!("Daemon failed to start");
}
Ok(())
}
pub fn stop_daemon(opts: &GlobalOpts) -> Result<()> {
match try_connect(opts) {
Some(mut stream) => {
let req = Request::new("shutdown");
let json = serde_json::to_string(&req)?;
writeln!(stream, "{json}")?;
stream.flush()?;
println!("Daemon stopped");
}
None => {
// Try to clean up stale socket
let path = socket_path(opts);
if path.exists() {
std::fs::remove_file(&path)?;
println!("Removed stale socket: {}", path.display());
} else {
println!("Daemon not running");
}
}
}
Ok(())
}
pub fn daemon_status(opts: &GlobalOpts) -> Result<()> {
if try_connect(opts).is_some() {
println!("Daemon running ({})", socket_path(opts).display());
} else {
println!("Daemon not running");
}
Ok(())
}