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 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 "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
104pub fn install(window:&WebviewWindow<Wry>) {
112 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
130fn 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
162fn handle_connection(window_opt:&Option<Arc<WebviewWindow<Wry>>>, stream:&mut std::net::TcpStream) -> io::Result<()> {
164 if window_opt.is_none() {
166 send_json(stream, 503, &json!({"error": "debug server not initialized"}))?;
167 return Ok(());
168 }
169
170 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 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 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 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 let (status, response_json) = match (method.as_str(), path) {
219 ("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 ("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 ("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 ("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
408fn 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
424fn 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 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
462fn 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}