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
418 const svcId = require(vs/platform/instantiation/common/instantiation).IInstantiationService;
419
420 const ws = (globalThis.MonacoEnvironment || globalThis).__workbench__;
421
422 // Resolve through the workbench command service if available.
423 const cmdSvc = ws?.commandService
424 || ws?.services?.get?.(require(vs/platform/commands/common/commands).ICommandService);
425
426 if (!cmdSvc) return JSON.stringify({{error:"command service unavailable"}});
427
428 const args = {0};
429
430 const result = await cmdSvc.executeCommand({1}, ...args);
431
432 return JSON.stringify({{ ok: true, result: result ?? null }});
433 }} catch (e) {{ return JSON.stringify({{ ok:false, error: String(e?.stack||e) }}); }}
434 }})()"#,
435 args,
436 json!(id)
437 );
438
439 match eval_js(window_opt, &js) {
440 Ok(v) => (200, v),
441
442 Err(e) => (500, json!({"error": e})),
443 }
444 },
445
446 ("POST", "/vscode/diff") => {
447 let parsed_body:Value =
448 serde_json::from_str(&body).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
449
450 let left = parsed_body["left"].as_str().unwrap_or("");
451
452 let right = parsed_body["right"].as_str().unwrap_or("");
453
454 let title = parsed_body["title"].as_str().unwrap_or("Diff");
455
456 if left.is_empty() || right.is_empty() {
457 (400, json!({"error":"left and right required"}))
458 } else {
459 let js = format!(
460 r#"(async function(){{
461 try {{
462 const URI = require(vs/base/common/uri).URI;
463
464 const cmdSvc = (globalThis).__workbench__?.commandService;
465
466 if (!cmdSvc) return JSON.stringify({{error:"command service unavailable"}});
467
468 await cmdSvc.executeCommand(vscode.diff, URI.parse({0}), URI.parse({1}), {2});
469
470 return JSON.stringify({{ok:true}});
471 }} catch (e) {{ return JSON.stringify({{ok:false,error:String(e?.stack||e)}}); }}
472 }})()"#,
473 json!(left),
474 json!(right),
475 json!(title)
476 );
477
478 match eval_js(window_opt, &js) {
479 Ok(v) => (200, v),
480
481 Err(e) => (500, json!({"error": e})),
482 }
483 }
484 },
485
486 ("GET", "/extensions") => proxy_to_cocoon("GET", "/extensions", ""),
488
489 _ => (404, json!({"error": "not found", "method": method, "path": path})),
490 };
491
492 send_json(stream, status, &response_json)
493}
494
495fn eval_js(window_opt:&Option<Arc<WebviewWindow<Wry>>>, js:&str) -> Result<Value, String> {
498 let window = window_opt.as_ref().ok_or("debug server not initialized")?;
499
500 let (tx, rx) = std::sync::mpsc::sync_channel(1);
501
502 window
503 .eval_with_callback(js.to_string(), move |result| {
504 let _ = tx.send(result);
505 })
506 .map_err(|e| e.to_string())?;
507
508 let result_str = rx
509 .recv_timeout(Duration::from_secs(5))
510 .map_err(|_| "timeout waiting for eval result".to_string())?;
511
512 serde_json::from_str(&result_str).map_err(|e| e.to_string())
513}
514
515fn proxy_to_cocoon(method:&str, path:&str, body:&str) -> (u16, Value) {
518 use std::net::TcpStream;
519
520 let port = cocoon_port();
521
522 let addr = format!("127.0.0.1:{}", port);
523
524 let mut stream = match TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_millis(300)) {
525 Ok(s) => s,
526
527 Err(e) => {
528 return (
529 502,
530 json!({"error":"cocoon layer unreachable","detail":e.to_string(),"port":port}),
531 );
532 },
533 };
534
535 let _ = stream.set_read_timeout(Some(Duration::from_secs(5)));
536
537 let req = format!(
538 "{} {} HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: \
539 close\r\n\r\n{}",
540 method,
541 path,
542 body.len(),
543 body
544 );
545
546 if stream.write_all(req.as_bytes()).is_err() {
547 return (502, json!({"error":"cocoon write failed"}));
548 }
549
550 let mut buf = String::new();
551
552 if stream.read_to_string(&mut buf).is_err() {
553 return (502, json!({"error":"cocoon read failed"}));
554 }
555
556 let body_idx = buf.find("\r\n\r\n").map(|i| i + 4).unwrap_or(buf.len());
558
559 let body_str = &buf[body_idx..];
560
561 let parsed:Value = serde_json::from_str(body_str).unwrap_or_else(|_| json!({"raw": body_str}));
562
563 (200, parsed)
564}
565
566fn send_json(stream:&mut std::net::TcpStream, status:u16, value:&Value) -> io::Result<()> {
568 let body =
569 serde_json::to_string(value).map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "serialization error"))?;
570
571 let status_text = match status {
572 200 => "OK",
573
574 400 => "Bad Request",
575
576 404 => "Not Found",
577
578 500 => "Internal Server Error",
579
580 502 => "Bad Gateway",
581
582 503 => "Service Unavailable",
583
584 _ => "OK",
585 };
586
587 let headers = format!(
588 "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
589 status,
590 status_text,
591 body.len()
592 );
593
594 stream.write_all(headers.as_bytes())?;
595
596 stream.write_all(body.as_bytes())?;
597
598 stream.flush()?;
599
600 Ok(())
601}