mirror of
https://github.com/harivansh-afk/deskctl.git
synced 2026-04-18 05:01:56 +00:00
Phase 1: project scaffold, clap CLI, self-re-exec daemon, NDJSON IPC
- Cargo.toml with clap, tokio, serde, anyhow dependencies - Entry point with env-var routing to daemon or CLI mode - Core protocol types (Request/Response NDJSON wire format) - Session detection (X11 check with DISPLAY/XDG_SESSION_TYPE) - RefMap with @wN selector resolution (direct, prefix, substring) - Snapshot/WindowInfo shared types with Display impl - clap derive CLI with all subcommands (snapshot, click, type, etc.) - Client connection: socket path resolution, daemon auto-start via self-re-exec, NDJSON send/receive with retry backoff - Tokio async daemon: Unix socket listener, accept loop, graceful shutdown via notify - DaemonState holding session info and ref map - Placeholder handler returning hardcoded snapshot response
This commit is contained in:
parent
17d4a1edeb
commit
dfaa339594
13 changed files with 1735 additions and 0 deletions
31
src/daemon/handler.rs
Normal file
31
src/daemon/handler.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::core::protocol::{Request, Response};
|
||||
use super::state::DaemonState;
|
||||
|
||||
pub async fn handle_request(
|
||||
request: &Request,
|
||||
_state: &Arc<Mutex<DaemonState>>,
|
||||
) -> Response {
|
||||
match request.action.as_str() {
|
||||
"snapshot" => {
|
||||
Response::ok(serde_json::json!({
|
||||
"screenshot": "/tmp/desktop-ctl-placeholder.png",
|
||||
"windows": [
|
||||
{
|
||||
"ref_id": "w1",
|
||||
"xcb_id": 0,
|
||||
"title": "Placeholder Window",
|
||||
"app_name": "placeholder",
|
||||
"x": 0, "y": 0, "width": 1920, "height": 1080,
|
||||
"focused": true, "minimized": false
|
||||
}
|
||||
]
|
||||
}))
|
||||
}
|
||||
action => {
|
||||
Response::err(format!("Unknown action: {action}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
127
src/daemon/mod.rs
Normal file
127
src/daemon/mod.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
mod handler;
|
||||
mod state;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::UnixListener;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::core::session;
|
||||
use state::DaemonState;
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
// Validate session before starting
|
||||
session::detect_session()?;
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
|
||||
runtime.block_on(async_run())
|
||||
}
|
||||
|
||||
async fn async_run() -> Result<()> {
|
||||
let socket_path = std::env::var("DESKTOP_CTL_SOCKET_PATH")
|
||||
.map(PathBuf::from)
|
||||
.context("DESKTOP_CTL_SOCKET_PATH not set")?;
|
||||
|
||||
let pid_path = std::env::var("DESKTOP_CTL_PID_PATH")
|
||||
.map(PathBuf::from)
|
||||
.ok();
|
||||
|
||||
// Clean up stale socket
|
||||
if socket_path.exists() {
|
||||
std::fs::remove_file(&socket_path)?;
|
||||
}
|
||||
|
||||
// Write PID file
|
||||
if let Some(ref pid_path) = pid_path {
|
||||
std::fs::write(pid_path, std::process::id().to_string())?;
|
||||
}
|
||||
|
||||
let listener = UnixListener::bind(&socket_path)
|
||||
.context(format!("Failed to bind socket: {}", socket_path.display()))?;
|
||||
|
||||
let session = std::env::var("DESKTOP_CTL_SESSION").unwrap_or_else(|_| "default".to_string());
|
||||
let state = Arc::new(Mutex::new(DaemonState::new(session, socket_path.clone())));
|
||||
|
||||
let shutdown = Arc::new(tokio::sync::Notify::new());
|
||||
let shutdown_clone = shutdown.clone();
|
||||
|
||||
// Accept loop
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = listener.accept() => {
|
||||
match result {
|
||||
Ok((stream, _addr)) => {
|
||||
let state = state.clone();
|
||||
let shutdown = shutdown.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_connection(stream, state, shutdown).await {
|
||||
eprintln!("Connection error: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Accept error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = shutdown_clone.notified() => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if socket_path.exists() {
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
}
|
||||
if let Some(ref pid_path) = pid_path {
|
||||
let _ = std::fs::remove_file(pid_path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
stream: tokio::net::UnixStream,
|
||||
state: Arc<Mutex<DaemonState>>,
|
||||
shutdown: Arc<tokio::sync::Notify>,
|
||||
) -> Result<()> {
|
||||
let (reader, mut writer) = stream.into_split();
|
||||
let mut reader = BufReader::new(reader);
|
||||
let mut line = String::new();
|
||||
|
||||
reader.read_line(&mut line).await?;
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let request: crate::core::protocol::Request = serde_json::from_str(line)?;
|
||||
|
||||
// Handle shutdown specially - notify before writing so the accept loop
|
||||
// exits even if the client has already closed the connection.
|
||||
if request.action == "shutdown" {
|
||||
shutdown.notify_one();
|
||||
let response = crate::core::protocol::Response::ok(
|
||||
serde_json::json!({"message": "Shutting down"})
|
||||
);
|
||||
let json = serde_json::to_string(&response)?;
|
||||
// Ignore write errors - client may have already closed the connection.
|
||||
let _ = writer.write_all(format!("{json}\n").as_bytes()).await;
|
||||
let _ = writer.flush().await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let response = handler::handle_request(&request, &state).await;
|
||||
let json = serde_json::to_string(&response)?;
|
||||
writer.write_all(format!("{json}\n").as_bytes()).await?;
|
||||
writer.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
19
src/daemon/state.rs
Normal file
19
src/daemon/state.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use std::path::PathBuf;
|
||||
use crate::core::refs::RefMap;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct DaemonState {
|
||||
pub session: String,
|
||||
pub socket_path: PathBuf,
|
||||
pub ref_map: RefMap,
|
||||
}
|
||||
|
||||
impl DaemonState {
|
||||
pub fn new(session: String, socket_path: PathBuf) -> Self {
|
||||
Self {
|
||||
session,
|
||||
socket_path,
|
||||
ref_map: RefMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue