Skip to main content

Mountain/Binary/Debug/
WebkitServer.rs

1//! Debug server for inspecting webview content via HTTP API.
2//! Only compiled in debug builds.
3
4use std::{
5	collections::HashMap,
6	io::{self, BufRead, BufReader, Read, Write},
7	net::TcpListener,
8	sync::{Arc, Mutex},
9	time::Duration,
10};
11
12use once_cell::sync::Lazy;
13use serde_json::{Value, json};
14use tauri::{WebviewWindow, Wry};
15use url::Url;
16
17/// Global storage for the webview window used by the debug server.
18static WINDOW:Lazy<Mutex<Option<Arc<WebviewWindow<Wry>>>>> = Lazy::new(|| Mutex::new(None));
19
20/// Installs the debug server and stores a reference to the webview window.
21/// This function should be called once during app setup in debug builds.
22pub fn install(window:&WebviewWindow<Wry>) {
23	// Store the window for later use
24	let mut guard = WINDOW.lock().unwrap();
25	*guard = Some(Arc::new(window.clone()));
26
27	// Start the HTTP server in a background thread if enabled
28	let enable = std::env::var("DebugServer").map(|v| !v.is_empty() && v != "0").unwrap_or(false);
29	if enable {
30		std::thread::spawn(|| {
31			start_server();
32		});
33	}
34}
35
36/// Main server loop listening for TCP connections.
37fn start_server() {
38	let port = std::env::var("DebugServerPort")
39		.ok()
40		.and_then(|p| p.parse().ok())
41		.unwrap_or(9933);
42
43	let listener = match TcpListener::bind(("127.0.0.1", port)) {
44		Ok(l) => l,
45		Err(e) => {
46			eprintln!("[WebkitDebug] Failed to bind to 127.0.0.1:{}: {}", port, e);
47			return;
48		},
49	};
50	eprintln!("[WebkitDebug] Server listening on http://127.0.0.1:{}", port);
51
52	for stream in listener.incoming() {
53		match stream {
54			Ok(mut stream) => {
55				let window_opt = WINDOW.lock().unwrap().clone();
56				std::thread::spawn(move || {
57					if let Err(e) = handle_connection(&window_opt, &mut stream) {
58						eprintln!("[WebkitDebug] Connection error: {}", e);
59					}
60				});
61			},
62			Err(e) => eprintln!("[WebkitDebug] Accept error: {}", e),
63		}
64	}
65}
66
67/// Handles a single HTTP connection, dispatches based on method and path.
68fn handle_connection(window_opt:&Option<Arc<WebviewWindow<Wry>>>, stream:&mut std::net::TcpStream) -> io::Result<()> {
69	// Early check for window initialization
70	if window_opt.is_none() {
71		send_json(stream, 503, &json!({"error": "debug server not initialized"}))?;
72		return Ok(());
73	}
74
75	// Read request data (method, path_and_query, body)
76	let (method, path_and_query, body) = {
77		let mut reader = BufReader::new(&mut *stream);
78		let mut request_line = String::new();
79		reader.read_line(&mut request_line)?;
80		let request_line = request_line.trim_end();
81		let parts:Vec<&str> = request_line.split_whitespace().collect();
82		if parts.len() != 3 {
83			// Malformed request line; return early after dropping reader
84			return Err(io::Error::new(io::ErrorKind::InvalidInput, "bad request line"));
85		}
86		let method = parts[0].to_string();
87		let path_and_query = parts[1].to_string();
88
89		// Read headers
90		let mut headers = HashMap::new();
91		loop {
92			let mut line = String::new();
93			let n = reader.read_line(&mut line)?;
94			if n == 0 || line == "\r\n" {
95				break;
96			}
97			if let Some(idx) = line.find(':') {
98				let name = line[..idx].trim().to_uppercase();
99				let value = line[idx + 1..].trim().to_string();
100				headers.insert(name, value);
101			}
102		}
103
104		// Read body if Content-Length present
105		let body = if let Some(len_str) = headers.get("CONTENT-LENGTH") {
106			let len:usize = len_str.parse().unwrap_or(0);
107			let mut body_bytes = vec![0; len];
108			reader.read_exact(&mut body_bytes)?;
109			String::from_utf8_lossy(&body_bytes).to_string()
110		} else {
111			String::new()
112		};
113
114		(method, path_and_query, body)
115	};
116
117	// Parse URL to get path and query
118	let full_url = format!("http://localhost{}", path_and_query);
119	let parsed = Url::parse(&full_url).map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid URL"))?;
120	let path = parsed.path();
121	let mut query_pairs = parsed.query_pairs();
122
123	// Dispatch request
124	let (status, response_json) = match method.as_str() {
125		"GET" if path == "/console" => {
126			let js = r#"(function() {
127                const logs = window.__MOUNTAIN_DEBUG_CONSOLE || [];
128                window.__MOUNTAIN_DEBUG_CONSOLE = [];
129                return JSON.stringify(logs);
130            })()"#;
131			match eval_js(window_opt, js) {
132				Ok(value) => (200, json!({"logs": value})),
133				Err(e) => (500, json!({"error": e})),
134			}
135		},
136		"GET" if path == "/eval" => {
137			let js = query_pairs
138				.find(|(k, _)| k == "js")
139				.map(|(_, v)| v.into_owned())
140				.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing js parameter"))?;
141			match eval_js(window_opt, &js) {
142				Ok(value) => (200, json!({"result": value})),
143				Err(e) => (500, json!({"error": e})),
144			}
145		},
146		"POST" if path == "/execute" => {
147			let parsed_body:Value =
148				serde_json::from_str(&body).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
149			let js = parsed_body["js"]
150				.as_str()
151				.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing js field"))?;
152			if js.is_empty() {
153				(400, json!({"error": "empty js"}))
154			} else {
155				match eval_js(window_opt, js) {
156					Ok(val) => (200, json!({"result": val})),
157					Err(e) => (500, json!({"error": e})),
158				}
159			}
160		},
161		"GET" if path == "/iframes" => {
162			let js = r#"(function() {
163                const frames = document.querySelectorAll('iframe');
164                const arr = [];
165                frames.forEach(f => {
166                    arr.push({
167                        src: f.src,
168                        id: f.id,
169                        name: f.name,
170                        contentWindow: !!f.contentWindow
171                    });
172                });
173                return JSON.stringify(arr);
174            })()"#;
175			match eval_js(window_opt, js) {
176				Ok(value) => (200, json!({"iframes": value})),
177				Err(e) => (500, json!({"error": e})),
178			}
179		},
180		_ => (404, json!({"error": "not found"})),
181	};
182
183	send_json(stream, status, &response_json)
184}
185
186/// Evaluates JavaScript in the webview and returns the result as a
187/// serde_json::Value.
188fn eval_js(window_opt:&Option<Arc<WebviewWindow<Wry>>>, js:&str) -> Result<Value, String> {
189	let window = window_opt.as_ref().ok_or("debug server not initialized")?;
190	let (tx, rx) = std::sync::mpsc::sync_channel(1);
191	window
192		.eval_with_callback(js.to_string(), move |result| {
193			let _ = tx.send(result);
194		})
195		.map_err(|e| e.to_string())?;
196	let result_str = rx
197		.recv_timeout(Duration::from_secs(5))
198		.map_err(|_| "timeout waiting for eval result".to_string())?;
199	serde_json::from_str(&result_str).map_err(|e| e.to_string())
200}
201
202/// Sends a JSON response with the given status code.
203fn send_json(stream:&mut std::net::TcpStream, status:u16, value:&Value) -> io::Result<()> {
204	let body =
205		serde_json::to_string(value).map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "serialization error"))?;
206	let status_text = match status {
207		200 => "OK",
208		400 => "Bad Request",
209		404 => "Not Found",
210		500 => "Internal Server Error",
211		503 => "Service Unavailable",
212		_ => "OK",
213	};
214	let headers = format!(
215		"HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
216		status,
217		status_text,
218		body.len()
219	);
220	stream.write_all(headers.as_bytes())?;
221	stream.write_all(body.as_bytes())?;
222	stream.flush()?;
223	Ok(())
224}