1use lingxia_log::{AttachedLogStream, LogLevel, LogMessage, LogTag, attach_log_stream_default};
7use std::sync::OnceLock;
8use std::thread;
9use std::time::Duration;
10use tungstenite::protocol::Message;
11use tungstenite::stream::MaybeTlsStream;
12use tungstenite::{Error as WsError, WebSocket, connect};
13
14mod app;
15mod browser;
16mod lxapp;
17mod lxapp_nav;
18mod lxapp_page;
19mod util;
20
21pub use lingxia_devtool_protocol::{
22 DevtoolsLogLevel, DevtoolsLogMessage, DevtoolsLogSource, DevtoolsPeerRole, DevtoolsWireMessage,
23 handlers,
24};
25
26const DEV_WS_URL_ENV: &str = "LINGXIA_DEV_WS_URL";
27
28pub fn start_devtool_bridge_from_env() {
29 static STARTED: OnceLock<()> = OnceLock::new();
30 if STARTED.set(()).is_err() {
31 return;
32 }
33
34 let ws_url = match dev_ws_url() {
35 Some(value) => value,
36 None => {
37 log::info!("Devtool bridge disabled because no dev websocket URL is configured");
38 return;
39 }
40 };
41
42 thread::spawn(move || run_dev_bridge(ws_url));
43}
44
45fn dev_ws_url() -> Option<String> {
46 std::env::var(DEV_WS_URL_ENV)
47 .ok()
48 .map(|value| value.trim().to_string())
49 .filter(|value| !value.is_empty())
50 .or_else(|| {
51 lingxia_app_context::app_config()
52 .and_then(|config| config.dev_ws_url.as_deref())
53 .map(str::trim)
54 .filter(|value| !value.is_empty())
55 .map(ToOwned::to_owned)
56 })
57}
58
59fn run_dev_bridge(ws_url: String) {
60 let mut connect_failures = 0u32;
61 loop {
62 match connect(ws_url.as_str()) {
63 Ok((mut websocket, _)) => {
64 if connect_failures > 0 {
65 log::info!(
66 "Connected devtool websocket after {} failed attempts",
67 connect_failures
68 );
69 }
70 connect_failures = 0;
71 if let Err(err) = send_wire_message(
72 &mut websocket,
73 &DevtoolsWireMessage::Hello {
74 role: DevtoolsPeerRole::Devtool,
75 },
76 ) {
77 log::warn!("Failed to send devtool hello: {}", err);
78 thread::sleep(Duration::from_millis(500));
79 continue;
80 }
81
82 configure_read_timeout(&mut websocket);
83
84 let attached = match attach_log_stream_default() {
85 Ok(attached) => attached,
86 Err(err) => {
87 log::warn!("Failed to attach devtool log stream: {}", err);
88 thread::sleep(Duration::from_millis(500));
89 continue;
90 }
91 };
92
93 if let Err(err) = bridge_loop(&mut websocket, attached) {
94 log::warn!("Devtool bridge disconnected: {}", err);
95 }
96 }
97 Err(err) => {
98 connect_failures = connect_failures.saturating_add(1);
99 log_connect_failure(connect_failures, &err);
100 }
101 }
102
103 thread::sleep(reconnect_delay(connect_failures));
104 }
105}
106
107fn reconnect_delay(connect_failures: u32) -> Duration {
108 match connect_failures {
109 0 => Duration::from_millis(500),
110 1 => Duration::from_secs(1),
111 2 => Duration::from_secs(2),
112 _ => Duration::from_secs(5),
113 }
114}
115
116fn log_connect_failure(attempt: u32, err: &WsError) {
117 if attempt == 1 {
118 log::warn!(
119 "Failed to connect devtool websocket; retrying in background: {}",
120 err
121 );
122 } else if attempt.is_multiple_of(12) {
123 log::warn!(
124 "Still unable to connect devtool websocket after {} attempts: {}",
125 attempt,
126 err
127 );
128 } else {
129 log::debug!(
130 "Failed to connect devtool websocket attempt {}: {}",
131 attempt,
132 err
133 );
134 }
135}
136
137fn bridge_loop(
138 websocket: &mut WebSocket<MaybeTlsStream<std::net::TcpStream>>,
139 attached: AttachedLogStream,
140) -> Result<(), String> {
141 let (recent, mut receiver) = attached.into_parts();
142 for chunk in recent.chunks(128) {
143 send_log_batch(websocket, chunk)?;
144 }
145
146 loop {
147 let mut batch = Vec::new();
148 while batch.len() < 64 {
149 match receiver.try_recv() {
150 Ok(message) => batch.push(message),
151 Err(tokio::sync::broadcast::error::TryRecvError::Empty) => break,
152 Err(tokio::sync::broadcast::error::TryRecvError::Lagged(skipped)) => {
153 log::warn!("Devtool log stream lagged and skipped {} messages", skipped);
154 break;
155 }
156 Err(tokio::sync::broadcast::error::TryRecvError::Closed) => {
157 return Err("log stream closed".to_string());
158 }
159 }
160 }
161
162 if !batch.is_empty() {
163 send_log_batch(websocket, &batch)?;
164 }
165
166 match websocket.read() {
167 Ok(message) => {
168 if let Some(wire) = parse_wire_message(message)? {
169 handle_incoming_message(websocket, wire)?;
170 }
171 }
172 Err(WsError::Io(err))
173 if err.kind() == std::io::ErrorKind::WouldBlock
174 || err.kind() == std::io::ErrorKind::TimedOut => {}
175 Err(WsError::ConnectionClosed) | Err(WsError::AlreadyClosed) => {
176 return Err("websocket closed".to_string());
177 }
178 Err(err) => return Err(err.to_string()),
179 }
180
181 thread::sleep(Duration::from_millis(50));
182 }
183}
184
185fn handle_incoming_message(
186 websocket: &mut WebSocket<MaybeTlsStream<std::net::TcpStream>>,
187 message: DevtoolsWireMessage,
188) -> Result<(), String> {
189 let DevtoolsWireMessage::Command {
190 command_id,
191 handler,
192 args,
193 } = message
194 else {
195 return Ok(());
196 };
197
198 let result = if let Some(result) = app::handle_app_command(&handler, args.clone()) {
199 command_result(command_id, result)
200 } else if let Some(result) = browser::handle_browser_command(&handler, args.clone()) {
201 command_result(command_id, result)
202 } else if let Some(result) = lxapp_nav::handle_lxapp_nav_command(&handler, args.clone()) {
203 command_result(command_id, result)
204 } else if let Some(result) = lxapp_page::handle_lxapp_page_command(&handler, args.clone()) {
205 command_result(command_id, result)
206 } else if let Some(result) = lxapp::handle_lxapp_command(&handler, args.clone()) {
207 command_result(command_id, result)
208 } else {
209 match handler.as_str() {
210 handlers::ECHO => DevtoolsWireMessage::Result {
211 command_id,
212 ok: true,
213 data: args,
214 error: None,
215 },
216 other => DevtoolsWireMessage::Result {
217 command_id,
218 ok: false,
219 data: None,
220 error: Some(format!("unknown handler: {}", other)),
221 },
222 }
223 };
224 send_wire_message(websocket, &result)
225}
226
227fn command_result(
228 command_id: String,
229 result: Result<Option<serde_json::Value>, String>,
230) -> DevtoolsWireMessage {
231 match result {
232 Ok(data) => DevtoolsWireMessage::Result {
233 command_id,
234 ok: true,
235 data,
236 error: None,
237 },
238 Err(error) => DevtoolsWireMessage::Result {
239 command_id,
240 ok: false,
241 data: None,
242 error: Some(error),
243 },
244 }
245}
246
247fn send_log_batch(
248 websocket: &mut WebSocket<MaybeTlsStream<std::net::TcpStream>>,
249 logs: &[LogMessage],
250) -> Result<(), String> {
251 send_wire_message(
252 websocket,
253 &DevtoolsWireMessage::LogBatch {
254 logs: logs.iter().map(devtools_log_message).collect(),
255 },
256 )
257}
258
259fn devtools_log_message(value: &LogMessage) -> DevtoolsLogMessage {
260 DevtoolsLogMessage {
261 timestamp_ms: value.timestamp_ms,
262 source: devtools_log_source(value.tag),
263 level: devtools_log_level(value.level),
264 appid: value.appid.clone(),
265 path: value.path.clone(),
266 message: value.message.clone(),
267 }
268}
269
270fn devtools_log_level(value: LogLevel) -> DevtoolsLogLevel {
271 match value {
272 LogLevel::Verbose => DevtoolsLogLevel::Verbose,
273 LogLevel::Debug => DevtoolsLogLevel::Debug,
274 LogLevel::Info => DevtoolsLogLevel::Info,
275 LogLevel::Warn => DevtoolsLogLevel::Warn,
276 LogLevel::Error => DevtoolsLogLevel::Error,
277 }
278}
279
280fn devtools_log_source(value: LogTag) -> DevtoolsLogSource {
281 match value {
282 LogTag::Native => DevtoolsLogSource::Native,
283 LogTag::WebViewConsole => DevtoolsLogSource::WebViewConsole,
284 LogTag::LxAppServiceConsole => DevtoolsLogSource::LxAppServiceConsole,
285 }
286}
287
288fn send_wire_message(
289 websocket: &mut WebSocket<MaybeTlsStream<std::net::TcpStream>>,
290 message: &DevtoolsWireMessage,
291) -> Result<(), String> {
292 let text = serde_json::to_string(message).map_err(|err| err.to_string())?;
293 websocket
294 .send(Message::Text(text.into()))
295 .map_err(|err| err.to_string())
296}
297
298fn parse_wire_message(message: Message) -> Result<Option<DevtoolsWireMessage>, String> {
299 match message {
300 Message::Text(text) => serde_json::from_str(&text)
301 .map(Some)
302 .map_err(|err| err.to_string()),
303 Message::Ping(_) | Message::Pong(_) | Message::Close(_) | Message::Frame(_) => Ok(None),
304 Message::Binary(_) => Err("binary websocket messages are not supported".to_string()),
305 }
306}
307
308fn configure_read_timeout(websocket: &mut WebSocket<MaybeTlsStream<std::net::TcpStream>>) {
309 if let MaybeTlsStream::Plain(stream) = websocket.get_mut() {
310 let _ = stream.set_read_timeout(Some(Duration::from_millis(100)));
311 }
312}