From 80049fb4b2fd591cd69b69cdf7bb993bc0e5db92 Mon Sep 17 00:00:00 2001 From: Soham Das Date: Tue, 4 Aug 2026 14:47:23 -0700 Subject: [PATCH 1/3] Add WSLc state-aware daemon and IPC control plane --- build.bat | 4 + src/Cargo.lock | 20 + src/Cargo.toml | 1 + src/backends/wslc/common/Cargo.toml | 4 + .../wslc/common/src/container_steps.rs | 895 ++++++++++++++++++ src/backends/wslc/common/src/daemon_client.rs | 366 +++++++ .../wslc/common/src/daemon_protocol.rs | 441 +++++++++ src/backends/wslc/common/src/daemon_record.rs | 588 ++++++++++++ src/backends/wslc/common/src/lib.rs | 4 + .../wslc/common/src/wsl_container_runner.rs | 6 +- src/backends/wslc/daemon/Cargo.toml | 26 + src/backends/wslc/daemon/build.rs | 6 + .../wslc/daemon/src/control_server.rs | 277 ++++++ src/backends/wslc/daemon/src/main.rs | 129 +++ .../wslc/daemon/src/session_manager.rs | 630 ++++++++++++ src/backends/wslc/daemon/tests/daemon_ipc.rs | 145 +++ 16 files changed, 3539 insertions(+), 3 deletions(-) create mode 100644 src/backends/wslc/common/src/container_steps.rs create mode 100644 src/backends/wslc/common/src/daemon_client.rs create mode 100644 src/backends/wslc/common/src/daemon_protocol.rs create mode 100644 src/backends/wslc/common/src/daemon_record.rs create mode 100644 src/backends/wslc/daemon/Cargo.toml create mode 100644 src/backends/wslc/daemon/build.rs create mode 100644 src/backends/wslc/daemon/src/control_server.rs create mode 100644 src/backends/wslc/daemon/src/main.rs create mode 100644 src/backends/wslc/daemon/src/session_manager.rs create mode 100644 src/backends/wslc/daemon/tests/daemon_ipc.rs diff --git a/build.bat b/build.bat index c38f40780..d25284316 100644 --- a/build.bat +++ b/build.bat @@ -140,6 +140,10 @@ for %%T in (x86_64-pc-windows-msvc aarch64-pc-windows-msvc) do ( ) ) if "%WITH_WSLC%"=="1" ( + if exist "!BIN_DIR!\wxc-wslc-daemon.exe" ( + copy /Y "!BIN_DIR!\wxc-wslc-daemon.exe" "sdk\node\bin\!SDK_ARCH!\" >nul + echo Copied !SDK_ARCH!\wxc-wslc-daemon.exe + ) if exist "!BIN_DIR!\wslcsdk.dll" ( copy /Y "!BIN_DIR!\wslcsdk.dll" "sdk\node\bin\!SDK_ARCH!\" >nul echo Copied !SDK_ARCH!\wslcsdk.dll diff --git a/src/Cargo.lock b/src/Cargo.lock index 15e7a1b9b..bd658fea3 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -3105,10 +3105,14 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" name = "wslc_common" version = "0.7.0" dependencies = [ + "anyhow", "libloading", + "serde", + "serde_json", "sha2 0.10.9", "tar", "tempfile", + "uuid", "windows", "wxc_common", "zip", @@ -3252,6 +3256,22 @@ dependencies = [ "wxc_common", ] +[[package]] +name = "wxc_wslc_daemon" +version = "0.7.0" +dependencies = [ + "anyhow", + "mxc_build_common", + "serde", + "serde_json", + "tempfile", + "tokio", + "uuid", + "windows", + "wslc_common", + "wxc_common", +] + [[package]] name = "xattr" version = "1.6.1" diff --git a/src/Cargo.toml b/src/Cargo.toml index 4a9f7e2ce..66d68131b 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -27,6 +27,7 @@ members = [ "backends/lxc/common", "backends/bubblewrap/common", "backends/wslc/common", + "backends/wslc/daemon", "backends/seatbelt/common", "host/wxc_host_prep", "host/wxc_winhttp_proxy_shim", diff --git a/src/backends/wslc/common/Cargo.toml b/src/backends/wslc/common/Cargo.toml index 2ba1b71b2..c130b9890 100644 --- a/src/backends/wslc/common/Cargo.toml +++ b/src/backends/wslc/common/Cargo.toml @@ -11,6 +11,10 @@ link-wslcsdk = [] [dependencies] wxc_common = { workspace = true } windows = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +anyhow = { workspace = true } +uuid = { workspace = true } tar = "0.4" libloading = "0.8" diff --git a/src/backends/wslc/common/src/container_steps.rs b/src/backends/wslc/common/src/container_steps.rs new file mode 100644 index 000000000..e5460d8a5 --- /dev/null +++ b/src/backends/wslc/common/src/container_steps.rs @@ -0,0 +1,895 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Reusable WSLc SDK building blocks shared by the one-shot `ScriptRunner` +//! ([`crate::wsl_container_runner`]) and the state-aware daemon +//! (`wxc_wslc_daemon::session_manager`). +//! +//! # Why this module exists +//! The one-shot runner builds a `WslcProcessSettings` and a +//! `WslcContainerSettings` inline, then creates the container in a single +//! function whose stack locals keep the backing string/pointer buffers alive. +//! The WSLc SDK **stores raw pointers into those caller buffers** (it does not +//! copy them) and dereferences them at `WslcCreateContainer` / +//! `WslcCreateContainerProcess` time, so the buffers must outlive the create +//! call. In the one-shot path that "outlives" is expressed by stack scope. +//! +//! The state-aware daemon needs the *same* marshalling but across separate +//! phase calls, so the lifetime contract cannot be expressed by a single +//! function's stack. This module reifies each settings blob as a +//! **buffer-owning struct** ([`ProcessSettings`] / [`ContainerSettings`]) that +//! bundles the `Wslc*Settings` value together with every heap buffer its +//! pointers reference. Both callers hold the struct for as long as the SDK +//! needs the pointers valid. +//! +//! # Move safety +//! Every buffer a settings pointer references is heap-allocated (`Vec`, `Arc`) +//! or `'static`. Moving one of these structs copies only the owning headers — +//! the referenced heap allocations do not relocate — so the raw pointers the +//! SDK stored remain valid across a move. The one rule callers must honor: +//! **do not move the struct after handing a `&raw` (or a settings value that +//! embeds a pointer to it, e.g. an init-process settings) to the SDK.** In +//! practice the builders return the fully-populated struct by value (a single +//! move that happens *before* any `&raw` is taken), after which callers keep it +//! as a stationary local. + +use std::ffi::c_void; +use std::fmt::Write; +use std::ptr; +use std::sync::{Arc, Condvar, Mutex}; +use std::time::Duration; + +use wxc_common::logger::Logger; +use wxc_common::models::{PortMapping, ScriptResponse}; +use wxc_common::string_util::{to_wide, CoTaskMemPWSTR}; + +use crate::policy_mapping::{self, VolumeMount}; +use crate::wsl_container_runner::{wslc_prerequisite_error, WSLContainerRunner}; +use crate::wslc_bindings::*; + +// --------------------------------------------------------------------------- +// Shared error helper +// --------------------------------------------------------------------------- + +/// Build a `ScriptResponse` error from an HRESULT failure with an optional +/// SDK-provided message. +pub(crate) fn sdk_error(context: &str, hr: HRESULT, sdk_msg: &str) -> ScriptResponse { + let msg = if sdk_msg.is_empty() { + format!("{}: HRESULT 0x{:08X}", context, hr as u32) + } else { + format!("{}: {} (HRESULT 0x{:08X})", context, sdk_msg, hr as u32) + }; + ScriptResponse::error(&msg) +} + +// --------------------------------------------------------------------------- +// Process I/O capture plumbing +// --------------------------------------------------------------------------- + +/// Shared buffer for capturing process I/O via SDK callbacks. Fields are +/// `pub(crate)` so the one-shot runner's wait/collect helpers can read the +/// captured bytes and exit signal. +pub struct IoContext { + pub(crate) stdout: Arc>>, + pub(crate) stderr: Arc>>, + pub(crate) exited: Arc<(Mutex, Condvar)>, +} + +/// RAII guard that reclaims an `Arc` from a raw pointer on drop. +/// Prevents leaking the `Arc` reference count on early returns. +pub(crate) struct IoCtxRawGuard { + ptr: *mut c_void, +} + +impl IoCtxRawGuard { + fn new(ptr: *mut c_void) -> Self { + Self { ptr } + } +} + +impl Drop for IoCtxRawGuard { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { + eprintln!("[WSLC][debug] IoCtxRawGuard dropped -- reclaiming Arc"); + let _ = Arc::from_raw(self.ptr as *const IoContext); + } + } + } +} + +/// Callback invoked by the WSLc SDK for stdout/stderr data. +/// +/// # Safety +/// `context` must be a valid pointer obtained from `Arc::into_raw(Arc)`. +/// The `Arc` is kept alive by the owning [`ProcessSettings`] (via [`IoCtxRawGuard`], +/// which reclaims it on drop), so the pointer remains valid for the duration of +/// all callbacks. The SDK guarantees `data` is valid for `data_size` bytes. +unsafe extern "C" fn io_callback( + io_handle: WslcProcessIOHandle, + data: *const BYTE, + data_size: u32, + context: *mut c_void, +) { + if context.is_null() || data.is_null() || data_size == 0 { + return; + } + let ctx = &*(context as *const IoContext); + let bytes = std::slice::from_raw_parts(data, data_size as usize); + match io_handle { + WslcProcessIOHandle::WSLC_PROCESS_IO_HANDLE_STDOUT => { + let mut buf = ctx.stdout.lock().unwrap_or_else(|e| e.into_inner()); + buf.extend_from_slice(bytes); + } + WslcProcessIOHandle::WSLC_PROCESS_IO_HANDLE_STDERR => { + let mut buf = ctx.stderr.lock().unwrap_or_else(|e| e.into_inner()); + buf.extend_from_slice(bytes); + } + _ => {} + } +} + +/// Callback invoked when the process exits and all I/O has been flushed. +/// Per SDK docs: "Once this callback is invoked, any registered IO callbacks +/// will no longer be called." This guarantees buffers are complete. +/// +/// # Safety +/// Same lifetime requirements as [`io_callback`]. +unsafe extern "C" fn exit_callback(_exit_code: i32, context: *mut c_void) { + if context.is_null() { + return; + } + let ctx = &*(context as *const IoContext); + let mut exited = ctx.exited.0.lock().unwrap_or_else(|e| e.into_inner()); + *exited = true; + ctx.exited.1.notify_all(); +} + +// --------------------------------------------------------------------------- +// ProcessSettings builder +// --------------------------------------------------------------------------- + +/// A fully-populated `WslcProcessSettings` together with every heap buffer its +/// pointers reference (cmdline argv, env, working dir) and the I/O-capture +/// context. Safe to move (all referenced data is heap/`'static`); do not move +/// after taking `&raw`. +pub struct ProcessSettings { + raw: WslcProcessSettings, + io_ctx: Arc, + _io_guard: IoCtxRawGuard, + _sh: Vec, + _dash_c: Vec, + _script_cstr: Vec, + _argv: Vec, + _env_cstrings: Vec>, + _env_ptrs: Vec, + _cwd_cstr: Option>, +} + +impl ProcessSettings { + /// Build process settings that run `script_code` under `/bin/sh -c`, with + /// the given `env` (already proxy-adjusted by the caller) and + /// `working_directory` (a Windows path mapped to its container path; empty = + /// container default). Registers stdout/stderr/exit capture callbacks. + /// + /// # Safety + /// `sdk` must hold valid, currently-loaded function pointers and COM must be + /// initialized on the calling thread. + pub unsafe fn build( + sdk: &WslcSdk, + script_code: &str, + env: &[String], + working_directory: &str, + ) -> Result { + Self::build_inner(sdk, script_code, env, working_directory, true) + } + + /// Like [`build`](Self::build) but registers no stdio callbacks and shares no + /// `IoContext` with the SDK, for a detached init whose output is never + /// streamed. [`io_ctx`](Self::io_ctx) is inert for the returned value. + /// + /// # Safety + /// Same contract as [`build`](Self::build). + pub unsafe fn build_detached( + sdk: &WslcSdk, + script_code: &str, + env: &[String], + working_directory: &str, + ) -> Result { + Self::build_inner(sdk, script_code, env, working_directory, false) + } + + unsafe fn build_inner( + sdk: &WslcSdk, + script_code: &str, + env: &[String], + working_directory: &str, + register_callbacks: bool, + ) -> Result { + let mut raw = std::mem::zeroed::(); + let hr = sdk.WslcInitProcessSettings(&mut raw); + if hr != S_OK { + return Err(sdk_error("WslcInitProcessSettings failed", hr, "")); + } + + // Register I/O callbacks to capture stdout/stderr. We hand the SDK an + // Arc reference via raw pointer; IoCtxRawGuard reconstructs it on drop + // so the reference count is not leaked, and the memory stays alive while + // the SDK may still invoke callbacks on its internal threads. + let io_ctx = Arc::new(IoContext { + stdout: Arc::new(Mutex::new(Vec::new())), + stderr: Arc::new(Mutex::new(Vec::new())), + exited: Arc::new((Mutex::new(false), Condvar::new())), + }); + let io_ctx_raw = Arc::into_raw(Arc::clone(&io_ctx)) as *mut c_void; + let io_guard = IoCtxRawGuard::new(io_ctx_raw); + + // Callbacks are registered only for the streamed path; a detached init + // shares no IoContext with the SDK. + if register_callbacks { + let callbacks = WslcProcessCallbacks { + onStdOut: Some(io_callback), + onStdErr: Some(io_callback), + onExit: Some(exit_callback), + }; + let hr = sdk.WslcSetProcessSettingsCallbacks(&mut raw, &callbacks, io_ctx_raw); + if hr != S_OK { + return Err(sdk_error("WslcSetProcessSettingsCallbacks failed", hr, "")); + } + } + + // Command line: /bin/sh -c