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
|
|
@ -19,32 +19,57 @@ pub async fn ws_upgrade(
|
|||
}
|
||||
|
||||
async fn handle_socket(socket: WebSocket, state: AppState) {
|
||||
let session_id = Uuid::new_v4();
|
||||
let (cmd_tx, mut cmd_rx) = mpsc::channel::<ServerMessage>(64);
|
||||
let (mut ws_tx, mut ws_rx) = socket.split();
|
||||
|
||||
// Register session (label filled in on Hello)
|
||||
// Wait for the Hello message to get the device label
|
||||
let label = loop {
|
||||
match ws_rx.next().await {
|
||||
Some(Ok(Message::Text(text))) => {
|
||||
match serde_json::from_str::<ClientMessage>(&text) {
|
||||
Ok(ClientMessage::Hello { label }) => {
|
||||
if label.is_empty() {
|
||||
warn!("Client sent empty label, disconnecting");
|
||||
return;
|
||||
}
|
||||
break label;
|
||||
}
|
||||
Ok(_) => {
|
||||
warn!("Expected Hello as first message, got something else");
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Invalid JSON on handshake: {e}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Close(_))) | None => return,
|
||||
_ => continue,
|
||||
}
|
||||
};
|
||||
|
||||
// Register session by label
|
||||
let session = Session {
|
||||
id: session_id,
|
||||
label: None,
|
||||
label: label.clone(),
|
||||
cmd_tx,
|
||||
};
|
||||
state.sessions.insert(session);
|
||||
info!("Client connected: session={session_id}");
|
||||
|
||||
let (mut ws_tx, mut ws_rx) = socket.split();
|
||||
info!("Client connected: device={label}");
|
||||
|
||||
// Spawn task: forward server commands → WS
|
||||
let label_clone = label.clone();
|
||||
let send_task = tokio::spawn(async move {
|
||||
while let Some(msg) = cmd_rx.recv().await {
|
||||
match serde_json::to_string(&msg) {
|
||||
Ok(json) => {
|
||||
if let Err(e) = ws_tx.send(Message::Text(json.into())).await {
|
||||
error!("WS send error for session={session_id}: {e}");
|
||||
error!("WS send error for device={label_clone}: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Serialization error for session={session_id}: {e}");
|
||||
error!("Serialization error for device={label_clone}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -55,36 +80,33 @@ async fn handle_socket(socket: WebSocket, state: AppState) {
|
|||
match result {
|
||||
Ok(Message::Text(text)) => {
|
||||
match serde_json::from_str::<ClientMessage>(&text) {
|
||||
Ok(msg) => handle_client_message(session_id, msg, &state).await,
|
||||
Ok(msg) => handle_client_message(&label, msg, &state).await,
|
||||
Err(e) => {
|
||||
warn!("Invalid JSON from session={session_id}: {e} | raw={text}");
|
||||
warn!("Invalid JSON from device={label}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Message::Close(_)) => {
|
||||
info!("Client disconnected gracefully: session={session_id}");
|
||||
info!("Client disconnected gracefully: device={label}");
|
||||
break;
|
||||
}
|
||||
Ok(Message::Ping(_)) | Ok(Message::Pong(_)) | Ok(Message::Binary(_)) => {}
|
||||
Err(e) => {
|
||||
error!("WS receive error for session={session_id}: {e}");
|
||||
error!("WS receive error for device={label}: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send_task.abort();
|
||||
state.sessions.remove(&session_id);
|
||||
info!("Session cleaned up: session={session_id}");
|
||||
state.sessions.remove(&label);
|
||||
info!("Session cleaned up: device={label}");
|
||||
}
|
||||
|
||||
async fn handle_client_message(session_id: Uuid, msg: ClientMessage, state: &AppState) {
|
||||
async fn handle_client_message(label: &str, msg: ClientMessage, state: &AppState) {
|
||||
match &msg {
|
||||
ClientMessage::Hello { label } => {
|
||||
if let Some(lbl) = label {
|
||||
state.sessions.set_label(&session_id, lbl.clone());
|
||||
}
|
||||
debug!("Hello from session={session_id}, label={label:?}");
|
||||
ClientMessage::Hello { .. } => {
|
||||
debug!("Duplicate Hello from device={label}, ignoring");
|
||||
}
|
||||
ClientMessage::ScreenshotResponse { request_id, .. }
|
||||
| ClientMessage::ExecResponse { request_id, .. }
|
||||
|
|
@ -98,7 +120,7 @@ async fn handle_client_message(session_id: Uuid, msg: ClientMessage, state: &App
|
|||
| ClientMessage::Error { request_id, .. } => {
|
||||
let rid = *request_id;
|
||||
if !state.sessions.resolve_pending(rid, msg) {
|
||||
warn!("No pending request for request_id={rid} (session={session_id})");
|
||||
warn!("No pending request for request_id={rid} (device={label})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue