Skip to content
Merged
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
28 changes: 14 additions & 14 deletions src/backends/wslc/common/src/container_steps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ use wxc_common::logger::Logger;
use wxc_common::models::{PortMapping, ScriptResponse};
use wxc_common::string_util::{to_wide, CoTaskMemPWSTR};

use crate::error::WslcError;
use crate::policy_mapping::VolumeMount;
use crate::wsl_container_runner::{wslc_prerequisite_error, WSLContainerRunner};
use crate::wslc_bindings::*;
Expand All @@ -52,14 +53,10 @@ use crate::wslc_bindings::*;
// ---------------------------------------------------------------------------

/// Build a `ScriptResponse` error from an HRESULT failure with an optional
/// SDK-provided message.
/// SDK-provided message. Thin wrapper over [`WslcError::Sdk`], which owns the
/// message formatting and the `LaunchFailed` phase attribution.
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)
WslcError::sdk(context, hr, sdk_msg).into_response()
}

/// NUL-terminate `value` for a C string the WSLc SDK will read, rejecting an
Expand All @@ -73,9 +70,10 @@ fn cstr_bytes(field: &str, value: &str) -> Result<Vec<u8>, ScriptResponse> {
std::ffi::CString::new(value)
.map(|c| c.into_bytes_with_nul())
.map_err(|_| {
ScriptResponse::error(&format!(
WslcError::Rejected(format!(
"{field} contains an interior NUL byte, which is not a valid C string"
))
.into_response()
})
}

Expand Down Expand Up @@ -425,9 +423,10 @@ impl ProcessSettings {
let mut cwd_cstr: Option<Vec<u8>> = None;
if !working_directory.is_empty() {
if !working_directory.starts_with('/') {
return Err(ScriptResponse::error(&format!(
return Err(WslcError::Rejected(format!(
"working_directory must be an absolute in-container path (got {working_directory:?})"
)));
))
.into_response());
}
let c = cstr_bytes("working_directory", working_directory)?;
let hr = sdk.WslcSetProcessSettingsWorkingDirectory(&mut raw, c.as_ptr() as PCSTR);
Expand Down Expand Up @@ -666,15 +665,15 @@ pub const KEEPALIVE_SCRIPT: &str = "while true; do sleep 86400; done";
/// resolvable. The returned [`WslcSdk`] holds raw function pointers; keep it
/// alive for the duration of all SDK use.
pub unsafe fn load_sdk_checked(logger: &mut Logger) -> Result<WslcSdk, ScriptResponse> {
let sdk = WslcSdk::load().map_err(|e| ScriptResponse::error(&e))?;
let sdk = WslcSdk::load().map_err(|e| WslcError::Unavailable(e).into_response())?;

let mut missing = WslcComponentFlags::WSLC_COMPONENT_FLAG_NONE;
let hr = sdk.WslcGetMissingComponents(&mut missing);
if hr != S_OK {
return Err(sdk_error("WslcGetMissingComponents failed", hr, ""));
}
if missing.any_missing() {
return Err(ScriptResponse::error(&wslc_prerequisite_error(missing)));
return Err(WslcError::Unavailable(wslc_prerequisite_error(missing)).into_response());
}
let _ = writeln!(logger, "[WSLC][daemon] Runtime check passed");
Ok(sdk)
Expand Down Expand Up @@ -789,14 +788,15 @@ pub unsafe fn resolve_image(
),
None => (String::new(), String::new()),
};
Err(ScriptResponse::error(&format!(
Err(WslcError::Rejected(format!(
"WSLC image '{}' not found locally. Pre-pull it with: \
wxc-exec.exe --setup-wslc --image {}{} \
(or scripts\\setup-wslc.ps1 -Image {}{}). \
MXC does not pull images at run time; \
see docs/wsl/wsl-container-support-plan.md.",
image, image, storage_arg_wxc, image, storage_arg_ps,
)))
))
.into_response())
}

/// Create a daemon-owned container with `keepalive` as its init process, so it
Expand Down
200 changes: 200 additions & 0 deletions src/backends/wslc/common/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Typed failure modes for the WSLc backend.
//!
//! WSLc previously built every failure as a free-text [`ScriptResponse`], which
//! left `failure_phase` at its `None` default. Because
//! `mxc_engine::dispatch::map_spawn_error` discriminates on exactly that field,
//! every WSLc failure reached the Rust SDK as an opaque `backend_error` β€” so a
//! caller could only tell a missing-WSL host from a rejected policy by parsing
//! the message. Each variant here attributes the failure to a lifecycle phase
//! instead.
Comment on lines +6 to +12

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Folded into #843

//!
//! `Display` reproduces the pre-existing message verbatim and
//! [`WslcError::into_response`] fills the same fields `ScriptResponse::error`
//! did, so the text a user sees is unchanged; `failure_phase` is the only
//! addition.

use std::fmt;

use wxc_common::models::{FailurePhase, ScriptResponse};

use crate::wslc_bindings::HRESULT;

/// A WSLc backend failure, tagged with the phase it is attributable to.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum WslcError {
/// A WSLc SDK call returned a failing `HRESULT`.
Sdk {
context: String,
hr: HRESULT,
sdk_msg: String,
},
/// The SDK, or a WSL component it depends on, is missing on this host.
Unavailable(String),
/// The request cannot be honored as written β€” a policy or config rejection
/// that the same input will not get past on a retry.
Rejected(String),
/// Host-side bring-up failed before the container process started.
Host(String),
/// The container started, but the run could not be carried to a clean exit.
Runtime(String),
}

impl WslcError {
/// A failing SDK call. Mirrors the message the former `sdk_error` helper
/// built, including the `HRESULT` rendering.
pub(crate) fn sdk(context: impl Into<String>, hr: HRESULT, sdk_msg: impl Into<String>) -> Self {
WslcError::Sdk {
context: context.into(),
hr,
sdk_msg: sdk_msg.into(),
}
}

/// Lifecycle phase this failure is attributed to, so a caller can tell a
/// retryable launch failure from a rejection or an unusable host.
pub(crate) fn failure_phase(&self) -> FailurePhase {
match self {
// Host cannot run WSLc at all β€” callers may fall back to another tier.
WslcError::Unavailable(_) => FailurePhase::BackendUnavailable,
// Non-retryable preflight: the input itself has to change.
WslcError::Rejected(_) => FailurePhase::Rejected,
// The SDK call or host bring-up failed; generally worth retrying.
WslcError::Sdk { .. } | WslcError::Host(_) => FailurePhase::LaunchFailed,
// The container was up but the run broke.
WslcError::Runtime(_) => FailurePhase::PostLaunchFailed,
}
}

/// Render as a [`ScriptResponse`], preserving the exact message text the
/// untyped construction produced.
pub(crate) fn into_response(self) -> ScriptResponse {
let failure_phase = self.failure_phase();
let msg = self.to_string();
ScriptResponse {
exit_code: -1,
standard_err: msg.clone(),
error_message: msg,
failure_phase,
..Default::default()
}
}
}

impl fmt::Display for WslcError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WslcError::Sdk {
context,
hr,
sdk_msg,
} if sdk_msg.is_empty() => {
write!(f, "{}: HRESULT 0x{:08X}", context, *hr as u32)
}
WslcError::Sdk {
context,
hr,
sdk_msg,
} => write!(f, "{}: {} (HRESULT 0x{:08X})", context, sdk_msg, *hr as u32),
WslcError::Unavailable(m)
| WslcError::Rejected(m)
| WslcError::Host(m)
| WslcError::Runtime(m) => f.write_str(m),
}
}
}

impl From<WslcError> for ScriptResponse {
fn from(err: WslcError) -> Self {
err.into_response()
}
}

#[cfg(test)]
mod tests {
use super::*;

/// The former helper, kept verbatim so the typed path can be proven to
/// produce byte-identical text.
fn legacy_sdk_message(context: &str, hr: HRESULT, sdk_msg: &str) -> String {
if sdk_msg.is_empty() {
format!("{}: HRESULT 0x{:08X}", context, hr as u32)
} else {
format!("{}: {} (HRESULT 0x{:08X})", context, sdk_msg, hr as u32)
}
}

#[test]
fn sdk_message_is_unchanged_from_the_untyped_helper() {
// 0x8007_0490 as a c_long is negative; the `as u32` cast in both the old
// and new renderings must agree on the printed form.
for (ctx, hr, sdk_msg) in [
("WslcCreateContainer failed", 0x8007_0490_u32 as HRESULT, ""),
(
"WslcCreateContainerProcess failed",
0x8007_0490_u32 as HRESULT,
"no such image",
),
("WslcGetMissingComponents failed", -1, ""),
] {
assert_eq!(
WslcError::sdk(ctx, hr, sdk_msg).to_string(),
legacy_sdk_message(ctx, hr, sdk_msg),
"typed rendering must match the legacy message exactly"
);
}
}

#[test]
fn message_carrying_variants_render_verbatim() {
let msg = "WSLc: network.allowLocalNetwork=true is not supported.";
for err in [
WslcError::Unavailable(msg.to_string()),
WslcError::Rejected(msg.to_string()),
WslcError::Host(msg.to_string()),
WslcError::Runtime(msg.to_string()),
] {
assert_eq!(err.to_string(), msg, "message must not be reworded");
}
}

#[test]
fn response_matches_the_untyped_shape_apart_from_the_phase() {
let typed = WslcError::Rejected("bad path".to_string()).into_response();
let untyped = ScriptResponse::error("bad path");

assert_eq!(typed.exit_code, untyped.exit_code);
assert_eq!(typed.standard_err, untyped.standard_err);
assert_eq!(typed.error_message, untyped.error_message);
// The untyped helper left this at the default, which is the bug.
assert_eq!(untyped.failure_phase, FailurePhase::None);
assert_eq!(typed.failure_phase, FailurePhase::Rejected);
}

#[test]
fn each_variant_maps_to_its_lifecycle_phase() {
let s = || "x".to_string();
assert_eq!(
WslcError::Unavailable(s()).failure_phase(),
FailurePhase::BackendUnavailable
);
assert_eq!(
WslcError::Rejected(s()).failure_phase(),
FailurePhase::Rejected
);
assert_eq!(
WslcError::sdk("op", -1, "").failure_phase(),
FailurePhase::LaunchFailed
);
assert_eq!(
WslcError::Host(s()).failure_phase(),
FailurePhase::LaunchFailed
);
assert_eq!(
WslcError::Runtime(s()).failure_phase(),
FailurePhase::PostLaunchFailed
);
}
}
1 change: 1 addition & 0 deletions src/backends/wslc/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod container_steps;
pub mod daemon_client;
pub mod daemon_protocol;
pub mod daemon_record;
pub mod error;
pub mod policy;
pub mod policy_mapping;
pub mod sandbox;
Expand Down
Loading
Loading