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
7 changes: 7 additions & 0 deletions docs/api-reference/dotnet/v1/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,13 @@ public class MxcProcess : IMxcProcess
```


`TryGetExitCode`, `Wait`, `WaitAsync`, and `WaitForExitWithOutputAsync` throw
`MxcException` when MXC cannot obtain a terminal process result after
admission. The exception preserves `Code`, `Message`, `Operation`,
Comment thread
SohamDas2021 marked this conversation as resolved.
Resolved
`NativeCode`, and `Remediation`; the failure is not converted into an exit
code. `MxcPtyProcess` does not preserve these fields.


## `Microsoft.Mxc.Sdk.V1.MxcPtyProcess`

A live container process attached to a caller-driven pseudo-terminal.
Expand Down
7 changes: 7 additions & 0 deletions docs/api-reference/node/v1/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,13 @@ export class MxcProcess {
```


`wait()` rejects with `MxcError` when MXC cannot obtain a terminal process
result after admission. The rejection preserves `code`, `message`,
`operation`, `nativeCode`, and `remediation`; it is not converted into an exit
result. Its `details` carry only the native status code (`ffiStatus`), not
backend details. `MxcPtyProcess` does not preserve these fields.


## `@microsoft/mxc-sdk/v1::MxcPtyProcess`

A container process attached to an MXC-owned pseudo-terminal.
Expand Down
9 changes: 9 additions & 0 deletions docs/api-reference/rust/v1/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,15 +329,24 @@ impl MxcProcess {
pub fn stdout_closer(&self) -> Option<StreamCloser>;
pub fn stderr_closer(&self) -> Option<StreamCloser>;
pub fn try_wait(&mut self) -> std::io::Result<Option<i32>>;
pub fn try_wait_detailed(&mut self) -> Result<Option<WaitResult>, Error>;
pub fn id(&self) -> u32;
pub fn kill(&mut self) -> std::io::Result<()>;
pub fn kill_for_timeout(&mut self) -> std::io::Result<()>;
pub fn wait(&mut self) -> std::io::Result<WaitResult>;
pub fn wait_detailed(&mut self) -> Result<WaitResult, Error>;
pub fn wait_with_output(mut self) -> std::io::Result<ExecutionResult>;
pub fn wait_with_output_detailed(self) -> Result<ExecutionResult, Error>;
}
```


The detailed wait methods preserve structured backend failures that occur
after process admission. The existing methods retain their `std::io::Error`
contracts and render the same message text. A cached terminal failure is
replayed on later waits and does not prove that the process exited.


## `mxc_sdk::v1::MxcPtyProcess`

A live container process attached to a caller-controlled pseudo-terminal.
Expand Down
15 changes: 14 additions & 1 deletion docs/backends/wslc/wslc-state-aware.md
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,12 @@ pipe from the `WithStdin` variants. Rust and
CLI attached relay surfaces as a backend error because that path cannot return a typed timeout;
the streaming path reports it through its wait result.

An exec can be admitted and produce output before a WSLc SDK call needed to
obtain its terminal status fails. Captured SDK execution then fails, and a live
process's wait rejects, with the original structured WSLc error rather than an
exit code. The error identifies the failed SDK function in `operation` and
includes its HRESULT in `nativeCode` when available.

### exec stdin

An exec reads the stdin its caller supplies:
Expand Down Expand Up @@ -624,6 +630,12 @@ daemon attempts immediate deletion. If deletion fails, the quarantined entry is
retained so `deprovision` can retry cleanup; it is not removed from tracking
while a potentially running workload remains.

A terminal-status failure is cached by the live SDK process handle, so another
wait reports the same failure instead of claiming a different outcome. Because
that failure does not confirm the process is gone, explicit termination and
handle disposal still attempt cleanup. The daemon's quarantine behavior above
continues to protect the container when it cannot prove termination.

## Supported and rejected policy fields

WSLc networking is **all-or-nothing** (`WslcContainerNetworkingMode` `None` vs
Expand Down Expand Up @@ -705,7 +717,8 @@ without matching the message.

When a WSLc SDK call fails, including in an exec that has already started, the
error envelope also carries the SDK function name in `operation`, and its HRESULT
in `nativeCode` when the call returned one.
in `nativeCode` when the call returned one. Captured execution and live-process
waits in the Rust detailed API, .NET, and Node preserve those fields.

`ErrKind` is additive: a kind a client does not recognize decodes to
`ErrKind::Unknown` and maps to `backend_error`, so a daemon from a newer
Expand Down
16 changes: 15 additions & 1 deletion docs/development/architecture/container-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,8 @@ administrative restriction. Options do not change the owned wire contract.
and `validateProcess` use native dry-run validation and return no execution
result. There is no `dryRun` execution option or public attached-exec API.
Captured and streaming execution are asynchronous. Live processes own wait,
kill, and disposal.
kill, and disposal. A failure reported after exec admission but before MXC can
confirm the terminal result is an operation failure, not an exit result.
IsolationSession provides stdin, stdout, and stderr. WSLC provides stdout and
stderr; only `spawnInContainerWithStdin` also gives it a writable stdin, as
`spawnWithStdin` does for create-and-run execution.
Expand Down Expand Up @@ -956,6 +957,19 @@ Process-runtime kill conditions (a script exceeding its `timeout`, a backend for
terminating a process) are not represented as typed wire-format errors. They surface as
sentinel exit codes from the exec process, matching the existing one-shot convention.

A backend can also fail while observing the process after admission. Captured
execution and the .NET and Node live-process wait APIs preserve the resulting
error code, message, operation, native code, and remediation instead of
reporting a successful exit. PTY processes do not preserve these fields. Rust
`MxcProcess::try_wait`, `wait`, and
`wait_with_output` retain their existing `std::io::Error` contracts and
message text. Their `*_detailed` counterparts return `mxc_sdk::v1::Error` with
the structured fields intact.

Once a live-process handle has observed a terminal failure, later wait calls
replay that failure. The failure does not prove that the process exited, so the
handle retains its termination path for an explicit kill or disposal cleanup.

### 8.2 Details payload

`details` (introduced in Β§7.3) is an open `Record<string, unknown>`. Backends use it to
Expand Down
68 changes: 68 additions & 0 deletions scripts/ci/build-windows-sdk-test-bundle.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,23 @@ function Invoke-CargoBuild {
} | Where-Object { $_.reason -eq 'compiler-artifact' }
}

function Invoke-CargoDebugBuild {
param([Parameter(Mandatory)][string[]]$Arguments)

Push-Location $srcRoot
try {
$messages = Invoke-Checked "cargo $($Arguments -join ' ')" {
cargo @Arguments --locked --target $triple --message-format=json
}
}
finally {
Pop-Location
}
$messages | ForEach-Object {
try { $_ | ConvertFrom-Json -ErrorAction Stop } catch { }
} | Where-Object { $_.reason -eq 'compiler-artifact' }
}

function Get-CargoExecutable {
param([object[]]$Artifacts, [string]$TargetName)

Expand Down Expand Up @@ -115,7 +132,18 @@ $nativeLibrary = Invoke-CargoBuild @('build', '-p', 'mxc_ffi', '--features', 'is
if (-not $nativeLibrary) {
throw 'cargo reported no mxc_ffi.dll.'
}
$testNativeLibrary = Invoke-CargoDebugBuild @(
'build', '-p', 'mxc_ffi', '--features', 'isolation_session,wslc,test-support') |
Where-Object { $_.target.name -eq 'mxc_ffi' } |
ForEach-Object { $_.filenames } |
Where-Object { [IO.Path]::GetExtension($_) -eq '.dll' } |
Select-Object -Last 1
if (-not $testNativeLibrary) {
throw 'cargo reported no test-support mxc_ffi.dll.'
}
$wslcDaemon = Get-CargoExecutable (Invoke-CargoBuild @('build', '-p', 'mxc-sdk', '--features', 'wslc', '--bin', 'wxc-wslc-daemon')) 'wxc-wslc-daemon'
$testWslcDaemon = Get-CargoExecutable (Invoke-CargoDebugBuild @(
'build', '-p', 'mxc-sdk', '--features', 'wslc,test-support', '--bin', 'wxc-wslc-daemon')) 'wxc-wslc-daemon'

# The wslc build feature stages the WSLC SDK library beside mxc_ffi.
$wslcSdk = Join-Path (Split-Path -Parent $nativeLibrary) 'wslcsdk.dll'
Expand Down Expand Up @@ -145,6 +173,13 @@ if ($NuGetSource) {
$publish += @('--source', $NuGetSource)
}
Invoke-Checked 'dotnet publish' { dotnet @publish }
foreach ($source in $testNativeLibrary, $testWslcDaemon) {
$destination = Join-Path $dotnetOut (Split-Path -Leaf $source)
Copy-Item -LiteralPath $source -Destination $destination -Force
if ((Get-FileHash -LiteralPath $source).Hash -ne (Get-FileHash -LiteralPath $destination).Hash) {
throw "The published .NET test bundle did not stage $(Split-Path -Leaf $source)."
}
}

Copy-Item -LiteralPath $WxcExecPath -Destination (New-Directory (Join-Path $OutputDirectory "bin\$Architecture"))

Expand Down Expand Up @@ -189,6 +224,14 @@ foreach ($name in 'bin', 'dist', 'node_modules', 'package.json', 'LICENSE.md') {
Copy-Item -LiteralPath $source -Destination $sdkOut -Recurse -Force
}
}
$nodeSdkBin = Join-Path $sdkOut "bin\$Architecture"
foreach ($source in $testNativeLibrary, $testWslcDaemon) {
$destination = Join-Path $nodeSdkBin (Split-Path -Leaf $source)
Copy-Item -LiteralPath $source -Destination $destination -Force
if ((Get-FileHash -LiteralPath $source).Hash -ne (Get-FileHash -LiteralPath $destination).Hash) {
throw "The Node integration bundle did not stage $(Split-Path -Leaf $source)."
}
}

$manifest = [ordered]@{
format_version = 1
Expand Down Expand Up @@ -227,6 +270,7 @@ $required = @(
'sdk-integration\dist\dev.test.js',
'sdk-integration\dist\wslc-state-aware.test.js',
'sdk-integration\dist\wslc-e2e.test.js',
"sdk-integration\node_modules\@microsoft\mxc-sdk\bin\$Architecture\mxc_ffi.dll",
"sdk-integration\node_modules\@microsoft\mxc-sdk\bin\$Architecture\wxc-wslc-daemon.exe",
"sdk-integration\node_modules\@microsoft\mxc-sdk\bin\$Architecture\wslcsdk.dll",
'test_scripts\run_isolation_session_tests.ps1',
Expand Down Expand Up @@ -294,9 +338,21 @@ Assert-Discovery '.NET exact-JSON WSLC lifecycle tests' 'MxcLifecycleWslcE2ETest
Assert-Discovery '.NET WSLC lifecycle tests' 'MxcLifecycleWslcE2ETests\.' {
& (Join-Path $dotnetOut 'Microsoft.Mxc.Sdk.Tests.exe') -list full -class '*.MxcLifecycleWslcE2ETests'
}
foreach ($test in 'RunInContainer_PreservesStructuredPostAdmissionWaitFailure',
'SpawnInContainer_WaitPreservesStructuredPostAdmissionWaitFailure') {
Assert-Discovery ".NET WSLC $test" "MxcLifecycleWslcE2ETests\.${test}" {
& (Join-Path $dotnetOut 'Microsoft.Mxc.Sdk.Tests.exe') -list full -class '*.MxcLifecycleWslcE2ETests'
}
}
Assert-Discovery '.NET WSLC create-and-run tests' 'MxcContainerWslcE2ETests\.' {
& (Join-Path $dotnetOut 'Microsoft.Mxc.Sdk.Tests.exe') -list full -class '*.MxcContainerWslcE2ETests'
}
foreach ($test in 'Run_PreservesStructuredPostAdmissionWaitFailure',
'Spawn_WaitPreservesStructuredPostAdmissionWaitFailure') {
Assert-Discovery ".NET WSLC $test" "MxcContainerWslcE2ETests\.${test}" {
& (Join-Path $dotnetOut 'Microsoft.Mxc.Sdk.Tests.exe') -list full -class '*.MxcContainerWslcE2ETests'
}
}
Push-Location $integrationOut
$env:MXC_SKIP_OS_BUILD_DEPENDENT_TESTS = '1'
try {
Expand All @@ -312,9 +368,21 @@ try {
Assert-Discovery 'Node WSLC lifecycle suite' 'WSLC state-aware lifecycle E2E' {
& (Join-Path $OutputDirectory 'node\node.exe') --test --test-force-exit dist/wslc-state-aware.test.js
}
foreach ($test in 'captured exec preserves structured post-admission wait failures',
'process wait preserves structured post-admission wait failures') {
if (-not (Select-String -LiteralPath 'dist/wslc-state-aware.test.js' -SimpleMatch $test -Quiet)) {
throw "The Node WSLC lifecycle payload is missing $test."
}
}
Assert-Discovery 'Node WSLC create-and-run suite' 'WSLC SDK E2E' {
& (Join-Path $OutputDirectory 'node\node.exe') --test --test-force-exit dist/wslc-e2e.test.js
}
foreach ($test in 'captured run preserves structured post-admission wait failures',
'spawned process wait preserves structured post-admission wait failures') {
if (-not (Select-String -LiteralPath 'dist/wslc-e2e.test.js' -SimpleMatch $test -Quiet)) {
throw "The Node WSLC create-and-run payload is missing $test."
}
}
}
finally {
Remove-Item Env:MXC_SKIP_OS_BUILD_DEPENDENT_TESTS
Expand Down
36 changes: 33 additions & 3 deletions scripts/ci/run_backend_validation_tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,17 @@ function Invoke-NamedSuites {
}
}

# Daemon discovery reuses any running daemon, including one another suite started.
function Stop-WslcDaemons {
foreach ($daemon in @(Get-Process -Name 'wxc-wslc-daemon' -ErrorAction SilentlyContinue)) {
Write-Host "Stopping wxc-wslc-daemon $($daemon.Id) from an earlier suite."
Stop-Process -Id $daemon.Id -Force -ErrorAction SilentlyContinue
Comment on lines +324 to +326
if (-not $daemon.WaitForExit(30000)) {
throw "wxc-wslc-daemon $($daemon.Id) did not exit within 30 s."
}
}
}

function Invoke-WslcSuites {
$bundle = Join-Path $binaryDirectoryPath 'windows-sdk-bundle'
foreach ($file in 'dotnet\Microsoft.Mxc.Sdk.Tests.exe', 'node\node.exe', 'sdk-integration\dist\wslc-state-aware.test.js', 'sdk-integration\dist\wslc-e2e.test.js') {
Expand Down Expand Up @@ -348,9 +359,28 @@ function Invoke-WslcSuites {
& (Join-Path $testScriptRoot 'run_wslc_all_tests.ps1') -WxcExecPath $wxc | Out-Host
if ($LASTEXITCODE -ne 0) { "exited $LASTEXITCODE" }
}
'Node' = { Invoke-NodeIntegrationSuite -Bundle $bundle -Name 'wslc' -TestFile 'wslc-state-aware.test.js' -RequiredTests 'V1.Dev preserves WSLC lifecycle responses, captured exec, pipes, and caller ownership' }
'Node create-and-run' = { Invoke-NodeIntegrationSuite -Bundle $bundle -Name 'wslc-create-and-run' -TestFile 'wslc-e2e.test.js' -RequiredTests 'should reject an unsupported targetOs before starting a container' }
'C#' = { Invoke-XunitClasses -Bundle $bundle -Name 'wslc' -Classes 'MxcLifecycleWslcE2ETests', 'MxcContainerWslcE2ETests' -RequiredMethods 'DevJson_WslcLifecyclePreservesResponsesExecutionAndCallerOwnership' }
'Node' = {
Stop-WslcDaemons
Invoke-NodeIntegrationSuite -Bundle $bundle -Name 'wslc' -TestFile 'wslc-state-aware.test.js' `
-RequiredTests @('V1.Dev preserves WSLC lifecycle responses, captured exec, pipes, and caller ownership',
'captured exec preserves structured post-admission wait failures',
'process wait preserves structured post-admission wait failures')
Comment thread
Copilot marked this conversation as resolved.
Resolved
}
'Node create-and-run' = {
Invoke-NodeIntegrationSuite -Bundle $bundle -Name 'wslc-create-and-run' -TestFile 'wslc-e2e.test.js' `
-RequiredTests @('should reject an unsupported targetOs before starting a container',
'captured run preserves structured post-admission wait failures',
'spawned process wait preserves structured post-admission wait failures')
}
'C#' = {
Stop-WslcDaemons
Invoke-XunitClasses -Bundle $bundle -Name 'wslc' -Classes 'MxcLifecycleWslcE2ETests', 'MxcContainerWslcE2ETests' `
-RequiredMethods @('DevJson_WslcLifecyclePreservesResponsesExecutionAndCallerOwnership',
'RunInContainer_PreservesStructuredPostAdmissionWaitFailure',
'SpawnInContainer_WaitPreservesStructuredPostAdmissionWaitFailure',
'Run_PreservesStructuredPostAdmissionWaitFailure',
'Spawn_WaitPreservesStructuredPostAdmissionWaitFailure')
}
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
<DefineConstants Condition="'$(MxcWithIsolationSession)' == 'true'">$(DefineConstants);MXC_WITH_ISOLATION_SESSION</DefineConstants>
<MxcSrcDir>$(MSBuildProjectDirectory)/../../../src</MxcSrcDir>
<MxcTestCargoTargetDir>$(MxcSrcDir)/target/dotnet-test-support</MxcTestCargoTargetDir>
<MxcTestWslcCargoTargetDir Condition="'$(CARGO_TARGET_DIR)' != ''">$(CARGO_TARGET_DIR)/dotnet-wslc-test-support</MxcTestWslcCargoTargetDir>
<MxcTestRepositoryName>$([System.IO.Path]::GetFileName($([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/../../..'))))</MxcTestRepositoryName>
<MxcTestWslcCargoRoot Condition="'$(LOCALAPPDATA)' != ''">$(LOCALAPPDATA)</MxcTestWslcCargoRoot>
<MxcTestWslcCargoRoot Condition="'$(MxcTestWslcCargoRoot)' == ''">$(TEMP)</MxcTestWslcCargoRoot>
<MxcTestWslcCargoTargetDir Condition="'$(MxcTestWslcCargoTargetDir)' == '' and $([MSBuild]::IsOSPlatform('Windows'))">$(MxcTestWslcCargoRoot)/mxct/dotnet-wslc-test-support/$(MxcTestRepositoryName)</MxcTestWslcCargoTargetDir>
<!-- Test isolation overrides are compiled only with Rust debug assertions,
including when the managed tests themselves run in Release. -->
<MxcTestCargoProfileDir>debug</MxcTestCargoProfileDir>
Expand Down Expand Up @@ -52,13 +57,25 @@
<PropertyGroup>
<MxcTestRustHostTriple>$(MxcTestRustcHostLine.Replace('host: ', '').Trim())</MxcTestRustHostTriple>
<MxcTestNativeOutput>$(MxcTestCargoTargetDir)/$(MxcTestRustHostTriple)/$(MxcTestCargoProfileDir)/$(MxcTestNativeFileName)</MxcTestNativeOutput>
<MxcTestWslcDaemonOutput>$(MxcTestWslcCargoTargetDir)/$(MxcTestRustHostTriple)/$(MxcTestCargoProfileDir)/wxc-wslc-daemon.exe</MxcTestWslcDaemonOutput>
</PropertyGroup>
<Message Importance="high" Text="mxc: building isolated test-support native library for $(MxcTestRustHostTriple)" />
<Exec Command="cargo build -p mxc_ffi --features &quot;$(MxcTestCargoFeatures)&quot; --target $(MxcTestRustHostTriple) --target-dir &quot;$(MxcTestCargoTargetDir)&quot;" WorkingDirectory="$(MxcSrcDir)" />
<Error Condition="!Exists('$(MxcTestNativeOutput)')"
Text="The test-support native build did not produce $(MxcTestNativeOutput)." />
<Copy SourceFiles="$(MxcTestNativeOutput)"
DestinationFolder="$(TargetDir)" />
<Message Importance="high"
Condition="'$(MxcWithWslc)' == 'true' and $([MSBuild]::IsOSPlatform('Windows'))"
Text="mxc: building isolated test-support WSLC daemon for $(MxcTestRustHostTriple)" />
<Exec Command="cargo build -p mxc-sdk --bin wxc-wslc-daemon --features &quot;wslc test-support&quot; --target $(MxcTestRustHostTriple) --target-dir &quot;$(MxcTestWslcCargoTargetDir)&quot;"
WorkingDirectory="$(MxcSrcDir)"
Condition="'$(MxcWithWslc)' == 'true' and $([MSBuild]::IsOSPlatform('Windows'))" />
<Error Condition="'$(MxcWithWslc)' == 'true' and $([MSBuild]::IsOSPlatform('Windows')) and !Exists('$(MxcTestWslcDaemonOutput)')"
Text="The test-support WSLC daemon build did not produce $(MxcTestWslcDaemonOutput)." />
<Copy SourceFiles="$(MxcTestWslcDaemonOutput)"
DestinationFolder="$(TargetDir)"
Condition="'$(MxcWithWslc)' == 'true' and $([MSBuild]::IsOSPlatform('Windows'))" />
</Target>

<Target Name="StageMxcTestSupportNativeLibrary"
Expand All @@ -68,6 +85,11 @@
Text="The prebuilt test-support native library does not exist at $(MxcTestPrebuiltNativeLibrary)." />
<Copy SourceFiles="$(MxcTestPrebuiltNativeLibrary)"
DestinationFolder="$(TargetDir)" />
<Error Condition="'$(MxcTestPrebuiltWslcDaemon)' != '' and !Exists('$(MxcTestPrebuiltWslcDaemon)')"
Text="The prebuilt test-support WSLC daemon does not exist at $(MxcTestPrebuiltWslcDaemon)." />
<Copy SourceFiles="$(MxcTestPrebuiltWslcDaemon)"
DestinationFiles="$(TargetDir)wxc-wslc-daemon.exe"
Condition="'$(MxcTestPrebuiltWslcDaemon)' != ''" />
</Target>

<ItemGroup>
Expand Down
Loading
Loading