Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 37 additions & 29 deletions crates/native-sidecar/src/execution/child_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ const SYNTHETIC_V8_TERMINATION_STDERR: &[u8] = b"Error: Execution terminated\n";
type OwnedChildExecutionStart =
Pin<Box<dyn Future<Output = Result<ActiveExecution, SidecarError>> + 'static>>;

fn is_owned_process_control_rpc(method: &str) -> bool {
matches!(
method,
"child_process.spawn"
| "child_process.spawn_sync"
| "child_process.poll"
| "child_process.write_stdin"
| "child_process.close_stdin"
| "child_process.kill"
| "process.exec_fd_image_commit"
| "process.exec"
| "process.signal_state"
| "process.kill"
)
}

fn settle_owned_javascript_process_event_target<B>(
target: OwnedJavascriptEventService,
response: Result<JavascriptSyncRpcServiceResponse, SidecarError>,
Expand Down Expand Up @@ -2756,8 +2772,9 @@ where

// The standalone WASM runner pulls descendant output through
// child_process.poll while implementing waitpid. Keep stream
// and exit delivery single-owner; the parked kernel wait was
// already rechecked above without leasing either event lane.
// and exit delivery single-owner, but still claim control RPCs:
// the parent's poll requeues nested spawn/exec requests for this
// pump, so skipping the child entirely would starve them.
let parent_is_pull_driven_wasm = self
.vms
.get(vm_id)
Expand All @@ -2769,7 +2786,21 @@ where
})
.unwrap_or(false);
if parent_is_pull_driven_wasm {
continue;
let queued_control_request = self.vms.get(vm_id).is_some_and(|vm| {
vm.active_processes
.get(process_id)
.and_then(|root| Self::active_process_by_path(root, &parent_path))
.and_then(|parent| parent.child_processes.get(&child_process_id))
.and_then(|child| child.pending_execution_events.front())
.is_some_and(|event| {
matches!(event,
ActiveExecutionEvent::JavascriptSyncRpcRequest(request)
if is_owned_process_control_rpc(&request.method))
})
});
if !queued_control_request {
continue;
}
}
self.expire_child_process_sync_if_needed(
vm_id,
Expand All @@ -2783,7 +2814,7 @@ where
process_id,
&parent_path,
&child_process_id,
false,
parent_is_pull_driven_wasm,
javascript_services,
python_services,
python_socket_completions,
Expand Down Expand Up @@ -7008,18 +7039,7 @@ where
_ => {}
}

let special = matches!(
target.request.method.as_str(),
"child_process.poll"
| "child_process.write_stdin"
| "child_process.close_stdin"
| "child_process.kill"
| "process.exec_fd_image_commit"
| "process.exec"
| "process.signal_state"
| "process.kill"
);
if !special {
if !is_owned_process_control_rpc(&target.request.method) {
return Box::pin(async move {
let result = service_owned_javascript_sync_rpc_request(
&bridge,
Expand Down Expand Up @@ -7365,19 +7385,7 @@ where
drop(reservation);
continue;
}
if matches!(
request.method.as_str(),
"child_process.spawn"
| "child_process.spawn_sync"
| "child_process.poll"
| "child_process.write_stdin"
| "child_process.close_stdin"
| "child_process.kill"
| "process.exec_fd_image_commit"
| "process.exec"
| "process.signal_state"
| "process.kill"
) {
if is_owned_process_control_rpc(&request.method) {
vm.try_command("requeue nested special process RPC", |state| {
let path = current_process_path
.iter()
Expand Down
82 changes: 82 additions & 0 deletions crates/native-sidecar/tests/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25585,6 +25585,88 @@ console.log(JSON.stringify({
}
}

#[test]
fn wasm_parent_child_spawn_is_claimed_without_consuming_output() {
let mut sidecar = create_test_sidecar();
let (connection_id, session_id) =
authenticate_and_open_session(&mut sidecar).expect("authenticate sidecar");
let vm_id = create_vm(
&mut sidecar,
&connection_id,
&session_id,
PermissionsPolicy::allow_all(),
)
.expect("create vm");

let root_kernel_handle = create_kernel_process_handle_for_tests();
let mut root = active_process_for_tests(
root_kernel_handle.pid(),
root_kernel_handle,
GuestRuntimeKind::WebAssembly,
ActiveExecution::HostFunction(HostFunctionExecution::default()),
);
let child_kernel_handle = create_kernel_process_handle_for_tests();
let mut child = active_process_for_tests(
child_kernel_handle.pid(),
child_kernel_handle,
GuestRuntimeKind::WebAssembly,
ActiveExecution::HostFunction(HostFunctionExecution::default()),
);
child
.queue_pending_execution_event(ActiveExecutionEvent::JavascriptSyncRpcRequest(
JavascriptSyncRpcRequest {
raw_bytes_args: std::collections::HashMap::new(),
id: 1,
method: String::from("child_process.spawn"),
args: vec![json!("/bin/ls"), json!([]), json!({})],
},
))
.expect("queue nested child spawn");
child
.queue_pending_execution_event(ActiveExecutionEvent::Stdout(b"pull-owned".to_vec()))
.expect("queue child output");
root.child_processes.insert(String::from("child-1"), child);
sidecar
.vms
.get_mut(&vm_id)
.expect("test vm")
.active_processes
.insert(String::from("wasm-root"), root);

let mut javascript_services = Vec::new();
sidecar
.pump_child_process_events_nowait(
&vm_id,
&mut javascript_services,
&mut Vec::new(),
&mut Vec::new(),
&mut Vec::new(),
8,
)
.expect("claim nested spawn");
assert_eq!(
javascript_services.len(),
1,
"WASM-owned children must still service spawn requests"
);
assert_eq!(javascript_services[0].request.method, "child_process.spawn");
assert_eq!(
javascript_services[0].child_path,
vec![String::from("child-1")]
);
let vm = sidecar.vms.get(&vm_id).expect("test vm");
let queued = vm
.active_processes
.get("wasm-root")
.and_then(|root| root.child_processes.get("child-1"))
.and_then(|child| child.pending_execution_events.front())
.expect("WASM child output should remain available to child_process.poll");
match queued {
ActiveExecutionEvent::Stdout(chunk) => assert_eq!(chunk, b"pull-owned"),
other => panic!("expected queued child stdout, got {other:?}"),
}
}

#[test]
fn wasm_parent_child_write_deadline_wakes_after_parent_stops_polling() {
assert_node_available();
Expand Down