use makepad_live_id::LiveId; use makepad_micro_serde::{DeBin, SerBin}; use makepad_script_std::makepad_network::{ HttpMethod, HttpRequest, NetworkConfig, NetworkResponse, NetworkRuntime, WsMessage, WsSend, }; use makepad_studio_hub::{HubConfig, MountConfig, StudioHub}; use makepad_studio_protocol::hub_protocol::{ ClientId, ClientToHub, ClientToHubEnvelope, HubToClient, QueryId, }; use makepad_studio_protocol::{ AppToStudio, AppToStudioVec, StudioToApp, StudioToAppVec, WidgetQueryResponse, WidgetSnapshot, WidgetSnapshotResponse, WidgetTreeDumpResponse, }; use std::fs; use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}; use std::time::{Duration, Instant}; fn find_free_port() -> Option { let listener = TcpListener::bind(("127.0.0.1", 0)).ok()?; Some(listener.local_addr().ok()?.port()) } fn wait_for_event( runtime: &NetworkRuntime, timeout: Duration, mut matcher: F, ) -> Option where F: FnMut(&NetworkResponse) -> bool, { let deadline = Instant::now() + timeout; while Instant::now() < deadline { if let Some(event) = runtime.recv_timeout(Duration::from_millis(50)) { if matcher(&event) { return Some(event); } } } None } fn wait_for_ws_binary(runtime: &NetworkRuntime, socket_id: LiveId, timeout: Duration) -> Vec { let event = wait_for_event(runtime, timeout, |event| { matches!( event, NetworkResponse::WsMessage { socket_id: id, message: WsMessage::Binary(_) } if *id == socket_id ) }) .expect("did not receive ws binary message"); match event { NetworkResponse::WsMessage { message: WsMessage::Binary(data), .. } => data, _ => unreachable!(), } } fn wait_for_app_socket_registration( runtime: &NetworkRuntime, ui_socket: LiveId, client_id: ClientId, build_id: QueryId, ) { let deadline = Instant::now() + Duration::from_secs(3); let mut counter = 99u64; while Instant::now() < deadline { let request = ClientToHubEnvelope { query_id: QueryId::new(client_id, counter), msg: ClientToHub::ListAppSockets, }; runtime .ws_send(ui_socket, WsSend::Binary(request.serialize_bin())) .expect("send app socket list request"); counter = counter.wrapping_add(1); let Some(event) = wait_for_event(runtime, Duration::from_millis(50), |event| { matches!( event, NetworkResponse::WsMessage { socket_id: id, message: WsMessage::Binary(_) } if *id == ui_socket ) }) else { continue; }; let NetworkResponse::WsMessage { message: WsMessage::Binary(data), .. } = event else { continue; }; let Ok(msg) = HubToClient::deserialize_bin(&data) else { continue; }; if let HubToClient::AppSockets { sockets } = msg { if sockets .iter() .any(|socket| socket.build_id == Some(build_id)) { return; } } } panic!("app socket for build {} did not register", build_id.0); } #[test] fn websocket_app_bridge_widget_dump_roundtrip() { let dir = makepad_studio_hub::test_support::tempdir().unwrap(); fs::create_dir_all(dir.path().join("src")).unwrap(); fs::write(dir.path().join("src/lib.rs"), "fn main() {}\n").unwrap(); let Some(port) = find_free_port() else { return; }; let config = HubConfig { listen_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port), post_max_size: 1024 * 1024, mounts: vec![MountConfig { name: "repo".to_string(), path: dir.path().to_path_buf(), }], enable_in_process_gateway: false, }; let _backend = match StudioHub::start_headless(config) { Ok(v) => v, Err(err) => { if err.contains("failed to bind") { return; } panic!("start backend failed: {}", err); } }; let runtime = NetworkRuntime::new(NetworkConfig::default()); let ui_socket = LiveId::from_str("studio2.backend.ui"); let ui_request = HttpRequest::new(format!("ws://127.0.0.1:{port}/ui"), HttpMethod::GET); if runtime.ws_open(ui_socket, ui_request).is_err() { return; } let ui_opened = wait_for_event( &runtime, Duration::from_secs(3), |event| matches!(event, NetworkResponse::WsOpened { socket_id: id } if *id == ui_socket), ); assert!(ui_opened.is_some(), "did not receive ui WsOpened"); let hello_bin = wait_for_ws_binary(&runtime, ui_socket, Duration::from_secs(3)); let hello = HubToClient::deserialize_bin(&hello_bin).expect("decode hello"); let client_id = match hello { HubToClient::Hello { client_id } => client_id, other => panic!("expected hello, got {:?}", other), }; assert_ne!(client_id, ClientId(u16::MAX)); let build_id = QueryId::new(client_id, 100); let app_socket = LiveId::from_str("studio2.backend.app"); let app_request = HttpRequest::new( format!( "ws://127.0.0.1:{port}/app?build={}&crate=makepad-example-xr", build_id.0 ), HttpMethod::GET, ); runtime .ws_open(app_socket, app_request) .expect("open app socket"); let app_opened = wait_for_event( &runtime, Duration::from_secs(3), |event| matches!(event, NetworkResponse::WsOpened { socket_id: id } if *id == app_socket), ); assert!(app_opened.is_some(), "did not receive app WsOpened"); wait_for_app_socket_registration(&runtime, ui_socket, client_id, build_id); let query_id = QueryId::new(client_id, 1); let ui_request = ClientToHubEnvelope { query_id, msg: ClientToHub::WidgetTreeDump { build_id }, }; runtime .ws_send(ui_socket, WsSend::Binary(ui_request.serialize_bin())) .expect("send widget dump request"); let app_incoming = wait_for_ws_binary(&runtime, app_socket, Duration::from_secs(3)); let app_msg = StudioToAppVec::deserialize_bin(&app_incoming).expect("decode app command"); assert_eq!(app_msg.0.len(), 1); match &app_msg.0[0] { StudioToApp::WidgetTreeDump(request) => assert_eq!(request.request_id, query_id.0), other => panic!("unexpected app message: {:?}", other), } let response = AppToStudioVec(vec![AppToStudio::WidgetTreeDump(WidgetTreeDumpResponse { request_id: query_id.0, dump: "W1 root View 0 0 10 10".to_string(), })]); runtime .ws_send(app_socket, WsSend::Binary(response.serialize_bin())) .expect("send app response"); let ui_incoming = wait_for_ws_binary(&runtime, ui_socket, Duration::from_secs(3)); let ui_msg = HubToClient::deserialize_bin(&ui_incoming).expect("decode ui response"); match ui_msg { HubToClient::WidgetTreeDump { query_id: got_query, build_id: got_build, dump, } => { assert_eq!(got_query, query_id); assert_eq!(got_build, build_id); assert!(dump.contains("root")); } other => panic!("unexpected ui response: {:?}", other), } let _ = runtime.ws_close(app_socket); let _ = runtime.ws_close(ui_socket); } #[test] fn websocket_app_bridge_widget_query_roundtrip() { let dir = makepad_studio_hub::test_support::tempdir().unwrap(); fs::create_dir_all(dir.path().join("src")).unwrap(); fs::write(dir.path().join("src/lib.rs"), "fn main() {}\n").unwrap(); let Some(port) = find_free_port() else { return; }; let config = HubConfig { listen_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port), post_max_size: 1024 * 1024, mounts: vec![MountConfig { name: "repo".to_string(), path: dir.path().to_path_buf(), }], enable_in_process_gateway: false, }; let _backend = match StudioHub::start_headless(config) { Ok(v) => v, Err(err) => { if err.contains("failed to bind") { return; } panic!("start backend failed: {}", err); } }; let runtime = NetworkRuntime::new(NetworkConfig::default()); let ui_socket = LiveId::from_str("studio2.backend.ui.query"); let ui_request = HttpRequest::new(format!("ws://127.0.0.1:{port}/ui"), HttpMethod::GET); if runtime.ws_open(ui_socket, ui_request).is_err() { return; } let ui_opened = wait_for_event( &runtime, Duration::from_secs(3), |event| matches!(event, NetworkResponse::WsOpened { socket_id: id } if *id == ui_socket), ); assert!(ui_opened.is_some(), "did not receive ui WsOpened"); let hello_bin = wait_for_ws_binary(&runtime, ui_socket, Duration::from_secs(3)); let hello = HubToClient::deserialize_bin(&hello_bin).expect("decode hello"); let client_id = match hello { HubToClient::Hello { client_id } => client_id, other => panic!("expected hello, got {:?}", other), }; let build_id = QueryId::new(client_id, 100); let app_socket = LiveId::from_str("studio2.backend.app.query"); let app_request = HttpRequest::new( format!( "ws://127.0.0.1:{port}/app?build={}&crate=makepad-example-xr", build_id.0 ), HttpMethod::GET, ); runtime .ws_open(app_socket, app_request) .expect("open app socket"); let app_opened = wait_for_event( &runtime, Duration::from_secs(3), |event| matches!(event, NetworkResponse::WsOpened { socket_id: id } if *id == app_socket), ); assert!(app_opened.is_some(), "did not receive app WsOpened"); wait_for_app_socket_registration(&runtime, ui_socket, client_id, build_id); let query_id = QueryId::new(client_id, 2); let ui_request = ClientToHubEnvelope { query_id, msg: ClientToHub::WidgetQuery { build_id, query: "id:math_tab".to_string(), }, }; runtime .ws_send(ui_socket, WsSend::Binary(ui_request.serialize_bin())) .expect("send widget query request"); let app_incoming = wait_for_ws_binary(&runtime, app_socket, Duration::from_secs(3)); let app_msg = StudioToAppVec::deserialize_bin(&app_incoming).expect("decode app command"); assert_eq!(app_msg.0.len(), 1); match &app_msg.0[0] { StudioToApp::WidgetQuery(request) => { assert_eq!(request.request_id, query_id.0); assert_eq!(request.query, "id:math_tab"); } other => panic!("unexpected app message: {:?}", other), } let response = AppToStudioVec(vec![AppToStudio::WidgetQuery(WidgetQueryResponse { request_id: query_id.0, query: "id:math_tab".to_string(), rects: vec!["DT math_tab DockTab 10 20 30 40".to_string()], })]); runtime .ws_send(app_socket, WsSend::Binary(response.serialize_bin())) .expect("send app response"); let ui_incoming = wait_for_ws_binary(&runtime, ui_socket, Duration::from_secs(3)); let ui_msg = HubToClient::deserialize_bin(&ui_incoming).expect("decode ui response"); match ui_msg { HubToClient::WidgetQuery { query_id: got_query, build_id: got_build, query, rects, } => { assert_eq!(got_query, query_id); assert_eq!(got_build, build_id); assert_eq!(query, "id:math_tab"); assert_eq!(rects, vec!["DT math_tab DockTab 10 20 30 40".to_string()]); } other => panic!("unexpected ui response: {:?}", other), } let _ = runtime.ws_close(app_socket); let _ = runtime.ws_close(ui_socket); } #[test] fn websocket_app_bridge_widget_snapshot_roundtrip() { let dir = makepad_studio_hub::test_support::tempdir().unwrap(); fs::create_dir_all(dir.path().join("src")).unwrap(); fs::write(dir.path().join("src/lib.rs"), "fn main() {}\n").unwrap(); let Some(port) = find_free_port() else { return; }; let config = HubConfig { listen_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port), post_max_size: 1024 * 1024, mounts: vec![MountConfig { name: "repo".to_string(), path: dir.path().to_path_buf(), }], enable_in_process_gateway: false, }; let _backend = match StudioHub::start_headless(config) { Ok(v) => v, Err(err) => { if err.contains("failed to bind") { return; } panic!("start backend failed: {}", err); } }; let runtime = NetworkRuntime::new(NetworkConfig::default()); let ui_socket = LiveId::from_str("studio2.backend.ui.snapshot"); let ui_request = HttpRequest::new(format!("ws://127.0.0.1:{port}/ui"), HttpMethod::GET); if runtime.ws_open(ui_socket, ui_request).is_err() { return; } let ui_opened = wait_for_event( &runtime, Duration::from_secs(3), |event| matches!(event, NetworkResponse::WsOpened { socket_id: id } if *id == ui_socket), ); assert!(ui_opened.is_some(), "did not receive ui WsOpened"); let hello_bin = wait_for_ws_binary(&runtime, ui_socket, Duration::from_secs(3)); let hello = HubToClient::deserialize_bin(&hello_bin).expect("decode hello"); let client_id = match hello { HubToClient::Hello { client_id } => client_id, other => panic!("expected hello, got {:?}", other), }; let build_id = QueryId::new(client_id, 100); let app_socket = LiveId::from_str("studio2.backend.app.snapshot"); let app_request = HttpRequest::new( format!( "ws://127.0.0.1:{port}/app?build={}&crate=makepad-example-xr", build_id.0 ), HttpMethod::GET, ); runtime .ws_open(app_socket, app_request) .expect("open app socket"); let app_opened = wait_for_event( &runtime, Duration::from_secs(3), |event| matches!(event, NetworkResponse::WsOpened { socket_id: id } if *id == app_socket), ); assert!(app_opened.is_some(), "did not receive app WsOpened"); wait_for_app_socket_registration(&runtime, ui_socket, client_id, build_id); let query_id = QueryId::new(client_id, 3); let ui_request = ClientToHubEnvelope { query_id, msg: ClientToHub::WidgetSnapshot { build_id }, }; runtime .ws_send(ui_socket, WsSend::Binary(ui_request.serialize_bin())) .expect("send widget snapshot request"); let app_incoming = wait_for_ws_binary(&runtime, app_socket, Duration::from_secs(3)); let app_msg = StudioToAppVec::deserialize_bin(&app_incoming).expect("decode app command"); assert_eq!(app_msg.0.len(), 1); match &app_msg.0[0] { StudioToApp::WidgetSnapshot(request) => { assert_eq!(request.request_id, query_id.0); } other => panic!("unexpected app message: {:?}", other), } let widgets = vec![WidgetSnapshot { id: "panel_input".to_string(), widget_type: "TextInput".to_string(), window_id: "panel_window".to_string(), window_index: 1, visible: true, enabled: true, x: 120, y: 240, width: 180, height: 44, text: None, value: Some("hello".to_string()), checked: None, selected: None, }]; let response = AppToStudioVec(vec![AppToStudio::WidgetSnapshot(WidgetSnapshotResponse { request_id: query_id.0, widgets: widgets.clone(), })]); runtime .ws_send(app_socket, WsSend::Binary(response.serialize_bin())) .expect("send app response"); let ui_incoming = wait_for_ws_binary(&runtime, ui_socket, Duration::from_secs(3)); let ui_msg = HubToClient::deserialize_bin(&ui_incoming).expect("decode ui response"); match ui_msg { HubToClient::WidgetSnapshot { query_id: got_query, build_id: got_build, widgets: got_widgets, } => { assert_eq!(got_query, query_id); assert_eq!(got_build, build_id); assert_eq!(got_widgets, widgets); } other => panic!("unexpected ui response: {:?}", other), } let _ = runtime.ws_close(app_socket); let _ = runtime.ws_close(ui_socket); }