Skip to content
Draft
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
330 changes: 330 additions & 0 deletions doc/docs/technical-documentation/wslc-get-cli-session-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
# WslcGetCLISession API Design

## Overview

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.

### Motivation

The WSLC architecture follows an **app-owns-lifecycle** model:

```
App → Library → Session → Container
```

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).

`WslcGetCLISession` provides a stable, public mechanism to retrieve the session that the WSLC toolchain has established for the current process.

### Scope

- **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.
- **Out of scope**: Cross-process session sharing, remote session access, session creation.
- **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.

## API Design

### Prerequisites: Existing WSLC Types

The following types are established in the WSLC public API surface and are referenced by this design:

```c
// Opaque session handle (ref-counted)
typedef struct WslcSession_s* WslcSession;

// Standard WSLC lifecycle APIs (already exist)
STDAPI WslcCreateSession(_In_ const WslcSessionConfig* config, _Out_ WslcSession* session);
STDAPI WslcCloseSession(_In_ WslcSession session);

// Increment session reference count (already exists)
void WslcSessionAddRef(_In_ WslcSession session);
```
Comment on lines +29 to +41
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

This section states the WSLC public API types/functions “already exist”, but there are currently no Wslc* symbols in this repo (e.g., no WslcCreateSession/WslcCloseSession definitions under src/). To avoid readers assuming these are already available, either link to the existing header location or reword this block as proposed prerequisite types for the future WSLC surface.

Copilot uses AI. Check for mistakes.

> **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.

### New API

```c
// ---------------------------------------------------------------------------
// WslcGetCLISession
// ---------------------------------------------------------------------------
//
// Retrieves the active WSLC CLI session for the current process.
//
// The WSLC toolchain (wslc CLI, MSBuild targets, or IDE integration)
// publishes a session during the build/run/debug flow. This API returns
// that session to any code running in the same process — including the
// application being developed.
//
// The returned session handle is ref-counted. The caller receives an owned
// reference and MUST call WslcCloseSession() when finished. Closing the
// returned handle only releases the caller's reference — it does NOT close
// or destroy the underlying CLI session. The toolchain holds its own
// independent reference.
//
// If the process exits or is terminated, all in-process ref counts are
// reclaimed by the OS. There is no leak concern.
//
// Thread safety:
// - This function is safe to call concurrently from multiple threads.
// - The returned WslcSession handle is safe for concurrent use across
// threads (all WslcSession operations are internally synchronized).
//
// Lifetime:
// - Behavior is undefined if called during CRT static destruction
// (DLL_PROCESS_DETACH). Callers should release their session handles
// before process teardown begins.
//
// Parameters:
// session - [out] Receives the CLI session handle. On failure, set to NULL.
//
// Return values:
// S_OK - Session retrieved successfully.
// WSLC_E_NO_CLI_SESSION - No CLI session has been published in the
// current process (toolchain not initialized).
// E_POINTER - The session parameter is NULL.
//
STDAPI WslcGetCLISession(_Out_ WslcSession* session);
```

> **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`).

### Error Code Definition

```c
// WSLC-specific error codes use FACILITY_ITF (standard for interface-specific errors)
#define WSLC_E_NO_CLI_SESSION MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x8100)
```

`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.

## Design Decisions

### 1. HRESULT Return Type

**Decision**: Use `HRESULT`, not a custom `WslcResult` enum.

**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:

- Fragment the error handling surface
- Require conversion at every WSL/WSLC boundary
- Lose compatibility with standard Windows tooling (`SUCCEEDED()`, `FAILED()`, `FormatMessage()`)

Custom WSLC-specific error conditions are expressed as custom `HRESULT` values (e.g., `WSLC_E_NO_CLI_SESSION`), which is the standard Windows pattern.

### 2. Ref-Counted Owned Handle

**Decision**: `WslcGetCLISession` returns an **owned, ref-counted** handle. The caller must call `WslcCloseSession()` when finished.

**Rationale**: Returning a borrowed (non-owning) handle would be simpler but introduces safety risks:

| Concern | Borrowed Handle | Owned Handle |
|---------|----------------|--------------|
| Caller accidentally closes it | Use-after-free for CLI | Safe — only releases caller's ref |
| Toolchain tears down during async work | Dangling pointer | Caller's ref keeps session alive |
| Type confusion with owned handles | Same type, different rules | Same type, same rules |
| API surface complexity | Lower | Slightly higher (caller must close) |

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.

> **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`.

### 3. Process-Scoped, Publish-Once Semantics

**Decision**: The CLI session is published once during CLI initialization and is never replaced or cleared during the process lifetime.

**Rationale**: This provides a strong invariant that simplifies reasoning about the API:

- **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.
- **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.
- **No replacement**: Calling `WslcGetCLISession` at different times within the same process always returns the same underlying session.

### 4. Internal Publication Mechanism

The CLI runtime publishes its session via an **internal** (non-exported) function:

```c
// Internal — not part of the public API surface
void WslcPublishCLISession(_In_ WslcSession session);
```

This function:

1. Stores the session in a process-global atomic pointer with release semantics.
2. Calls `AddRef` on the session (the CLI retains its own reference separately).
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.
Comment on lines +153 to +155
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

The “Internal Publication Mechanism” text says subsequent publishes are “silently ignored” via std::call_once, but the implementation sketch uses only a CAS and then assert(false) on the else-path. Please make the design intent consistent (ignore vs fail-fast) and align the described mechanism (call_once vs CAS-only).

Suggested change
1. Stores the session in a process-global atomic pointer with release semantics.
2. Calls `AddRef` on the session (the CLI retains its own reference separately).
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.
1. Attempts to publish the session into a process-global atomic pointer using a compare-and-swap operation with release semantics; only the first successful call stores the pointer.
2. Calls `AddRef` on the session only when publication succeeds (the CLI retains its own reference separately).
3. Does not replace an already-published session. Subsequent calls are ignored after the CAS fails; debug builds should assert to catch unexpected duplicate publication attempts.

Copilot uses AI. Check for mistakes.

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.

### 6. Implementation Location

The new API is implemented within the WSLC library. The following files are involved:

| File | Action | Purpose |
|------|--------|---------|
| `src/windows/wslc/inc/wslc.h` | **Modify** | Public header — add `WslcGetCLISession` declaration and `WSLC_E_NO_CLI_SESSION` error code |
| `src/windows/wslc/core/cli_session.h` | **Add new** | Internal header — declare `WslcPublishCLISession` |
| `src/windows/wslc/core/cli_session.cpp` | **Add new** | Implementation — `g_cliSession` atomic, `WslcPublishCLISession`, `WslcGetCLISession` |
| `src/windows/wslc/wslc.def` | **Modify** | DLL exports — add `WslcGetCLISession` entry |
| `src/windows/wslc/core/session.cpp` | **Modify** | Existing session init — call `WslcPublishCLISession` after session creation |
| `src/windows/wslc/core/session.h` | **Reference only** | Existing session internals — understand `WslcSession_s` and `WslcSessionAddRef` |
Comment on lines +159 to +170
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

The proposed implementation file paths reference src/windows/wslc/... and test/windows/wslc/..., but this repository snapshot doesn’t currently contain a src/windows/wslc tree. If those directories will be introduced later, consider calling that out explicitly (or link to the planned location) so the design doc doesn’t read as if it matches current repo structure.

Copilot uses AI. Check for mistakes.
| `src/windows/wslc/CMakeLists.txt` | **Modify** | Build config — add `core/cli_session.cpp` to source list |
| `test/windows/wslc/GetCLISessionTests.cpp` | **Add new** | Unit tests for the new API |

> **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.

### 5. Naming: `WslcGetCLISession`
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Section numbering is out of order here ("### 6. Implementation Location" appears before "### 5. Naming"). Reordering or renumbering would make the document easier to follow and reduces confusion when referencing sections in future discussions.

Suggested change
### 5. Naming: `WslcGetCLISession`
### 7. Naming: `WslcGetCLISession`

Copilot uses AI. Check for mistakes.

**Decision**: Use `WslcGetCLISession`.

| Alternative | Why Rejected |
|-------------|-------------|
| `WslcGetCurrentSession` | Ambiguous — "current" could mean the most recently created session |
| `WslcGetProcessSession` | Too generic — doesn't convey it's the toolchain-established session |
| `WslcAcquireCLISession` | "Acquire" implies lock semantics or exclusive access |
| `WslcGetDefaultSession` | Confusing — "default" has a different meaning in WSL (default distro) |

`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.

## Implementation Sketch

### Session Storage

```cpp
// wslc_cli_session.cpp (internal)
namespace {
std::atomic<WslcSession_s*> g_cliSession{nullptr};
}
```

### Publication (CLI startup)

```cpp
void WslcPublishCLISession(WslcSession session)
{
// CAS ensures only the first caller succeeds; subsequent calls
// see non-null and hit the assert.
WslcSession_s* expected = nullptr;
if (g_cliSession.compare_exchange_strong(expected, session, std::memory_order_release, std::memory_order_relaxed))
{
// AddRef for the global reference (intentionally leaked — see below)
WslcSessionAddRef(session);
}
else
{
assert(false && "CLI session published more than once");
}
}
```

> **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.
Comment on lines +218 to +220
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

The “Global reference lifetime” note says the global AddRef is never released (so the session won’t be destroyed until process exit), but then claims destructor-side cleanup is handled when the toolchain releases its own reference during normal shutdown. Those two statements conflict: if the global ref remains, the destructor won’t run at toolchain shutdown. Please clarify the intended cleanup model (process-exit only vs explicit shutdown/Reset API) so implementers don’t rely on destructor cleanup that won’t happen.

Copilot uses AI. Check for mistakes.

### Retrieval (Public API)

```cpp
STDAPI WslcGetCLISession(_Out_ WslcSession* session)
{
RETURN_HR_IF(E_POINTER, session == nullptr);
*session = nullptr;

// Acquire load pairs with release store in WslcPublishCLISession.
// Readers that run before publication see nullptr (-> WSLC_E_NO_CLI_SESSION).
auto* raw = g_cliSession.load(std::memory_order_acquire);
RETURN_HR_IF(WSLC_E_NO_CLI_SESSION, raw == nullptr);

// AddRef for the caller's reference
WslcSessionAddRef(raw);
*session = raw;
return S_OK;
}
```

### Thread Safety Analysis

| Operation | Synchronization | Notes |
|-----------|----------------|-------|
| `g_cliSession` write | `compare_exchange_strong` + release | Exactly once; CAS guarantees atomicity; second publish asserts in debug |
| `g_cliSession` read | Acquire load | Sees fully initialized session or nullptr (→ `WSLC_E_NO_CLI_SESSION`) |
| `WslcSessionAddRef` | Internal atomic increment | Standard ref-count thread safety |
| Session operations | Internal session locks | All `WslcSession` operations are synchronized |
| Global reference cleanup | Intentional leak | Reclaimed by process exit; see "Global reference lifetime" note |

## Usage Example

### Inner-Loop: Build, Run, and Debug

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:

```c
// --- Session acquisition (the only part that differs) ---
WslcSession session = NULL;

#ifdef _DEBUG
// Debug build: reuse the session published by the WSLC toolchain
// (wslc CLI, MSBuild targets, or IDE integration)
HRESULT hr = WslcGetCLISession(&session);
#else
// Release build: create a standalone session
WslcSessionConfig config = {};
HRESULT hr = WslcCreateSession(&config, &session);
#endif

if (FAILED(hr))
{
return hr;
}

// --- Everything below is the same for debug and release ---

// Create and run the container
WslcContainerConfig containerConfig = {};
WslcInitContainerConfig("my-image:latest", &containerConfig);

WslcContainer container = NULL;
hr = WslcCreateContainer(session, &containerConfig, &container);
if (FAILED(hr))
{
WslcCloseSession(session);
return hr;
}

hr = WslcStartContainer(container, WSLC_CONTAINER_START_FLAG_NONE);
// ... container is running ...

WslcCloseContainer(container);

// Release our reference (does NOT close the CLI session in debug builds)
WslcCloseSession(session);
```

## Testing Strategy

### Unit Tests

| Test Case | Description |
|-----------|-------------|
| `GetCLISession_BeforePublish` | Call `WslcGetCLISession` before any session is published. Expect `WSLC_E_NO_CLI_SESSION`. |
| `GetCLISession_AfterPublish` | Publish a session, then call `WslcGetCLISession`. Expect `S_OK` and valid handle. |
| `GetCLISession_NullParam` | Pass `NULL` output parameter. Expect `E_POINTER`. |
| `GetCLISession_RefCounting` | Call `WslcGetCLISession` twice, verify both handles are valid and independent. Close one, verify the other still works. |
| `GetCLISession_ConcurrentAccess` | Call `WslcGetCLISession` from multiple threads simultaneously. Verify all succeed and return the same underlying session. |
| `GetCLISession_SessionIdentity` | Verify that multiple calls return handles to the same underlying session (same session ID / properties). |
| `GetCLISession_PublishOnce` | Attempt to publish a second session. Verify the invariant holds (assertion fires / second publish is ignored). |

### Integration Tests

| Test Case | Description |
|-----------|-------------|
| `CLISession_BuildAndRun` | Run `wslc build && wslc run`, verify that application code calling `WslcGetCLISession` during the run receives the same session the toolchain created. |
| `CLISession_ProcessExit` | Obtain a session handle, then exit the process without calling `WslcCloseSession`. Verify no resource leak or crash (OS reclaims ref counts). |

## Appendix: Relationship to Existing WSL APIs

| Existing Pattern | WSLC Equivalent | Notes |
|-----------------|----------------|-------|
| `WslConfig_t` opaque handle | `WslcSession` opaque handle | Same pattern: typedef to opaque struct pointer |
| `CreateWslConfig` / `FreeWslConfig` | `WslcCreateSession` / `WslcCloseSession` | Create/Free lifecycle pair |
| `GetWslConfigFilePath` (returns existing) | `WslcGetCLISession` (returns existing) | "Get" = retrieve, not create |
| `HRESULT` return codes | `HRESULT` return codes | Standard Win32 error handling |
| `WSL_E_*` custom errors | `WSLC_E_*` custom errors | FACILITY_ITF with distinct ranges |
| `ILxssUserSession` per-user COM singleton | CLI session per-process singleton | Different scope, same singleton concept |
Loading