|
| 1 | +# WslcGetCLISession API Design |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This document describes the design of `WslcGetCLISession`, a public API in the WSLC (WSL Containers) library that returns a reference to the active CLI session for the current process. This is primarily used during the **inner-loop development experience** — build, run, and debug flows for Windows applications that use Linux containers via WSLC. |
| 6 | + |
| 7 | +### Motivation |
| 8 | + |
| 9 | +The WSLC architecture follows an **app-owns-lifecycle** model: |
| 10 | + |
| 11 | +``` |
| 12 | +App → Library → Session → Container |
| 13 | +``` |
| 14 | + |
| 15 | +During the inner-loop development flow, the WSLC toolchain (MSBuild targets, `wslc` CLI, or IDE integration) creates a `WslcSession` to manage container operations. Application code running in the same process — such as build tasks, debug launch helpers, or the app itself during F5 — needs access to this session to interact with the container (e.g., attach a debugger, inspect state, or run additional commands). |
| 16 | + |
| 17 | +`WslcGetCLISession` provides a stable, public mechanism to retrieve the session that the WSLC toolchain has established for the current process. |
| 18 | + |
| 19 | +### Scope |
| 20 | + |
| 21 | +- **In scope**: Retrieving the WSLC CLI session from any code running in the same process where the WSLC toolchain has published a session. This includes the `wslc` CLI process, MSBuild task host processes, Windows app processes launched with WSLC integration, and IDE extension hosts. |
| 22 | +- **Out of scope**: Cross-process session sharing, remote session access, session creation. |
| 23 | +- **Usage context**: Inner-loop developer experience — build, run, debug. Not a primary production API, but designed to the same quality standards as all WSLC public APIs. |
| 24 | + |
| 25 | +## API Design |
| 26 | + |
| 27 | +### Prerequisites: Existing WSLC Types |
| 28 | + |
| 29 | +The following types are established in the WSLC public API surface and are referenced by this design: |
| 30 | + |
| 31 | +```c |
| 32 | +// Opaque session handle (ref-counted) |
| 33 | +typedef struct WslcSession_s* WslcSession; |
| 34 | + |
| 35 | +// Standard WSLC lifecycle APIs (already exist) |
| 36 | +STDAPI WslcCreateSession(_In_ const WslcSessionConfig* config, _Out_ WslcSession* session); |
| 37 | +STDAPI WslcCloseSession(_In_ WslcSession session); |
| 38 | + |
| 39 | +// Increment session reference count (already exists) |
| 40 | +void WslcSessionAddRef(_In_ WslcSession session); |
| 41 | +``` |
| 42 | +
|
| 43 | +> **Note**: WSLC uses `Close` instead of `Free` (contrast with `FreeWslConfig` in the WSL Config API) to emphasize ref-counted release semantics — `WslcCloseSession` decrements the reference count and only destroys the session when it reaches zero. |
| 44 | +
|
| 45 | +### New API |
| 46 | +
|
| 47 | +```c |
| 48 | +// --------------------------------------------------------------------------- |
| 49 | +// WslcGetCLISession |
| 50 | +// --------------------------------------------------------------------------- |
| 51 | +// |
| 52 | +// Retrieves the active WSLC CLI session for the current process. |
| 53 | +// |
| 54 | +// The WSLC toolchain (wslc CLI, MSBuild targets, or IDE integration) |
| 55 | +// publishes a session during the build/run/debug flow. This API returns |
| 56 | +// that session to any code running in the same process — including the |
| 57 | +// application being developed. |
| 58 | +// |
| 59 | +// The returned session handle is ref-counted. The caller receives an owned |
| 60 | +// reference and MUST call WslcCloseSession() when finished. Closing the |
| 61 | +// returned handle only releases the caller's reference — it does NOT close |
| 62 | +// or destroy the underlying CLI session. The toolchain holds its own |
| 63 | +// independent reference. |
| 64 | +// |
| 65 | +// If the process exits or is terminated, all in-process ref counts are |
| 66 | +// reclaimed by the OS. There is no leak concern. |
| 67 | +// |
| 68 | +// Thread safety: |
| 69 | +// - This function is safe to call concurrently from multiple threads. |
| 70 | +// - The returned WslcSession handle is safe for concurrent use across |
| 71 | +// threads (all WslcSession operations are internally synchronized). |
| 72 | +// |
| 73 | +// Lifetime: |
| 74 | +// - Behavior is undefined if called during CRT static destruction |
| 75 | +// (DLL_PROCESS_DETACH). Callers should release their session handles |
| 76 | +// before process teardown begins. |
| 77 | +// |
| 78 | +// Parameters: |
| 79 | +// session - [out] Receives the CLI session handle. On failure, set to NULL. |
| 80 | +// |
| 81 | +// Return values: |
| 82 | +// S_OK - Session retrieved successfully. |
| 83 | +// WSLC_E_NO_CLI_SESSION - No CLI session has been published in the |
| 84 | +// current process (toolchain not initialized). |
| 85 | +// E_POINTER - The session parameter is NULL. |
| 86 | +// |
| 87 | +STDAPI WslcGetCLISession(_Out_ WslcSession* session); |
| 88 | +``` |
| 89 | + |
| 90 | +> **Export**: `WslcGetCLISession` is exported from the WSLC library DLL via its module `.def` file. Consumers link against the WSLC import library or call `GetProcAddress` on the loaded DLL. The declaration lives in the WSLC public header (`wslc.h`). |
| 91 | +
|
| 92 | +### Error Code Definition |
| 93 | + |
| 94 | +```c |
| 95 | +// WSLC-specific error codes use FACILITY_ITF (standard for interface-specific errors) |
| 96 | +#define WSLC_E_NO_CLI_SESSION MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x8100) |
| 97 | +``` |
| 98 | +
|
| 99 | +`FACILITY_ITF` is the standard COM facility for interface-specific error codes. The CODE offset `0x8100` is well-separated from the existing `WSL_E_*` error codes, which use CODE offsets in the `0x0300–0x03xx` range within `FACILITY_ITF`. Both produce HRESULTs in the `0x8004xxxx` range, but the CODE field separation avoids collisions. |
| 100 | +
|
| 101 | +## Design Decisions |
| 102 | +
|
| 103 | +### 1. HRESULT Return Type |
| 104 | +
|
| 105 | +**Decision**: Use `HRESULT`, not a custom `WslcResult` enum. |
| 106 | +
|
| 107 | +**Rationale**: Every existing WSL public API uses `HRESULT` — the Plugin API, the Config API, the COM service interface, and internal helpers. Introducing a separate error type would: |
| 108 | +
|
| 109 | +- Fragment the error handling surface |
| 110 | +- Require conversion at every WSL/WSLC boundary |
| 111 | +- Lose compatibility with standard Windows tooling (`SUCCEEDED()`, `FAILED()`, `FormatMessage()`) |
| 112 | +
|
| 113 | +Custom WSLC-specific error conditions are expressed as custom `HRESULT` values (e.g., `WSLC_E_NO_CLI_SESSION`), which is the standard Windows pattern. |
| 114 | +
|
| 115 | +### 2. Ref-Counted Owned Handle |
| 116 | +
|
| 117 | +**Decision**: `WslcGetCLISession` returns an **owned, ref-counted** handle. The caller must call `WslcCloseSession()` when finished. |
| 118 | +
|
| 119 | +**Rationale**: Returning a borrowed (non-owning) handle would be simpler but introduces safety risks: |
| 120 | +
|
| 121 | +| Concern | Borrowed Handle | Owned Handle | |
| 122 | +|---------|----------------|--------------| |
| 123 | +| Caller accidentally closes it | Use-after-free for CLI | Safe — only releases caller's ref | |
| 124 | +| Toolchain tears down during async work | Dangling pointer | Caller's ref keeps session alive | |
| 125 | +| Type confusion with owned handles | Same type, different rules | Same type, same rules | |
| 126 | +| API surface complexity | Lower | Slightly higher (caller must close) | |
| 127 | +
|
| 128 | +Since `WslcSession` is already a ref-counted type in the WSLC API, returning an owned handle is consistent. The minor overhead of an `AddRef` call is negligible compared to the safety benefits. |
| 129 | +
|
| 130 | +> **Important**: Calling `WslcCloseSession` on a handle returned by `WslcGetCLISession` **never** closes the actual CLI session. It only decrements the caller's reference. The CLI session continues to operate normally. When the process exits or is terminated, the OS reclaims all in-process memory — including any ref counts — so there is no leak concern regardless of whether the caller remembers to call `WslcCloseSession`. |
| 131 | +
|
| 132 | +### 3. Process-Scoped, Publish-Once Semantics |
| 133 | +
|
| 134 | +**Decision**: The CLI session is published once during CLI initialization and is never replaced or cleared during the process lifetime. |
| 135 | +
|
| 136 | +**Rationale**: This provides a strong invariant that simplifies reasoning about the API: |
| 137 | +
|
| 138 | +- **Publish-once**: After `WslcGetCLISession` returns `S_OK` for a given process, it will always return the same session handle (with a new reference) for the remainder of the process lifetime. |
| 139 | +- **Never cleared**: The CLI runtime does not unpublish the session. Even during CLI shutdown, the session remains accessible (but see Lifetime note above — behavior is undefined during CRT static destruction). Ref-counting ensures the session is only destroyed when all references (including the CLI's own reference) are released. |
| 140 | +- **No replacement**: Calling `WslcGetCLISession` at different times within the same process always returns the same underlying session. |
| 141 | +
|
| 142 | +### 4. Internal Publication Mechanism |
| 143 | +
|
| 144 | +The CLI runtime publishes its session via an **internal** (non-exported) function: |
| 145 | +
|
| 146 | +```c |
| 147 | +// Internal — not part of the public API surface |
| 148 | +void WslcPublishCLISession(_In_ WslcSession session); |
| 149 | +``` |
| 150 | + |
| 151 | +This function: |
| 152 | + |
| 153 | +1. Stores the session in a process-global atomic pointer with release semantics. |
| 154 | +2. Calls `AddRef` on the session (the CLI retains its own reference separately). |
| 155 | +3. Silently ignores subsequent calls (the `std::call_once` guard ensures only the first invocation takes effect). Debug builds should assert that this function is not called more than once. |
| 156 | + |
| 157 | +The publication happens during the WSLC toolchain initialization — when `wslc` CLI starts, when MSBuild targets load the WSLC library, or when the IDE integration initializes the container environment — after the session is fully initialized but before any build/run/debug operations begin. |
| 158 | + |
| 159 | +### 6. Implementation Location |
| 160 | + |
| 161 | +The new API is implemented within the WSLC library. The following files are involved: |
| 162 | + |
| 163 | +| File | Action | Purpose | |
| 164 | +|------|--------|---------| |
| 165 | +| `src/windows/wslc/inc/wslc.h` | **Modify** | Public header — add `WslcGetCLISession` declaration and `WSLC_E_NO_CLI_SESSION` error code | |
| 166 | +| `src/windows/wslc/core/cli_session.h` | **Add new** | Internal header — declare `WslcPublishCLISession` | |
| 167 | +| `src/windows/wslc/core/cli_session.cpp` | **Add new** | Implementation — `g_cliSession` atomic, `WslcPublishCLISession`, `WslcGetCLISession` | |
| 168 | +| `src/windows/wslc/wslc.def` | **Modify** | DLL exports — add `WslcGetCLISession` entry | |
| 169 | +| `src/windows/wslc/core/session.cpp` | **Modify** | Existing session init — call `WslcPublishCLISession` after session creation | |
| 170 | +| `src/windows/wslc/core/session.h` | **Reference only** | Existing session internals — understand `WslcSession_s` and `WslcSessionAddRef` | |
| 171 | +| `src/windows/wslc/CMakeLists.txt` | **Modify** | Build config — add `core/cli_session.cpp` to source list | |
| 172 | +| `test/windows/wslc/GetCLISessionTests.cpp` | **Add new** | Unit tests for the new API | |
| 173 | + |
| 174 | +> **Note**: The exact file paths follow the WSLC project structure convention. The key principle is: the public declaration goes in the public header (`wslc.h`), the implementation goes in the `core/` subdirectory alongside other session management code, and the export goes in the `.def` file. |
| 175 | +
|
| 176 | +### 5. Naming: `WslcGetCLISession` |
| 177 | + |
| 178 | +**Decision**: Use `WslcGetCLISession`. |
| 179 | + |
| 180 | +| Alternative | Why Rejected | |
| 181 | +|-------------|-------------| |
| 182 | +| `WslcGetCurrentSession` | Ambiguous — "current" could mean the most recently created session | |
| 183 | +| `WslcGetProcessSession` | Too generic — doesn't convey it's the toolchain-established session | |
| 184 | +| `WslcAcquireCLISession` | "Acquire" implies lock semantics or exclusive access | |
| 185 | +| `WslcGetDefaultSession` | Confusing — "default" has a different meaning in WSL (default distro) | |
| 186 | + |
| 187 | +`WslcGetCLISession` is clear: it returns the session established by the WSLC CLI toolchain. The `Get` prefix aligns with existing WSL patterns (`GetWslConfigFilePath`, `GetDefaultDistribution`, `GetDistributionId`). The name remains appropriate even when called from non-CLI contexts (MSBuild tasks, IDE hosts) because the session originates from the CLI/toolchain layer. |
| 188 | + |
| 189 | +## Implementation Sketch |
| 190 | + |
| 191 | +### Session Storage |
| 192 | + |
| 193 | +```cpp |
| 194 | +// wslc_cli_session.cpp (internal) |
| 195 | +namespace { |
| 196 | + std::atomic<WslcSession_s*> g_cliSession{nullptr}; |
| 197 | +} |
| 198 | +``` |
| 199 | + |
| 200 | +### Publication (CLI startup) |
| 201 | + |
| 202 | +```cpp |
| 203 | +void WslcPublishCLISession(WslcSession session) |
| 204 | +{ |
| 205 | + // CAS ensures only the first caller succeeds; subsequent calls |
| 206 | + // see non-null and hit the assert. |
| 207 | + WslcSession_s* expected = nullptr; |
| 208 | + if (g_cliSession.compare_exchange_strong(expected, session, std::memory_order_release, std::memory_order_relaxed)) |
| 209 | + { |
| 210 | + // AddRef for the global reference (intentionally leaked — see below) |
| 211 | + WslcSessionAddRef(session); |
| 212 | + } |
| 213 | + else |
| 214 | + { |
| 215 | + assert(false && "CLI session published more than once"); |
| 216 | + } |
| 217 | +} |
| 218 | +``` |
| 219 | +
|
| 220 | +> **Global reference lifetime**: The `AddRef` performed by `WslcPublishCLISession` is intentionally never released. The global reference is "leaked" and reclaimed by process exit. This is a deliberate design choice — there is no safe point during shutdown to release the global reference because other code may still hold derived references obtained from `WslcGetCLISession`. When the Windows app closes or is terminated, the OS reclaims all process memory including ref counts — no cleanup is needed. The session's destructor-side cleanup (e.g., closing hvsocket connections) is handled by the toolchain's own reference, which it releases during its normal shutdown path. |
| 221 | +
|
| 222 | +### Retrieval (Public API) |
| 223 | +
|
| 224 | +```cpp |
| 225 | +STDAPI WslcGetCLISession(_Out_ WslcSession* session) |
| 226 | +{ |
| 227 | + RETURN_HR_IF(E_POINTER, session == nullptr); |
| 228 | + *session = nullptr; |
| 229 | +
|
| 230 | + // Acquire load pairs with release store in WslcPublishCLISession. |
| 231 | + // Readers that run before publication see nullptr (-> WSLC_E_NO_CLI_SESSION). |
| 232 | + auto* raw = g_cliSession.load(std::memory_order_acquire); |
| 233 | + RETURN_HR_IF(WSLC_E_NO_CLI_SESSION, raw == nullptr); |
| 234 | +
|
| 235 | + // AddRef for the caller's reference |
| 236 | + WslcSessionAddRef(raw); |
| 237 | + *session = raw; |
| 238 | + return S_OK; |
| 239 | +} |
| 240 | +``` |
| 241 | + |
| 242 | +### Thread Safety Analysis |
| 243 | + |
| 244 | +| Operation | Synchronization | Notes | |
| 245 | +|-----------|----------------|-------| |
| 246 | +| `g_cliSession` write | `compare_exchange_strong` + release | Exactly once; CAS guarantees atomicity; second publish asserts in debug | |
| 247 | +| `g_cliSession` read | Acquire load | Sees fully initialized session or nullptr (→ `WSLC_E_NO_CLI_SESSION`) | |
| 248 | +| `WslcSessionAddRef` | Internal atomic increment | Standard ref-count thread safety | |
| 249 | +| Session operations | Internal session locks | All `WslcSession` operations are synchronized | |
| 250 | +| Global reference cleanup | Intentional leak | Reclaimed by process exit; see "Global reference lifetime" note | |
| 251 | + |
| 252 | +## Usage Example |
| 253 | + |
| 254 | +### Inner-Loop: Build, Run, and Debug |
| 255 | + |
| 256 | +The `#ifdef _DEBUG` guard controls **only** session acquisition — in debug builds, the app reuses the CLI session established by the WSLC toolchain; in release builds, it creates its own. The rest of the container lifecycle code is identical: |
| 257 | + |
| 258 | +```c |
| 259 | +// --- Session acquisition (the only part that differs) --- |
| 260 | +WslcSession session = NULL; |
| 261 | + |
| 262 | +#ifdef _DEBUG |
| 263 | +// Debug build: reuse the session published by the WSLC toolchain |
| 264 | +// (wslc CLI, MSBuild targets, or IDE integration) |
| 265 | +HRESULT hr = WslcGetCLISession(&session); |
| 266 | +#else |
| 267 | +// Release build: create a standalone session |
| 268 | +WslcSessionConfig config = {}; |
| 269 | +HRESULT hr = WslcCreateSession(&config, &session); |
| 270 | +#endif |
| 271 | + |
| 272 | +if (FAILED(hr)) |
| 273 | +{ |
| 274 | + return hr; |
| 275 | +} |
| 276 | + |
| 277 | +// --- Everything below is the same for debug and release --- |
| 278 | + |
| 279 | +// Create and run the container |
| 280 | +WslcContainerConfig containerConfig = {}; |
| 281 | +WslcInitContainerConfig("my-image:latest", &containerConfig); |
| 282 | + |
| 283 | +WslcContainer container = NULL; |
| 284 | +hr = WslcCreateContainer(session, &containerConfig, &container); |
| 285 | +if (FAILED(hr)) |
| 286 | +{ |
| 287 | + WslcCloseSession(session); |
| 288 | + return hr; |
| 289 | +} |
| 290 | + |
| 291 | +hr = WslcStartContainer(container, WSLC_CONTAINER_START_FLAG_NONE); |
| 292 | +// ... container is running ... |
| 293 | + |
| 294 | +WslcCloseContainer(container); |
| 295 | + |
| 296 | +// Release our reference (does NOT close the CLI session in debug builds) |
| 297 | +WslcCloseSession(session); |
| 298 | +``` |
| 299 | +
|
| 300 | +## Testing Strategy |
| 301 | +
|
| 302 | +### Unit Tests |
| 303 | +
|
| 304 | +| Test Case | Description | |
| 305 | +|-----------|-------------| |
| 306 | +| `GetCLISession_BeforePublish` | Call `WslcGetCLISession` before any session is published. Expect `WSLC_E_NO_CLI_SESSION`. | |
| 307 | +| `GetCLISession_AfterPublish` | Publish a session, then call `WslcGetCLISession`. Expect `S_OK` and valid handle. | |
| 308 | +| `GetCLISession_NullParam` | Pass `NULL` output parameter. Expect `E_POINTER`. | |
| 309 | +| `GetCLISession_RefCounting` | Call `WslcGetCLISession` twice, verify both handles are valid and independent. Close one, verify the other still works. | |
| 310 | +| `GetCLISession_ConcurrentAccess` | Call `WslcGetCLISession` from multiple threads simultaneously. Verify all succeed and return the same underlying session. | |
| 311 | +| `GetCLISession_SessionIdentity` | Verify that multiple calls return handles to the same underlying session (same session ID / properties). | |
| 312 | +| `GetCLISession_PublishOnce` | Attempt to publish a second session. Verify the invariant holds (assertion fires / second publish is ignored). | |
| 313 | +
|
| 314 | +### Integration Tests |
| 315 | +
|
| 316 | +| Test Case | Description | |
| 317 | +|-----------|-------------| |
| 318 | +| `CLISession_BuildAndRun` | Run `wslc build && wslc run`, verify that application code calling `WslcGetCLISession` during the run receives the same session the toolchain created. | |
| 319 | +| `CLISession_ProcessExit` | Obtain a session handle, then exit the process without calling `WslcCloseSession`. Verify no resource leak or crash (OS reclaims ref counts). | |
| 320 | +
|
| 321 | +## Appendix: Relationship to Existing WSL APIs |
| 322 | +
|
| 323 | +| Existing Pattern | WSLC Equivalent | Notes | |
| 324 | +|-----------------|----------------|-------| |
| 325 | +| `WslConfig_t` opaque handle | `WslcSession` opaque handle | Same pattern: typedef to opaque struct pointer | |
| 326 | +| `CreateWslConfig` / `FreeWslConfig` | `WslcCreateSession` / `WslcCloseSession` | Create/Free lifecycle pair | |
| 327 | +| `GetWslConfigFilePath` (returns existing) | `WslcGetCLISession` (returns existing) | "Get" = retrieve, not create | |
| 328 | +| `HRESULT` return codes | `HRESULT` return codes | Standard Win32 error handling | |
| 329 | +| `WSL_E_*` custom errors | `WSLC_E_*` custom errors | FACILITY_ITF with distinct ranges | |
| 330 | +| `ILxssUserSession` per-user COM singleton | CLI session per-process singleton | Different scope, same singleton concept | |
0 commit comments