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