refactor: enforce device labels, unify screenshot, remove deprecated commands, session-id-less design
- Device labels: lowercase, no whitespace, only a-z 0-9 - _ (enforced at config time) - Session IDs removed: device label is the sole identifier - Routes changed: /sessions/:id → /devices/:label - Removed commands: click, type, find-window, wait-for-window, label, old version, server-version - Renamed: status → version (compares relay/remote.py/client commits) - Unified screenshot: takes 'screen' or a window label as argument - Windows listed with human-readable labels (same format as device labels) - Single instance enforcement via PID lock file - Removed input.rs (click/type functionality) - All docs and code in English - Protocol: Hello.label is now required (String, not Option<String>) - Client auto-migrates invalid labels on startup
This commit is contained in:
parent
5fd01a423d
commit
0b4a6de8ae
14 changed files with 736 additions and 1180 deletions
|
|
@ -1,27 +1,58 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Information about a single window on the client machine
|
||||
/// Information about a single window on the client machine.
|
||||
/// `label` is a human-readable, lowercase identifier (e.g. "google_chrome", "discord").
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WindowInfo {
|
||||
pub id: u64,
|
||||
pub title: String,
|
||||
pub label: String,
|
||||
pub visible: bool,
|
||||
}
|
||||
|
||||
/// Validate a device/window label: lowercase, no whitespace, only a-z 0-9 - _
|
||||
pub fn is_valid_label(s: &str) -> bool {
|
||||
!s.is_empty()
|
||||
&& s.chars()
|
||||
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
|
||||
}
|
||||
|
||||
/// Convert an arbitrary string into a valid label.
|
||||
/// Lowercase, replace whitespace and invalid chars with '_', collapse runs.
|
||||
pub fn sanitize_label(s: &str) -> String {
|
||||
let mut result = String::with_capacity(s.len());
|
||||
let mut prev_underscore = false;
|
||||
for c in s.chars() {
|
||||
if c.is_ascii_alphanumeric() {
|
||||
result.push(c.to_ascii_lowercase());
|
||||
prev_underscore = false;
|
||||
} else if c == '-' {
|
||||
result.push('-');
|
||||
prev_underscore = false;
|
||||
} else {
|
||||
// Replace whitespace and other chars with _
|
||||
if !prev_underscore && !result.is_empty() {
|
||||
result.push('_');
|
||||
prev_underscore = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Trim trailing _
|
||||
result.trim_end_matches('_').to_string()
|
||||
}
|
||||
|
||||
/// Messages sent from the relay server to a connected client
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ServerMessage {
|
||||
/// Request a screenshot from the client
|
||||
/// Request a full-screen screenshot
|
||||
ScreenshotRequest { request_id: Uuid },
|
||||
/// Capture a specific window by its HWND (works even if behind other windows)
|
||||
/// Capture a specific window by its HWND
|
||||
WindowScreenshotRequest { request_id: Uuid, window_id: u64 },
|
||||
/// Fetch the last N lines of the client log file
|
||||
LogsRequest { request_id: Uuid, lines: u32 },
|
||||
/// Show a MessageBox on the client asking the user to do something.
|
||||
/// Blocks until the user clicks OK — use this when you need the user
|
||||
/// to perform a manual action before continuing.
|
||||
/// Show a MessageBox on the client asking the user to do something
|
||||
PromptRequest {
|
||||
request_id: Uuid,
|
||||
message: String,
|
||||
|
|
@ -31,21 +62,8 @@ pub enum ServerMessage {
|
|||
ExecRequest {
|
||||
request_id: Uuid,
|
||||
command: String,
|
||||
/// Timeout in milliseconds. None = use client default (30s)
|
||||
timeout_ms: Option<u64>,
|
||||
},
|
||||
/// Simulate a mouse click
|
||||
ClickRequest {
|
||||
request_id: Uuid,
|
||||
x: i32,
|
||||
y: i32,
|
||||
button: MouseButton,
|
||||
},
|
||||
/// Type text on the client
|
||||
TypeRequest {
|
||||
request_id: Uuid,
|
||||
text: String,
|
||||
},
|
||||
/// Acknowledge a client message
|
||||
Ack { request_id: Uuid },
|
||||
/// Server-side error response
|
||||
|
|
@ -90,8 +108,8 @@ pub enum ServerMessage {
|
|||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ClientMessage {
|
||||
/// Client registers itself with optional display name
|
||||
Hello { label: Option<String> },
|
||||
/// Client registers itself with its device label
|
||||
Hello { label: String },
|
||||
/// Response to a screenshot request — base64-encoded PNG
|
||||
ScreenshotResponse {
|
||||
request_id: Uuid,
|
||||
|
|
@ -106,7 +124,7 @@ pub enum ClientMessage {
|
|||
stderr: String,
|
||||
exit_code: i32,
|
||||
},
|
||||
/// Generic acknowledgement for click/type/minimize-all/focus/maximize
|
||||
/// Generic acknowledgement
|
||||
Ack { request_id: Uuid },
|
||||
/// Client error response
|
||||
Error {
|
||||
|
|
@ -137,29 +155,33 @@ pub enum ClientMessage {
|
|||
},
|
||||
/// Response to a clipboard-get request
|
||||
ClipboardGetResponse { request_id: Uuid, text: String },
|
||||
/// Response to a prompt request — contains the user's typed answer
|
||||
/// Response to a prompt request
|
||||
PromptResponse { request_id: Uuid, answer: String },
|
||||
}
|
||||
|
||||
/// Mouse button variants
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum MouseButton {
|
||||
Left,
|
||||
Right,
|
||||
Middle,
|
||||
}
|
||||
|
||||
impl Default for MouseButton {
|
||||
fn default() -> Self {
|
||||
MouseButton::Left
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_valid_labels() {
|
||||
assert!(is_valid_label("moritz_pc"));
|
||||
assert!(is_valid_label("my-desktop"));
|
||||
assert!(is_valid_label("pc01"));
|
||||
assert!(!is_valid_label("Moritz PC"));
|
||||
assert!(!is_valid_label(""));
|
||||
assert!(!is_valid_label("has spaces"));
|
||||
assert!(!is_valid_label("UPPER"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_label() {
|
||||
assert_eq!(sanitize_label("Moritz PC"), "moritz_pc");
|
||||
assert_eq!(sanitize_label("My Desktop!!"), "my_desktop");
|
||||
assert_eq!(sanitize_label("hello-world"), "hello-world");
|
||||
assert_eq!(sanitize_label("DESKTOP-ABC123"), "desktop-abc123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_server_message_serialization() {
|
||||
let msg = ServerMessage::ExecRequest {
|
||||
|
|
@ -174,25 +196,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_client_message_serialization() {
|
||||
let msg = ClientMessage::Hello { label: Some("test-pc".into()) };
|
||||
let msg = ClientMessage::Hello { label: "test-pc".into() };
|
||||
let json = serde_json::to_string(&msg).unwrap();
|
||||
assert!(json.contains("hello"));
|
||||
assert!(json.contains("test-pc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip() {
|
||||
let msg = ClientMessage::ExecResponse {
|
||||
request_id: Uuid::nil(),
|
||||
stdout: "hello\n".into(),
|
||||
stderr: String::new(),
|
||||
exit_code: 0,
|
||||
};
|
||||
let json = serde_json::to_string(&msg).unwrap();
|
||||
let decoded: ClientMessage = serde_json::from_str(&json).unwrap();
|
||||
match decoded {
|
||||
ClientMessage::ExecResponse { exit_code, .. } => assert_eq!(exit_code, 0),
|
||||
_ => panic!("wrong variant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue