479 lines
16 KiB
Rust
479 lines
16 KiB
Rust
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<u16> {
|
|
let listener = TcpListener::bind(("127.0.0.1", 0)).ok()?;
|
|
Some(listener.local_addr().ok()?.port())
|
|
}
|
|
|
|
fn wait_for_event<F>(
|
|
runtime: &NetworkRuntime,
|
|
timeout: Duration,
|
|
mut matcher: F,
|
|
) -> Option<NetworkResponse>
|
|
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<u8> {
|
|
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);
|
|
}
|