Skip to content

Commit 64e7eca

Browse files
Add WslcGetCLISession API design document
Design document for the WslcGetCLISession public API in the WSLC library. This API returns the active CLI session for the current process, primarily for inner-loop development (build, run, debug) flows. Key design decisions: - STDAPI/HRESULT return type (consistent with existing WSL APIs) - Ref-counted owned handle (safe, consistent with WslcSession lifecycle) - Process-scoped publish-once via CAS (thread-safe singleton) - Intentional global ref leak (reclaimed by process exit) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ff358a0 commit 64e7eca

File tree

2 files changed

+332
-0
lines changed

2 files changed

+332
-0
lines changed
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
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 |

doc/mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ nav:
2525
- 'Interop': technical-documentation/interop.md
2626
- 'Drvfs & Plan9': technical-documentation/drvfs.md
2727
- 'Systemd': technical-documentation/systemd.md
28+
- 'WSLC API':
29+
- 'WslcGetCLISession': technical-documentation/wslc-get-cli-session-api.md
2830

2931

3032
plugins:

0 commit comments

Comments
 (0)