Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/Binary/Debug/
WebkitServer.rs

1//! Debug server for inspecting webview content via HTTP API.
2//! Only compiled in debug builds.
3//!
4//! # Layered DebugServer (Mountain layer)
5//!
6//! This is the **Mountain** half of the dual-layer DebugServer. It exposes
7//! an HTTP surface bound to `127.0.0.1` for live inspection and command
8//! invocation against the running renderer (workbench webview).
9//!
10//! Activation is gated by the unified `DebugServer` env var:
11//!
12//! | Value                          | Mountain layer | Cocoon layer |
13//! |--------------------------------|----------------|--------------|
14//! | unset, `0`, `false`, `off`     | off            | off          |
15//! | `1`, `true`, `on`              | on             | off (compat) |
16//! | `mountain`, `m`                | on             | off          |
17//! | `cocoon`, `c`, `eh`            | off            | on           |
18//! | `both`, `all`                  | on             | on           |
19//!
20//! Ports: `DebugServerPort` (legacy alias `DebugServerPortMountain`,
21//! default `9933`) - Mountain. Cocoon uses `DebugServerPortCocoon`
22//! (default `9934`).
23//!
24//! ## Endpoints
25//!
26//! | Method | Path             | Purpose                                            |
27//! |--------|------------------|----------------------------------------------------|
28//! | GET    | `/health`        | Layer identity + capability advertisement          |
29//! | GET    | `/layers`        | Discoverability: lists every reachable layer       |
30//! | GET    | `/eval?js=…`     | Eval JS in the renderer; returns parsed JSON       |
31//! | POST   | `/execute`       | Body `{js,target?}`; target=`renderer\|iframe:<id>` |
32//! | GET    | `/iframes`       | Walk DOM iframes (src/id/name)                     |
33//! | GET    | `/console`       | Drain the renderer console mirror buffer          |
34//! | GET    | `/commands`      | Enumerate registered workbench commands           |
35//! | POST   | `/command`       | Body `{id,args?}` - invoke a workbench command    |
36//! | POST   | `/vscode/diff`   | Body `{left,right,title?}` - open diff editor      |
37//! | GET    | `/extensions`    | Proxies Cocoon `/extensions` if reachable          |
38//!
39//! All responses are JSON. Mountain-layer endpoints execute in the renderer
40//! via `WebviewWindow::eval_with_callback`. Cocoon-targeted requests are
41//! HTTP-forwarded to the Cocoon DebugServer when present, transparently.
42
43use std::{
44	collections::HashMap,
45	io::{self, BufRead, BufReader, Read, Write},
46	net::TcpListener,
47	sync::{Arc, Mutex},
48	time::Duration,
49};
50
51use once_cell::sync::Lazy;
52use serde_json::{Value, json};
53use tauri::{WebviewWindow, Wry};
54use url::Url;
55
56/// Global storage for the webview window used by the debug server.
57static WINDOW:Lazy<Mutex<Option<Arc<WebviewWindow<Wry>>>>> = Lazy::new(|| Mutex::new(None));
58
59/// Parsed Mountain-layer activation mode. See module docs for the matrix.
60#[derive(Copy, Clone, Debug)]
61enum LayerMode {
62	Off,
63	Mountain,
64	Cocoon,
65	Both,
66}
67
68fn parse_mode() -> LayerMode {
69	match std::env::var("DebugServer").ok().as_deref().map(str::trim) {
70		None | Some("") | Some("0") | Some("false") | Some("off") | Some("no") => LayerMode::Off,
71		Some(v) => {
72			let v = v.to_ascii_lowercase();
73			match v.as_str() {
74				"mountain" | "m" | "native" | "rust" => LayerMode::Mountain,
75				"cocoon" | "c" | "eh" | "extension-host" | "node" => LayerMode::Cocoon,
76				"both" | "all" | "dual" => LayerMode::Both,
77				// Legacy compat: 1/true/on enables Mountain-only.
78				"1" | "true" | "on" | "yes" => LayerMode::Mountain,
79				_ => LayerMode::Off,
80			}
81		},
82	}
83}
84
85fn mountain_enabled(m:LayerMode) -> bool { matches!(m, LayerMode::Mountain | LayerMode::Both) }
86
87fn cocoon_enabled(m:LayerMode) -> bool { matches!(m, LayerMode::Cocoon | LayerMode::Both) }
88
89fn mountain_port() -> u16 {
90	std::env::var("DebugServerPortMountain")
91		.or_else(|_| std::env::var("DebugServerPort"))
92		.ok()
93		.and_then(|p| p.parse().ok())
94		.unwrap_or(9933)
95}
96
97fn cocoon_port() -> u16 {
98	std::env::var("DebugServerPortCocoon")
99		.ok()
100		.and_then(|p| p.parse().ok())
101		.unwrap_or(9934)
102}
103
104/// Installs the Mountain-layer debug server and stores a reference to the
105/// renderer webview window. Called once during app setup in debug builds.
106///
107/// Activation is gated by the unified `DebugServer` env var (see module
108/// docs). When `cocoon`/`both` is selected, this function still installs
109/// the window handle (so `/eval` keeps working for proxy requests) but
110/// skips starting the Mountain HTTP listener.
111pub fn install(window:&WebviewWindow<Wry>) {
112	// Always store the window: even in cocoon-only mode the eval pipeline
113	// stays useful for tests that imported this module directly.
114	let mut guard = WINDOW.lock().unwrap();
115	*guard = Some(Arc::new(window.clone()));
116	drop(guard);
117
118	let mode = parse_mode();
119	if mountain_enabled(mode) {
120		std::thread::spawn(|| start_server());
121	}
122	if cocoon_enabled(mode) {
123		eprintln!(
124			"[WebkitDebug] Cocoon layer requested (port {}). Cocoon must start its own listener.",
125			cocoon_port()
126		);
127	}
128}
129
130/// Main server loop listening for TCP connections.
131fn start_server() {
132	let port = mountain_port();
133
134	let listener = match TcpListener::bind(("127.0.0.1", port)) {
135		Ok(l) => l,
136		Err(e) => {
137			eprintln!("[WebkitDebug] Failed to bind to 127.0.0.1:{}: {}", port, e);
138			return;
139		},
140	};
141	eprintln!(
142		"[WebkitDebug] Mountain layer listening on http://127.0.0.1:{} (mode={:?})",
143		port,
144		parse_mode()
145	);
146
147	for stream in listener.incoming() {
148		match stream {
149			Ok(mut stream) => {
150				let window_opt = WINDOW.lock().unwrap().clone();
151				std::thread::spawn(move || {
152					if let Err(e) = handle_connection(&window_opt, &mut stream) {
153						eprintln!("[WebkitDebug] Connection error: {}", e);
154					}
155				});
156			},
157			Err(e) => eprintln!("[WebkitDebug] Accept error: {}", e),
158		}
159	}
160}
161
162/// Handles a single HTTP connection, dispatches based on method and path.
163fn handle_connection(window_opt:&Option<Arc<WebviewWindow<Wry>>>, stream:&mut std::net::TcpStream) -> io::Result<()> {
164	// Early check for window initialization
165	if window_opt.is_none() {
166		send_json(stream, 503, &json!({"error": "debug server not initialized"}))?;
167		return Ok(());
168	}
169
170	// Read request data (method, path_and_query, body)
171	let (method, path_and_query, body) = {
172		let mut reader = BufReader::new(&mut *stream);
173		let mut request_line = String::new();
174		reader.read_line(&mut request_line)?;
175		let request_line = request_line.trim_end();
176		let parts:Vec<&str> = request_line.split_whitespace().collect();
177		if parts.len() != 3 {
178			return Err(io::Error::new(io::ErrorKind::InvalidInput, "bad request line"));
179		}
180		let method = parts[0].to_string();
181		let path_and_query = parts[1].to_string();
182
183		// Read headers
184		let mut headers = HashMap::new();
185		loop {
186			let mut line = String::new();
187			let n = reader.read_line(&mut line)?;
188			if n == 0 || line == "\r\n" {
189				break;
190			}
191			if let Some(idx) = line.find(':') {
192				let name = line[..idx].trim().to_uppercase();
193				let value = line[idx + 1..].trim().to_string();
194				headers.insert(name, value);
195			}
196		}
197
198		// Read body if Content-Length present
199		let body = if let Some(len_str) = headers.get("CONTENT-LENGTH") {
200			let len:usize = len_str.parse().unwrap_or(0);
201			let mut body_bytes = vec![0; len];
202			reader.read_exact(&mut body_bytes)?;
203			String::from_utf8_lossy(&body_bytes).to_string()
204		} else {
205			String::new()
206		};
207
208		(method, path_and_query, body)
209	};
210
211	// Parse URL to get path and query
212	let full_url = format!("http://localhost{}", path_and_query);
213	let parsed = Url::parse(&full_url).map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid URL"))?;
214	let path = parsed.path();
215	let mut query_pairs = parsed.query_pairs();
216
217	// Dispatch request
218	let (status, response_json) = match (method.as_str(), path) {
219		// ---------- Layer discovery -------------------------------------
220		("GET", "/health") => {
221			(
222				200,
223				json!({
224					"layer": "mountain",
225					"version": env!("CARGO_PKG_VERSION"),
226					"pid": std::process::id(),
227					"mode": format!("{:?}", parse_mode()),
228					"capabilities": [
229						"eval","execute","iframes","console","commands",
230						"command","vscode/diff","extensions(proxy)","layers"
231					],
232				}),
233			)
234		},
235		("GET", "/layers") => {
236			(
237				200,
238				json!({
239					"mountain": { "enabled": mountain_enabled(parse_mode()), "port": mountain_port() },
240					"cocoon":   { "enabled": cocoon_enabled(parse_mode()),   "port": cocoon_port()   },
241					"mode": format!("{:?}", parse_mode()),
242				}),
243			)
244		},
245
246		// ---------- Renderer-side primitives ---------------------------
247		("GET", "/console") => {
248			let js = r#"(function() {
249                const logs = window.__MOUNTAIN_DEBUG_CONSOLE || [];
250                window.__MOUNTAIN_DEBUG_CONSOLE = [];
251                return JSON.stringify(logs);
252            })()"#;
253			match eval_js(window_opt, js) {
254				Ok(value) => (200, json!({"logs": value})),
255				Err(e) => (500, json!({"error": e})),
256			}
257		},
258		("GET", "/eval") => {
259			let js = query_pairs
260				.find(|(k, _)| k == "js")
261				.map(|(_, v)| v.into_owned())
262				.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing js parameter"))?;
263			match eval_js(window_opt, &js) {
264				Ok(value) => (200, json!({"result": value})),
265				Err(e) => (500, json!({"error": e})),
266			}
267		},
268		("POST", "/execute") => {
269			let parsed_body:Value =
270				serde_json::from_str(&body).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
271			let js = parsed_body["js"]
272				.as_str()
273				.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing js field"))?;
274			let target = parsed_body["target"].as_str().unwrap_or("renderer");
275			match target {
276				"extension-host" | "eh" | "cocoon" => proxy_to_cocoon("POST", "/execute", &body),
277				"iframe" => {
278					let iframe_id = parsed_body["iframeId"].as_str().unwrap_or("");
279					let wrapped = format!(
280						r#"(function() {{
281                            const ifr = document.querySelector({0});
282                            if (!ifr) return JSON.stringify({{ error: "iframe not found" }});
283                            try {{
284                                return JSON.stringify(ifr.contentWindow.eval({1}));
285                            }} catch (e) {{ return JSON.stringify({{ error: String(e) }}); }}
286                        }})()"#,
287						json!(format!("iframe#{}", iframe_id)),
288						json!(js)
289					);
290					match eval_js(window_opt, &wrapped) {
291						Ok(v) => (200, json!({"result": v})),
292						Err(e) => (500, json!({"error": e})),
293					}
294				},
295				_ => {
296					if js.is_empty() {
297						(400, json!({"error": "empty js"}))
298					} else {
299						match eval_js(window_opt, js) {
300							Ok(val) => (200, json!({"result": val})),
301							Err(e) => (500, json!({"error": e})),
302						}
303					}
304				},
305			}
306		},
307		("GET", "/iframes") => {
308			let js = r#"(function() {
309                const frames = document.querySelectorAll(iframe);
310                const arr = [];
311                frames.forEach(f => {
312                    arr.push({
313                        src: f.src, id: f.id, name: f.name,
314                        contentWindow: !!f.contentWindow
315                    });
316                });
317                return JSON.stringify(arr);
318            })()"#;
319			match eval_js(window_opt, js) {
320				Ok(value) => (200, json!({"iframes": value})),
321				Err(e) => (500, json!({"error": e})),
322			}
323		},
324
325		// ---------- Workbench command surface ---------------------------
326		("GET", "/commands") => {
327			let js = r#"(async function(){
328                try {
329                  const r = require(vs/platform/commands/common/commands);
330                  const all = r.CommandsRegistry.getCommands();
331                  return JSON.stringify(Array.from(all.keys()).slice(0, 5000));
332                } catch (e) { return JSON.stringify({error:String(e)}); }
333            })()"#;
334			match eval_js(window_opt, js) {
335				Ok(v) => (200, json!({"commands": v})),
336				Err(e) => (500, json!({"error": e})),
337			}
338		},
339		("POST", "/command") => {
340			let parsed_body:Value =
341				serde_json::from_str(&body).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
342			let id = parsed_body["id"]
343				.as_str()
344				.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing id"))?;
345			let args = parsed_body.get("args").cloned().unwrap_or_else(|| json!([]));
346			let js = format!(
347				r#"(async function(){{
348                    try {{
349                      const cs = require(vs/platform/commands/common/commands).CommandsRegistry;
350                      const svcId = require(vs/platform/instantiation/common/instantiation).IInstantiationService;
351                      const ws = (globalThis.MonacoEnvironment || globalThis).__workbench__;
352                      // Resolve through the workbench command service if available.
353                      const cmdSvc = ws?.commandService
354                        || ws?.services?.get?.(require(vs/platform/commands/common/commands).ICommandService);
355                      if (!cmdSvc) return JSON.stringify({{error:"command service unavailable"}});
356                      const args = {0};
357                      const result = await cmdSvc.executeCommand({1}, ...args);
358                      return JSON.stringify({{ ok: true, result: result ?? null }});
359                    }} catch (e) {{ return JSON.stringify({{ ok:false, error: String(e?.stack||e) }}); }}
360                }})()"#,
361				args,
362				json!(id)
363			);
364			match eval_js(window_opt, &js) {
365				Ok(v) => (200, v),
366				Err(e) => (500, json!({"error": e})),
367			}
368		},
369		("POST", "/vscode/diff") => {
370			let parsed_body:Value =
371				serde_json::from_str(&body).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
372			let left = parsed_body["left"].as_str().unwrap_or("");
373			let right = parsed_body["right"].as_str().unwrap_or("");
374			let title = parsed_body["title"].as_str().unwrap_or("Diff");
375			if left.is_empty() || right.is_empty() {
376				(400, json!({"error":"left and right required"}))
377			} else {
378				let js = format!(
379					r#"(async function(){{
380                        try {{
381                          const URI = require(vs/base/common/uri).URI;
382                          const cmdSvc = (globalThis).__workbench__?.commandService;
383                          if (!cmdSvc) return JSON.stringify({{error:"command service unavailable"}});
384                          await cmdSvc.executeCommand(vscode.diff, URI.parse({0}), URI.parse({1}), {2});
385                          return JSON.stringify({{ok:true}});
386                        }} catch (e) {{ return JSON.stringify({{ok:false,error:String(e?.stack||e)}}); }}
387                    }})()"#,
388					json!(left),
389					json!(right),
390					json!(title)
391				);
392				match eval_js(window_opt, &js) {
393					Ok(v) => (200, v),
394					Err(e) => (500, json!({"error": e})),
395				}
396			}
397		},
398
399		// ---------- Cocoon proxy ---------------------------------------
400		("GET", "/extensions") => proxy_to_cocoon("GET", "/extensions", ""),
401
402		_ => (404, json!({"error": "not found", "method": method, "path": path})),
403	};
404
405	send_json(stream, status, &response_json)
406}
407
408/// Evaluates JavaScript in the webview and returns the result as a
409/// serde_json::Value.
410fn eval_js(window_opt:&Option<Arc<WebviewWindow<Wry>>>, js:&str) -> Result<Value, String> {
411	let window = window_opt.as_ref().ok_or("debug server not initialized")?;
412	let (tx, rx) = std::sync::mpsc::sync_channel(1);
413	window
414		.eval_with_callback(js.to_string(), move |result| {
415			let _ = tx.send(result);
416		})
417		.map_err(|e| e.to_string())?;
418	let result_str = rx
419		.recv_timeout(Duration::from_secs(5))
420		.map_err(|_| "timeout waiting for eval result".to_string())?;
421	serde_json::from_str(&result_str).map_err(|e| e.to_string())
422}
423
424/// Best-effort forward to the Cocoon DebugServer over loopback.
425/// Returns (status, json). If Cocoon is unreachable, returns 502.
426fn proxy_to_cocoon(method:&str, path:&str, body:&str) -> (u16, Value) {
427	use std::net::TcpStream;
428	let port = cocoon_port();
429	let addr = format!("127.0.0.1:{}", port);
430	let mut stream = match TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_millis(300)) {
431		Ok(s) => s,
432		Err(e) => {
433			return (
434				502,
435				json!({"error":"cocoon layer unreachable","detail":e.to_string(),"port":port}),
436			);
437		},
438	};
439	let _ = stream.set_read_timeout(Some(Duration::from_secs(5)));
440	let req = format!(
441		"{} {} HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: \
442		 close\r\n\r\n{}",
443		method,
444		path,
445		body.len(),
446		body
447	);
448	if stream.write_all(req.as_bytes()).is_err() {
449		return (502, json!({"error":"cocoon write failed"}));
450	}
451	let mut buf = String::new();
452	if stream.read_to_string(&mut buf).is_err() {
453		return (502, json!({"error":"cocoon read failed"}));
454	}
455	// Split headers/body.
456	let body_idx = buf.find("\r\n\r\n").map(|i| i + 4).unwrap_or(buf.len());
457	let body_str = &buf[body_idx..];
458	let parsed:Value = serde_json::from_str(body_str).unwrap_or_else(|_| json!({"raw": body_str}));
459	(200, parsed)
460}
461
462/// Sends a JSON response with the given status code.
463fn send_json(stream:&mut std::net::TcpStream, status:u16, value:&Value) -> io::Result<()> {
464	let body =
465		serde_json::to_string(value).map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "serialization error"))?;
466	let status_text = match status {
467		200 => "OK",
468		400 => "Bad Request",
469		404 => "Not Found",
470		500 => "Internal Server Error",
471		502 => "Bad Gateway",
472		503 => "Service Unavailable",
473		_ => "OK",
474	};
475	let headers = format!(
476		"HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
477		status,
478		status_text,
479		body.len()
480	);
481	stream.write_all(headers.as_bytes())?;
482	stream.write_all(body.as_bytes())?;
483	stream.flush()?;
484	Ok(())
485}