mirror of
https://github.com/harivansh-afk/deskctl.git
synced 2026-04-17 14:01:22 +00:00
clean out src, move mod into lib, remove trash
This commit is contained in:
parent
d0041589b3
commit
68b68a3c82
8 changed files with 51 additions and 267 deletions
|
|
@ -6,10 +6,10 @@ use std::process::{Command, Stdio};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{bail, Context, Result};
|
||||||
|
|
||||||
use crate::cli::GlobalOpts;
|
use crate::cli::GlobalOpts;
|
||||||
use crate::core::doctor::{DoctorReport, run as run_doctor_report};
|
use crate::core::doctor::{run as run_doctor_report, DoctorReport};
|
||||||
use crate::core::paths::{pid_path_for_session, socket_dir, socket_path_for_session};
|
use crate::core::paths::{pid_path_for_session, socket_dir, socket_path_for_session};
|
||||||
use crate::core::protocol::{Request, Response};
|
use crate::core::protocol::{Request, Response};
|
||||||
|
|
||||||
|
|
@ -95,7 +95,8 @@ fn send_request_over_stream(mut stream: UnixStream, request: &Request) -> Result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ping_daemon(opts: &GlobalOpts) -> Result<()> {
|
fn ping_daemon(opts: &GlobalOpts) -> Result<()> {
|
||||||
let response = send_request_over_stream(connect_socket(&socket_path(opts))?, &Request::new("ping"))?;
|
let response =
|
||||||
|
send_request_over_stream(connect_socket(&socket_path(opts))?, &Request::new("ping"))?;
|
||||||
if response.success {
|
if response.success {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -212,7 +213,9 @@ pub fn daemon_status(opts: &GlobalOpts) -> Result<()> {
|
||||||
let path = socket_path(opts);
|
let path = socket_path(opts);
|
||||||
match ping_daemon(opts) {
|
match ping_daemon(opts) {
|
||||||
Ok(()) => println!("Daemon running ({})", path.display()),
|
Ok(()) => println!("Daemon running ({})", path.display()),
|
||||||
Err(_) if path.exists() => println!("Daemon socket exists but is unhealthy ({})", path.display()),
|
Err(_) if path.exists() => {
|
||||||
|
println!("Daemon socket exists but is unhealthy ({})", path.display())
|
||||||
|
}
|
||||||
Err(_) => println!("Daemon not running"),
|
Err(_) => println!("Daemon not running"),
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -226,7 +229,11 @@ fn print_doctor_report(report: &DoctorReport, json_output: bool) -> Result<()> {
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"deskctl doctor: {}",
|
"deskctl doctor: {}",
|
||||||
if report.healthy { "healthy" } else { "issues found" }
|
if report.healthy {
|
||||||
|
"healthy"
|
||||||
|
} else {
|
||||||
|
"issues found"
|
||||||
|
}
|
||||||
);
|
);
|
||||||
for check in &report.checks {
|
for check in &report.checks {
|
||||||
let status = if check.ok { "OK" } else { "FAIL" };
|
let status = if check.ok { "OK" } else { "FAIL" };
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
mod connection;
|
pub mod connection;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
|
|
||||||
|
|
@ -84,13 +84,16 @@ pub fn run(socket_path: &Path) -> DoctorReport {
|
||||||
checks.push(match backend.capture_screenshot() {
|
checks.push(match backend.capture_screenshot() {
|
||||||
Ok(image) => check_ok(
|
Ok(image) => check_ok(
|
||||||
"screenshot",
|
"screenshot",
|
||||||
format!("Captured {}x{} desktop image", image.width(), image.height()),
|
format!(
|
||||||
|
"Captured {}x{} desktop image",
|
||||||
|
image.width(),
|
||||||
|
image.height()
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Err(error) => check_fail(
|
Err(error) => check_fail(
|
||||||
"screenshot",
|
"screenshot",
|
||||||
error.to_string(),
|
error.to_string(),
|
||||||
"Verify the X11 session permits desktop capture on the active display."
|
"Verify the X11 session permits desktop capture on the active display.".to_string(),
|
||||||
.to_string(),
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -117,7 +120,10 @@ fn check_socket_dir(socket_path: &Path) -> DoctorCheck {
|
||||||
let Some(socket_dir) = socket_path.parent() else {
|
let Some(socket_dir) = socket_path.parent() else {
|
||||||
return check_fail(
|
return check_fail(
|
||||||
"socket-dir",
|
"socket-dir",
|
||||||
format!("Socket path {} has no parent directory", socket_path.display()),
|
format!(
|
||||||
|
"Socket path {} has no parent directory",
|
||||||
|
socket_path.display()
|
||||||
|
),
|
||||||
"Use a socket path inside a writable directory.".to_string(),
|
"Use a socket path inside a writable directory.".to_string(),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -203,37 +209,3 @@ fn check_fail(name: &str, details: String, fix: String) -> DoctorCheck {
|
||||||
fix: Some(fix),
|
fix: Some(fix),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, target_os = "linux"))]
|
|
||||||
mod tests {
|
|
||||||
use super::run;
|
|
||||||
use crate::test_support::{X11TestEnv, env_lock};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn doctor_reports_healthy_x11_environment_under_xvfb() {
|
|
||||||
let _guard = env_lock().lock().unwrap();
|
|
||||||
let Some(env) = X11TestEnv::new().unwrap() else {
|
|
||||||
eprintln!("Skipping Xvfb-dependent doctor test");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
env.create_window("deskctl doctor test", "DeskctlDoctor").unwrap();
|
|
||||||
|
|
||||||
let socket_path = std::env::temp_dir().join("deskctl-doctor-test.sock");
|
|
||||||
let report = run(&socket_path);
|
|
||||||
|
|
||||||
assert!(report.checks.iter().any(|check| check.name == "display" && check.ok));
|
|
||||||
assert!(report.checks.iter().any(|check| check.name == "backend" && check.ok));
|
|
||||||
assert!(
|
|
||||||
report
|
|
||||||
.checks
|
|
||||||
.iter()
|
|
||||||
.any(|check| check.name == "window-enumeration" && check.ok)
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
report
|
|
||||||
.checks
|
|
||||||
.iter()
|
|
||||||
.any(|check| check.name == "screenshot" && check.ok)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -136,8 +136,12 @@ impl RefMap {
|
||||||
|
|
||||||
/// Resolve a selector to the center coordinates of the window.
|
/// Resolve a selector to the center coordinates of the window.
|
||||||
pub fn resolve_to_center(&self, selector: &str) -> Option<(i32, i32)> {
|
pub fn resolve_to_center(&self, selector: &str) -> Option<(i32, i32)> {
|
||||||
self.resolve(selector)
|
self.resolve(selector).map(|entry| {
|
||||||
.map(|entry| (entry.x + entry.width as i32 / 2, entry.y + entry.height as i32 / 2))
|
(
|
||||||
|
entry.x + entry.width as i32 / 2,
|
||||||
|
entry.y + entry.height as i32 / 2,
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entries(&self) -> impl Iterator<Item = (&String, &RefEntry)> {
|
pub fn entries(&self) -> impl Iterator<Item = (&String, &RefEntry)> {
|
||||||
|
|
@ -182,7 +186,10 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(refs.resolve("@w1").unwrap().window_id, window_id);
|
assert_eq!(refs.resolve("@w1").unwrap().window_id, window_id);
|
||||||
assert_eq!(refs.resolve(&window_id).unwrap().backend_window_id, 42);
|
assert_eq!(refs.resolve(&window_id).unwrap().backend_window_id, 42);
|
||||||
assert_eq!(refs.resolve(&format!("id={window_id}")).unwrap().title, "Editor");
|
assert_eq!(
|
||||||
|
refs.resolve(&format!("id={window_id}")).unwrap().title,
|
||||||
|
"Editor"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -394,14 +394,13 @@ fn capture_snapshot(
|
||||||
) -> Result<Snapshot> {
|
) -> Result<Snapshot> {
|
||||||
let windows = refresh_windows(state)?;
|
let windows = refresh_windows(state)?;
|
||||||
let screenshot_path = path.unwrap_or_else(temp_screenshot_path);
|
let screenshot_path = path.unwrap_or_else(temp_screenshot_path);
|
||||||
let screenshot = capture_and_save_screenshot(
|
let screenshot =
|
||||||
state,
|
capture_and_save_screenshot(state, &screenshot_path, annotate, Some(&windows))?;
|
||||||
&screenshot_path,
|
|
||||||
annotate,
|
|
||||||
Some(&windows),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Snapshot { screenshot, windows })
|
Ok(Snapshot {
|
||||||
|
screenshot,
|
||||||
|
windows,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn capture_and_save_screenshot(
|
fn capture_and_save_screenshot(
|
||||||
|
|
@ -439,55 +438,3 @@ fn parse_coords(value: &str) -> Option<(i32, i32)> {
|
||||||
let y = parts[1].trim().parse().ok()?;
|
let y = parts[1].trim().parse().ok()?;
|
||||||
Some((x, y))
|
Some((x, y))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, target_os = "linux"))]
|
|
||||||
mod tests {
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use tokio::runtime::Builder;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
use super::handle_request;
|
|
||||||
use crate::core::protocol::Request;
|
|
||||||
use crate::daemon::state::DaemonState;
|
|
||||||
use crate::test_support::{X11TestEnv, deskctl_tmp_screenshot_count, env_lock};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn list_windows_is_side_effect_free_under_xvfb() {
|
|
||||||
let _guard = env_lock().lock().unwrap();
|
|
||||||
let Some(env) = X11TestEnv::new().unwrap() else {
|
|
||||||
eprintln!("Skipping Xvfb-dependent list-windows test");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
env.create_window("deskctl list-windows test", "DeskctlList").unwrap();
|
|
||||||
|
|
||||||
let before = deskctl_tmp_screenshot_count();
|
|
||||||
let runtime = Builder::new_current_thread().enable_all().build().unwrap();
|
|
||||||
let state = Arc::new(Mutex::new(
|
|
||||||
DaemonState::new(
|
|
||||||
"test".to_string(),
|
|
||||||
std::env::temp_dir().join("deskctl-list-windows.sock"),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
));
|
|
||||||
|
|
||||||
let response = runtime.block_on(handle_request(&Request::new("list-windows"), &state));
|
|
||||||
assert!(response.success);
|
|
||||||
|
|
||||||
let data = response.data.unwrap();
|
|
||||||
let windows = data
|
|
||||||
.get("windows")
|
|
||||||
.and_then(|value| value.as_array())
|
|
||||||
.unwrap();
|
|
||||||
assert!(windows.iter().any(|window| {
|
|
||||||
window
|
|
||||||
.get("title")
|
|
||||||
.and_then(|value| value.as_str())
|
|
||||||
.map(|title| title == "deskctl list-windows test")
|
|
||||||
.unwrap_or(false)
|
|
||||||
}));
|
|
||||||
|
|
||||||
let after = deskctl_tmp_screenshot_count();
|
|
||||||
assert_eq!(before, after, "list-windows should not create screenshot artifacts");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
11
src/lib.rs
Normal file
11
src/lib.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
pub mod backend;
|
||||||
|
pub mod cli;
|
||||||
|
pub mod core;
|
||||||
|
pub mod daemon;
|
||||||
|
|
||||||
|
pub fn run() -> anyhow::Result<()> {
|
||||||
|
if std::env::var("DESKCTL_DAEMON").is_ok() {
|
||||||
|
return daemon::run();
|
||||||
|
}
|
||||||
|
cli::run()
|
||||||
|
}
|
||||||
12
src/main.rs
12
src/main.rs
|
|
@ -1,13 +1,3 @@
|
||||||
mod backend;
|
|
||||||
mod cli;
|
|
||||||
mod core;
|
|
||||||
mod daemon;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test_support;
|
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
if std::env::var("DESKCTL_DAEMON").is_ok() {
|
deskctl::run()
|
||||||
return daemon::run();
|
|
||||||
}
|
|
||||||
cli::run()
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
#![cfg(all(test, target_os = "linux"))]
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
use std::process::{Child, Command, Stdio};
|
|
||||||
use std::sync::{Mutex, OnceLock};
|
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use x11rb::connection::Connection;
|
|
||||||
use x11rb::protocol::xproto::{
|
|
||||||
AtomEnum, ConnectionExt as XprotoConnectionExt, CreateWindowAux, EventMask, PropMode,
|
|
||||||
WindowClass,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn env_lock() -> &'static Mutex<()> {
|
|
||||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
|
||||||
LOCK.get_or_init(|| Mutex::new(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct X11TestEnv {
|
|
||||||
child: Child,
|
|
||||||
old_display: Option<String>,
|
|
||||||
old_session_type: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl X11TestEnv {
|
|
||||||
pub fn new() -> Result<Option<Self>> {
|
|
||||||
if Command::new("Xvfb")
|
|
||||||
.arg("-help")
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.status()
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
for display_num in 90..110 {
|
|
||||||
let display = format!(":{display_num}");
|
|
||||||
let lock_path = format!("/tmp/.X{display_num}-lock");
|
|
||||||
let unix_socket = format!("/tmp/.X11-unix/X{display_num}");
|
|
||||||
if Path::new(&lock_path).exists() || Path::new(&unix_socket).exists() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let child = Command::new("Xvfb")
|
|
||||||
.arg(&display)
|
|
||||||
.arg("-screen")
|
|
||||||
.arg("0")
|
|
||||||
.arg("1024x768x24")
|
|
||||||
.arg("-nolisten")
|
|
||||||
.arg("tcp")
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.spawn()
|
|
||||||
.with_context(|| format!("Failed to launch Xvfb on {display}"))?;
|
|
||||||
|
|
||||||
thread::sleep(Duration::from_millis(250));
|
|
||||||
|
|
||||||
let old_display = std::env::var("DISPLAY").ok();
|
|
||||||
let old_session_type = std::env::var("XDG_SESSION_TYPE").ok();
|
|
||||||
std::env::set_var("DISPLAY", &display);
|
|
||||||
std::env::set_var("XDG_SESSION_TYPE", "x11");
|
|
||||||
|
|
||||||
return Ok(Some(Self {
|
|
||||||
child,
|
|
||||||
old_display,
|
|
||||||
old_session_type,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
anyhow::bail!("Failed to find a free Xvfb display")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_window(&self, title: &str, app_class: &str) -> Result<()> {
|
|
||||||
let (conn, screen_num) =
|
|
||||||
x11rb::connect(None).context("Failed to connect to test Xvfb display")?;
|
|
||||||
let screen = &conn.setup().roots[screen_num];
|
|
||||||
let window = conn.generate_id()?;
|
|
||||||
|
|
||||||
conn.create_window(
|
|
||||||
x11rb::COPY_DEPTH_FROM_PARENT,
|
|
||||||
window,
|
|
||||||
screen.root,
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
320,
|
|
||||||
180,
|
|
||||||
0,
|
|
||||||
WindowClass::INPUT_OUTPUT,
|
|
||||||
0,
|
|
||||||
&CreateWindowAux::new()
|
|
||||||
.background_pixel(screen.white_pixel)
|
|
||||||
.event_mask(EventMask::EXPOSURE),
|
|
||||||
)?;
|
|
||||||
conn.change_property8(
|
|
||||||
PropMode::REPLACE,
|
|
||||||
window,
|
|
||||||
AtomEnum::WM_NAME,
|
|
||||||
AtomEnum::STRING,
|
|
||||||
title.as_bytes(),
|
|
||||||
)?;
|
|
||||||
let class_bytes = format!("{app_class}\0{app_class}\0");
|
|
||||||
conn.change_property8(
|
|
||||||
PropMode::REPLACE,
|
|
||||||
window,
|
|
||||||
AtomEnum::WM_CLASS,
|
|
||||||
AtomEnum::STRING,
|
|
||||||
class_bytes.as_bytes(),
|
|
||||||
)?;
|
|
||||||
conn.map_window(window)?;
|
|
||||||
conn.flush()?;
|
|
||||||
|
|
||||||
thread::sleep(Duration::from_millis(150));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for X11TestEnv {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let _ = self.child.kill();
|
|
||||||
let _ = self.child.wait();
|
|
||||||
|
|
||||||
match &self.old_display {
|
|
||||||
Some(value) => std::env::set_var("DISPLAY", value),
|
|
||||||
None => std::env::remove_var("DISPLAY"),
|
|
||||||
}
|
|
||||||
|
|
||||||
match &self.old_session_type {
|
|
||||||
Some(value) => std::env::set_var("XDG_SESSION_TYPE", value),
|
|
||||||
None => std::env::remove_var("XDG_SESSION_TYPE"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deskctl_tmp_screenshot_count() -> usize {
|
|
||||||
std::fs::read_dir("/tmp")
|
|
||||||
.ok()
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|iter| iter.filter_map(Result::ok))
|
|
||||||
.filter(|entry| {
|
|
||||||
entry
|
|
||||||
.file_name()
|
|
||||||
.to_str()
|
|
||||||
.map(|name| name.starts_with("deskctl-") && name.ends_with(".png"))
|
|
||||||
.unwrap_or(false)
|
|
||||||
})
|
|
||||||
.count()
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue