Implement the WSLC plugin API (#40521)
* Save state * Save state * Save state * Save state * Save state * Format * Save state * Save state * Format * Save state * Save state * Reorganize tests * Cleanup before review * Add missing file * Apply PR feedback * Apply PR feedback * Format * Apply PR feedback * merge * Apply PR feedback * Format * Apply PR feedback * Use iswalnum()
Blue committed
May 15, 2026 at 17:08 UTC
c9e4671b27e9318730d52963700e0d05c29e7ef7
27 files changed
+1450
-171
msipackage/package.wix.in
+8
@@ -354,6 +354,14 @@
354
</RegistryKey>
355
</RegistryKey>
356
357
+ <!-- IWSLCPluginNotifier-->
358
+ <RegistryKey Root="HKCR" Key="Interface\{F3E6D5B2-1D40-4E8B-9C39-7A45D1C0F8A2}">
359
+ <RegistryValue Value="IWSLCPluginNotifier" Type="string" />
360
+ <RegistryKey Key="ProxyStubClsid32">
361
+ <RegistryValue Value="{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}" Type="string" />
362
+ </RegistryKey>
363
+ </RegistryKey>
364
+
365
<File Id="wslcsession.exe" Source="${BIN}/wslcsession.exe" />
366
</Component>
367
<Component Id="wslg" Guid="F0C8D6BA-1502-41E7-BF72-D93DFA134731" UninstallWhenSuperseded="yes" DisableRegistryReflection="yes" Bitness="always64">
src/windows/inc/WslPluginApi.h
+102
-1
@@ -26,6 +26,9 @@ extern "C" {
26
#define WSLPLUGINAPI_ENTRYPOINTV1 WSLPluginAPIV1_EntryPoint
27
#define WSL_E_PLUGIN_REQUIRES_UPDATE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x032A)
28
29
+// Maximum size for mount points returned by WSLCPluginAPI_MountFolder. This includes the null terminator.
30
+#define WSLC_MOUNTPOINT_LENGTH 256
31
+
32
#define WSL_PLUGIN_REQUIRE_VERSION(_Major, _Minor, _Revision, Api) \
33
if (Api->Version.Major < (_Major) || (Api->Version.Major == (_Major) && Api->Version.Minor < (_Minor)) || \
34
(Api->Version.Major == (_Major) && Api->Version.Minor == (_Minor) && Api->Version.Revision < (_Revision))) \
@@ -85,6 +88,30 @@ struct WslOfflineDistributionInformation
88
LPCWSTR Version; // Distribution version. Introduced in 2.4.4
89
};
90
91
+// Identifies a WSLC session inside the WSLC plugin API. Distinct from WSLSessionId.
92
+typedef DWORD WSLCSessionId;
93
+
94
+// Information about a WSLC session passed to plugin notifications.
95
+struct WSLCSessionInformation
96
+{
97
+ WSLCSessionId SessionId;
98
+ LPCWSTR DisplayName;
99
+ DWORD ApplicationPid;
100
+ HANDLE UserToken;
101
+ PSID UserSid;
102
+};
103
+
104
+// Opaque handle to a WSLC process created via WSLCPluginAPI_CreateProcess.
105
+// Must be released with WSLCPluginAPI_ReleaseProcess.
106
+typedef void* WSLCProcessHandle;
107
+
108
+typedef enum _WSLCProcessFd
109
+{
110
+ WSLCProcessFdStdin = 0,
111
+ WSLCProcessFdStdout = 1,
112
+ WSLCProcessFdStderr = 2
113
+} WSLCProcessFd;
114
+
115
// Create plan9 mount between Windows & Linux
116
typedef HRESULT (*WSLPluginAPI_MountFolder)(WSLSessionId Session, LPCWSTR WindowsPath, LPCWSTR LinuxPath, BOOL ReadOnly, LPCWSTR Name);
117
@@ -92,6 +119,63 @@ typedef HRESULT (*WSLPluginAPI_MountFolder)(WSLSessionId Session, LPCWSTR Window
119
// On success, 'Socket' is connected to stdin & stdout (stderr goes to dmesg) // 'Arguments' is expected to be NULL terminated
120
typedef HRESULT (*WSLPluginAPI_ExecuteBinary)(WSLSessionId Session, LPCSTR Path, LPCSTR* Arguments, SOCKET* Socket);
121
122
+//
123
+// WSLC plugin hooks.
124
+//
125
+
126
+// Called when a WSLC session is created. Returning an error prevents the session creation.
127
+typedef HRESULT (*WSLPluginAPI_OnSessionCreated)(const struct WSLCSessionInformation* Session);
128
+
129
+// Called when a WSLC session is about to stop. Errors are ignored.
130
+typedef HRESULT (*WSLPluginAPI_OnSessionStopping)(const struct WSLCSessionInformation* Session);
131
+
132
+// Called when a container starts. Returning an error prevents the container creation.
133
+// 'InspectContainer' is a JSON document that follows the wslc_schema::InspectContainer format.
134
+typedef HRESULT (*WSLPluginAPI_ContainerStarted)(const struct WSLCSessionInformation* Session, LPCSTR InspectContainer);
135
+
136
+// Called when a container is about to stop. 'ContainerId' is the container identifier. Errors are ignored.
137
+typedef HRESULT (*WSLPluginAPI_ContainerStopping)(const struct WSLCSessionInformation* Session, LPCSTR ContainerId);
138
+
139
+// Called when an image is created (either pulled, or imported). Errors are ignored.
140
+// 'InspectImage' is a JSON document that follows the wslc_schema::InspectImage format.
141
+// N.B. This callback is currently only invoked when images are pulled or imported. Images created via load or build are not reported.
142
+typedef HRESULT (*WSLPluginAPI_ImageCreated)(const struct WSLCSessionInformation* Session, LPCSTR InspectImage);
143
+
144
+// Called when an image is deleted. 'ImageId' is the deleted image identifier. Errors are ignored.
145
+typedef HRESULT (*WSLPluginAPI_ImageDeleted)(const struct WSLCSessionInformation* Session, LPCSTR ImageId);
146
+
147
+//
148
+// WSLC plugin API calls.
149
+//
150
+
151
+// Mount a Windows folder into the WSLC session VM. The mount path is returned via 'Mountpoint'.
152
+// 'Mountpoint' must point to a buffer of at least WSLC_MOUNTPOINT_LENGTH chars, including the null terminator.
153
+typedef HRESULT (*WSLCPluginAPI_MountFolder)(WSLCSessionId Session, LPCWSTR WindowsPath, BOOL ReadOnly, LPCWSTR Name, LPSTR Mountpoint);
154
+
155
+// Unmount a folder previously mounted via WSLCPluginAPI_MountFolder.
156
+typedef HRESULT (*WSLCPluginAPI_UnmountFolder)(WSLCSessionId Session, LPCSTR Mountpoint);
157
+
158
+// Create a process in the WSLC session's root namespace.
159
+// 'Arguments' and 'Env' are NULL-terminated arrays. 'Env' may be NULL.
160
+// 'Errno' is optional and receives the errno value if the process creation fails.
161
+// On success, 'Process' receives an opaque handle that must be released with WSLCPluginAPI_ReleaseProcess.
162
+typedef HRESULT (*WSLCPluginAPI_CreateProcess)(
163
+ WSLCSessionId Session, LPCSTR Executable, LPCSTR* Arguments, LPCSTR* Env, WSLCProcessHandle* Process, int* Errno);
164
+
165
+// Get a stdio handle from a WSLC process. The caller takes ownership and must close it with CloseHandle().
166
+typedef HRESULT (*WSLCPluginAPI_ProcessGetFd)(WSLCProcessHandle Process, WSLCProcessFd Fd, HANDLE* Handle);
167
+
168
+// Get the exit event for a WSLC process. Signaled when the process exits.
169
+// The caller takes ownership and must close it with CloseHandle().
170
+typedef HRESULT (*WSLCPluginAPI_ProcessGetExitEvent)(WSLCProcessHandle Process, HANDLE* ExitEvent);
171
+
172
+// Get the exit code of a WSLC process. The process must have exited.
173
+typedef HRESULT (*WSLCPluginAPI_ProcessGetExitCode)(WSLCProcessHandle Process, int* ExitCode);
174
+
175
+// Release a WSLC process handle. All outstanding handles obtained via
176
+// WSLCPluginAPI_ProcessGetFd/GetExitEvent must be closed before calling this.
177
+typedef void (*WSLCPluginAPI_ReleaseProcess)(WSLCProcessHandle Process);
178
+
179
// Execute a program in a user distribution
180
// On success, 'Socket' is connected to stdin & stdout (stderr goes to dmesg) // 'Arguments' is expected to be NULL terminated
181
typedef HRESULT (*WSLPluginAPI_ExecuteBinaryInDistribution)(WSLSessionId Session, const GUID* Distribution, LPCSTR Path, LPCSTR* Arguments, SOCKET* Socket);
@@ -132,6 +216,14 @@ struct WSLPluginHooksV1
216
WSLPluginAPI_OnDistributionStopping OnDistributionStopping;
217
WSLPluginAPI_OnDistributionRegistered OnDistributionRegistered; // Introduced in 2.1.2
218
WSLPluginAPI_OnDistributionRegistered OnDistributionUnregistered; // Introduced in 2.1.2
219
+
220
+ // WSLC hooks. Plugins compiled against older headers leave these zero-initialized.
221
+ WSLPluginAPI_OnSessionCreated OnSessionCreated;
222
+ WSLPluginAPI_OnSessionStopping OnSessionStopping;
223
+ WSLPluginAPI_ContainerStarted ContainerStarted;
224
+ WSLPluginAPI_ContainerStopping ContainerStopping;
225
+ WSLPluginAPI_ImageCreated ImageCreated;
226
+ WSLPluginAPI_ImageDeleted ImageDeleted;
227
};
228
229
struct WSLPluginAPIV1
@@ -141,10 +233,19 @@ struct WSLPluginAPIV1
233
WSLPluginAPI_ExecuteBinary ExecuteBinary;
234
WSLPluginAPI_PluginError PluginError;
235
WSLPluginAPI_ExecuteBinaryInDistribution ExecuteBinaryInDistribution; // Introduced in 2.1.2
236
+
237
+ // WSLC API calls.
238
+ WSLCPluginAPI_MountFolder WSLCMountFolder; // Introduced in 2.9.0
239
+ WSLCPluginAPI_UnmountFolder WSLCUnmountFolder; // Introduced in 2.9.0
240
+ WSLCPluginAPI_CreateProcess WSLCCreateProcess; // Introduced in 2.9.0
241
+ WSLCPluginAPI_ProcessGetFd WSLCProcessGetFd; // Introduced in 2.9.0
242
+ WSLCPluginAPI_ProcessGetExitEvent WSLCProcessGetExitEvent; // Introduced in 2.9.0
243
+ WSLCPluginAPI_ProcessGetExitCode WSLCProcessGetExitCode; // Introduced in 2.9.0
244
+ WSLCPluginAPI_ReleaseProcess WSLCReleaseProcess; // Introduced in 2.9.0
245
};
246
247
typedef HRESULT (*WSLPluginAPI_EntryPointV1)(const struct WSLPluginAPIV1* Api, struct WSLPluginHooksV1* Hooks);
248
249
#ifdef __cplusplus
250
}
150
-#endif
\ No newline at end of file
251
+#endif
src/windows/service/exe/CMakeLists.txt
+3
-1
@@ -23,6 +23,7 @@ set(SOURCES
23
HcsVirtualMachine.cpp
24
WSLCSessionManager.cpp
25
WSLCSessionManagerFactory.cpp
26
+ WSLCPluginNotifier.cpp
27
main.rc
28
${CMAKE_CURRENT_BINARY_DIR}/../mc/${TARGET_PLATFORM}/${CMAKE_BUILD_TYPE}/wsleventschema.rc
29
application.manifest)
@@ -53,7 +54,8 @@ set(HEADERS
54
WslCoreVm.h
55
HcsVirtualMachine.h
56
WSLCSessionManager.h
56
- WSLCSessionManagerFactory.h)
57
+ WSLCSessionManagerFactory.h
58
+ WSLCPluginNotifier.h)
59
60
add_executable(wslservice ${SOURCES} ${HEADERS})
61
add_dependencies(wslservice wslserviceidl wslservicemc)
src/windows/service/exe/LxssUserSessionFactory.cpp
+2
-11
@@ -30,7 +30,7 @@ srwlock g_sessionLock;
30
std::optional<std::vector<std::shared_ptr<LxssUserSessionImpl>>> g_sessions =
31
std::make_optional<std::vector<std::shared_ptr<LxssUserSessionImpl>>>();
32
33
-std::optional<wsl::windows::service::PluginManager> g_pluginManager;
33
+extern wsl::windows::service::PluginManager g_pluginManager;
34
35
extern unique_event g_networkingReady;
36
extern bool g_lxcoreInitialized;
@@ -53,9 +53,6 @@ void ClearSessionsAndBlockNewInstancesLockHeld(std::optional<std::vector<std::sh
53
54
sessions.reset();
55
}
56
-
57
- // Unload plugins
58
- g_pluginManager.reset();
56
}
57
58
void ClearSessionsAndBlockNewInstances()
@@ -84,12 +81,6 @@ void SetSessionPolicy(_In_ bool enabled)
81
{
82
g_sessions = std::make_optional<std::vector<std::shared_ptr<LxssUserSessionImpl>>>();
83
}
87
-
88
- if (!g_pluginManager.has_value())
89
- {
90
- g_pluginManager.emplace();
91
- g_pluginManager->LoadPlugins();
92
- }
84
}
85
else
86
{
@@ -236,7 +227,7 @@ std::weak_ptr<LxssUserSessionImpl> CreateInstanceForCurrentUser()
227
228
if (!userSession)
229
{
239
- userSession.reset(new LxssUserSessionImpl(tokenInfo->User.Sid, sessionId, *g_pluginManager));
230
+ userSession.reset(new LxssUserSessionImpl(tokenInfo->User.Sid, sessionId, g_pluginManager));
231
g_sessions->emplace_back(userSession);
232
}
233
}
src/windows/service/exe/PluginManager.cpp
+389
-4
@@ -17,6 +17,7 @@ Abstract:
17
#include "PluginManager.h"
18
#include "WslPluginApi.h"
19
#include "LxssUserSessionFactory.h"
20
+#include "WSLCSessionManager.h"
21
22
using wsl::windows::common::Context;
23
using wsl::windows::common::ExecutionContext;
@@ -99,7 +100,264 @@ try
100
CATCH_RETURN();
101
}
102
102
-static constexpr WSLPluginAPIV1 ApiV1 = {Version, &MountFolder, &ExecuteBinary, &PluginError, &ExecuteBinaryInDistribution};
103
+namespace {
104
+
105
+// Opaque wrapper around IWSLCProcess, handed out as WSLCProcessHandle to plugins.
106
+struct WslcProcessWrapper
107
+{
108
+ wil::com_ptr<IWSLCProcess> Process;
109
+};
110
+
111
+wil::com_ptr<IWSLCSession> ResolveWslcSession(WSLCSessionId Session)
112
+{
113
+ auto* mgr = wsl::windows::service::wslc::WSLCSessionManagerImpl::Instance();
114
+ THROW_HR_IF(RPC_E_DISCONNECTED, mgr == nullptr);
115
+
116
+ return mgr->FindSession(static_cast<ULONG>(Session));
117
+}
118
+
119
+} // namespace
120
+
121
+extern "C" {
122
+
123
+HRESULT WSLCMountFolder(WSLCSessionId Session, LPCWSTR WindowsPath, BOOL ReadOnly, LPCWSTR Name, LPSTR Mountpoint)
124
+try
125
+{
126
+ RETURN_HR_IF(E_POINTER, WindowsPath == nullptr || Name == nullptr || Mountpoint == nullptr);
127
+ auto nameLength = wcslen(Name);
128
+
129
+ RETURN_HR_IF_MSG(
130
+ E_INVALIDARG,
131
+ nameLength == 0 ||
132
+ !std::ranges::all_of(Name, Name + nameLength, [&](auto c) { return c == '-' || c == '_' || iswalnum(c); }),
133
+ "Invalid mount name: %ls",
134
+ Name);
135
+
136
+ auto session = ResolveWslcSession(Session);
137
+
138
+ // Mount the folder under /mnt/wsl-plugin/<Name>. Convert Name to UTF-8 for the Linux path.
139
+ const auto linuxPath = std::format("/mnt/wsl-plugin/{}", Name);
140
+
141
+ THROW_HR_IF_MSG(E_INVALIDARG, linuxPath.length() >= WSLC_MOUNTPOINT_LENGTH, "Mountpoint too long: %hs", linuxPath.c_str());
142
+
143
+ auto result = session->MountWindowsFolder(WindowsPath, linuxPath.c_str(), ReadOnly);
144
+
145
+ WSL_LOG(
146
+ "WslcPluginMountFolderCall",
147
+ TraceLoggingValue(Session, "SessionId"),
148
+ TraceLoggingValue(WindowsPath, "WindowsPath"),
149
+ TraceLoggingValue(linuxPath.c_str(), "LinuxPath"),
150
+ TraceLoggingValue(ReadOnly, "ReadOnly"),
151
+ TraceLoggingValue(Name, "Name"),
152
+ TraceLoggingValue(result, "Result"));
153
+
154
+ if (SUCCEEDED(result))
155
+ {
156
+ THROW_HR_IF(E_UNEXPECTED, strcpy_s(Mountpoint, WSLC_MOUNTPOINT_LENGTH, linuxPath.c_str()) != 0);
157
+ }
158
+
159
+ return result;
160
+}
161
+CATCH_RETURN();
162
+
163
+HRESULT WSLCUnmountFolder(WSLCSessionId Session, LPCSTR Mountpoint)
164
+try
165
+{
166
+ RETURN_HR_IF(E_POINTER, Mountpoint == nullptr);
167
+
168
+ auto session = ResolveWslcSession(Session);
169
+
170
+ auto result = session->UnmountWindowsFolder(Mountpoint);
171
+
172
+ WSL_LOG(
173
+ "WslcPluginUnmountFolderCall",
174
+ TraceLoggingValue(Session, "SessionId"),
175
+ TraceLoggingValue(Mountpoint, "Mountpoint"),
176
+ TraceLoggingValue(result, "Result"));
177
+
178
+ return result;
179
+}
180
+CATCH_RETURN();
181
+
182
+HRESULT WSLCCreateProcess(WSLCSessionId Session, LPCSTR Executable, LPCSTR* Arguments, LPCSTR* Env, WSLCProcessHandle* Process, int* Errno)
183
+try
184
+{
185
+ RETURN_HR_IF(E_POINTER, Executable == nullptr || Process == nullptr);
186
+
187
+ *Process = nullptr;
188
+ if (Errno != nullptr)
189
+ {
190
+ *Errno = 0;
191
+ }
192
+
193
+ auto session = ResolveWslcSession(Session);
194
+
195
+ // Count NULL-terminated arrays.
196
+ auto countArray = [](LPCSTR* arr) -> ULONG {
197
+ if (arr == nullptr)
198
+ {
199
+ return 0;
200
+ }
201
+ ULONG count = 0;
202
+ while (arr[count] != nullptr)
203
+ {
204
+ ++count;
205
+ }
206
+ return count;
207
+ };
208
+
209
+ WSLCProcessOptions options{};
210
+ options.CommandLine.Values = Arguments;
211
+ options.CommandLine.Count = countArray(Arguments);
212
+ options.Environment.Values = Env;
213
+ options.Environment.Count = countArray(Env);
214
+ options.Flags = WSLCProcessFlagsStdin;
215
+
216
+ wil::com_ptr<IWSLCProcess> process;
217
+ int errnoValue = 0;
218
+ auto result = session->CreateRootNamespaceProcess(Executable, &options, &process, &errnoValue);
219
+
220
+ if (Errno != nullptr)
221
+ {
222
+ *Errno = errnoValue;
223
+ }
224
+
225
+ if (FAILED(result))
226
+ {
227
+ WSL_LOG(
228
+ "WslcPluginCreateProcessCall",
229
+ TraceLoggingValue(Session, "SessionId"),
230
+ TraceLoggingValue(Executable, "Executable"),
231
+ TraceLoggingValue(result, "Result"),
232
+ TraceLoggingValue(errnoValue, "Errno"));
233
+ return result;
234
+ }
235
+
236
+ auto wrapper = std::make_unique<WslcProcessWrapper>();
237
+ wrapper->Process = std::move(process);
238
+ *Process = wrapper.release();
239
+
240
+ WSL_LOG(
241
+ "WslcPluginCreateProcessCall",
242
+ TraceLoggingValue(Session, "SessionId"),
243
+ TraceLoggingValue(Executable, "Executable"),
244
+ TraceLoggingValue(*Process, "Process"),
245
+ TraceLoggingValue(S_OK, "Result"));
246
+
247
+ return S_OK;
248
+}
249
+CATCH_RETURN();
250
+
251
+HRESULT WSLCProcessGetFd(WSLCProcessHandle Process, WSLCProcessFd Fd, HANDLE* Handle)
252
+try
253
+{
254
+ RETURN_HR_IF(E_POINTER, Process == nullptr || Handle == nullptr);
255
+
256
+ *Handle = nullptr;
257
+
258
+ auto* wrapper = static_cast<WslcProcessWrapper*>(Process);
259
+
260
+ WSLCFD wslcFd{};
261
+ switch (Fd)
262
+ {
263
+ case WSLCProcessFdStdin:
264
+ wslcFd = WSLCFDStdin;
265
+ break;
266
+ case WSLCProcessFdStdout:
267
+ wslcFd = WSLCFDStdout;
268
+ break;
269
+ case WSLCProcessFdStderr:
270
+ wslcFd = WSLCFDStderr;
271
+ break;
272
+ default:
273
+ WSL_LOG(
274
+ "WslcPluginProcessGetFd", TraceLoggingValue(static_cast<int>(Fd), "Fd"), TraceLoggingValue(E_INVALIDARG, "Result"));
275
+ return E_INVALIDARG;
276
+ }
277
+
278
+ WSLCHandle handle{};
279
+ auto result = wrapper->Process->GetStdHandle(wslcFd, &handle);
280
+
281
+ WSL_LOG(
282
+ "WslcPluginProcessGetFd",
283
+ TraceLoggingValue(static_cast<int>(Fd), "Fd"),
284
+ TraceLoggingValue(handle.Handle.Socket, "Handle"),
285
+ TraceLoggingValue(result, "Result"));
286
+
287
+ RETURN_IF_FAILED(result);
288
+ WI_ASSERT(handle.Type == WSLCHandleTypeSocket);
289
+
290
+ *Handle = handle.Handle.Socket;
291
+ return S_OK;
292
+}
293
+CATCH_RETURN();
294
+
295
+HRESULT WSLCProcessGetExitEvent(WSLCProcessHandle Process, HANDLE* ExitEvent)
296
+try
297
+{
298
+ RETURN_HR_IF(E_POINTER, Process == nullptr || ExitEvent == nullptr);
299
+
300
+ *ExitEvent = nullptr;
301
+
302
+ auto* wrapper = static_cast<WslcProcessWrapper*>(Process);
303
+ auto result = wrapper->Process->GetExitEvent(ExitEvent);
304
+
305
+ WSL_LOG("WslcPluginProcessGetExitEvent", TraceLoggingValue(*ExitEvent, "ExitEvent"), TraceLoggingValue(result, "Result"));
306
+
307
+ return result;
308
+}
309
+CATCH_RETURN();
310
+
311
+HRESULT WSLCProcessGetExitCode(WSLCProcessHandle Process, int* ExitCode)
312
+try
313
+{
314
+ RETURN_HR_IF(E_POINTER, Process == nullptr || ExitCode == nullptr);
315
+
316
+ *ExitCode = -1;
317
+ auto* wrapper = static_cast<WslcProcessWrapper*>(Process);
318
+
319
+ WSLCProcessState state{};
320
+ auto result = wrapper->Process->GetState(&state, ExitCode);
321
+
322
+ if (SUCCEEDED(result) && state != WslcProcessStateExited && state != WslcProcessStateSignalled)
323
+ {
324
+ result = HRESULT_FROM_WIN32(ERROR_INVALID_STATE);
325
+ }
326
+
327
+ WSL_LOG(
328
+ "WslcPluginProcessGetExitCode",
329
+ TraceLoggingValue(*ExitCode, "ExitCode"),
330
+ TraceLoggingValue(static_cast<int>(state), "State"),
331
+ TraceLoggingValue(result, "Result"));
332
+
333
+ return result;
334
+}
335
+CATCH_RETURN();
336
+
337
+void WSLCReleaseProcess(WSLCProcessHandle Process)
338
+{
339
+ if (Process != nullptr)
340
+ {
341
+ WSL_LOG("WslcPluginReleaseProcess", TraceLoggingValue(Process, "Process"));
342
+ delete static_cast<WslcProcessWrapper*>(Process);
343
+ }
344
+}
345
+
346
+} // extern "C"
347
+
348
+static constexpr WSLPluginAPIV1 ApiV1 = {
349
+ Version,
350
+ &MountFolder,
351
+ &ExecuteBinary,
352
+ &PluginError,
353
+ &ExecuteBinaryInDistribution,
354
+ &WSLCMountFolder,
355
+ &WSLCUnmountFolder,
356
+ &WSLCCreateProcess,
357
+ &WSLCProcessGetFd,
358
+ &WSLCProcessGetExitEvent,
359
+ &WSLCProcessGetExitCode,
360
+ &WSLCReleaseProcess};
361
362
void PluginManager::LoadPlugins()
363
{
@@ -181,7 +439,7 @@ void PluginManager::OnVmStarted(const WSLSessionInformation* Session, const WSLV
439
WSL_LOG(
440
"PluginOnVmStartedCall", TraceLoggingValue(e.name.c_str(), "Plugin"), TraceLoggingValue(Session->UserSid, "Sid"));
441
184
- ThrowIfPluginError(e.hooks.OnVMStarted(Session, Settings), Session->SessionId, e.name.c_str());
442
+ ThrowIfPluginError(e.hooks.OnVMStarted(Session, Settings), e.name.c_str());
443
}
444
}
445
}
@@ -217,7 +475,7 @@ void PluginManager::OnDistributionStarted(const WSLSessionInformation* Session,
475
TraceLoggingValue(Session->UserSid, "Sid"),
476
TraceLoggingValue(Distribution->Id, "DistributionId"));
477
220
- ThrowIfPluginError(e.hooks.OnDistributionStarted(Session, Distribution), Session->SessionId, e.name.c_str());
478
+ ThrowIfPluginError(e.hooks.OnDistributionStarted(Session, Distribution), e.name.c_str());
479
}
480
}
481
}
@@ -282,7 +540,7 @@ void PluginManager::OnDistributionUnregistered(const WSLSessionInformation* Sess
540
}
541
}
542
285
-void PluginManager::ThrowIfPluginError(HRESULT Result, WSLSessionId Session, LPCWSTR Plugin)
543
+void PluginManager::ThrowIfPluginError(HRESULT Result, LPCWSTR Plugin)
544
{
545
const auto message = std::move(g_pluginErrorMessage);
546
g_pluginErrorMessage.reset(); // std::move() doesn't clear the previous std::optional
@@ -322,3 +580,130 @@ void PluginManager::ThrowIfFatalPluginError() const
580
THROW_HR_WITH_USER_ERROR(m_pluginError->error, wsl::shared::Localization::MessageFatalPluginError(m_pluginError->plugin));
581
}
582
}
583
+
584
+void PluginManager::OnWslcSessionCreated(const WSLCSessionInformation* Session)
585
+{
586
+ ExecutionContext context(Context::Plugin);
587
+
588
+ for (const auto& e : m_plugins)
589
+ {
590
+ if (e.hooks.OnSessionCreated != nullptr)
591
+ {
592
+ auto result = e.hooks.OnSessionCreated(Session);
593
+ WSL_LOG(
594
+ "PluginOnWslcSessionCreatedCall",
595
+ TraceLoggingValue(e.name.c_str(), "Plugin"),
596
+ TraceLoggingValue(Session->SessionId, "SessionId"),
597
+ TraceLoggingValue(Session->DisplayName, "DisplayName"),
598
+ TraceLoggingValue(result, "Result"));
599
+
600
+ ThrowIfPluginError(result, e.name.c_str());
601
+ }
602
+ }
603
+}
604
+
605
+void PluginManager::OnWslcSessionStopping(const WSLCSessionInformation* Session) const
606
+{
607
+ ExecutionContext context(Context::Plugin);
608
+
609
+ for (const auto& e : m_plugins)
610
+ {
611
+ if (e.hooks.OnSessionStopping != nullptr)
612
+ {
613
+ const auto result = e.hooks.OnSessionStopping(Session);
614
+ WSL_LOG(
615
+ "PluginOnWslcSessionStoppingCall",
616
+ TraceLoggingValue(e.name.c_str(), "Plugin"),
617
+ TraceLoggingValue(Session->SessionId, "SessionId"),
618
+ TraceLoggingValue(result, "Result"));
619
+
620
+ LOG_IF_FAILED_MSG(result, "Error thrown from plugin: '%ls'", e.name.c_str());
621
+ }
622
+ }
623
+}
624
+
625
+HRESULT PluginManager::OnWslcContainerStarted(const WSLCSessionInformation* Session, LPCSTR InspectJson) const
626
+try
627
+{
628
+ ExecutionContext context(Context::Plugin);
629
+
630
+ for (const auto& e : m_plugins)
631
+ {
632
+ if (e.hooks.ContainerStarted != nullptr)
633
+ {
634
+ // Failure here aborts the container creation. Surface the first error.
635
+ const auto result = e.hooks.ContainerStarted(Session, InspectJson);
636
+ WSL_LOG(
637
+ "PluginOnWslcContainerStartedCall",
638
+ TraceLoggingValue(e.name.c_str(), "Plugin"),
639
+ TraceLoggingValue(Session->SessionId, "SessionId"),
640
+ TraceLoggingValue(result, "Result"));
641
+
642
+ ThrowIfPluginError(result, e.name.c_str());
643
+ }
644
+ }
645
+ return S_OK;
646
+}
647
+CATCH_RETURN()
648
+
649
+void PluginManager::OnWslcContainerStopping(const WSLCSessionInformation* Session, LPCSTR ContainerId) const
650
+{
651
+ ExecutionContext context(Context::Plugin);
652
+
653
+ for (const auto& e : m_plugins)
654
+ {
655
+ if (e.hooks.ContainerStopping != nullptr)
656
+ {
657
+
658
+ const auto result = e.hooks.ContainerStopping(Session, ContainerId);
659
+ WSL_LOG(
660
+ "PluginOnWslcContainerStoppingCall",
661
+ TraceLoggingValue(e.name.c_str(), "Plugin"),
662
+ TraceLoggingValue(Session->SessionId, "SessionId"),
663
+ TraceLoggingValue(ContainerId, "ContainerId"),
664
+ TraceLoggingValue(result, "Result"));
665
+
666
+ LOG_IF_FAILED_MSG(result, "Error thrown from plugin: '%ls'", e.name.c_str());
667
+ }
668
+ }
669
+}
670
+
671
+void PluginManager::OnWslcImageCreated(const WSLCSessionInformation* Session, LPCSTR InspectJson) const
672
+{
673
+ ExecutionContext context(Context::Plugin);
674
+
675
+ for (const auto& e : m_plugins)
676
+ {
677
+ if (e.hooks.ImageCreated != nullptr)
678
+ {
679
+ const auto result = e.hooks.ImageCreated(Session, InspectJson);
680
+ WSL_LOG(
681
+ "PluginOnWslcImageCreatedCall",
682
+ TraceLoggingValue(e.name.c_str(), "Plugin"),
683
+ TraceLoggingValue(Session->SessionId, "SessionId"),
684
+ TraceLoggingValue(result, "Result"));
685
+
686
+ LOG_IF_FAILED_MSG(result, "Error thrown from plugin: '%ls'", e.name.c_str());
687
+ }
688
+ }
689
+}
690
+
691
+void PluginManager::OnWslcImageDeleted(const WSLCSessionInformation* Session, LPCSTR ImageId) const
692
+{
693
+ ExecutionContext context(Context::Plugin);
694
+
695
+ for (const auto& e : m_plugins)
696
+ {
697
+ if (e.hooks.ImageDeleted != nullptr)
698
+ {
699
+ const auto result = e.hooks.ImageDeleted(Session, ImageId);
700
+ WSL_LOG(
701
+ "PluginOnWslcImageDeletedCall",
702
+ TraceLoggingValue(e.name.c_str(), "Plugin"),
703
+ TraceLoggingValue(Session->SessionId, "SessionId"),
704
+ TraceLoggingValue(ImageId, "ImageId"),
705
+ TraceLoggingValue(result, "Result"));
706
+ LOG_IF_FAILED_MSG(result, "Error thrown from plugin: '%ls'", e.name.c_str());
707
+ }
708
+ }
709
+}
src/windows/service/exe/PluginManager.h
+12
-2
@@ -43,11 +43,21 @@ public:
43
void OnDistributionStopping(const WSLSessionInformation* Session, const WSLDistributionInformation* distro) const;
44
void OnDistributionRegistered(const WSLSessionInformation* Session, const WslOfflineDistributionInformation* distro) const;
45
void OnDistributionUnregistered(const WSLSessionInformation* Session, const WslOfflineDistributionInformation* distro) const;
46
+
47
+ // WSLC notifications. Returning failure from OnSessionCreated/OnContainerStarted causes the
48
+ // corresponding operation to be aborted. Other notifications log errors and continue.
49
+ void OnWslcSessionCreated(const WSLCSessionInformation* Session);
50
+ void OnWslcSessionStopping(const WSLCSessionInformation* Session) const;
51
+ HRESULT OnWslcContainerStarted(const WSLCSessionInformation* Session, LPCSTR InspectJson) const;
52
+ void OnWslcContainerStopping(const WSLCSessionInformation* Session, LPCSTR ContainerId) const;
53
+ void OnWslcImageCreated(const WSLCSessionInformation* Session, LPCSTR InspectJson) const;
54
+ void OnWslcImageDeleted(const WSLCSessionInformation* Session, LPCSTR ImageId) const;
55
+
56
void ThrowIfFatalPluginError() const;
57
58
private:
59
void LoadPlugin(LPCWSTR Name, LPCWSTR Path);
50
- static void ThrowIfPluginError(HRESULT Result, WSLSessionId session, LPCWSTR Plugin);
60
+ static void ThrowIfPluginError(HRESULT Result, LPCWSTR Plugin);
61
62
struct LoadedPlugin
63
{
@@ -60,4 +70,4 @@ private:
70
std::optional<PluginError> m_pluginError;
71
};
72
63
-} // namespace wsl::windows::service
\ No newline at end of file
73
+} // namespace wsl::windows::service
src/windows/service/exe/ServiceMain.cpp
+5
@@ -29,6 +29,8 @@ using namespace wsl::windows::policies;
29
bool g_lxcoreInitialized{false};
30
wil::unique_event g_networkingReady{wil::EventOptions::ManualReset};
31
32
+wsl::windows::service::PluginManager g_pluginManager;
33
+
34
// Declare the LxssUserSession COM class.
35
CoCreatableClassWrlCreatorMapInclude(LxssUserSession);
36
@@ -178,6 +180,9 @@ try
180
WSADATA Data;
181
THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &Data));
182
183
+ // Load plugins.
184
+ g_pluginManager.LoadPlugins();
185
+
186
// Check if WSL is disabled via policy and set up a registry watcher to watch for changes.
187
//
188
// N.B. The registry watcher must be created before checking the policy to avoid missing notifications.
src/windows/service/exe/WSLCPluginNotifier.cpp
new
+66
@@ -0,0 +1,66 @@
1
+// Copyright (C) Microsoft Corporation. All rights reserved.
2
+
3
+#include "precomp.h"
4
+#include "WSLCPluginNotifier.h"
5
+
6
+using wsl::windows::common::COMServiceExecutionContext;
7
+using wsl::windows::service::wslc::WSLCPluginNotifier;
8
+
9
+WSLCPluginNotifier::WSLCPluginNotifier(
10
+ wsl::windows::service::PluginManager& Plugins,
11
+ ULONG SessionId,
12
+ DWORD CreatorPid,
13
+ std::wstring DisplayName,
14
+ wil::shared_handle UserToken,
15
+ std::vector<BYTE>&& UserSid) :
16
+ m_plugins(Plugins), m_displayName(std::move(DisplayName)), m_userToken(std::move(UserToken)), m_userSid(std::move(UserSid))
17
+{
18
+ m_sessionInfo.SessionId = static_cast<WSLCSessionId>(SessionId);
19
+ m_sessionInfo.DisplayName = m_displayName.c_str();
20
+ m_sessionInfo.ApplicationPid = CreatorPid;
21
+ m_sessionInfo.UserToken = m_userToken.get();
22
+ m_sessionInfo.UserSid = m_userSid.empty() ? nullptr : reinterpret_cast<PSID>(m_userSid.data());
23
+}
24
+
25
+HRESULT WSLCPluginNotifier::OnContainerStarted(LPCSTR InspectJson)
26
+try
27
+{
28
+ COMServiceExecutionContext context;
29
+
30
+ RETURN_HR_IF(E_POINTER, InspectJson == nullptr);
31
+ return m_plugins.OnWslcContainerStarted(&m_sessionInfo, InspectJson);
32
+}
33
+CATCH_RETURN();
34
+
35
+HRESULT WSLCPluginNotifier::OnContainerStopping(LPCSTR ContainerId)
36
+try
37
+{
38
+ COMServiceExecutionContext context;
39
+
40
+ RETURN_HR_IF(E_POINTER, ContainerId == nullptr);
41
+ m_plugins.OnWslcContainerStopping(&m_sessionInfo, ContainerId);
42
+ return S_OK;
43
+}
44
+CATCH_RETURN();
45
+
46
+HRESULT WSLCPluginNotifier::OnImageCreated(LPCSTR InspectJson)
47
+try
48
+{
49
+ COMServiceExecutionContext context;
50
+
51
+ RETURN_HR_IF(E_POINTER, InspectJson == nullptr);
52
+ m_plugins.OnWslcImageCreated(&m_sessionInfo, InspectJson);
53
+ return S_OK;
54
+}
55
+CATCH_RETURN();
56
+
57
+HRESULT WSLCPluginNotifier::OnImageDeleted(LPCSTR ImageId)
58
+try
59
+{
60
+ COMServiceExecutionContext context;
61
+
62
+ RETURN_HR_IF(E_POINTER, ImageId == nullptr);
63
+ m_plugins.OnWslcImageDeleted(&m_sessionInfo, ImageId);
64
+ return S_OK;
65
+}
66
+CATCH_RETURN();
src/windows/service/exe/WSLCPluginNotifier.h
new
+46
@@ -0,0 +1,46 @@
1
+// Copyright (C) Microsoft Corporation. All rights reserved.
2
+
3
+#pragma once
4
+
5
+#include "wslc.h"
6
+#include "PluginManager.h"
7
+#include <wil/resource.h>
8
+#include <vector>
9
+
10
+namespace wsl::windows::service::wslc {
11
+
12
+//
13
+// WSLCPluginNotifier - SYSTEM service implementation of IWSLCPluginNotifier.
14
+// Lives in the SYSTEM service and is passed (via COM marshalling) as a top-level
15
+// parameter to the per-user WSLC session process via IWSLCSessionFactory::CreateSession.
16
+// The per-user process invokes the On* methods, which dispatch to PluginManager.
17
+//
18
+class DECLSPEC_UUID("E29B0F1A-4E18-4F09-83A2-2D6B1B9F8C4D") WSLCPluginNotifier
19
+ : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::WinRtClassicComMix>, IWSLCPluginNotifier, IFastRundown>
20
+{
21
+public:
22
+ NON_COPYABLE(WSLCPluginNotifier);
23
+ NON_MOVABLE(WSLCPluginNotifier);
24
+
25
+ WSLCPluginNotifier(
26
+ wsl::windows::service::PluginManager& Plugins,
27
+ ULONG SessionId,
28
+ DWORD CreatorPid,
29
+ std::wstring DisplayName,
30
+ wil::shared_handle UserToken,
31
+ std::vector<BYTE>&& UserSid);
32
+
33
+ IFACEMETHOD(OnContainerStarted)(_In_ LPCSTR InspectJson) override;
34
+ IFACEMETHOD(OnContainerStopping)(_In_ LPCSTR ContainerId) override;
35
+ IFACEMETHOD(OnImageCreated)(_In_ LPCSTR InspectJson) override;
36
+ IFACEMETHOD(OnImageDeleted)(_In_ LPCSTR ImageId) override;
37
+
38
+private:
39
+ wsl::windows::service::PluginManager& m_plugins;
40
+ std::wstring m_displayName;
41
+ wil::shared_handle m_userToken;
42
+ std::vector<BYTE> m_userSid;
43
+ WSLCSessionInformation m_sessionInfo{};
44
+};
45
+
46
+} // namespace wsl::windows::service::wslc
src/windows/service/exe/WSLCSessionManager.cpp
+130
-11
@@ -31,17 +31,26 @@ Abstract:
31
#include "HcsVirtualMachine.h"
32
#include "WSLCUserSettings.h"
33
#include "WSLCSessionDefaults.h"
34
+#include "WSLCPluginNotifier.h"
35
+#include "PluginManager.h"
36
+#include "ExecutionContext.h"
37
#include "wslutil.h"
38
#include "filesystem.hpp"
39
40
+extern wsl::windows::service::PluginManager g_pluginManager;
41
+
42
+using wsl::windows::common::COMServiceExecutionContext;
43
using wsl::windows::service::wslc::CallingProcessTokenInfo;
44
using wsl::windows::service::wslc::HcsVirtualMachine;
45
+using wsl::windows::service::wslc::WSLCPluginNotifier;
46
using wsl::windows::service::wslc::WSLCSessionManagerImpl;
47
namespace wslutil = wsl::windows::common::wslutil;
48
namespace settings = wsl::windows::wslc::settings;
49
50
namespace {
51
52
+std::atomic<wsl::windows::service::wslc::WSLCSessionManagerImpl*> g_managerInstance{nullptr};
53
+
54
// Session settings built server-side from the caller's settings.yaml.
55
struct SessionSettings
56
{
@@ -114,8 +123,15 @@ private:
123
124
} // namespace
125
126
+WSLCSessionManagerImpl::WSLCSessionManagerImpl()
127
+{
128
+ g_managerInstance.store(this);
129
+}
130
+
131
WSLCSessionManagerImpl::~WSLCSessionManagerImpl()
132
{
133
+ g_managerInstance.store(nullptr);
134
+
135
// Terminate all sessions on shutdown.
136
// Call Terminate() directly rather than going through ForEachSession(),
137
// which would needlessly resolve weak references and call GetState().
@@ -123,10 +139,30 @@ WSLCSessionManagerImpl::~WSLCSessionManagerImpl()
139
std::lock_guard lock(m_wslcSessionsLock);
140
for (auto& entry : m_sessions)
141
{
142
+ NotifySessionStoppingLockHeld(entry);
143
LOG_IF_FAILED(entry.Ref->Terminate());
144
}
145
}
146
147
+void WSLCSessionManagerImpl::NotifySessionStoppingLockHeld(SessionEntry& entry) noexcept
148
+try
149
+{
150
+ if (entry.StoppingNotified)
151
+ {
152
+ return;
153
+ }
154
+
155
+ entry.StoppingNotified = true;
156
+ WSLCSessionInformation info{};
157
+ info.SessionId = static_cast<WSLCSessionId>(entry.SessionId);
158
+ info.DisplayName = entry.DisplayName.c_str();
159
+ info.ApplicationPid = entry.CreatorPid;
160
+ info.UserToken = entry.UserToken.get();
161
+ info.UserSid = entry.UserSid.data();
162
+ g_pluginManager.OnWslcSessionStopping(&info);
163
+}
164
+CATCH_LOG()
165
+
166
void WSLCSessionManagerImpl::CreateSession(const WSLCSessionSettings* Settings, WSLCSessionFlags Flags, IWSLCSession** WslcSession)
167
{
168
auto tokenInfo = GetCallingProcessTokenInfo();
@@ -144,7 +180,7 @@ void WSLCSessionManagerImpl::CreateSession(const WSLCSessionSettings* Settings,
180
{
181
THROW_HR_IF(WSLC_E_INVALID_SESSION_NAME, Settings->DisplayName == nullptr || wcslen(Settings->DisplayName) == 0);
182
THROW_HR_IF(E_INVALIDARG, Settings->StoragePath != nullptr && wcslen(Settings->StoragePath) == 0);
147
- THROW_HR_IF(WSLC_E_INVALID_SESSION_NAME, wcslen(Settings->DisplayName) >= std::size(WSLCSessionInformation{}.DisplayName));
183
+ THROW_HR_IF(WSLC_E_INVALID_SESSION_NAME, wcslen(Settings->DisplayName) >= std::size(WSLCSessionListEntry{}.DisplayName));
184
THROW_HR_IF_MSG(
185
E_INVALIDARG,
186
WI_IsAnyFlagSet(Settings->StorageFlags, ~WSLCSessionStorageFlagsValid),
@@ -208,6 +244,23 @@ void WSLCSessionManagerImpl::CreateSession(const WSLCSessionSettings* Settings,
244
245
const auto userToken = wsl::windows::common::security::GetUserToken(TokenImpersonation);
246
247
+ // Capture a duplicated user token + raw SID so PluginManager can build
248
+ // WSLCSessionInformation later (e.g. on shutdown) without re-impersonating.
249
+ // The token is shared between the SessionEntry and the WSLCPluginNotifier.
250
+ wil::unique_handle dupToken;
251
+ THROW_IF_WIN32_BOOL_FALSE(DuplicateTokenEx(
252
+ userToken.get(), TOKEN_QUERY | TOKEN_DUPLICATE, nullptr, SecurityImpersonation, TokenImpersonation, &dupToken));
253
+ wil::shared_handle sharedToken{dupToken.release()};
254
+
255
+ const DWORD sidLen = GetLengthSid(tokenInfo.TokenInfo->User.Sid);
256
+ std::vector<BYTE> storedSid(sidLen);
257
+ THROW_IF_WIN32_BOOL_FALSE(CopySid(sidLen, storedSid.data(), tokenInfo.TokenInfo->User.Sid));
258
+
259
+ // Build the plugin notifier service-side. Lifetime tracked via the SessionEntry.
260
+ Microsoft::WRL::ComPtr<IWSLCPluginNotifier> notifier;
261
+ notifier = wil::MakeOrThrow<WSLCPluginNotifier>(
262
+ g_pluginManager, sessionId, creatorPid, std::wstring(resolvedDisplayName), wil::shared_handle(sharedToken), std::vector<BYTE>(storedSid));
263
+
264
// Create the VM in the SYSTEM service (privileged).
265
auto vm = Microsoft::WRL::Make<HcsVirtualMachine>(Settings);
266
@@ -215,14 +268,14 @@ void WSLCSessionManagerImpl::CreateSession(const WSLCSessionSettings* Settings,
268
auto factory = wslutil::CreateComServerAsUser<IWSLCSessionFactory>(__uuidof(WSLCSessionFactory), userToken.get());
269
AddSessionProcessToJobObject(factory.get());
270
218
- // Create the session via the factory.
219
- const auto sessionSettings = CreateSessionSettings(sessionId, creatorPid, Settings, resolvedDisplayName.c_str());
271
+ auto sessionSettings = CreateSessionSettings(sessionId, creatorPid, Settings, resolvedDisplayName.c_str());
272
wil::com_ptr<IWSLCSession> session;
273
wil::com_ptr<IWSLCSessionReference> serviceRef;
222
- THROW_IF_FAILED(factory->CreateSession(&sessionSettings, vm.Get(), &session, &serviceRef));
274
+ THROW_IF_FAILED(factory->CreateSession(&sessionSettings, vm.Get(), notifier.Get(), &session, &serviceRef));
275
276
// Track the session via its service ref, along with metadata and security info.
225
- m_sessions.push_back({std::move(serviceRef), sessionId, creatorPid, resolvedDisplayName, std::move(tokenInfo)});
277
+ m_sessions.push_back(SessionEntry{
278
+ std::move(serviceRef), sessionId, creatorPid, resolvedDisplayName, std::move(tokenInfo), notifier, false, sharedToken, std::move(storedSid)});
279
280
// For persistent sessions, also hold a strong reference to keep them alive.
281
const bool persistent = WI_IsFlagSet(Flags, WSLCSessionFlagsPersistent);
@@ -231,6 +284,33 @@ void WSLCSessionManagerImpl::CreateSession(const WSLCSessionSettings* Settings,
284
m_persistentSessions.emplace_back(sessionId, session);
285
}
286
287
+ // Notify plugins that the session was created. A failure here aborts session creation.
288
+ try
289
+ {
290
+ auto& entry = m_sessions.back();
291
+ WSLCSessionInformation info{};
292
+ info.SessionId = static_cast<WSLCSessionId>(entry.SessionId);
293
+ info.DisplayName = entry.DisplayName.c_str();
294
+ info.ApplicationPid = entry.CreatorPid;
295
+ info.UserToken = entry.UserToken.get();
296
+ info.UserSid = entry.UserSid.data();
297
+ g_pluginManager.OnWslcSessionCreated(&info);
298
+ }
299
+ catch (...)
300
+ {
301
+ const auto error = wil::ResultFromCaughtException();
302
+
303
+ // Plugin rejected the session: tear it down before propagating.
304
+ m_sessions.back().StoppingNotified = true; // Don't fire stopping for a session that never started successfully.
305
+ LOG_IF_FAILED(m_sessions.back().Ref->Terminate());
306
+ m_sessions.pop_back();
307
+
308
+ auto remove = std::ranges::remove_if(m_persistentSessions, [&](const auto& e) { return e.first == sessionId; });
309
+ m_persistentSessions.erase(remove.begin(), remove.end());
310
+
311
+ THROW_HR(error);
312
+ }
313
+
314
*WslcSession = session.detach();
315
});
316
@@ -297,9 +377,9 @@ void WSLCSessionManagerImpl::OpenSessionByName(LPCWSTR DisplayName, IWSLCSession
377
THROW_IF_FAILED_MSG(result.value_or(HRESULT_FROM_WIN32(ERROR_NOT_FOUND)), "Session '%ls' not found", DisplayName);
378
}
379
300
-void WSLCSessionManagerImpl::ListSessions(_Out_ WSLCSessionInformation** Sessions, _Out_ ULONG* SessionsCount)
380
+void WSLCSessionManagerImpl::ListSessions(_Out_ WSLCSessionListEntry** Sessions, _Out_ ULONG* SessionsCount)
381
{
302
- std::vector<WSLCSessionInformation> sessionInfo;
382
+ std::vector<WSLCSessionListEntry> sessionInfo;
383
384
ForEachSession<void>([&](auto& entry, const auto&) noexcept {
385
try
@@ -307,15 +387,15 @@ void WSLCSessionManagerImpl::ListSessions(_Out_ WSLCSessionInformation** Session
387
wil::unique_hlocal_string sidString;
388
THROW_IF_WIN32_BOOL_FALSE(ConvertSidToStringSidW(entry.Owner.TokenInfo->User.Sid, &sidString));
389
310
- auto& it = sessionInfo.emplace_back(WSLCSessionInformation{.SessionId = entry.SessionId, .CreatorPid = entry.CreatorPid});
390
+ auto& it = sessionInfo.emplace_back(WSLCSessionListEntry{.SessionId = entry.SessionId, .CreatorPid = entry.CreatorPid});
391
wcscpy_s(it.Sid, _countof(it.Sid), sidString.get());
392
wcscpy_s(it.DisplayName, _countof(it.DisplayName), entry.DisplayName.c_str());
393
}
394
CATCH_LOG()
395
});
396
317
- auto output = wil::make_unique_cotaskmem<WSLCSessionInformation[]>(sessionInfo.size());
318
- memcpy(output.get(), sessionInfo.data(), sessionInfo.size() * sizeof(WSLCSessionInformation));
397
+ auto output = wil::make_unique_cotaskmem<WSLCSessionListEntry[]>(sessionInfo.size());
398
+ memcpy(output.get(), sessionInfo.data(), sessionInfo.size() * sizeof(WSLCSessionListEntry));
399
400
*Sessions = output.release();
401
*SessionsCount = static_cast<ULONG>(sessionInfo.size());
@@ -478,26 +558,65 @@ try
558
CATCH_RETURN();
559
560
HRESULT WSLCSessionManager::CreateSession(const WSLCSessionSettings* WslcSessionSettings, WSLCSessionFlags Flags, IWSLCSession** WslcSession)
561
+try
562
{
563
+ COMServiceExecutionContext context;
564
+
565
return CallImpl(&WSLCSessionManagerImpl::CreateSession, WslcSessionSettings, Flags, WslcSession);
566
}
567
+CATCH_RETURN();
568
569
HRESULT WSLCSessionManager::EnterSession(_In_ LPCWSTR DisplayName, _In_ LPCWSTR StoragePath, IWSLCSession** WslcSession)
570
{
571
+ COMServiceExecutionContext context;
572
+
573
return CallImpl(&WSLCSessionManagerImpl::EnterSession, DisplayName, StoragePath, WslcSession);
574
}
575
490
-HRESULT WSLCSessionManager::ListSessions(_Out_ WSLCSessionInformation** Sessions, _Out_ ULONG* SessionsCount)
576
+HRESULT WSLCSessionManager::ListSessions(_Out_ WSLCSessionListEntry** Sessions, _Out_ ULONG* SessionsCount)
577
{
578
+ COMServiceExecutionContext context;
579
+
580
return CallImpl(&WSLCSessionManagerImpl::ListSessions, Sessions, SessionsCount);
581
}
582
583
HRESULT WSLCSessionManager::OpenSession(_In_ ULONG Id, _Out_ IWSLCSession** Session)
584
{
585
+ COMServiceExecutionContext context;
586
+
587
return CallImpl(&WSLCSessionManagerImpl::OpenSession, Id, Session);
588
}
589
590
HRESULT WSLCSessionManager::OpenSessionByName(_In_ LPCWSTR DisplayName, _Out_ IWSLCSession** Session)
591
{
592
+ COMServiceExecutionContext context;
593
+
594
return CallImpl(&WSLCSessionManagerImpl::OpenSessionByName, DisplayName, Session);
595
}
596
+
597
+namespace wsl::windows::service::wslc {
598
+
599
+WSLCSessionManagerImpl* WSLCSessionManagerImpl::Instance() noexcept
600
+{
601
+ return g_managerInstance.load();
602
+}
603
+
604
+wil::com_ptr<IWSLCSession> WSLCSessionManagerImpl::FindSession(ULONG Id)
605
+{
606
+ wil::com_ptr<IWSLCSession> result;
607
+
608
+ ForEachSession<HRESULT>([&](SessionEntry& entry, const wil::com_ptr<IWSLCSession>& session) noexcept -> std::optional<HRESULT> {
609
+ if (entry.SessionId != Id)
610
+ {
611
+ return std::nullopt;
612
+ }
613
+
614
+ result = session;
615
+ return S_OK;
616
+ });
617
+
618
+ THROW_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), !result, "WSLC session %lu not found", Id);
619
+ return result;
620
+}
621
+
622
+} // namespace wsl::windows::service::wslc
src/windows/service/exe/WSLCSessionManager.h
+21
-4
@@ -60,6 +60,14 @@ struct SessionEntry
60
DWORD CreatorPid = 0;
61
std::wstring DisplayName;
62
CallingProcessTokenInfo Owner;
63
+
64
+ Microsoft::WRL::ComPtr<IWSLCPluginNotifier> PluginNotifier;
65
+
66
+ // Whether OnSessionStopping has been fired already; ensures it is fired exactly once.
67
+ bool StoppingNotified = false;
68
+
69
+ wil::shared_handle UserToken;
70
+ std::vector<BYTE> UserSid;
71
};
72
73
class WSLCSessionManagerImpl
@@ -68,15 +76,20 @@ public:
76
NON_COPYABLE(WSLCSessionManagerImpl);
77
NON_MOVABLE(WSLCSessionManagerImpl);
78
71
- WSLCSessionManagerImpl() = default;
79
+ WSLCSessionManagerImpl();
80
~WSLCSessionManagerImpl();
81
82
void CreateSession(const WSLCSessionSettings* WslcSessionSettings, WSLCSessionFlags Flags, IWSLCSession** WslcSession);
83
void EnterSession(_In_ LPCWSTR DisplayName, _In_ LPCWSTR StoragePath, IWSLCSession** WslcSession);
76
- void ListSessions(_Out_ WSLCSessionInformation** Sessions, _Out_ ULONG* SessionsCount);
84
+ void ListSessions(_Out_ WSLCSessionListEntry** Sessions, _Out_ ULONG* SessionsCount);
85
void OpenSession(_In_ ULONG Id, _Out_ IWSLCSession** Session);
86
void OpenSessionByName(_In_ LPCWSTR DisplayName, _Out_ IWSLCSession** Session);
87
88
+ // Resolves a session by ID for plugin->API calls. Throws ERROR_NOT_FOUND if no session matches.
89
+ wil::com_ptr<IWSLCSession> FindSession(ULONG Id);
90
+
91
+ static WSLCSessionManagerImpl* Instance() noexcept;
92
+
93
private:
94
// Resolves the default session name for a caller: appends the username
95
// from the token SID so different users don't collide.
@@ -109,7 +122,9 @@ private:
122
wil::com_ptr<IWSLCSession> lockedSession;
123
if (FAILED_LOG(entry.Ref->OpenSession(&lockedSession)))
124
{
112
- // Session is gone, drop the persistent reference if any.
125
+ // Session is gone: notify plugins (if not already), then drop persistent reference if any.
126
+ NotifySessionStoppingLockHeld(entry);
127
+
128
auto remove =
129
std::ranges::remove_if(m_persistentSessions, [&](const auto& e) { return e.first == entry.SessionId; });
130
m_persistentSessions.erase(remove.begin(), remove.end());
@@ -151,6 +166,8 @@ private:
166
static CallingProcessTokenInfo GetCallingProcessTokenInfo();
167
static HRESULT CheckTokenAccess(const SessionEntry& Entry, const CallingProcessTokenInfo& TokenInfo);
168
169
+ void NotifySessionStoppingLockHeld(SessionEntry& entry) noexcept;
170
+
171
std::atomic<ULONG> m_nextSessionId{1};
172
std::recursive_mutex m_wslcSessionsLock;
173
@@ -183,7 +200,7 @@ public:
200
IFACEMETHOD(IsClientVersionSupported)(_In_ const WSLCVersion* ClientVersion, _Out_ BOOL* IsSupported) override;
201
IFACEMETHOD(CreateSession)(const WSLCSessionSettings* WslcSessionSettings, WSLCSessionFlags Flags, IWSLCSession** WslcSession) override;
202
IFACEMETHOD(EnterSession)(_In_ LPCWSTR DisplayName, _In_ LPCWSTR StoragePath, IWSLCSession** WslcSession) override;
186
- IFACEMETHOD(ListSessions)(_Out_ WSLCSessionInformation** Sessions, _Out_ ULONG* SessionsCount) override;
203
+ IFACEMETHOD(ListSessions)(_Out_ WSLCSessionListEntry** Sessions, _Out_ ULONG* SessionsCount) override;
204
IFACEMETHOD(OpenSession)(_In_ ULONG Id, _Out_ IWSLCSession** Session) override;
205
IFACEMETHOD(OpenSessionByName)(_In_ LPCWSTR DisplayName, _Out_ IWSLCSession** Session) override;
206
};
src/windows/service/exe/WSLCSessionManagerFactory.cpp
+1
-1
@@ -82,4 +82,4 @@ void wsl::windows::service::wslc::ClearWslcSessionsAndBlockNewInstances()
82
}
83
84
g_sessionManagerImpl.reset();
85
-}
\ No newline at end of file
85
+}
src/windows/service/inc/wslc.idl
+27
-4
@@ -117,6 +117,27 @@ interface IProgressCallback : IUnknown
117
HRESULT OnProgress(LPCSTR Status, LPCSTR Id, ULONGLONG Current, ULONGLONG Total);
118
};
119
120
+[
121
+ uuid(F3E6D5B2-1D40-4E8B-9C39-7A45D1C0F8A2),
122
+ pointer_default(unique),
123
+ object
124
+]
125
+interface IWSLCPluginNotifier : IUnknown
126
+{
127
+ // 'InspectJson' follows the wslc_schema::InspectContainer format.
128
+ // Returning failure prevents the container creation.
129
+ HRESULT OnContainerStarted([in] LPCSTR InspectJson);
130
+
131
+ // Called when a container is about to stop. 'ContainerId' is the container identifier. Errors are logged but ignored.
132
+ HRESULT OnContainerStopping([in] LPCSTR ContainerId);
133
+
134
+ // 'InspectJson' follows the wslc_schema::InspectImage format. Errors are logged but ignored.
135
+ HRESULT OnImageCreated([in] LPCSTR InspectJson);
136
+
137
+ // Called when an image is deleted. 'ImageId' is the image identifier. Errors are logged but ignored.
138
+ HRESULT OnImageDeleted([in] LPCSTR ImageId);
139
+};
140
+
141
typedef struct _WSLCImageInformation
142
{
143
char Image[WSLC_MAX_IMAGE_NAME_LENGTH + 1];
@@ -773,7 +794,8 @@ interface IWSLCSession : IUnknown
794
// Initializes the session with a pre-created VM.
795
HRESULT Initialize(
796
[in] const WSLCSessionInitSettings* Settings,
776
- [in] IWSLCVirtualMachine* Vm);
797
+ [in] IWSLCVirtualMachine* Vm,
798
+ [in] IWSLCPluginNotifier* PluginNotifier);
799
800
// Volume management.
801
HRESULT CreateVolume([in] const WSLCVolumeOptions* Options, [out] WSLCVolumeInformation* VolumeInfo);
@@ -827,6 +849,7 @@ interface IWSLCSessionFactory : IUnknown
849
HRESULT CreateSession(
850
[in] const WSLCSessionInitSettings* Settings,
851
[in] IWSLCVirtualMachine* Vm,
852
+ [in] IWSLCPluginNotifier* PluginNotifier,
853
[out] IWSLCSession** Session,
854
[out] IWSLCSessionReference** ServiceRef);
855
@@ -834,13 +857,13 @@ interface IWSLCSessionFactory : IUnknown
857
HRESULT GetProcessHandle([out, system_handle(sh_process)] HANDLE* ProcessHandle);
858
}
859
837
-typedef struct _WSLCSessionInformation
860
+typedef struct _WSLCSessionListEntry
861
{
862
ULONG SessionId;
863
DWORD CreatorPid;
864
wchar_t DisplayName[256];
865
wchar_t Sid[256 + 1]; // MAX_SID_SIZE = 256
843
-} WSLCSessionInformation;
866
+} WSLCSessionListEntry;
867
868
typedef enum _WSLCSessionFlags
869
{
@@ -865,7 +888,7 @@ interface IWSLCSessionManager : IUnknown
888
// Session management.
889
HRESULT CreateSession([in, unique] const WSLCSessionSettings* Settings, WSLCSessionFlags Flags, [out] IWSLCSession** Session);
890
HRESULT EnterSession([in, ref] LPCWSTR DisplayName, [in, ref] LPCWSTR StoragePath, [out] IWSLCSession** Session);
868
- HRESULT ListSessions([out, size_is(, *SessionsCount)] WSLCSessionInformation** Sessions, [out] ULONG* SessionsCount);
891
+ HRESULT ListSessions([out, size_is(, *SessionsCount)] WSLCSessionListEntry** Sessions, [out] ULONG* SessionsCount);
892
HRESULT OpenSession([in] ULONG Id, [out] IWSLCSession** Session);
893
HRESULT OpenSessionByName([in, unique] LPCWSTR DisplayName, [out] IWSLCSession** Session);
894
}
src/windows/wslc/services/SessionService.cpp
+1
-1
@@ -149,7 +149,7 @@ std::vector<SessionInformation> SessionService::List()
149
THROW_IF_FAILED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
150
wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
151
152
- wil::unique_cotaskmem_array_ptr<WSLCSessionInformation> sessions;
152
+ wil::unique_cotaskmem_array_ptr<WSLCSessionListEntry> sessions;
153
THROW_IF_FAILED(sessionManager->ListSessions(&sessions, sessions.size_address<ULONG>()));
154
for (size_t i = 0; i < sessions.size(); ++i)
155
{
src/windows/wslcsession/WSLCContainer.cpp
+53
-9
@@ -579,6 +579,7 @@ WSLCPortMapping ContainerPortMapping::Serialize() const
579
WSLCContainerImpl::WSLCContainerImpl(
580
WSLCSession& wslcSession,
581
WSLCVirtualMachine& virtualMachine,
582
+ IWSLCPluginNotifier* pluginNotifier,
583
std::string&& Id,
584
std::string&& Name,
585
std::string&& Image,
@@ -595,6 +596,7 @@ WSLCContainerImpl::WSLCContainerImpl(
596
WSLCProcessFlags InitProcessFlags,
597
WSLCContainerFlags ContainerFlags) :
598
m_wslcSession(wslcSession),
599
+ m_pluginNotifier(pluginNotifier),
600
m_virtualMachine(virtualMachine),
601
m_name(std::move(Name)),
602
m_image(std::move(Image)),
@@ -851,6 +853,30 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, LPCSTR DetachKeys)
853
}
854
CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to start container '%hs'", m_id.c_str());
855
856
+ auto inspectJson = InspectLockHeld();
857
+ const auto pluginResult = m_pluginNotifier->OnContainerStarted(inspectJson.c_str());
858
+ if (FAILED(pluginResult))
859
+ {
860
+ // Forward the COM error message, if available.
861
+ auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
862
+
863
+ LOG_HR_MSG(pluginResult, "Plugin rejected start of container '%hs' (0x%x)", m_id.c_str(), pluginResult);
864
+ try
865
+ {
866
+ m_dockerClient.StopContainer(m_id.c_str(), {}, {});
867
+ }
868
+ CATCH_LOG();
869
+
870
+ if (comError.has_value() && comError->Message)
871
+ {
872
+ THROW_HR_WITH_USER_ERROR(pluginResult, comError->Message.get());
873
+ }
874
+ else
875
+ {
876
+ THROW_HR(pluginResult);
877
+ }
878
+ }
879
+
880
portCleanup.release();
881
volumeCleanup.release();
882
@@ -994,6 +1020,16 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::
1020
{
1021
unique_com_disconnect comWrapper;
1022
1023
+ // Notify plugin manager that the container is stopping. Errors are ignored.
1024
+ if (m_state == WslcContainerStateRunning)
1025
+ {
1026
+ try
1027
+ {
1028
+ LOG_IF_FAILED(m_pluginNotifier->OnContainerStopping(m_id.c_str()));
1029
+ }
1030
+ CATCH_LOG();
1031
+ }
1032
+
1033
ReleaseProcesses();
1034
ReleaseRuntimeResources();
1035
@@ -1349,6 +1385,7 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1385
const std::string& containerName,
1386
WSLCSession& wslcSession,
1387
WSLCVirtualMachine& virtualMachine,
1388
+ IWSLCPluginNotifier* pluginNotifier,
1389
const std::unordered_map<std::string, NetworkEntry>& sessionNetworks,
1390
std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
1391
DockerEventTracker& EventTracker,
@@ -1703,6 +1740,7 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1740
auto container = std::make_unique<WSLCContainerImpl>(
1741
wslcSession,
1742
virtualMachine,
1743
+ pluginNotifier,
1744
std::move(result.Id),
1745
CleanContainerName(inspectData.Name),
1746
std::string(containerOptions.Image),
@@ -1727,6 +1765,7 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
1765
const common::docker_schema::ContainerInfo& dockerContainer,
1766
WSLCSession& wslcSession,
1767
WSLCVirtualMachine& virtualMachine,
1768
+ IWSLCPluginNotifier* pluginNotifier,
1769
WSLCVolumes& volumes,
1770
std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
1771
DockerEventTracker& EventTracker,
@@ -1792,6 +1831,7 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
1831
auto container = std::make_unique<WSLCContainerImpl>(
1832
wslcSession,
1833
virtualMachine,
1834
+ pluginNotifier,
1835
std::string(dockerContainer.Id),
1836
std::move(name),
1837
std::string(dockerContainer.Image),
@@ -1836,19 +1876,23 @@ void WSLCContainerImpl::Inspect(LPSTR* Output) const
1876
1877
try
1878
{
1839
- // Get Docker inspect data
1840
- auto dockerInspect = m_dockerClient.InspectContainer(m_id);
1841
-
1842
- // Convert to WSLC schema
1843
- auto wslcInspect = BuildInspectContainer(dockerInspect);
1844
-
1845
- // Serialize WSLC schema to JSON
1846
- std::string wslcJson = wsl::shared::ToJson(wslcInspect);
1847
- *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(wslcJson.c_str()).release();
1879
+ *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(InspectLockHeld().c_str()).release();
1880
}
1881
CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to inspect container '%hs'", m_id.c_str());
1882
}
1883
1884
+std::string WSLCContainerImpl::InspectLockHeld() const
1885
+{
1886
+ // Get Docker inspect data
1887
+ auto dockerInspect = m_dockerClient.InspectContainer(m_id);
1888
+
1889
+ // Convert to WSLC schema
1890
+ auto wslcInspect = BuildInspectContainer(dockerInspect);
1891
+
1892
+ // Serialize WSLC schema to JSON
1893
+ return wsl::shared::ToJson(wslcInspect);
1894
+}
1895
+
1896
void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, ULONGLONG Since, ULONGLONG Until, ULONGLONG Tail) const
1897
{
1898
auto lock = m_lock.lock_shared();
src/windows/wslcsession/WSLCContainer.h
+6
@@ -72,6 +72,7 @@ public:
72
WSLCContainerImpl(
73
WSLCSession& wslcSession,
74
WSLCVirtualMachine& virtualMachine,
75
+ IWSLCPluginNotifier* pluginNotifier,
76
std::string&& Id,
77
std::string&& Name,
78
std::string&& Image,
@@ -130,6 +131,7 @@ public:
131
const std::string& Name,
132
WSLCSession& wslcSession,
133
WSLCVirtualMachine& virtualMachine,
134
+ IWSLCPluginNotifier* pluginNotifier,
135
const std::unordered_map<std::string, NetworkEntry>& SessionNetworks,
136
std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
137
DockerEventTracker& EventTracker,
@@ -140,6 +142,7 @@ public:
142
const common::docker_schema::ContainerInfo& DockerContainer,
143
WSLCSession& wslcSession,
144
WSLCVirtualMachine& virtualMachine,
145
+ IWSLCPluginNotifier* pluginNotifier,
146
WSLCVolumes& Volumes,
147
std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
148
DockerEventTracker& EventTracker,
@@ -169,6 +172,8 @@ private:
172
void MapPorts();
173
void UnmapPorts();
174
175
+ __requires_shared_lock_held(m_lock) std::string InspectLockHeld() const;
176
+
177
mutable wil::srwlock m_lock;
178
std::string m_name;
179
std::string m_image;
@@ -197,6 +202,7 @@ private:
202
std::uint64_t m_createdAt{};
203
WSLCContainerState m_state = WslcContainerStateInvalid;
204
WSLCSession& m_wslcSession;
205
+ IWSLCPluginNotifier* m_pluginNotifier;
206
WSLCVirtualMachine& m_virtualMachine;
207
std::vector<ContainerPortMapping> m_mappedPorts;
208
std::vector<WSLCVolumeMount> m_mountedVolumes;
src/windows/wslcsession/WSLCSession.cpp
+42
-8
@@ -257,7 +257,7 @@ try
257
}
258
CATCH_RETURN();
259
260
-HRESULT WSLCSession::Initialize(_In_ const WSLCSessionInitSettings* Settings, _In_ IWSLCVirtualMachine* Vm)
260
+HRESULT WSLCSession::Initialize(_In_ const WSLCSessionInitSettings* Settings, _In_ IWSLCVirtualMachine* Vm, _In_ IWSLCPluginNotifier* PluginNotifier)
261
try
262
{
263
RETURN_HR_IF(E_POINTER, Settings == nullptr || Vm == nullptr);
@@ -267,6 +267,7 @@ try
267
m_id = Settings->SessionId;
268
m_displayName = Settings->DisplayName ? Settings->DisplayName : L"";
269
m_featureFlags = Settings->FeatureFlags;
270
+ m_pluginNotifier = PluginNotifier;
271
272
// Get user token for the current process
273
const auto tokenInfo = wil::get_token_information<TOKEN_USER>(GetCurrentProcessToken());
@@ -645,6 +646,20 @@ void WSLCSession::StreamImageOperation(DockerHTTPClient::HTTPRequestContext& req
646
}
647
}
648
649
+void WSLCSession::OnImageCreated(const std::string& ImageNameOrId) noexcept
650
+try
651
+{
652
+ LOG_IF_FAILED(m_pluginNotifier->OnImageCreated(InspectImageLockHeld(ImageNameOrId).c_str()));
653
+}
654
+CATCH_LOG()
655
+
656
+void WSLCSession::OnImageDeleted(const std::string& ImageId) noexcept
657
+try
658
+{
659
+ LOG_IF_FAILED(m_pluginNotifier->OnImageDeleted(ImageId.c_str()));
660
+}
661
+CATCH_LOG()
662
+
663
HRESULT WSLCSession::PullImage(LPCSTR Image, LPCSTR RegistryAuthenticationInformation, IProgressCallback* ProgressCallback)
664
try
665
{
@@ -673,6 +688,8 @@ try
688
auto requestContext = m_dockerClient->PullImage(repo, tagOrDigest, registryAuth);
689
StreamImageOperation(*requestContext, Image, "Pull", ProgressCallback);
690
691
+ OnImageCreated(Image);
692
+
693
return S_OK;
694
}
695
CATCH_RETURN();
@@ -1014,6 +1031,7 @@ try
1031
auto requestContext = m_dockerClient->LoadImage(ContentSize);
1032
1033
ImportImageImpl(*requestContext, ImageHandle);
1034
+
1035
return S_OK;
1036
}
1037
CATCH_RETURN();
@@ -1039,6 +1057,8 @@ try
1057
auto requestContext = m_dockerClient->ImportImage(repo, tagOrDigest.value(), ContentSize);
1058
1059
ImportImageImpl(*requestContext, ImageHandle);
1060
+
1061
+ OnImageCreated(ImageName);
1062
return S_OK;
1063
}
1064
CATCH_RETURN();
@@ -1095,7 +1115,6 @@ void WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request,
1115
}
1116
else if (parsed.stream.has_value())
1117
{
1098
- // TODO: report progress to caller.
1118
WSL_LOG("ImageImportProgress", TraceLoggingValue(parsed.stream->c_str(), "Content"));
1119
}
1120
else
@@ -1420,6 +1439,15 @@ try
1439
*Count = static_cast<ULONG>(deletedImages.size());
1440
*DeletedImages = output.release();
1441
1442
+ // Notify plugin manager of all deleted image IDs.
1443
+ for (const auto& image : deletedImages)
1444
+ {
1445
+ if (!image.Deleted.empty())
1446
+ {
1447
+ OnImageDeleted(image.Deleted);
1448
+ }
1449
+ }
1450
+
1451
return S_OK;
1452
}
1453
CATCH_RETURN();
@@ -1497,10 +1525,18 @@ try
1525
auto lock = m_lock.lock_shared();
1526
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1527
1528
+ *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(InspectImageLockHeld(ImageNameOrId).c_str()).release();
1529
+
1530
+ return S_OK;
1531
+}
1532
+CATCH_RETURN();
1533
+
1534
+std::string WSLCSession::InspectImageLockHeld(const std::string& NameOrId)
1535
+{
1536
docker_schema::InspectImage dockerInspect;
1537
try
1538
{
1503
- dockerInspect = m_dockerClient->InspectImage(ImageNameOrId);
1539
+ dockerInspect = m_dockerClient->InspectImage(NameOrId);
1540
}
1541
catch (const DockerHTTPException& e)
1542
{
@@ -1519,12 +1555,8 @@ try
1555
auto wslcInspect = ConvertInspectImage(dockerInspect);
1556
1557
// Serialize to JSON
1522
- std::string wslcJson = wsl::shared::ToJson(wslcInspect);
1523
- *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(wslcJson.c_str()).release();
1524
-
1525
- return S_OK;
1558
+ return wsl::shared::ToJson(wslcInspect);
1559
}
1527
-CATCH_RETURN();
1560
1561
HRESULT WSLCSession::Authenticate(_In_ LPCSTR ServerAddress, _In_ LPCSTR Username, _In_ LPCSTR Password, _Out_ LPSTR* IdentityToken)
1562
try
@@ -1724,6 +1756,7 @@ try
1756
containerName,
1757
*this,
1758
m_virtualMachine.value(),
1759
+ m_pluginNotifier.get(),
1760
m_networks,
1761
std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
1762
m_eventTracker.value(),
@@ -2766,6 +2799,7 @@ void WSLCSession::RecoverExistingContainers()
2799
dockerContainer,
2800
*this,
2801
m_virtualMachine.value(),
2802
+ m_pluginNotifier.get(),
2803
*m_volumes,
2804
std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
2805
m_eventTracker.value(),
src/windows/wslcsession/WSLCSession.h
+12
-1
@@ -85,7 +85,7 @@ public:
85
86
// IWSLCSession - initialization methods
87
IFACEMETHOD(GetProcessHandle)(_Out_ HANDLE* ProcessHandle) override;
88
- IFACEMETHOD(Initialize)(_In_ const WSLCSessionInitSettings* Settings, _In_ IWSLCVirtualMachine* Vm) override;
88
+ IFACEMETHOD(Initialize)(_In_ const WSLCSessionInitSettings* Settings, _In_ IWSLCVirtualMachine* Vm, _In_ IWSLCPluginNotifier* PluginNotifier) override;
89
90
IFACEMETHOD(GetId)(_Out_ ULONG* Id) override;
91
IFACEMETHOD(GetState)(_Out_ WSLCSessionState* State) override;
@@ -177,7 +177,16 @@ private:
177
__requires_lock_held(m_userCOMCallbacksLock) void CancelUserCOMCallbacks();
178
void ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID UserSid);
179
void Ext4Format(const std::string& Device);
180
+ _Requires_shared_lock_held_(m_lock)
181
+ std::string InspectImageLockHeld(const std::string& Id);
182
void OnContainerDeleted(const WSLCContainerImpl* Container);
183
+
184
+ _Requires_shared_lock_held_(m_lock)
185
+ void OnImageCreated(const std::string& ImageNameOrId) noexcept;
186
+
187
+ _Requires_shared_lock_held_(m_lock)
188
+ void OnImageDeleted(const std::string& ImageId) noexcept;
189
+
190
void OnProcessLog(const gsl::span<char>& Data, PCSTR Source);
191
void OnContainerdExited();
192
void OnDockerdExited();
@@ -222,6 +231,8 @@ private:
231
std::atomic<bool> m_terminating{false};
232
std::atomic<bool> m_terminated{false};
233
234
+ wil::com_ptr<IWSLCPluginNotifier> m_pluginNotifier;
235
+
236
// User-provided handles that the session is currently doing IO on.
237
std::mutex m_userHandlesLock;
238
__guarded_by(m_userHandlesLock) std::vector<HANDLE> m_userHandles;
src/windows/wslcsession/WSLCSessionFactory.cpp
+6
-2
@@ -30,7 +30,11 @@ void wslc::WSLCSessionFactory::SetDestructionCallback(std::function<void()>&& ca
30
}
31
32
HRESULT wslc::WSLCSessionFactory::CreateSession(
33
- _In_ const WSLCSessionInitSettings* Settings, _In_ IWSLCVirtualMachine* Vm, _Out_ IWSLCSession** Session, _Out_ IWSLCSessionReference** ServiceRef)
33
+ _In_ const WSLCSessionInitSettings* Settings,
34
+ _In_ IWSLCVirtualMachine* Vm,
35
+ _In_ IWSLCPluginNotifier* PluginNotifier,
36
+ _Out_ IWSLCSession** Session,
37
+ _Out_ IWSLCSessionReference** ServiceRef)
38
try
39
{
40
*Session = nullptr;
@@ -44,7 +48,7 @@ try
48
session->SetDestructionCallback(std::move(m_destructionCallback));
49
50
// Initialize the session with the VM.
47
- RETURN_IF_FAILED(session->Initialize(Settings, Vm));
51
+ RETURN_IF_FAILED(session->Initialize(Settings, Vm, PluginNotifier));
52
53
// Create the service session ref. It extracts metadata and a weak reference from the session.
54
auto serviceRef = Microsoft::WRL::Make<wslc::WSLCSessionReference>(session.Get());
src/windows/wslcsession/WSLCSessionFactory.h
+5
-2
@@ -44,8 +44,11 @@ public:
44
45
// IWSLCSessionFactory
46
IFACEMETHOD(CreateSession)
47
- (_In_ const WSLCSessionInitSettings* Settings, _In_ IWSLCVirtualMachine* Vm, _Out_ IWSLCSession** Session, _Out_ IWSLCSessionReference** ServiceRef)
48
- override;
47
+ (_In_ const WSLCSessionInitSettings* Settings,
48
+ _In_ IWSLCVirtualMachine* Vm,
49
+ _In_ IWSLCPluginNotifier* PluginNotifier,
50
+ _Out_ IWSLCSession** Session,
51
+ _Out_ IWSLCSessionReference** ServiceRef) override;
52
53
IFACEMETHOD(GetProcessHandle)(_Out_ HANDLE* ProcessHandle) override;
54
test/windows/Common.cpp
+62
@@ -2904,6 +2904,19 @@ std::filesystem::path GetTestImagePath(std::string_view imageName)
2904
return result;
2905
}
2906
2907
+void LoadTestImage(IWSLCSession& session, std::string_view imageName)
2908
+{
2909
+ std::filesystem::path imagePath = GetTestImagePath(imageName);
2910
+ wil::unique_hfile imageFile{
2911
+ CreateFileW(imagePath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
2912
+ THROW_LAST_ERROR_IF(!imageFile);
2913
+
2914
+ LARGE_INTEGER fileSize{};
2915
+ THROW_LAST_ERROR_IF(!GetFileSizeEx(imageFile.get(), &fileSize));
2916
+
2917
+ THROW_IF_FAILED(session.LoadImage(wsl::windows::common::wslutil::ToCOMInputHandle(imageFile.get()), nullptr, fileSize.QuadPart));
2918
+}
2919
+
2920
void ExpectHttpResponse(LPCWSTR Url, std::optional<int> expectedCode, bool retry)
2921
{
2922
const winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter filter;
@@ -2991,3 +3004,52 @@ void WriteSocket(SOCKET Socket, const void* data, size_t size)
3004
data = static_cast<const char*>(data) + result;
3005
}
3006
}
3007
+
3008
+void ValidateCOMErrorMessage(const std::optional<std::wstring>& Expected, const std::source_location& Source)
3009
+{
3010
+ auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
3011
+
3012
+ if (comError.has_value())
3013
+ {
3014
+ if (!Expected.has_value())
3015
+ {
3016
+ LogError("Unexpected COM error: '%ls'. Source: %hs", comError->Message.get(), std::format("{}", Source).c_str());
3017
+ VERIFY_FAIL();
3018
+ }
3019
+
3020
+ VERIFY_ARE_EQUAL(Expected.value(), comError->Message.get());
3021
+ }
3022
+ else
3023
+ {
3024
+ if (Expected.has_value())
3025
+ {
3026
+ LogError("Expected COM error: '%ls' but none was set. Source: %hs", Expected->c_str(), std::format("{}", Source).c_str());
3027
+ VERIFY_FAIL();
3028
+ }
3029
+ }
3030
+}
3031
+
3032
+void ValidateCOMErrorMessageContains(const std::wstring& ExpectedSubstring)
3033
+{
3034
+ auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
3035
+
3036
+ if (comError.has_value())
3037
+ {
3038
+ if (!comError->Message)
3039
+ {
3040
+ LogError("Expected COM error containing: '%ls', but COM error message was null", ExpectedSubstring.c_str());
3041
+ VERIFY_FAIL();
3042
+ }
3043
+
3044
+ if (wcsstr(comError->Message.get(), ExpectedSubstring.c_str()) == nullptr)
3045
+ {
3046
+ LogError("Expected COM error containing: '%ls', but got: '%ls'", ExpectedSubstring.c_str(), comError->Message.get());
3047
+ VERIFY_FAIL();
3048
+ }
3049
+ }
3050
+ else
3051
+ {
3052
+ LogError("Expected COM error containing: '%ls' but none was set", ExpectedSubstring.c_str());
3053
+ VERIFY_FAIL();
3054
+ }
3055
+}
test/windows/Common.h
+6
@@ -617,6 +617,8 @@ void VerifyPatternMatch(const std::string& Content, const std::string& Pattern);
617
618
std::filesystem::path GetTestImagePath(std::string_view imageName);
619
620
+void LoadTestImage(IWSLCSession& session, std::string_view imageName);
621
+
622
void ExpectHttpResponse(LPCWSTR Url, std::optional<int> expectedCode, bool retry = false);
623
624
template <typename T>
@@ -678,3 +680,7 @@ void VerifyAreEqualUnordered(const std::vector<T>& expected, const std::vector<T
680
void SetPathAccess(const std::filesystem::path& path, DWORD Permissions, ACCESS_MODE Mode);
681
682
void WriteSocket(SOCKET Socket, const void* data, size_t size);
683
+
684
+void ValidateCOMErrorMessage(const std::optional<std::wstring>& Expected, const std::source_location& Source = std::source_location::current());
685
+
686
+void ValidateCOMErrorMessageContains(const std::wstring& ExpectedSubstring);
test/windows/PluginTests.cpp
+186
@@ -16,10 +16,16 @@ Abstract:
16
#include "Common.h"
17
#include "registry.hpp"
18
#include "PluginTests.h"
19
+#include "wslc.h"
20
+#include "WSLCContainerLauncher.h"
21
+#include "WSLCProcessLauncher.h"
22
+#include "wslc/e2e/WSLCE2EHelpers.h"
23
24
using namespace wsl::windows::common::registry;
25
+using WSLCE2ETests::StartLocalRegistry;
26
27
extern std::wstring g_testDistroPath;
28
+extern std::wstring g_testDataPath;
29
30
class PluginTests
31
{
@@ -592,6 +598,186 @@ class PluginTests
598
StartWsl(0);
599
ValidateLogFile(ExpectedOutput);
600
}
601
+ static wil::com_ptr<IWSLCSessionManager> OpenWslcSessionManager()
602
+ {
603
+ wil::com_ptr<IWSLCSessionManager> sessionManager;
604
+ VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager)));
605
+ wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get());
606
+ return sessionManager;
607
+ }
608
+
609
+ static wil::com_ptr<IWSLCSession> CreateWslcSession(LPCWSTR Name, WSLCNetworkingMode NetworkingMode = WSLCNetworkingModeNone)
610
+ {
611
+ WSLCSessionSettings settings{};
612
+ settings.DisplayName = Name;
613
+ settings.CpuCount = 4;
614
+ settings.MemoryMb = 4096;
615
+ settings.BootTimeoutMs = 30 * 1000;
616
+ settings.NetworkingMode = NetworkingMode;
617
+
618
+ auto manager = OpenWslcSessionManager();
619
+ wil::com_ptr<IWSLCSession> session;
620
+ VERIFY_SUCCEEDED(manager->CreateSession(&settings, WSLCSessionFlagsNone, &session));
621
+ wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
622
+
623
+ WSLCSessionState state{};
624
+ VERIFY_SUCCEEDED(session->GetState(&state));
625
+ VERIFY_ARE_EQUAL(state, WSLCSessionStateRunning);
626
+
627
+ return session;
628
+ }
629
+
630
+ WSL2_TEST_METHOD(WslcSuccess)
631
+ {
632
+ ConfigurePlugin(PluginTestType::WslcSuccess);
633
+
634
+ {
635
+ auto session = CreateWslcSession(L"plugin-wslc-test");
636
+
637
+ LoadTestImage(*session, "debian:latest");
638
+
639
+ // Create a container that will have a stuck process so it's still in a running state when the callback is made.
640
+ wsl::windows::common::WSLCContainerLauncher launcher(
641
+ "debian:latest", "wslc-plugin-container", {"/bin/sh", "-c", "sleep 120"});
642
+
643
+ auto container = launcher.Launch(*session, WSLCContainerStartFlagsAttach);
644
+ VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
645
+
646
+ // Delete the image so we get an ImageDeleted notification before the session goes away.
647
+ WSLCDeleteImageOptions options{.Image = "debian:latest", .Flags = WSLCDeleteImageFlagsForce};
648
+ wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
649
+ VERIFY_SUCCEEDED(session->DeleteImage(&options, deletedImages.addressof(), deletedImages.size_address<ULONG>()));
650
+ }
651
+
652
+ const auto ExpectedOutput = std::format(
653
+ LR"(Plugin loaded. TestMode=18
654
+ WSLC Session created, name=plugin-wslc-test, id=*, pid=*, token=set, sid=set
655
+ Command: 'echo -n stdout-ok && echo -n stderr-ok >&2', status=0, stdout: stdout-ok, stderr: stderr-ok
656
+ Command: 'cat', status=0, stdout: stdin-ok, stderr:
657
+ Command: 'exit 12', status=12, stdout: , stderr:
658
+ Command: 'echo -n $ENV', status=0, stdout: env-ok, stderr:
659
+ WSLCCreateProcess(does-not-exist): {:x}, errno=2
660
+ WSLCProcessGetFd(999): {}
661
+ WSLCProcessGetExitCode(<running>): {}
662
+ WSLC RW folder mounted at: /mnt/wsl-plugin/plugin-rw-test
663
+ Command: 'cat /mnt/wsl-plugin/plugin-rw-test/plugin-test.txt', status=0, stdout: Windows-content, stderr:
664
+ WSLC RO folder mounted at: /mnt/wsl-plugin/plugin-ro-test
665
+ Command: 'echo fail > /mnt/wsl-plugin/plugin-ro-test/should-not-exist.txt', status=1, stdout: , stderr: *
666
+ WSLCMountFolder(nonexistent): {}
667
+ WSLCMountFolder(../escape): {}
668
+ WSLCMountFolder(): {}
669
+ Test completed
670
+ WSLC Container started, session=*, id=*, name=wslc-plugin-container, image=debian:latest, state=*
671
+ WSLC Container stopping, session=*, id=*
672
+ WSLC Image deleted, session=*, id=*
673
+ WSLC Session stopping, name=plugin-wslc-test, id=*)",
674
+ static_cast<uint32_t>(E_FAIL),
675
+ E_INVALIDARG,
676
+ HRESULT_FROM_WIN32(ERROR_INVALID_STATE),
677
+ HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND),
678
+ E_INVALIDARG,
679
+ E_INVALIDARG);
680
+
681
+ ValidateLogFile(ExpectedOutput.c_str());
682
+ }
683
+
684
+ WSL2_TEST_METHOD(WslcPullImageNotification)
685
+ {
686
+ ConfigurePlugin(PluginTestType::WslcImagePull);
687
+
688
+ {
689
+ auto session = CreateWslcSession(L"plugin-wslc-pull-test", WSLCNetworkingModeVirtioProxy);
690
+
691
+ // Load the registry and debian images.
692
+ LoadTestImage(*session, "debian:latest");
693
+
694
+ // Start a local registry container.
695
+ auto [registryContainer, registryAddress] = StartLocalRegistry(*session);
696
+
697
+ // Tag debian:latest for the local registry and push it.
698
+ auto registryImage = std::format("{}/debian:latest", registryAddress);
699
+ auto registryRepo = std::format("{}/debian", registryAddress);
700
+ WSLCTagImageOptions tagOptions{};
701
+ tagOptions.Image = "debian:latest";
702
+ tagOptions.Repo = registryRepo.c_str();
703
+ tagOptions.Tag = "latest";
704
+ VERIFY_SUCCEEDED(session->TagImage(&tagOptions));
705
+
706
+ auto emptyAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "");
707
+ VERIFY_SUCCEEDED(session->PushImage(registryImage.c_str(), emptyAuth.c_str(), nullptr));
708
+
709
+ // Delete the local tagged copy so PullImage actually downloads it.
710
+ WSLCDeleteImageOptions deleteOpts{.Image = registryImage.c_str(), .Flags = WSLCDeleteImageFlagsNone};
711
+ wil::unique_cotaskmem_array_ptr<WSLCDeletedImageInformation> deletedImages;
712
+ VERIFY_SUCCEEDED(session->DeleteImage(&deleteOpts, deletedImages.addressof(), deletedImages.size_address<ULONG>()));
713
+
714
+ // Pull the image back — this should trigger the ImageCreated plugin callback.
715
+ VERIFY_SUCCEEDED(session->PullImage(registryImage.c_str(), nullptr, nullptr));
716
+ }
717
+
718
+ constexpr auto ExpectedOutput =
719
+ LR"(Plugin loaded. TestMode=21
720
+ WSLC Session created, name=plugin-wslc-pull-test, id=*, pid=*, token=set, sid=set
721
+ WSLC Container started, session=*, id=*, name=*, image=wslc-registry:latest, state=running
722
+ WSLC Image created, session=*, id=sha256:*, name=127.0.0.1:5000/debian:latest
723
+ WSLC Session stopping, name=plugin-wslc-pull-test, id=*)";
724
+
725
+ ValidateLogFile(ExpectedOutput);
726
+ }
727
+
728
+ WSL2_TEST_METHOD(WslcSessionRejected)
729
+ {
730
+ ConfigurePlugin(PluginTestType::WslcSessionRejected);
731
+
732
+ WSLCSessionSettings settings{};
733
+ settings.DisplayName = L"plugin-wslc-rejected";
734
+ settings.CpuCount = 4;
735
+ settings.MemoryMb = 2048;
736
+ settings.BootTimeoutMs = 30 * 1000;
737
+ settings.MaximumStorageSizeMb = 1024 * 20;
738
+ settings.NetworkingMode = WSLCNetworkingModeNone;
739
+
740
+ auto manager = OpenWslcSessionManager();
741
+ wil::com_ptr<IWSLCSession> session;
742
+ const auto hr = manager->CreateSession(&settings, WSLCSessionFlagsNone, &session);
743
+ ValidateCOMErrorMessageContains(L"A fatal error was returned by plugin 'TestPlugin'");
744
+ VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED));
745
+
746
+ constexpr auto ExpectedOutput =
747
+ LR"(Plugin loaded. TestMode=19
748
+ WSLC Session created, name=plugin-wslc-rejected, id=*, pid=*, token=set, sid=set
749
+ OnWslcSessionCreated: ERROR_ACCESS_DENIED)";
750
+
751
+ ValidateLogFile(ExpectedOutput);
752
+ }
753
+
754
+ WSL2_TEST_METHOD(WslcContainerRejected)
755
+ {
756
+ ConfigurePlugin(PluginTestType::WslcContainerRejected);
757
+
758
+ {
759
+ auto session = CreateWslcSession(L"plugin-wslc-container-rejected");
760
+
761
+ LoadTestImage(*session, "debian:latest");
762
+
763
+ wsl::windows::common::WSLCContainerLauncher launcher(
764
+ "debian:latest", "wslc-plugin-rejected-container", {"/bin/sh", "-c", "echo nope"});
765
+
766
+ auto [hr, container] = launcher.LaunchNoThrow(*session, WSLCContainerStartFlagsAttach);
767
+ ValidateCOMErrorMessageContains(L"A fatal error was returned by plugin 'TestPlugin'");
768
+ VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED));
769
+ }
770
+
771
+ constexpr auto ExpectedOutput =
772
+ LR"(Plugin loaded. TestMode=20
773
+ WSLC Session created, name=plugin-wslc-container-rejected, id=*, pid=*, token=set, sid=set
774
+ WSLC Container started, session=*, id=*, name=*, image=debian:latest, state=*
775
+ OnWslcContainerStarted: ERROR_ACCESS_DENIED
776
+ WSLC Session stopping, name=plugin-wslc-container-rejected, id=*)";
777
+
778
+ ValidateLogFile(ExpectedOutput);
779
+ }
780
+
781
// This test must run last so it doesn't break test cases that depends on plugin signature.
782
WSL2_TEST_METHOD(InvalidPluginSignature)
783
{
test/windows/PluginTests.h
+6
-2
@@ -37,7 +37,11 @@ enum class PluginTestType
37
InitPidIsDifferent,
38
FailToRegisterUnregisterDistro,
39
RunDistroCommand,
40
- GetUsername
40
+ GetUsername,
41
+ WslcSuccess,
42
+ WslcSessionRejected,
43
+ WslcContainerRejected,
44
+ WslcImagePull
45
};
46
47
constexpr auto c_testType = L"TestType";
@@ -46,4 +50,4 @@ constexpr auto c_logFile = L"LogFile";
50
inline wil::unique_hkey OpenTestRegistryKey(REGSAM AccessMask)
51
{
52
return wsl::windows::common::registry::CreateKey(HKEY_LOCAL_MACHINE, c_configKey, AccessMask, nullptr, REG_OPTION_VOLATILE);
49
-}
\ No newline at end of file
53
+}
test/windows/WSLCTests.cpp
+21
-104
@@ -20,6 +20,7 @@ Abstract:
20
#include "WslCoreFilesystem.h"
21
#include "hcs.hpp"
22
#include "ContainerNameGenerator.h"
23
+#include "wslc/e2e/WSLCE2EHelpers.h"
24
#include <nlohmann/json.hpp>
25
26
using namespace std::literals::chrono_literals;
@@ -31,6 +32,7 @@ using wsl::windows::common::WSLCProcessLauncher;
32
using wsl::windows::common::io::OverlappedIOHandle;
33
using wsl::windows::common::io::WriteHandle;
34
using namespace wsl::windows::common::wslutil;
35
+using WSLCE2ETests::StartLocalRegistry;
36
37
extern std::wstring g_testDataPath;
38
extern bool g_fastTestRun;
@@ -45,20 +47,6 @@ class WSLCTests
47
wil::com_ptr<IWSLCSession> m_defaultSession;
48
static inline auto c_testSessionName = L"wslc-test";
49
48
- void LoadTestImage(std::string_view imageName, IWSLCSession* session = nullptr)
49
- {
50
- std::filesystem::path imagePath = GetTestImagePath(imageName);
51
- wil::unique_hfile imageFile{
52
- CreateFileW(imagePath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
53
- THROW_LAST_ERROR_IF(!imageFile);
54
-
55
- LARGE_INTEGER fileSize{};
56
- THROW_LAST_ERROR_IF(!GetFileSizeEx(imageFile.get(), &fileSize));
57
-
58
- THROW_IF_FAILED(
59
- (session ? session : m_defaultSession.get())->LoadImage(ToCOMInputHandle(imageFile.get()), nullptr, fileSize.QuadPart));
60
- }
61
-
50
TEST_CLASS_SETUP(TestClassSetup)
51
{
52
THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &m_wsadata));
@@ -78,27 +66,27 @@ class WSLCTests
66
67
if (!hasImage("debian:latest"))
68
{
81
- LoadTestImage("debian:latest");
69
+ LoadTestImage(*m_defaultSession, "debian:latest");
70
}
71
72
if (!hasImage("python:3.12-alpine"))
73
{
86
- LoadTestImage("python:3.12-alpine");
74
+ LoadTestImage(*m_defaultSession, "python:3.12-alpine");
75
}
76
77
if (!hasImage("hello-world:latest"))
78
{
91
- LoadTestImage("hello-world:latest");
79
+ LoadTestImage(*m_defaultSession, "hello-world:latest");
80
}
81
82
if (!hasImage("alpine:latest"))
83
{
96
- LoadTestImage("alpine:latest");
84
+ LoadTestImage(*m_defaultSession, "alpine:latest");
85
}
86
87
if (!hasImage("wslc-registry:latest"))
88
{
101
- LoadTestImage("wslc-registry:latest");
89
+ LoadTestImage(*m_defaultSession, "wslc-registry:latest");
90
}
91
92
PruneResult result;
@@ -207,28 +195,6 @@ class WSLCTests
195
return result;
196
}
197
210
- std::pair<RunningWSLCContainer, std::string> StartLocalRegistry(const std::string& username = {}, const std::string& password = {}, USHORT port = 5000)
211
- {
212
- std::vector<std::string> env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)};
213
- if (!username.empty())
214
- {
215
- env.push_back(std::format("USERNAME={}", username));
216
- env.push_back(std::format("PASSWORD={}", password));
217
- }
218
-
219
- WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env);
220
- launcher.SetEntrypoint({"/entrypoint.sh"});
221
- launcher.AddPort(port, port, AF_INET);
222
-
223
- auto container = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone);
224
-
225
- auto registryAddress = std::format("127.0.0.1:{}", port);
226
- auto registryUrl = std::format(L"http://{}", registryAddress);
227
- ExpectHttpResponse(registryUrl.c_str(), 200, true);
228
-
229
- return {std::move(container), std::move(registryAddress)};
230
- }
231
-
198
std::string PushImageToRegistry(const std::string& imageName, const std::string& registryAddress, const std::string& registryAuth)
199
{
200
auto [repo, tag] = ParseImage(imageName);
@@ -394,7 +360,7 @@ class WSLCTests
360
361
// Act: list sessions
362
{
397
- wil::unique_cotaskmem_array_ptr<WSLCSessionInformation> sessions;
363
+ wil::unique_cotaskmem_array_ptr<WSLCSessionListEntry> sessions;
364
VERIFY_SUCCEEDED(sessionManager->ListSessions(&sessions, sessions.size_address<ULONG>()));
365
366
// Assert
@@ -409,7 +375,7 @@ class WSLCTests
375
{
376
auto session2 = CreateSession(GetDefaultSessionSettings(L"wslc-test-list-2"));
377
412
- wil::unique_cotaskmem_array_ptr<WSLCSessionInformation> sessions;
378
+ wil::unique_cotaskmem_array_ptr<WSLCSessionListEntry> sessions;
379
VERIFY_SUCCEEDED(sessionManager->ListSessions(&sessions, sessions.size_address<ULONG>()));
380
381
VERIFY_ARE_EQUAL(sessions.size(), 2);
@@ -455,7 +421,7 @@ class WSLCTests
421
422
// Reject DisplayName at exact boundary (no room for null terminator).
423
{
458
- std::wstring boundaryName(std::size(WSLCSessionInformation{}.DisplayName), L'x');
424
+ std::wstring boundaryName(std::size(WSLCSessionListEntry{}.DisplayName), L'x');
425
auto settings = GetDefaultSessionSettings(boundaryName.c_str());
426
wil::com_ptr<IWSLCSession> session;
427
VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), WSLC_E_INVALID_SESSION_NAME);
@@ -463,7 +429,7 @@ class WSLCTests
429
430
// Reject too long DisplayName.
431
{
466
- std::wstring longName(std::size(WSLCSessionInformation{}.DisplayName) + 1, L'x');
432
+ std::wstring longName(std::size(WSLCSessionListEntry{}.DisplayName) + 1, L'x');
433
auto settings = GetDefaultSessionSettings(longName.c_str());
434
wil::com_ptr<IWSLCSession> session;
435
VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), WSLC_E_INVALID_SESSION_NAME);
@@ -587,7 +553,7 @@ class WSLCTests
553
{
554
{
555
// Start a local registry without auth and push hello-world:latest to it.
590
- auto [registryContainer, registryAddress] = StartLocalRegistry();
556
+ auto [registryContainer, registryAddress] = StartLocalRegistry(*m_defaultSession);
557
558
auto image = PushImageToRegistry("hello-world:latest", registryAddress, BuildRegistryAuthHeader("", ""));
559
ExpectImagePresent(*m_defaultSession, image.c_str(), false);
@@ -630,7 +596,7 @@ class WSLCTests
596
WSLC_TEST_METHOD(PullImageAdvanced)
597
{
598
// Start a local registry without auth to avoid Docker Hub rate limits.
633
- auto [registryContainer, registryAddress] = StartLocalRegistry();
599
+ auto [registryContainer, registryAddress] = StartLocalRegistry(*m_defaultSession);
600
auto auth = BuildRegistryAuthHeader("", "");
601
602
auto validatePull = [&](const std::string& sourceImage) {
@@ -740,7 +706,7 @@ class WSLCTests
706
constexpr auto c_username = "wslctest";
707
constexpr auto c_password = "password";
708
743
- auto [registryContainer, registryAddress] = StartLocalRegistry(c_username, c_password);
709
+ auto [registryContainer, registryAddress] = StartLocalRegistry(*m_defaultSession, c_username, c_password);
710
711
wil::unique_cotaskmem_ansistring token;
712
VERIFY_ARE_EQUAL(m_defaultSession->Authenticate(registryAddress.c_str(), c_username, "wrong-password", &token), E_FAIL);
@@ -1017,7 +983,7 @@ class WSLCTests
983
LogInfo("Test: Dangling filter");
984
{
985
// Setup a dangling image
1020
- LoadTestImage("alpine:latest");
986
+ LoadTestImage(*m_defaultSession, "alpine:latest");
987
WSLCTagImageOptions tagOptions{};
988
tagOptions.Image = "debian:latest";
989
tagOptions.Repo = "alpine";
@@ -1301,7 +1267,7 @@ class WSLCTests
1267
WSLC_TEST_METHOD(DeleteImage)
1268
{
1269
// Prepare alpine image to delete.
1304
- LoadTestImage("alpine:latest");
1270
+ LoadTestImage(*m_defaultSession, "alpine:latest");
1271
1272
// Verify that the image is in the list of images.
1273
ExpectImagePresent(*m_defaultSession, "alpine:latest");
@@ -1338,55 +1304,6 @@ class WSLCTests
1304
}
1305
}
1306
1341
- void ValidateCOMErrorMessage(const std::optional<std::wstring>& Expected, const std::source_location& Source = std::source_location::current())
1342
- {
1343
- auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
1344
-
1345
- if (comError.has_value())
1346
- {
1347
- if (!Expected.has_value())
1348
- {
1349
- LogError("Unexpected COM error: '%ls'. Source: %hs", comError->Message.get(), std::format("{}", Source).c_str());
1350
- VERIFY_FAIL();
1351
- }
1352
-
1353
- VERIFY_ARE_EQUAL(Expected.value(), comError->Message.get());
1354
- }
1355
- else
1356
- {
1357
- if (Expected.has_value())
1358
- {
1359
- LogError("Expected COM error: '%ls' but none was set. Source: %hs", Expected->c_str(), std::format("{}", Source).c_str());
1360
- VERIFY_FAIL();
1361
- }
1362
- }
1363
- }
1364
-
1365
- void ValidateCOMErrorMessageContains(const std::wstring& ExpectedSubstring)
1366
- {
1367
- auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
1368
-
1369
- if (comError.has_value())
1370
- {
1371
- if (!comError->Message)
1372
- {
1373
- LogError("Expected COM error containing: '%ls', but COM error message was null", ExpectedSubstring.c_str());
1374
- VERIFY_FAIL();
1375
- }
1376
-
1377
- if (wcsstr(comError->Message.get(), ExpectedSubstring.c_str()) == nullptr)
1378
- {
1379
- LogError("Expected COM error containing: '%ls', but got: '%ls'", ExpectedSubstring.c_str(), comError->Message.get());
1380
- VERIFY_FAIL();
1381
- }
1382
- }
1383
- else
1384
- {
1385
- LogError("Expected COM error containing: '%ls' but none was set", ExpectedSubstring.c_str());
1386
- VERIFY_FAIL();
1387
- }
1388
- }
1389
-
1307
class CapturingProgressCallback
1308
: public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback>
1309
{
@@ -7864,7 +7781,7 @@ class WSLCTests
7781
auto manager = OpenSessionManager();
7782
7783
auto expectSessions = [&](const std::vector<std::wstring>& expectedSessions) {
7867
- wil::unique_cotaskmem_array_ptr<WSLCSessionInformation> sessions;
7784
+ wil::unique_cotaskmem_array_ptr<WSLCSessionListEntry> sessions;
7785
VERIFY_SUCCEEDED(manager->ListSessions(&sessions, sessions.size_address<ULONG>()));
7786
7787
std::set<std::wstring> displayNames;
@@ -9267,12 +9184,12 @@ class WSLCTests
9184
// Helper to create a dangling image using only test-local tags:
9185
// Load alpine and hello-world under unique tags, then overwrite one with the other.
9186
auto createDanglingImage = [this]() {
9270
- LoadTestImage("alpine:latest");
9187
+ LoadTestImage(*m_defaultSession, "alpine:latest");
9188
WSLCTagImageOptions tagA{.Image = "alpine:latest", .Repo = "prune-test-a", .Tag = "v1"};
9189
VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagA));
9190
DeleteImage("alpine:latest", WSLCDeleteImageFlagsNone);
9191
9275
- LoadTestImage("hello-world:latest");
9192
+ LoadTestImage(*m_defaultSession, "hello-world:latest");
9193
WSLCTagImageOptions tagB{.Image = "hello-world:latest", .Repo = "prune-test-b", .Tag = "v1"};
9194
VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagB));
9195
DeleteImage("hello-world:latest", WSLCDeleteImageFlagsNone);
@@ -9345,7 +9262,7 @@ class WSLCTests
9262
9263
// Validate null Options uses defaults (dangling-only prune).
9264
{
9348
- LoadTestImage("alpine:latest");
9265
+ LoadTestImage(*m_defaultSession, "alpine:latest");
9266
WSLCTagImageOptions renameOptions{.Image = "alpine:latest", .Repo = "prune-test-a", .Tag = "v1"};
9267
VERIFY_SUCCEEDED(m_defaultSession->TagImage(&renameOptions));
9268
DeleteImage("alpine:latest", WSLCDeleteImageFlagsNone);
@@ -9482,7 +9399,7 @@ class WSLCTests
9399
auto revert = wil::impersonate_token(nonElevatedToken.get());
9400
9401
nonElevatedSession = CreateSession(GetDefaultSessionSettings(L"non-elevated-session"), WSLCSessionFlagsNone);
9485
- LoadTestImage("debian:latest", nonElevatedSession.get());
9402
+ LoadTestImage(*nonElevatedSession, "debian:latest");
9403
9404
WSLCContainerLauncher launcher("debian:latest", "test-non-elevated-handles-1", {"echo", "OK"});
9405
auto container = launcher.Launch(*nonElevatedSession);
test/windows/testplugin/Plugin.cpp
+221
-2
@@ -14,10 +14,14 @@ Abstract:
14
15
#include "precomp.h"
16
#include "WslPluginApi.h"
17
+#include "wslc_schema.h"
18
19
#include "PluginTests.h"
20
21
using namespace wsl::windows::common::registry;
22
+using namespace wsl::windows::common::relay;
23
+using namespace wsl::shared::string;
24
+using namespace std::chrono_literals;
25
26
std::ofstream g_logfile;
27
std::optional<GUID> g_distroGuid;
@@ -338,6 +342,215 @@ HRESULT OnDistributionUnregistered(const WSLSessionInformation* Session, const W
342
return S_OK;
343
}
344
345
+HRESULT OnWslcSessionCreated(const WSLCSessionInformation* Session)
346
+try
347
+{
348
+ g_logfile << "WSLC Session created, name=" << wsl::shared::string::WideToMultiByte(Session->DisplayName) << ", id=" << Session->SessionId
349
+ << ", pid=" << Session->ApplicationPid << ", token=" << (Session->UserToken != nullptr ? "set" : "null")
350
+ << ", sid=" << (Session->UserSid != nullptr ? "set" : "null") << std::endl;
351
+
352
+ if (g_testType == PluginTestType::WslcSessionRejected)
353
+ {
354
+ g_logfile << "OnWslcSessionCreated: ERROR_ACCESS_DENIED" << std::endl;
355
+ return HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED);
356
+ }
357
+
358
+ if (g_testType == PluginTestType::WslcSuccess)
359
+ {
360
+ // Helper: run a command in the root namespace and return (status, stdout, stderr).
361
+ auto runCommand = [&](const char* cmd,
362
+ const std::optional<std::string>& input = {},
363
+ std::vector<const char*> env = {}) -> std::tuple<int, std::string, std::string> {
364
+ std::vector<const char*> arguments = {"/bin/sh", "-c", cmd, nullptr};
365
+ WSLCProcessHandle process = nullptr;
366
+ THROW_IF_FAILED(g_api->WSLCCreateProcess(
367
+ Session->SessionId, arguments[0], arguments.data(), env.empty() ? nullptr : env.data(), &process, nullptr));
368
+ auto releaseProcess = wil::scope_exit([&]() { g_api->WSLCReleaseProcess(process); });
369
+
370
+ wil::unique_handle stdinHandle;
371
+ wil::unique_handle stdoutHandle;
372
+ wil::unique_handle stderrHandle;
373
+ wil::unique_handle exitEvent;
374
+ THROW_IF_FAILED(g_api->WSLCProcessGetFd(process, WSLCProcessFdStdin, &stdinHandle));
375
+ THROW_IF_FAILED(g_api->WSLCProcessGetFd(process, WSLCProcessFdStdout, &stdoutHandle));
376
+ THROW_IF_FAILED(g_api->WSLCProcessGetFd(process, WSLCProcessFdStderr, &stderrHandle));
377
+ THROW_IF_FAILED(g_api->WSLCProcessGetExitEvent(process, &exitEvent));
378
+
379
+ std::string out;
380
+ std::string err;
381
+
382
+ MultiHandleWait io;
383
+ io.AddHandle(std::make_unique<ReadHandle>(
384
+ std::move(stdoutHandle), [&out](const auto& span) { out.append(span.begin(), span.end()); }));
385
+
386
+ io.AddHandle(std::make_unique<ReadHandle>(
387
+ std::move(stderrHandle), [&err](const auto& span) { err.append(span.begin(), span.end()); }));
388
+
389
+ io.AddHandle(std::make_unique<EventHandle>(std::move(exitEvent)));
390
+
391
+ if (input.has_value())
392
+ {
393
+ io.AddHandle(std::make_unique<WriteHandle>(std::move(stdinHandle), std::vector<char>(input->begin(), input->end())));
394
+ }
395
+ else
396
+ {
397
+ stdinHandle.reset();
398
+ }
399
+
400
+ io.Run(60000ms);
401
+
402
+ int status = 0;
403
+ THROW_IF_FAILED(g_api->WSLCProcessGetExitCode(process, &status));
404
+ g_logfile << "Command: '" << cmd << "', status=" << status << ", stdout: " << out << ", stderr: " << err << std::endl;
405
+
406
+ return {status, out, err};
407
+ };
408
+
409
+ // Test process creation (output & exit code validated by the test code).
410
+ {
411
+ runCommand("echo -n stdout-ok && echo -n stderr-ok >&2");
412
+ runCommand("cat", "stdin-ok");
413
+ runCommand("exit 12");
414
+ runCommand("echo -n $ENV", {}, {"ENV=env-ok", nullptr});
415
+ }
416
+
417
+ // Validate that trying to execute a non-existent file fails with the expected error code.
418
+ {
419
+ WSLCProcessHandle process = nullptr;
420
+ int errnoValue = 0;
421
+ std::vector<const char*> args = {"does-not-exist", nullptr};
422
+
423
+ auto hr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, &errnoValue);
424
+ g_logfile << "WSLCCreateProcess(does-not-exist): " << std::hex << hr << ", errno=" << std::dec << errnoValue << std::endl;
425
+ }
426
+
427
+ // Validate various error paths
428
+ {
429
+ std::vector<const char*> args = {"/bin/sh", "-c", "sleep 9999", nullptr};
430
+ WSLCProcessHandle process = nullptr;
431
+ THROW_IF_FAILED(g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr));
432
+ auto releaseProcess = wil::scope_exit([&]() { g_api->WSLCReleaseProcess(process); });
433
+
434
+ // Validate that getting an fd that doesn't exist fails with the expected error code.
435
+ HANDLE dummy = nullptr;
436
+ g_logfile << "WSLCProcessGetFd(999): " << g_api->WSLCProcessGetFd(process, static_cast<WSLCProcessFd>(999), &dummy) << std::endl;
437
+ int exitCode = -1;
438
+
439
+ g_logfile << "WSLCProcessGetExitCode(<running>): " << g_api->WSLCProcessGetExitCode(process, &exitCode) << std::endl;
440
+ }
441
+
442
+ const auto testFolder = L"C:\\";
443
+ constexpr auto testFileName = L"plugin-test.txt";
444
+
445
+ // Validate rw mounts.
446
+ {
447
+ auto rwCleanup = wil::scope_exit_log(
448
+ WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove(std::wstring(testFolder) + testFileName); });
449
+
450
+ {
451
+ std::ofstream file(std::wstring(testFolder) + testFileName);
452
+ file << "Windows-content";
453
+ }
454
+
455
+ // Mount read-write and verify the file can be read from Linux.
456
+ char rwMountpoint[WSLC_MOUNTPOINT_LENGTH] = {};
457
+ THROW_IF_FAILED(g_api->WSLCMountFolder(Session->SessionId, testFolder, false, L"plugin-rw-test", rwMountpoint));
458
+
459
+ g_logfile << "WSLC RW folder mounted at: " << rwMountpoint << std::endl;
460
+
461
+ auto readCmd = std::format("cat {}/{}", rwMountpoint, testFileName);
462
+ runCommand(readCmd.c_str());
463
+
464
+ THROW_IF_FAILED(g_api->WSLCUnmountFolder(Session->SessionId, rwMountpoint));
465
+ }
466
+
467
+ // Validate ro mounts.
468
+ {
469
+ char roMountpoint[WSLC_MOUNTPOINT_LENGTH] = {};
470
+ THROW_IF_FAILED(g_api->WSLCMountFolder(Session->SessionId, L"C:\\", TRUE, L"plugin-ro-test", roMountpoint));
471
+
472
+ g_logfile << "WSLC RO folder mounted at: " << roMountpoint << std::endl;
473
+
474
+ // Attempt to write from Linux — should fail on a read-only mount.
475
+ auto writeCmd = std::format("echo fail > {}/should-not-exist.txt", roMountpoint);
476
+ runCommand(writeCmd.c_str());
477
+
478
+ THROW_IF_FAILED(g_api->WSLCUnmountFolder(Session->SessionId, roMountpoint));
479
+ }
480
+
481
+ // Validate that trying to mount a folder that doesn't exist fails with the expected error code.
482
+ {
483
+ char mountpoint[WSLC_MOUNTPOINT_LENGTH] = {};
484
+ g_logfile << "WSLCMountFolder(nonexistent): "
485
+ << g_api->WSLCMountFolder(Session->SessionId, L"C:\\nonexistent", TRUE, L"plugin-ro-test", mountpoint) << std::endl;
486
+ }
487
+
488
+ // Validate that trying to escape the /mnt folder fails.
489
+ {
490
+ char mountpoint[WSLC_MOUNTPOINT_LENGTH] = {};
491
+ g_logfile << "WSLCMountFolder(../escape): " << g_api->WSLCMountFolder(Session->SessionId, L"C:\\", TRUE, L"../escape", mountpoint)
492
+ << std::endl;
493
+ }
494
+
495
+ // Validate that empty names are rejected.
496
+ {
497
+ char mountpoint[WSLC_MOUNTPOINT_LENGTH] = {};
498
+ g_logfile << "WSLCMountFolder(): " << g_api->WSLCMountFolder(Session->SessionId, L"C:\\", TRUE, L"", mountpoint) << std::endl;
499
+ }
500
+
501
+ g_logfile << "Test completed" << std::endl;
502
+ }
503
+
504
+ return S_OK;
505
+}
506
+CATCH_RETURN();
507
+
508
+HRESULT OnWslcSessionStopping(const WSLCSessionInformation* Session)
509
+{
510
+ g_logfile << "WSLC Session stopping, name=" << wsl::shared::string::WideToMultiByte(Session->DisplayName)
511
+ << ", id=" << Session->SessionId << std::endl;
512
+
513
+ return S_OK;
514
+}
515
+
516
+HRESULT OnWslcContainerStarted(const WSLCSessionInformation* Session, LPCSTR InspectJson)
517
+try
518
+{
519
+ auto container = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectContainer>(InspectJson);
520
+
521
+ g_logfile << "WSLC Container started, session=" << Session->SessionId << ", id=" << container.Id
522
+ << ", name=" << container.Name << ", image=" << container.Image << ", state=" << container.State.Status << std::endl;
523
+
524
+ if (g_testType == PluginTestType::WslcContainerRejected)
525
+ {
526
+ g_logfile << "OnWslcContainerStarted: ERROR_ACCESS_DENIED" << std::endl;
527
+ return HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED);
528
+ }
529
+
530
+ return S_OK;
531
+}
532
+CATCH_RETURN();
533
+
534
+HRESULT OnWslcContainerStopping(const WSLCSessionInformation* Session, LPCSTR ContainerId)
535
+{
536
+ g_logfile << "WSLC Container stopping, session=" << Session->SessionId << ", id=" << ContainerId << std::endl;
537
+ return S_OK;
538
+}
539
+
540
+HRESULT OnWslcImageCreated(const WSLCSessionInformation* Session, LPCSTR InspectJson)
541
+{
542
+ auto image = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectImage>(InspectJson);
543
+ auto name = (image.RepoTags.has_value() && !image.RepoTags->empty()) ? image.RepoTags->front() : "<none>";
544
+ g_logfile << "WSLC Image created, session=" << Session->SessionId << ", id=" << image.Id << ", name=" << name << std::endl;
545
+ return S_OK;
546
+}
547
+
548
+HRESULT OnWslcImageDeleted(const WSLCSessionInformation* Session, LPCSTR ImageId)
549
+{
550
+ g_logfile << "WSLC Image deleted, session=" << Session->SessionId << ", id=" << ImageId << std::endl;
551
+ return S_OK;
552
+}
553
+
554
EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPluginAPIV1* Api, WSLPluginHooksV1* Hooks)
555
{
556
try
@@ -349,7 +562,7 @@ EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPlugin
562
THROW_HR_IF(E_UNEXPECTED, !g_logfile);
563
564
g_testType = static_cast<PluginTestType>(ReadDword(key.get(), nullptr, c_testType, static_cast<DWORD>(PluginTestType::Invalid)));
352
- THROW_HR_IF(E_INVALIDARG, static_cast<DWORD>(g_testType) <= 0 || static_cast<DWORD>(g_testType) > static_cast<DWORD>(PluginTestType::GetUsername));
565
+ THROW_HR_IF(E_INVALIDARG, static_cast<DWORD>(g_testType) <= 0 || static_cast<DWORD>(g_testType) > static_cast<DWORD>(PluginTestType::WslcImagePull));
566
567
g_logfile << "Plugin loaded. TestMode=" << static_cast<DWORD>(g_testType) << std::endl;
568
g_api = Api;
@@ -359,6 +572,12 @@ EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPlugin
572
Hooks->OnDistributionStopping = &OnDistroStopping;
573
Hooks->OnDistributionRegistered = &OnDistributionRegistered;
574
Hooks->OnDistributionUnregistered = &OnDistributionUnregistered;
575
+ Hooks->OnSessionCreated = &OnWslcSessionCreated;
576
+ Hooks->OnSessionStopping = &OnWslcSessionStopping;
577
+ Hooks->ContainerStarted = &OnWslcContainerStarted;
578
+ Hooks->ContainerStopping = &OnWslcContainerStopping;
579
+ Hooks->ImageCreated = &OnWslcImageCreated;
580
+ Hooks->ImageDeleted = &OnWslcImageDeleted;
581
582
if (g_testType == PluginTestType::FailToLoad)
583
{
@@ -383,4 +602,4 @@ EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPlugin
602
return error;
603
}
604
return S_OK;
386
-}
\ No newline at end of file
605
+}
test/windows/wslc/e2e/WSLCE2EHelpers.cpp
+11
-1
@@ -440,7 +440,17 @@ wil::com_ptr<IWSLCSession> OpenDefaultElevatedSession()
440
441
std::pair<RunningWSLCContainer, std::string> StartLocalRegistry(IWSLCSession& session, const std::string& username, const std::string& password, USHORT port)
442
{
443
- EnsureImageIsLoaded({L"wslc-registry", L"latest", GetTestImagePath("wslc-registry:latest")});
443
+ // Check if the registry image is already loaded on this session.
444
+ wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
445
+ THROW_IF_FAILED(session.ListImages(nullptr, &images, images.size_address<ULONG>()));
446
+
447
+ bool found = std::ranges::any_of(
448
+ std::span{images.get(), images.size()}, [](const auto& e) { return std::strcmp(e.Image, "wslc-registry:latest") == 0; });
449
+
450
+ if (!found)
451
+ {
452
+ LoadTestImage(session, "wslc-registry:latest");
453
+ }
454
455
std::vector<std::string> env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)};
456