agentmux_cef\commands/
backend.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3//
4// Backend/sidecar management commands for the CEF host.
5// Ported from src-tauri/src/commands/backend.rs.
6
7use std::sync::Arc;
8
9use crate::state::AppState;
10
11/// Get the backend WebSocket and HTTP endpoints.
12pub fn get_backend_endpoints(state: &Arc<AppState>) -> Result<serde_json::Value, String> {
13    let endpoints = state.backend_endpoints.lock();
14
15    if endpoints.ws_endpoint.is_empty() {
16        return Err("Backend not ready yet".to_string());
17    }
18
19    Ok(serde_json::json!({
20        "ws": endpoints.ws_endpoint,
21        "web": endpoints.web_endpoint,
22    }))
23}
24
25/// Get the window initialization options (client/window/tab IDs).
26pub fn get_wave_init_opts(state: &Arc<AppState>) -> Result<serde_json::Value, String> {
27    let client_id = state.client_id.lock();
28    let window_id = state.window_id.lock();
29    let tab_id = state.active_tab_id.lock();
30
31    if client_id.is_none() || window_id.is_none() || tab_id.is_none() {
32        return Err("Window state not initialized yet".to_string());
33    }
34
35    Ok(serde_json::json!({
36        "clientId": client_id.as_ref().unwrap(),
37        "windowId": window_id.as_ref().unwrap(),
38        "tabId": tab_id.as_ref().unwrap(),
39        "activate": true,
40        "primaryTabStartup": true,
41    }))
42}
43
44/// Get backend process info for the status bar popover.
45pub fn get_backend_info(state: &Arc<AppState>) -> serde_json::Value {
46    let current_version = env!("CARGO_PKG_VERSION");
47    let endpoints = state.backend_endpoints.lock();
48    let pid = *state.backend_pid.lock();
49    let started_at = state.backend_started_at.lock().clone();
50
51    serde_json::json!({
52        "pid": pid,
53        "started_at": started_at,
54        "web_endpoint": endpoints.web_endpoint,
55        "version": current_version,
56    })
57}
58
59/// Log a message from the frontend.
60pub fn fe_log(args: &serde_json::Value) -> serde_json::Value {
61    let msg = args
62        .get("msg")
63        .and_then(|v| v.as_str())
64        .unwrap_or_default();
65    tracing::info!("[frontend] {}", msg);
66    serde_json::Value::Null
67}
68
69/// Structured log from the frontend.
70pub fn fe_log_structured(args: &serde_json::Value) -> serde_json::Value {
71    let level = args.get("level").and_then(|v| v.as_str()).unwrap_or("info");
72    let module = args.get("module").and_then(|v| v.as_str()).unwrap_or("unknown");
73    let message = args.get("message").and_then(|v| v.as_str()).unwrap_or("");
74    let data = args.get("data");
75
76    match level {
77        "error" => tracing::error!(module = %module, data = ?data, "[fe] {}", message),
78        "warn" => tracing::warn!(module = %module, data = ?data, "[fe] {}", message),
79        "debug" => tracing::debug!(module = %module, data = ?data, "[fe] {}", message),
80        _ => tracing::info!(module = %module, data = ?data, "[fe] {}", message),
81    }
82    serde_json::Value::Null
83}
84
85/// Restart the agentmux-srv backend sidecar.
86///
87/// Phase B.1: in launcher-managed runs (`AGENTMUX_BACKEND_PID` env
88/// is set by the launcher), the host does NOT own the srv child
89/// handle — `state.sidecar_child` stays None. Naively running the
90/// kill-then-spawn flow here would skip killing the launcher's srv
91/// (no handle to kill) and spawn a SECOND srv touching the same
92/// data dir, corrupting state. Refuse with a clear message until
93/// Phase B.2 wires a Quit command from host to launcher to do the
94/// restart cleanly. (codex P2 @ sidecar.rs:58, PR #571 round-3.)
95pub async fn restart_backend(state: Arc<AppState>) -> Result<serde_json::Value, String> {
96    tracing::info!("[restart_backend] user-initiated restart");
97
98    if std::env::var("AGENTMUX_BACKEND_PID").is_ok() {
99        let msg = "backend lifecycle is owned by the launcher in this run \
100                   (AGENTMUX_BACKEND_PID set); host-initiated restart is \
101                   disabled until Phase B.2 wires the launcher RPC. \
102                   Restart the entire app to get a fresh srv.";
103        tracing::warn!("[restart_backend] refused: {}", msg);
104        return Err(msg.to_string());
105    }
106
107    // Kill existing sidecar if still alive
108    {
109        let mut sidecar = state.sidecar_child.lock();
110        if let Some(ref mut child) = *sidecar {
111            let _ = child.kill();
112            tracing::info!("[restart_backend] killed stale sidecar");
113        }
114        *sidecar = None;
115    }
116
117    // Small delay to let the OS release the port
118    tokio::time::sleep(std::time::Duration::from_millis(500)).await;
119
120    // Spawn fresh backend
121    let result = crate::sidecar::spawn_backend(&state).await?;
122
123    // Update stored endpoints
124    {
125        let mut endpoints = state.backend_endpoints.lock();
126        endpoints.ws_endpoint = result.ws_endpoint.clone();
127        endpoints.web_endpoint = result.web_endpoint.clone();
128    }
129
130    // Emit backend-ready event
131    let payload = serde_json::json!({
132        "ws": result.ws_endpoint,
133        "web": result.web_endpoint,
134    });
135    crate::events::emit_event_from_state(&state, "backend-ready", &payload);
136
137    tracing::info!(
138        "[restart_backend] backend restarted: ws={} web={}",
139        result.ws_endpoint,
140        result.web_endpoint
141    );
142
143    Ok(serde_json::Value::Null)
144}
145
146/// Set the window initialization status.
147pub fn set_window_init_status(state: &Arc<AppState>, args: &serde_json::Value) -> serde_json::Value {
148    let status = args
149        .get("status")
150        .and_then(|v| v.as_str())
151        .unwrap_or_default();
152    let label = args
153        .get("label")
154        .and_then(|v| v.as_str())
155        .unwrap_or("main")
156        .to_string();
157    tracing::debug!("set_window_init_status status={} label={}", status, label);
158    *state.window_init_status.lock() = status.to_string();
159    // Capture HWND once the window is fully shown (CEF Views returns NULL at
160    // on_after_created time; the renderer-ready callback is the earliest safe moment).
161    #[cfg(target_os = "windows")]
162    if status == "ready" {
163        crate::commands::window::capture_hwnd_for_label(state, &label);
164    }
165    serde_json::Value::Null
166}