From 2ef5795b76aa370e29eb5b93cffdeac48d167382 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 16 May 2026 19:16:40 -0400 Subject: [PATCH 1/2] fix: keep Rozenite DevTools on Metro --- server/src/devtools.rs | 134 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 123 insertions(+), 11 deletions(-) diff --git a/server/src/devtools.rs b/server/src/devtools.rs index a538fd56..cd0c45cb 100644 --- a/server/src/devtools.rs +++ b/server/src/devtools.rs @@ -720,7 +720,11 @@ fn proxied_target_port(target_id: &str) -> Result { fn metro_devtools_frontend_url(port: u16, entry: &Value, web_socket_debugger_url: &str) -> String { let frontend = string_value(entry, "devtoolsFrontendUrl") .unwrap_or_else(|| "/debugger-frontend/rn_fusebox.html".to_owned()); - let (path, query) = split_path_query(&frontend); + let path = frontend_path_for_match(&frontend); + if is_rozenite_frontend_path(path) { + return metro_served_frontend_url(port, entry, &frontend, web_socket_debugger_url); + } + let (_, query) = split_path_query(&frontend); if path.ends_with("/rn_fusebox.html") { return local_metro_fusebox_frontend_url(query, web_socket_debugger_url); } @@ -733,16 +737,44 @@ fn metro_devtools_frontend_url(port: u16, entry: &Value, web_socket_debugger_url format!("http://{host}{frontend}") } -fn websocket_authority(value: &str) -> Option { - value - .strip_prefix("ws://") - .or_else(|| value.strip_prefix("wss://")) - .and_then(|rest| rest.split('/').next()) - .filter(|authority| !authority.is_empty()) - .map(ToOwned::to_owned) +fn frontend_path_for_match(frontend: &str) -> &str { + let (path, _) = split_path_query(frontend); + if let Some(rest) = path + .strip_prefix("http://") + .or_else(|| path.strip_prefix("https://")) + { + return rest + .find('/') + .and_then(|index| rest.get(index..)) + .unwrap_or("/"); + } + path } -fn local_metro_fusebox_frontend_url(query: Option<&str>, web_socket_debugger_url: &str) -> String { +fn is_rozenite_frontend_path(path: &str) -> bool { + path == "/rozenite" || path.starts_with("/rozenite/") +} + +fn metro_served_frontend_url( + port: u16, + entry: &Value, + frontend: &str, + web_socket_debugger_url: &str, +) -> String { + let (base, query) = split_path_query(frontend); + let base = if base.starts_with("http://") || base.starts_with("https://") { + base.to_owned() + } else { + let host = string_value(entry, "webSocketDebuggerUrl") + .and_then(|url| websocket_authority(&url)) + .unwrap_or_else(|| format!("{DEVTOOLS_HOST}:{port}")); + format!("http://{host}{base}") + }; + let query = metro_frontend_query_with_socket(query, web_socket_debugger_url); + format!("{base}?{query}") +} + +fn metro_frontend_query_with_socket(query: Option<&str>, web_socket_debugger_url: &str) -> String { let socket_param = web_socket_debugger_url .trim_start_matches("ws://") .trim_start_matches("wss://"); @@ -754,11 +786,29 @@ fn local_metro_fusebox_frontend_url(query: Option<&str>, web_socket_debugger_url params.extend( query .split('&') - .filter(|param| !param.is_empty() && !param.starts_with("ws=")) + .filter(|param| { + !param.is_empty() && !param.starts_with("ws=") && !param.starts_with("wss=") + }) .map(ToOwned::to_owned), ); } - format!("/chrome-devtools-ui/rn_fusebox.html?{}", params.join("&")) + params.join("&") +} + +fn websocket_authority(value: &str) -> Option { + value + .strip_prefix("ws://") + .or_else(|| value.strip_prefix("wss://")) + .and_then(|rest| rest.split('/').next()) + .filter(|authority| !authority.is_empty()) + .map(ToOwned::to_owned) +} + +fn local_metro_fusebox_frontend_url(query: Option<&str>, web_socket_debugger_url: &str) -> String { + format!( + "/chrome-devtools-ui/rn_fusebox.html?{}", + metro_frontend_query_with_socket(query, web_socket_debugger_url) + ) } fn split_path_query(value: &str) -> (&str, Option<&str>) { @@ -1619,3 +1669,65 @@ fn timestamp_ms() -> f64 { .as_secs_f64() * 1000.0 } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn metro_devtools_frontend_url_keeps_rozenite_frontend_on_metro() { + let entry = json!({ + "devtoolsFrontendUrl": "/rozenite/rn_fusebox.html?ws=127.0.0.1:8081/inspector/debug&device=ios", + "webSocketDebuggerUrl": "ws://127.0.0.1:8081/inspector/debug" + }); + + let url = metro_devtools_frontend_url( + 8081, + &entry, + "ws://127.0.0.1:4310/api/simulators/ABC/devtools/targets/metro-8081-target/socket", + ); + + assert_eq!( + url, + "http://127.0.0.1:8081/rozenite/rn_fusebox.html?ws=127.0.0.1%3A4310%2Fapi%2Fsimulators%2FABC%2Fdevtools%2Ftargets%2Fmetro-8081-target%2Fsocket&device=ios" + ); + } + + #[test] + fn metro_devtools_frontend_url_preserves_absolute_rozenite_origin() { + let entry = json!({ + "devtoolsFrontendUrl": "http://localhost:8081/rozenite/rn_fusebox.html?panel=redux&ws=localhost:8081/inspector/debug", + "webSocketDebuggerUrl": "ws://127.0.0.1:8081/inspector/debug" + }); + + let url = metro_devtools_frontend_url( + 8081, + &entry, + "ws://simdeck.local:4310/api/simulators/ABC/devtools/targets/metro-8081-target/socket", + ); + + assert_eq!( + url, + "http://localhost:8081/rozenite/rn_fusebox.html?ws=simdeck.local%3A4310%2Fapi%2Fsimulators%2FABC%2Fdevtools%2Ftargets%2Fmetro-8081-target%2Fsocket&panel=redux" + ); + } + + #[test] + fn metro_devtools_frontend_url_keeps_embedded_frontend_for_plain_fusebox() { + let entry = json!({ + "devtoolsFrontendUrl": "/debugger-frontend/rn_fusebox.html?ws=127.0.0.1:8081/inspector/debug&device=ios", + "webSocketDebuggerUrl": "ws://127.0.0.1:8081/inspector/debug" + }); + + let url = metro_devtools_frontend_url( + 8081, + &entry, + "ws://127.0.0.1:4310/api/simulators/ABC/devtools/targets/metro-8081-target/socket", + ); + + assert_eq!( + url, + "/chrome-devtools-ui/rn_fusebox.html?ws=127.0.0.1%3A4310%2Fapi%2Fsimulators%2FABC%2Fdevtools%2Ftargets%2Fmetro-8081-target%2Fsocket&device=ios" + ); + } +} From 15cb3f0c0e802497571859756b816077644c2c13 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 16 May 2026 20:58:30 -0400 Subject: [PATCH 2/2] fix: keep Metro DevTools reconnectable --- server/src/api/routes.rs | 1 + server/src/devtools.rs | 81 +++++++++++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/server/src/api/routes.rs b/server/src/api/routes.rs index 9abf5bc2..63d63568 100644 --- a/server/src/api/routes.rs +++ b/server/src/api/routes.rs @@ -998,6 +998,7 @@ async fn chrome_devtools_targets( devtools::discover_external_devtools_targets( &udid, origin.as_deref(), + Some(&state.config.access_token), simulator.as_ref().map(|simulator| simulator.name.as_str()), simulator .as_ref() diff --git a/server/src/devtools.rs b/server/src/devtools.rs index cd0c45cb..38e0fd56 100644 --- a/server/src/devtools.rs +++ b/server/src/devtools.rs @@ -216,6 +216,7 @@ pub fn build_target( pub async fn discover_external_devtools_targets( udid: &str, http_origin: Option<&str>, + access_token: Option<&str>, simulator_name: Option<&str>, simulator_device_type_name: Option<&str>, ) -> (Vec, Vec) { @@ -264,7 +265,7 @@ pub async fn discover_external_devtools_targets( if !metro_target_matches_simulator(entry, simulator_name, simulator_device_type_name) { continue; } - let target = build_metro_target(udid, http_origin, port, entry); + let target = build_metro_target(udid, http_origin, access_token, port, entry); if seen_ids.insert(target.id.clone()) { targets.push(target); } @@ -309,7 +310,7 @@ pub async fn proxied_websocket_url_for_target(target_id: &str) -> Result bool { fn build_metro_target( udid: &str, http_origin: Option<&str>, + access_token: Option<&str>, port: u16, entry: &Value, ) -> ChromeDevToolsTarget { @@ -612,6 +614,7 @@ fn build_metro_target( let description = string_value(entry, "description") .unwrap_or_else(|| "React Native Metro DevTools target".to_owned()); let web_socket_path = format!("/api/simulators/{udid}/devtools/targets/{id}/socket"); + let web_socket_path = websocket_path_with_access_token(web_socket_path, access_token); let web_socket_debugger_url = websocket_url(http_origin.unwrap_or(""), &web_socket_path); let devtools_frontend_url = metro_devtools_frontend_url(port, entry, &web_socket_debugger_url); let app_name = app_id.clone().or_else(|| Some(title.clone())); @@ -718,10 +721,11 @@ fn proxied_target_port(target_id: &str) -> Result { } fn metro_devtools_frontend_url(port: u16, entry: &Value, web_socket_debugger_url: &str) -> String { - let frontend = string_value(entry, "devtoolsFrontendUrl") - .unwrap_or_else(|| "/debugger-frontend/rn_fusebox.html".to_owned()); + let Some(frontend) = string_value(entry, "devtoolsFrontendUrl") else { + return local_metro_fusebox_frontend_url(None, web_socket_debugger_url); + }; let path = frontend_path_for_match(&frontend); - if is_rozenite_frontend_path(path) { + if is_metro_hosted_react_native_frontend_path(path) { return metro_served_frontend_url(port, entry, &frontend, web_socket_debugger_url); } let (_, query) = split_path_query(&frontend); @@ -751,8 +755,11 @@ fn frontend_path_for_match(frontend: &str) -> &str { path } -fn is_rozenite_frontend_path(path: &str) -> bool { - path == "/rozenite" || path.starts_with("/rozenite/") +fn is_metro_hosted_react_native_frontend_path(path: &str) -> bool { + path == "/rozenite" + || path.starts_with("/rozenite/") + || path == "/debugger-frontend" + || path.starts_with("/debugger-frontend/") } fn metro_served_frontend_url( @@ -804,6 +811,20 @@ fn websocket_authority(value: &str) -> Option { .map(ToOwned::to_owned) } +fn websocket_path_with_access_token(path: String, access_token: Option<&str>) -> String { + let Some(access_token) = access_token + .map(str::trim) + .filter(|access_token| !access_token.is_empty()) + else { + return path; + }; + let separator = if path.contains('?') { '&' } else { '?' }; + format!( + "{path}{separator}simdeckToken={}", + percent_encode_query_component(access_token) + ) +} + fn local_metro_fusebox_frontend_url(query: Option<&str>, web_socket_debugger_url: &str) -> String { format!( "/chrome-devtools-ui/rn_fusebox.html?{}", @@ -1713,7 +1734,7 @@ mod tests { } #[test] - fn metro_devtools_frontend_url_keeps_embedded_frontend_for_plain_fusebox() { + fn metro_devtools_frontend_url_keeps_explicit_fusebox_frontend_on_metro() { let entry = json!({ "devtoolsFrontendUrl": "/debugger-frontend/rn_fusebox.html?ws=127.0.0.1:8081/inspector/debug&device=ios", "webSocketDebuggerUrl": "ws://127.0.0.1:8081/inspector/debug" @@ -1727,7 +1748,49 @@ mod tests { assert_eq!( url, - "/chrome-devtools-ui/rn_fusebox.html?ws=127.0.0.1%3A4310%2Fapi%2Fsimulators%2FABC%2Fdevtools%2Ftargets%2Fmetro-8081-target%2Fsocket&device=ios" + "http://127.0.0.1:8081/debugger-frontend/rn_fusebox.html?ws=127.0.0.1%3A4310%2Fapi%2Fsimulators%2FABC%2Fdevtools%2Ftargets%2Fmetro-8081-target%2Fsocket&device=ios" + ); + } + + #[test] + fn metro_devtools_frontend_url_uses_embedded_frontend_when_metro_omits_frontend() { + let entry = json!({ + "webSocketDebuggerUrl": "ws://127.0.0.1:8081/inspector/debug" + }); + + let url = metro_devtools_frontend_url( + 8081, + &entry, + "ws://127.0.0.1:4310/api/simulators/ABC/devtools/targets/metro-8081-target/socket", + ); + + assert_eq!( + url, + "/chrome-devtools-ui/rn_fusebox.html?ws=127.0.0.1%3A4310%2Fapi%2Fsimulators%2FABC%2Fdevtools%2Ftargets%2Fmetro-8081-target%2Fsocket" ); } + + #[test] + fn build_metro_target_adds_access_token_to_proxied_socket() { + let entry = json!({ + "id": "target-1", + "devtoolsFrontendUrl": "/debugger-frontend/rn_fusebox.html?ws=127.0.0.1:8081/inspector/debug", + "webSocketDebuggerUrl": "ws://127.0.0.1:8081/inspector/debug" + }); + + let target = build_metro_target( + "ABC", + Some("http://127.0.0.1:4310"), + Some("secret token"), + 8081, + &entry, + ); + + assert!(target.web_socket_debugger_url.ends_with( + "/api/simulators/ABC/devtools/targets/metro-8081-target-1/socket?simdeckToken=secret%20token" + )); + assert!(target.devtools_frontend_url.contains( + "ws=127.0.0.1%3A4310%2Fapi%2Fsimulators%2FABC%2Fdevtools%2Ftargets%2Fmetro-8081-target-1%2Fsocket%3FsimdeckToken%3Dsecret%2520token" + )); + } }