Skip to main content

lingxia_devtool/
lib.rs

1//! Devtool runtime bridge and protocol helpers for LingXia apps.
2//!
3//! Host libraries decide how this service is installed into their own
4//! `HostAddon`; this crate only exposes the service entry points.
5
6use 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}