diff --git a/docs/backends/wslc/wsl-container-getting-started.md b/docs/backends/wslc/wsl-container-getting-started.md index 72f4cbcc6..9041aa7ea 100644 --- a/docs/backends/wslc/wsl-container-getting-started.md +++ b/docs/backends/wslc/wsl-container-getting-started.md @@ -352,6 +352,15 @@ WSLC-specific settings go under `wslc` in the JSON config: | `imageTarPath` | string | — | Path to a local tar file to import as the image | | `portMappings` | array | `[]` | Host→container TCP forwards; each entry takes `windowsPort` and `containerPort` | +### `containerId` names the WSL session + +A create-and-run request names its WSL session after the top-level +`containerId`. The field is optional: omit it and the run gets a generated +name of the form `mxc-wslc-`, fresh on every run. + +Supply your own only when you need a name you can recognise — two runs sharing +one `containerId` are not isolated from each other by the id alone. + ### Port mappings ```json diff --git a/docs/schema.md b/docs/schema.md index aeb1cdb72..c8df27bf0 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -181,7 +181,8 @@ that can be executed independently. ```json { "version": "1.0.0", // Exact schema version. Minimum supported: "0.9.0-alpha"; current stable: "1.0.0". - "containerId": "my-container", // Externally assigned container ID + "containerId": "my-container", // Optional externally assigned container ID. + // Omit it to use backend-defined internal naming. "containment": "processcontainer", // Backend (see table below) "lifecycle": { diff --git a/src/mxc-sdk/src/backends/wslc/common/wsl_container_runner.rs b/src/mxc-sdk/src/backends/wslc/common/wsl_container_runner.rs index 31a560e76..bc97cd469 100644 --- a/src/mxc-sdk/src/backends/wslc/common/wsl_container_runner.rs +++ b/src/mxc-sdk/src/backends/wslc/common/wsl_container_runner.rs @@ -492,6 +492,21 @@ fn reject_unsupported_lifecycle(request: &ExecutionRequest) -> Result<(), Script Ok(()) } +const GENERATED_SESSION_NAME_PREFIX: &str = "mxc-wslc-"; + +/// Name the WSLc session a run gets, minting one when `containerId` is absent: +/// WSLc refuses an empty name with `WSLC_E_INVALID_SESSION_NAME` (0x80040608). +fn resolve_session_name(container_id: &str) -> String { + if container_id.is_empty() { + format!( + "{GENERATED_SESSION_NAME_PREFIX}{}", + uuid::Uuid::new_v4().simple() + ) + } else { + container_id.to_string() + } +} + impl WSLContainerRunner { /// Configure session settings and create the session. /// Returns the session guard (RAII). @@ -506,7 +521,8 @@ impl WSLContainerRunner { request: &ExecutionRequest, logger: &mut Logger, ) -> Result { - let session_name: Vec = to_wide(&request.container_id); + let session_name_str = resolve_session_name(&request.container_id); + let session_name: Vec = to_wide(&session_name_str); let storage_path_str = self.config.storage_path.clone().unwrap_or_else(|| { std::env::temp_dir() .join("mxc-wslc-sessions") @@ -574,7 +590,7 @@ impl WSLContainerRunner { let msg = err_msg.to_string_lossy(); return Err(sdk_error("WslcCreateSession failed", hr, &msg)); } - let _ = writeln!(logger, "[WSLC] Session created"); + let _ = writeln!(logger, "[WSLC] Session created: {session_name_str}"); Ok(WslcSessionGuard::from_raw( session, @@ -2270,4 +2286,29 @@ mod tests { assert!(message.contains("ensure WSL2 and the WSLC SDK are installed")); } + + #[test] + fn session_name_is_the_container_id_when_the_request_names_one() { + assert_eq!(resolve_session_name("pitest"), "pitest"); + assert_eq!(resolve_session_name("wslc-hello-world"), "wslc-hello-world"); + } + + #[test] + fn absent_container_id_mints_a_session_name() { + let name = resolve_session_name(""); + + assert!( + name.starts_with(GENERATED_SESSION_NAME_PREFIX), + "expected a minted name, got: {name}" + ); + assert!( + name.len() > GENERATED_SESSION_NAME_PREFIX.len(), + "an empty name is what WSLc refuses with WSLC_E_INVALID_SESSION_NAME, got: {name}" + ); + } + + #[test] + fn minted_session_names_do_not_repeat() { + assert_ne!(resolve_session_name(""), resolve_session_name("")); + } } diff --git a/src/tools/wxc/src/main.rs b/src/tools/wxc/src/main.rs index 96b231547..685738be2 100644 --- a/src/tools/wxc/src/main.rs +++ b/src/tools/wxc/src/main.rs @@ -283,7 +283,7 @@ fn log_request(request: &ExecutionRequest, logger: &mut Logger) { logger, "Container name: {}", if request.container_id.is_empty() { - "CLI" + "(assigned by the backend)" } else { &request.container_id } diff --git a/tests/configs/wslc_no_container_id.json b/tests/configs/wslc_no_container_id.json new file mode 100644 index 000000000..5868ac6d8 --- /dev/null +++ b/tests/configs/wslc_no_container_id.json @@ -0,0 +1,14 @@ +{ + "$schema": "../../schemas/stable/mxc-config.schema.1.0.0.json", + "version": "1.0.0", + "containment": "wslc", + "process": { + "commandLine": "sh -c 'echo NO_CONTAINER_ID_OK'" + }, + "network": { + "egress": { "default": "deny" } + }, + "wslc": { + "image": "alpine:latest" + } +} diff --git a/tests/scripts/run_wslc_all_tests.ps1 b/tests/scripts/run_wslc_all_tests.ps1 index 922b498ef..389a8f927 100644 --- a/tests/scripts/run_wslc_all_tests.ps1 +++ b/tests/scripts/run_wslc_all_tests.ps1 @@ -248,6 +248,11 @@ $null = $results.Add((Run-WslcTest "wslc_exit_code.json" -ExpectedExit 42 -Outpu $null = $results.Add((Run-WslcTest "wslc_stderr.json" -OutputContains "stdout message")) $null = $results.Add((Run-WslcTest "wslc_large_output.json")) +# `containerId` is optional, and WSLc refuses the empty session name that an +# omitted one used to produce (WSLC_E_INVALID_SESSION_NAME, 0x80040608). Every +# other config here sets the field, so this is the only guard on that path. +$null = $results.Add((Run-WslcTest "wslc_no_container_id.json" -OutputContains "NO_CONTAINER_ID_OK")) + Write-Host "`n--- Environment Tests ---" -ForegroundColor Cyan # PYTHON_VERSION is baked into the image by ENV. Unlike PATH and TERM, no shell # fabricates it when it starts without one, so its absence is real evidence that