1use 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
56static WINDOW:Lazy<Mutex<Option<Arc<WebviewWindow<Wry>>>>> = Lazy::new(|| Mutex::new(None));
58
59#[derive(Copy, Clone, Debug)]
61enum LayerMode {
62 Off,
63
64 Mountain,
65
66 Cocoon,
67
68 Both,
69}
70
71fn parse_mode() -> LayerMode {
72 match std::env::var("DebugServer").ok().as_deref().map(str::trim) {
73 None | Some("") | Some("0") | Some("false") | Some("off") | Some("no") => LayerMode::Off,
74
75 Some(v) => {
76 let v = v.to_ascii_lowercase();
77
78 match v.as_str() {
79 "mountain" | "m" | "native" | "rust" => LayerMode::Mountain,
80
81 "cocoon" | "c" | "eh" | "extension-host" | "node" => LayerMode::Cocoon,
82
83 "both" | "all" | "dual" => LayerMode::Both,
84
85 "1" | "true" | "on" | "yes" => LayerMode::Mountain,
87
88 _ => LayerMode::Off,
89 }
90 },
91 }
92}
93
94fn mountain_enabled(m:LayerMode) -> bool { matches!(m, LayerMode::Mountain | LayerMode::Both) }
95
96fn cocoon_enabled(m:LayerMode) -> bool { matches!(m, LayerMode::Cocoon | LayerMode::Both) }
97
98fn mountain_port() -> u16 {
99 std::env::var("DebugServerPortMountain")
100 .or_else(|_| std::env::var("DebugServerPort"))
101 .ok()
102 .and_then(|p| p.parse().ok())
103 .unwrap_or(9933)
104}
105
106fn cocoon_port() -> u16 {
107 std::env::var("DebugServerPortCocoon")
108 .ok()
109 .and_then(|p| p.parse().ok())
110 .unwrap_or(9934)
111}
112
113pub fn install(window:&WebviewWindow<Wry>) {
121 let mut guard = WINDOW.lock().unwrap();
124
125 *guard = Some(Arc::new(window.clone()));
126 drop(guard);
127
128 let mode = parse_mode();
129
130 if mountain_enabled(mode) {
131 std::thread::spawn(|| start_server());
132 }
133
134 if cocoon_enabled(mode) {
135 eprintln!(
136 "[WebkitDebug] Cocoon layer requested (port {}). Cocoon must start its own listener.",
137 cocoon_port()
138 );
139 }
140}
141
142fn start_server() {
144 let port = mountain_port();
145
146 let listener = match TcpListener::bind(("127.0.0.1", port)) {
147 Ok(l) => l,
148
149 Err(e) => {
150 eprintln!("[WebkitDebug] Failed to bind to 127.0.0.1:{}: {}", port, e);
151
152 return;
153 },
154 };
155
156 eprintln!(
157 "[WebkitDebug] Mountain layer listening on http://127.0.0.1:{} (mode={:?})",
158 port,
159 parse_mode()
160 );
161
162 for stream in listener.incoming() {
163 match stream {
164 Ok(mut stream) => {
165 let window_opt = WINDOW.lock().unwrap().clone();
166
167 std::thread::spawn(move || {
168 if let Err(e) = handle_connection(&window_opt, &mut stream) {
169 eprintln!("[WebkitDebug] Connection error: {}", e);
170 }
171 });
172 },
173
174 Err(e) => eprintln!("[WebkitDebug] Accept error: {}", e),
175 }
176 }
177}
178
179fn handle_connection(window_opt:&Option<Arc<WebviewWindow<Wry>>>, stream:&mut std::net::TcpStream) -> io::Result<()> {
181 if window_opt.is_none() {
183 send_json(stream, 503, &json!({"error": "debug server not initialized"}))?;
184
185 return Ok(());
186 }
187
188 let (method, path_and_query, body) = {
190 let mut reader = BufReader::new(&mut *stream);
191
192 let mut request_line = String::new();
193
194 reader.read_line(&mut request_line)?;
195
196 let request_line = request_line.trim_end();
197
198 let parts:Vec<&str> = request_line.split_whitespace().collect();
199
200 if parts.len() != 3 {
201 return Err(io::Error::new(io::ErrorKind::InvalidInput, "bad request line"));
202 }
203
204 let method = parts[0].to_string();
205
206 let path_and_query = parts[1].to_string();
207
208 let mut headers = HashMap::new();
210
211 loop {
212 let mut line = String::new();
213
214 let n = reader.read_line(&mut line)?;
215
216 if n == 0 || line == "\r\n" {
217 break;
218 }
219
220 if let Some(idx) = line.find(':') {
221 let name = line[..idx].trim().to_uppercase();
222
223 let value = line[idx + 1..].trim().to_string();
224
225 headers.insert(name, value);
226 }
227 }
228
229 let body = if let Some(len_str) = headers.get("CONTENT-LENGTH") {
231 let len:usize = len_str.parse().unwrap_or(0);
232
233 let mut body_bytes = vec![0; len];
234
235 reader.read_exact(&mut body_bytes)?;
236
237 String::from_utf8_lossy(&body_bytes).to_string()
238 } else {
239 String::new()
240 };
241
242 (method, path_and_query, body)
243 };
244
245 let full_url = format!("http://localhost{}", path_and_query);
247
248 let parsed = Url::parse(&full_url).map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid URL"))?;
249
250 let path = parsed.path();
251
252 let mut query_pairs = parsed.query_pairs();
253
254 let (status, response_json) = match (method.as_str(), path) {
256 ("GET", "/health") => {
258 (
259 200,
260 json!({
261 "layer": "mountain",
262 "version": env!("CARGO_PKG_VERSION"),
263 "pid": std::process::id(),
264 "mode": format!("{:?}", parse_mode()),
265 "capabilities": [
266 "eval","execute","iframes","console","commands",
267 "command","vscode/diff","extensions(proxy)","layers"
268 ],
269 }),
270 )
271 },
272
273 ("GET", "/layers") => {
274 (
275 200,
276 json!({
277 "mountain": { "enabled": mountain_enabled(parse_mode()), "port": mountain_port() },
278 "cocoon": { "enabled": cocoon_enabled(parse_mode()), "port": cocoon_port() },
279 "mode": format!("{:?}", parse_mode()),
280 }),
281 )
282 },
283
284 ("GET", "/console") => {
286 let js = r#"(function() {
287 const logs = window.__MOUNTAIN_DEBUG_CONSOLE || [];
288
289 window.__MOUNTAIN_DEBUG_CONSOLE = [];
290
291 return JSON.stringify(logs);
292 })()"#;
293 match eval_js(window_opt, js) {
294 Ok(value) => (200, json!({"logs": value})),
295
296 Err(e) => (500, json!({"error": e})),
297 }
298 },
299
300 ("GET", "/eval") => {
301 let js = query_pairs
302 .find(|(k, _)| k == "js")
303 .map(|(_, v)| v.into_owned())
304 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing js parameter"))?;
305
306 match eval_js(window_opt, &js) {
307 Ok(value) => (200, json!({"result": value})),
308
309 Err(e) => (500, json!({"error": e})),
310 }
311 },
312
313 ("POST", "/execute") => {
314 let parsed_body:Value =
315 serde_json::from_str(&body).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
316
317 let js = parsed_body["js"]
318 .as_str()
319 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing js field"))?;
320
321 let target = parsed_body["target"].as_str().unwrap_or("renderer");
322
323 match target {
324 "extension-host" | "eh" | "cocoon" => proxy_to_cocoon("POST", "/execute", &body),
325
326 "iframe" => {
327 let iframe_id = parsed_body["iframeId"].as_str().unwrap_or("");
328
329 let wrapped = format!(
330 r#"(function() {{
331 const ifr = document.querySelector({0});
332 if (!ifr) return JSON.stringify({{ error: "iframe not found" }});
333 try {{
334 return JSON.stringify(ifr.contentWindow.eval({1}));
335 }} catch (e) {{ return JSON.stringify({{ error: String(e) }}); }}
336 }})()"#,
337 json!(format!("iframe#{}", iframe_id)),
338 json!(js)
339 );
340
341 match eval_js(window_opt, &wrapped) {
342 Ok(v) => (200, json!({"result": v})),
343
344 Err(e) => (500, json!({"error": e})),
345 }
346 },
347
348 _ => {
349 if js.is_empty() {
350 (400, json!({"error": "empty js"}))
351 } else {
352 match eval_js(window_opt, js) {
353 Ok(val) => (200, json!({"result": val})),
354
355 Err(e) => (500, json!({"error": e})),
356 }
357 }
358 },
359 }
360 },
361
362 ("GET", "/iframes") => {
363 let js = r#"(function() {
364 const frames = document.querySelectorAll(iframe);
365
366 const arr = [];
367
368 frames.forEach(f => {
369 arr.push({
370 src: f.src, id: f.id, name: f.name,
371 contentWindow: !!f.contentWindow
372 });
373 });
374
375 return JSON.stringify(arr);
376 })()"#;
377 match eval_js(window_opt, js) {
378 Ok(value) => (200, json!({"iframes": value})),
379
380 Err(e) => (500, json!({"error": e})),
381 }
382 },
383
384 ("GET", "/commands") => {
386 let js = r#"(async function(){
387 try {
388
389 const r = require(vs/platform/commands/common/commands);
390
391 const all = r.CommandsRegistry.getCommands();
392
393 return JSON.stringify(Array.from(all.keys()).slice(0, 5000));
394 } catch (e) { return JSON.stringify({error:String(e)}); }
395 })()"#;
396 match eval_js(window_opt, js) {
397 Ok(v) => (200, json!({"commands": v})),
398
399 Err(e) => (500, json!({"error": e})),
400 }
401 },
402
403 ("POST", "/command") => {
404 let parsed_body:Value =
405 serde_json::from_str(&body).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
406
407 let id = parsed_body["id"]
408 .as_str()
409 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing id"))?;
410
411 let args = parsed_body.get("args").cloned().unwrap_or_else(|| json!([]));
412
413 let js = format!(
414 r#"(async function(){{
415 try {{
416 const cs = require(vs/platform/commands/common/commands).CommandsRegistry;
417 const svcId = require(vs/platform/instantiation/common/instantiation).IInstantiationService;
418 const ws = (globalThis.MonacoEnvironment || globalThis).__workbench__;
419 // Resolve through the workbench command service if available.
420 const cmdSvc = ws?.commandService
421 || ws?.services?.get?.(require(vs/platform/commands/common/commands).ICommandService);
422 if (!cmdSvc) return JSON.stringify({{error:"command service unavailable"}});
423 const args = {0};
424 const result = await cmdSvc.executeCommand({1}, ...args);
425 return JSON.stringify({{ ok: true, result: result ?? null }});
426 }} catch (e) {{ return JSON.stringify({{ ok:false, error: String(e?.stack||e) }}); }}
427 }})()"#,
428 args,
429 json!(id)
430 );
431
432 match eval_js(window_opt, &js) {
433 Ok(v) => (200, v),
434
435 Err(e) => (500, json!({"error": e})),
436 }
437 },
438
439 ("POST", "/vscode/diff") => {
440 let parsed_body:Value =
441 serde_json::from_str(&body).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
442
443 let left = parsed_body["left"].as_str().unwrap_or("");
444
445 let right = parsed_body["right"].as_str().unwrap_or("");
446
447 let title = parsed_body["title"].as_str().unwrap_or("Diff");
448
449 if left.is_empty() || right.is_empty() {
450 (400, json!({"error":"left and right required"}))
451 } else {
452 let js = format!(
453 r#"(async function(){{
454 try {{
455 const URI = require(vs/base/common/uri).URI;
456 const cmdSvc = (globalThis).__workbench__?.commandService;
457 if (!cmdSvc) return JSON.stringify({{error:"command service unavailable"}});
458 await cmdSvc.executeCommand(vscode.diff, URI.parse({0}), URI.parse({1}), {2});
459 return JSON.stringify({{ok:true}});
460 }} catch (e) {{ return JSON.stringify({{ok:false,error:String(e?.stack||e)}}); }}
461 }})()"#,
462 json!(left),
463 json!(right),
464 json!(title)
465 );
466
467 match eval_js(window_opt, &js) {
468 Ok(v) => (200, v),
469
470 Err(e) => (500, json!({"error": e})),
471 }
472 }
473 },
474
475 ("GET", "/extensions") => proxy_to_cocoon("GET", "/extensions", ""),
477
478 _ => (404, json!({"error": "not found", "method": method, "path": path})),
479 };
480
481 send_json(stream, status, &response_json)
482}
483
484fn eval_js(window_opt:&Option<Arc<WebviewWindow<Wry>>>, js:&str) -> Result<Value, String> {
487 let window = window_opt.as_ref().ok_or("debug server not initialized")?;
488
489 let (tx, rx) = std::sync::mpsc::sync_channel(1);
490
491 window
492 .eval_with_callback(js.to_string(), move |result| {
493 let _ = tx.send(result);
494 })
495 .map_err(|e| e.to_string())?;
496
497 let result_str = rx
498 .recv_timeout(Duration::from_secs(5))
499 .map_err(|_| "timeout waiting for eval result".to_string())?;
500
501 serde_json::from_str(&result_str).map_err(|e| e.to_string())
502}
503
504fn proxy_to_cocoon(method:&str, path:&str, body:&str) -> (u16, Value) {
507 use std::net::TcpStream;
508
509 let port = cocoon_port();
510
511 let addr = format!("127.0.0.1:{}", port);
512
513 let mut stream = match TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_millis(300)) {
514 Ok(s) => s,
515
516 Err(e) => {
517 return (
518 502,
519 json!({"error":"cocoon layer unreachable","detail":e.to_string(),"port":port}),
520 );
521 },
522 };
523
524 let _ = stream.set_read_timeout(Some(Duration::from_secs(5)));
525
526 let req = format!(
527 "{} {} HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: \
528 close\r\n\r\n{}",
529 method,
530 path,
531 body.len(),
532 body
533 );
534
535 if stream.write_all(req.as_bytes()).is_err() {
536 return (502, json!({"error":"cocoon write failed"}));
537 }
538
539 let mut buf = String::new();
540
541 if stream.read_to_string(&mut buf).is_err() {
542 return (502, json!({"error":"cocoon read failed"}));
543 }
544
545 let body_idx = buf.find("\r\n\r\n").map(|i| i + 4).unwrap_or(buf.len());
547
548 let body_str = &buf[body_idx..];
549
550 let parsed:Value = serde_json::from_str(body_str).unwrap_or_else(|_| json!({"raw": body_str}));
551
552 (200, parsed)
553}
554
555fn send_json(stream:&mut std::net::TcpStream, status:u16, value:&Value) -> io::Result<()> {
557 let body =
558 serde_json::to_string(value).map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "serialization error"))?;
559
560 let status_text = match status {
561 200 => "OK",
562
563 400 => "Bad Request",
564
565 404 => "Not Found",
566
567 500 => "Internal Server Error",
568
569 502 => "Bad Gateway",
570
571 503 => "Service Unavailable",
572
573 _ => "OK",
574 };
575
576 let headers = format!(
577 "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
578 status,
579 status_text,
580 body.len()
581 );
582
583 stream.write_all(headers.as_bytes())?;
584
585 stream.write_all(body.as_bytes())?;
586
587 stream.flush()?;
588
589 Ok(())
590}