| 1 | /*++ |
| 2 | |
| 3 | Copyright (c) Microsoft. All rights reserved. |
| 4 | |
| 5 | Module Name: |
| 6 | |
| 7 | Plugin.cpp |
| 8 | |
| 9 | Abstract: |
| 10 | |
| 11 | This file contains a test plugin. |
| 12 | |
| 13 | --*/ |
| 14 | |
| 15 | #include "precomp.h" |
| 16 | #include <atomic> |
| 17 | #include <thread> |
| 18 | #include "WslPluginApi.h" |
| 19 | #include "wslc_schema.h" |
| 20 | |
| 21 | #include "PluginTests.h" |
| 22 | |
| 23 | using namespace wsl::windows::common::registry; |
| 24 | using namespace wsl::windows::common::relay; |
| 25 | using namespace wsl::shared::string; |
| 26 | using namespace std::chrono_literals; |
| 27 | |
| 28 | std::ofstream g_logfile; |
| 29 | std::optional<GUID> g_distroGuid; |
| 30 | |
| 31 | const WSLPluginAPIV1* g_api = nullptr; |
| 32 | PluginTestType g_testType = PluginTestType::Invalid; |
| 33 | |
| 34 | // Process deliberately left running across OnWslcVmStopping by the WslcVmStopCommitted test, to |
| 35 | // prove the announced teardown happens anyway. Never released: it dies with the VM. |
| 36 | // |
| 37 | // The exit event is fetched on the callback's own thread and cached here as a plain Win32 handle: |
| 38 | // the process itself is a COM proxy marshalled to that thread, so the stop-window thread below |
| 39 | // cannot call methods on it (RPC_E_WRONG_THREAD), but it can wait on the handle. |
| 40 | std::atomic<WSLCProcessHandle> g_leakedProcess = nullptr; |
| 41 | std::atomic<HANDLE> g_leakedProcessExitEvent = nullptr; |
| 42 | |
| 43 | // Set by the WslcVmStopCommitted test: a call issued from a thread the plugin owns while |
| 44 | // OnWslcVmStopping is running. Like the callback itself it is served by the VM that is stopping, and |
| 45 | // must not block on the teardown. It deliberately logs nothing of its own -- its results are written |
| 46 | // when it is joined -- so g_logfile keeps a single writer and the expected output stays ordered. |
| 47 | std::thread g_stopWindowCaller; |
| 48 | HRESULT g_stopWindowCallerResult = E_PENDING; |
| 49 | std::atomic<bool> g_leakedProcessDied = false; |
| 50 | |
| 51 | std::optional<uint32_t> g_previousInitPid; |
| 52 | |
| 53 | std::vector<char> ReadFromSocket(SOCKET socket) |
| 54 | { |
| 55 | // Simplified error handling for the sake of the demo. |
| 56 | int result = 0; |
| 57 | int offset = 0; |
| 58 | |
| 59 | std::vector<char> content(1024); |
| 60 | while ((result = recv(socket, content.data() + offset, 1024, 0)) > 0) |
| 61 | { |
| 62 | offset += result; |
| 63 | content.resize(offset + 1024); |
| 64 | } |
| 65 | |
| 66 | content.resize(offset); |
| 67 | return content; |
| 68 | } |
| 69 | |
| 70 | HRESULT OnVmStarted(const WSLSessionInformation* Session, const WSLVmCreationSettings* Settings) |
| 71 | { |
| 72 | g_logfile << "VM created (settings->CustomConfigurationFlags=" << Settings->CustomConfigurationFlags << ")" << std::endl; |
| 73 | |
| 74 | if (g_testType == PluginTestType::FailToStartVm) |
| 75 | { |
| 76 | g_logfile << "OnVmStarted: E_UNEXPECTED" << std::endl; |
| 77 | return E_UNEXPECTED; |
| 78 | } |
| 79 | else if (g_testType == PluginTestType::FailToStartVmWithPluginErrorMessage) |
| 80 | { |
| 81 | g_logfile << "OnVmStarted: E_UNEXPECTED" << std::endl; |
| 82 | g_api->PluginError(L"Plugin error message"); |
| 83 | return E_UNEXPECTED; |
| 84 | } |
| 85 | else if (WI_IsFlagSet(Settings->CustomConfigurationFlags, WSLUserConfigurationCustomKernel)) |
| 86 | { |
| 87 | g_logfile << "OnVmStarted: E_ACCESSDENIED" << std::endl; |
| 88 | return E_ACCESSDENIED; |
| 89 | } |
| 90 | else if (g_testType == PluginTestType::Success) |
| 91 | { |
| 92 | // Get the current module's directory |
| 93 | std::filesystem::path modulePath = wil::GetModuleFileNameW(wil::GetModuleInstanceHandle()).get(); |
| 94 | auto mountSource = modulePath.parent_path().wstring(); |
| 95 | |
| 96 | // Mount the folder with the linux binary in the vm |
| 97 | RETURN_IF_FAILED( |
| 98 | g_api->MountFolder(Session->SessionId, mountSource.c_str(), L"/test-plugin/deep/folder", true, L"test-plugin-mount")); |
| 99 | |
| 100 | g_logfile << "Folder mounted (" << wsl::shared::string::WideToMultiByte(mountSource) << " -> /test-plugin)" << std::endl; |
| 101 | |
| 102 | // Create a file with dummy content |
| 103 | std::ofstream file(mountSource + L"\\test-file.txt"); |
| 104 | if (!file || !(file << "OK")) |
| 105 | { |
| 106 | g_logfile << "Failed to open test-file.txt in: " << wsl::shared::string::WideToMultiByte(mountSource) << std::endl; |
| 107 | return E_ABORT; |
| 108 | } |
| 109 | |
| 110 | file.close(); |
| 111 | |
| 112 | // Launch the process |
| 113 | std::vector<const char*> arguments = {"/bin/cat", "/test-plugin/deep/folder/test-file.txt", nullptr}; |
| 114 | wil::unique_socket socket; |
| 115 | RETURN_IF_FAILED(g_api->ExecuteBinary(Session->SessionId, arguments[0], arguments.data(), &socket)); |
| 116 | g_logfile << "Process created" << std::endl; |
| 117 | |
| 118 | // Read the socket output |
| 119 | auto output = ReadFromSocket(socket.get()); |
| 120 | if (output != std::vector<char>{'O', 'K'}) |
| 121 | { |
| 122 | g_logfile << "Got unexpected output from bash" << std::endl; |
| 123 | return E_ABORT; |
| 124 | } |
| 125 | } |
| 126 | else if (g_testType == PluginTestType::ApiErrors) |
| 127 | { |
| 128 | auto result = g_api->MountFolder(Session->SessionId, L"C:\\DoesNotExit", L"/dummy", true, L"test-plugin-mount"); |
| 129 | if (result != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) |
| 130 | { |
| 131 | g_logfile << "Unexpected error for MountFolder(): " << result << std::endl; |
| 132 | return E_ABORT; |
| 133 | } |
| 134 | |
| 135 | wil::unique_socket socket; |
| 136 | std::vector<const char*> arguments = {"/bin/does-no-exist", nullptr}; |
| 137 | result = g_api->ExecuteBinary(Session->SessionId, arguments[0], arguments.data(), &socket); |
| 138 | if (result != E_FAIL) |
| 139 | { |
| 140 | g_logfile << "Unexpected error for ExecuteBinary(): " << result << std::endl; |
| 141 | return E_ABORT; |
| 142 | } |
| 143 | |
| 144 | result = g_api->ExecuteBinary(0xcafe, arguments[0], arguments.data(), &socket); |
| 145 | if (result != RPC_E_DISCONNECTED) |
| 146 | { |
| 147 | g_logfile << "Unexpected error for ExecuteBinary(): " << result << std::endl; |
| 148 | return E_ABORT; |
| 149 | } |
| 150 | |
| 151 | // Call PluginError asynchronously to verify that we handle this properly. |
| 152 | |
| 153 | std::thread thread{[Session]() { |
| 154 | const auto result = g_api->PluginError(L"Dummy"); |
| 155 | |
| 156 | if (result != E_ILLEGAL_METHOD_CALL) |
| 157 | { |
| 158 | g_logfile << "Unexpected error for async PluginError(): " << result << std::endl; |
| 159 | } |
| 160 | }}; |
| 161 | |
| 162 | thread.join(); |
| 163 | |
| 164 | g_logfile << "API error tests passed" << std::endl; |
| 165 | } |
| 166 | else if (g_testType == PluginTestType::ErrorMessageStartVm) |
| 167 | { |
| 168 | auto result = g_api->PluginError(L"StartVm plugin error message"); |
| 169 | if (FAILED(result)) |
| 170 | { |
| 171 | g_logfile << "Unexpected error from PluginError(): " << result << std::endl; |
| 172 | } |
| 173 | g_logfile << "OnVmStarted: E_FAIL" << std::endl; |
| 174 | return E_FAIL; |
| 175 | } |
| 176 | else if (g_testType == PluginTestType::GetUsername) |
| 177 | { |
| 178 | try |
| 179 | { |
| 180 | auto info = wil::get_token_information<TOKEN_USER>(Session->UserToken); |
| 181 | |
| 182 | DWORD size{}; |
| 183 | DWORD domainSize{}; |
| 184 | SID_NAME_USE use{}; |
| 185 | LookupAccountSid(nullptr, info->User.Sid, nullptr, &size, nullptr, &domainSize, &use); |
| 186 | |
| 187 | THROW_HR_IF(E_UNEXPECTED, size < 1); |
| 188 | std::wstring user(size - 1, '\0'); |
| 189 | std::wstring domain(domainSize - 1, '\0'); |
| 190 | |
| 191 | THROW_IF_WIN32_BOOL_FALSE(LookupAccountSid(nullptr, info->User.Sid, user.data(), &size, domain.data(), &domainSize, &use)); |
| 192 | |
| 193 | g_logfile << "Username: " << wsl::shared::string::WideToMultiByte(domain) << "\\" |
| 194 | << wsl::shared::string::WideToMultiByte(user) << std::endl; |
| 195 | } |
| 196 | catch (...) |
| 197 | { |
| 198 | g_logfile << "OnVmStarted: get_token_information failed: " << wil::ResultFromCaughtException() << std::endl; |
| 199 | return E_FAIL; |
| 200 | } |
| 201 | |
| 202 | return S_OK; |
| 203 | } |
| 204 | |
| 205 | return S_OK; |
| 206 | } |
| 207 | |
| 208 | HRESULT OnVmStopping(const WSLSessionInformation* Session) |
| 209 | { |
| 210 | g_logfile << "VM Stopping" << std::endl; |
| 211 | |
| 212 | if (g_testType == PluginTestType::FailToStopVm) |
| 213 | { |
| 214 | g_logfile << "OnVmStopping: E_UNEXPECTED" << std::endl; |
| 215 | return E_UNEXPECTED; |
| 216 | } |
| 217 | |
| 218 | return S_OK; |
| 219 | } |
| 220 | |
| 221 | HRESULT OnDistroStarted(const WSLSessionInformation* Session, const WSLDistributionInformation* Distribution) |
| 222 | { |
| 223 | g_logfile << "Distribution started, name=" << wsl::shared::string::WideToMultiByte(Distribution->Name) |
| 224 | << ", package=" << wsl::shared::string::WideToMultiByte(Distribution->PackageFamilyName) |
| 225 | << ", PidNs=" << Distribution->PidNamespace << ", InitPid=" << Distribution->InitPid |
| 226 | << ", Flavor=" << wsl::shared::string::WideToMultiByte(Distribution->Flavor) |
| 227 | << ", Version=" << wsl::shared::string::WideToMultiByte(Distribution->Version) << std::endl; |
| 228 | |
| 229 | if (g_testType == PluginTestType::FailToStartDistro) |
| 230 | { |
| 231 | g_logfile << "OnDistroStarted: E_UNEXPECTED" << std::endl; |
| 232 | return E_UNEXPECTED; |
| 233 | } |
| 234 | else if (g_testType == PluginTestType::SameDistroId) |
| 235 | { |
| 236 | if (g_distroGuid.has_value()) |
| 237 | { |
| 238 | if (IsEqualGUID(g_distroGuid.value(), Distribution->Id)) |
| 239 | { |
| 240 | g_logfile << "OnDistroStarted: received same GUID" << std::endl; |
| 241 | } |
| 242 | else |
| 243 | { |
| 244 | g_logfile << "OnDistroStarted: received different GUID" << std::endl; |
| 245 | } |
| 246 | } |
| 247 | else |
| 248 | { |
| 249 | g_distroGuid = Distribution->Id; |
| 250 | } |
| 251 | } |
| 252 | else if (g_testType == PluginTestType::ErrorMessageStartDistro) |
| 253 | { |
| 254 | g_logfile << "OnDistroStarted: E_FAIL" << std::endl; |
| 255 | g_api->PluginError(L"StartDistro plugin error message"); |
| 256 | return E_FAIL; |
| 257 | } |
| 258 | else if (g_testType == PluginTestType::InitPidIsDifferent) |
| 259 | { |
| 260 | if (g_previousInitPid.has_value()) |
| 261 | { |
| 262 | if (g_previousInitPid.value() != Distribution->InitPid) |
| 263 | { |
| 264 | g_logfile << "Init's pid is different (" << Distribution->InitPid << " ! = " << g_previousInitPid.value() << ")" << std::endl; |
| 265 | } |
| 266 | else |
| 267 | { |
| 268 | g_logfile << "Init's pid did not change (" << g_previousInitPid.value() << ")" << std::endl; |
| 269 | return E_FAIL; |
| 270 | } |
| 271 | } |
| 272 | else |
| 273 | { |
| 274 | g_previousInitPid = Distribution->InitPid; |
| 275 | } |
| 276 | } |
| 277 | else if (g_testType == PluginTestType::RunDistroCommand) |
| 278 | { |
| 279 | // Launch a process |
| 280 | std::vector<const char*> arguments = {"/bin/sh", "-c", "cat /etc/issue.net", nullptr}; |
| 281 | wil::unique_socket socket; |
| 282 | RETURN_IF_FAILED(g_api->ExecuteBinaryInDistribution(Session->SessionId, &Distribution->Id, arguments[0], arguments.data(), &socket)); |
| 283 | g_logfile << "Process created" << std::endl; |
| 284 | |
| 285 | // Validate that the process actually ran inside the distro. |
| 286 | auto output = ReadFromSocket(socket.get()); |
| 287 | const auto expected = "Debian GNU/Linux 13\n"; |
| 288 | if (std::string(output.begin(), output.end()) != expected) |
| 289 | { |
| 290 | g_logfile << "Got unexpected output from bash: " << std::string(output.begin(), output.end()) |
| 291 | << ", expected: " << expected << std::endl; |
| 292 | return E_ABORT; |
| 293 | } |
| 294 | |
| 295 | // Verify that failure to launch a process behaves properly. |
| 296 | arguments = {"/does-not-exist"}; |
| 297 | g_logfile << "Failed process launch returned: " |
| 298 | << g_api->ExecuteBinaryInDistribution(Session->SessionId, &Distribution->Id, arguments[0], arguments.data(), &socket) |
| 299 | << std::endl; |
| 300 | |
| 301 | const GUID guid{}; |
| 302 | g_logfile << "Invalid distro launch returned: " |
| 303 | << g_api->ExecuteBinaryInDistribution(Session->SessionId, &guid, arguments[0], arguments.data(), &socket) << std::endl; |
| 304 | } |
| 305 | |
| 306 | return S_OK; |
| 307 | } |
| 308 | |
| 309 | HRESULT OnDistroStopping(const WSLSessionInformation* Session, const WSLDistributionInformation* Distribution) |
| 310 | { |
| 311 | g_logfile << "Distribution Stopping, name=" << wsl::shared::string::WideToMultiByte(Distribution->Name) |
| 312 | << ", package=" << wsl::shared::string::WideToMultiByte(Distribution->PackageFamilyName) |
| 313 | << ", PidNs=" << Distribution->PidNamespace << ", Flavor=" << wsl::shared::string::WideToMultiByte(Distribution->Flavor) |
| 314 | << ", Version=" << wsl::shared::string::WideToMultiByte(Distribution->Version) << std::endl; |
| 315 | |
| 316 | if (g_testType == PluginTestType::FailToStopDistro) |
| 317 | { |
| 318 | g_logfile << "OnDistroStopping: E_UNEXPECTED" << std::endl; |
| 319 | return E_UNEXPECTED; |
| 320 | } |
| 321 | else if (g_testType == PluginTestType::SameDistroId && g_distroGuid.has_value()) |
| 322 | { |
| 323 | if (!IsEqualGUID(g_distroGuid.value(), Distribution->Id)) |
| 324 | { |
| 325 | g_logfile << "OnDistroStarted: received different GUID" << std::endl; |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | return S_OK; |
| 330 | } |
| 331 | |
| 332 | HRESULT OnDistributionRegistered(const WSLSessionInformation* Session, const WslOfflineDistributionInformation* Distribution) |
| 333 | { |
| 334 | g_logfile << "Distribution registered, name=" << wsl::shared::string::WideToMultiByte(Distribution->Name) |
| 335 | << ", package=" << wsl::shared::string::WideToMultiByte(Distribution->PackageFamilyName) |
| 336 | << ", Flavor=" << wsl::shared::string::WideToMultiByte(Distribution->Flavor) |
| 337 | << ", Version=" << wsl::shared::string::WideToMultiByte(Distribution->Version) << std::endl; |
| 338 | |
| 339 | if (g_testType == PluginTestType::FailToRegisterUnregisterDistro) |
| 340 | { |
| 341 | g_logfile << "OnDistributionRegistered: E_UNEXPECTED" << std::endl; |
| 342 | return E_UNEXPECTED; |
| 343 | } |
| 344 | |
| 345 | return S_OK; |
| 346 | } |
| 347 | |
| 348 | HRESULT OnDistributionUnregistered(const WSLSessionInformation* Session, const WslOfflineDistributionInformation* Distribution) |
| 349 | { |
| 350 | g_logfile << "Distribution unregistered, name=" << wsl::shared::string::WideToMultiByte(Distribution->Name) |
| 351 | << ", package=" << wsl::shared::string::WideToMultiByte(Distribution->PackageFamilyName) |
| 352 | << ", Flavor=" << wsl::shared::string::WideToMultiByte(Distribution->Flavor) |
| 353 | << ", Version=" << wsl::shared::string::WideToMultiByte(Distribution->Version) << std::endl; |
| 354 | |
| 355 | if (g_testType == PluginTestType::FailToRegisterUnregisterDistro) |
| 356 | { |
| 357 | g_logfile << "OnDistributionUnregistered: E_UNEXPECTED" << std::endl; |
| 358 | return E_UNEXPECTED; |
| 359 | } |
| 360 | |
| 361 | return S_OK; |
| 362 | } |
| 363 | |
| 364 | HRESULT OnWslcSessionCreated(const WSLCSessionInformation* Session) |
| 365 | try |
| 366 | { |
| 367 | g_logfile << "WSLC Session created, name=" << wsl::shared::string::WideToMultiByte(Session->DisplayName) << ", id=" << Session->SessionId |
| 368 | << ", pid=" << Session->ApplicationPid << ", token=" << (Session->UserToken != nullptr ? "set" : "null") |
| 369 | << ", sid=" << (Session->UserSid != nullptr ? "set" : "null") << std::endl; |
| 370 | |
| 371 | if (g_testType == PluginTestType::WslcVmNeverStarted) |
| 372 | { |
| 373 | // A plugin call is never a reason to create a VM. This one has to be rejected rather than |
| 374 | // bringing one up, which the absence of any VM notification in the expected output confirms. |
| 375 | std::vector<const char*> args = {"/bin/true", nullptr}; |
| 376 | WSLCProcessHandle process = nullptr; |
| 377 | const auto hr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr); |
| 378 | if (SUCCEEDED(hr)) |
| 379 | { |
| 380 | g_api->WSLCReleaseProcess(process); |
| 381 | } |
| 382 | |
| 383 | g_logfile << "WSLC no-vm caller: " << (hr == WSLC_E_VM_NOT_RUNNING ? "rejected" : "unexpected") << std::endl; |
| 384 | return S_OK; |
| 385 | } |
| 386 | |
| 387 | if (g_testType == PluginTestType::WslcSessionRejected) |
| 388 | { |
| 389 | g_logfile << "OnWslcSessionCreated: ERROR_ACCESS_DENIED" << std::endl; |
| 390 | return HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED); |
| 391 | } |
| 392 | |
| 393 | return S_OK; |
| 394 | } |
| 395 | CATCH_RETURN(); |
| 396 | |
| 397 | // These checks need a running VM, and a plugin call never creates one, so they run from the VM-started |
| 398 | // hook rather than from session creation. |
| 399 | void RunWslcSuccessChecks(const WSLCSessionInformation* Session) |
| 400 | { |
| 401 | { |
| 402 | // Helper: run a command in the root namespace and return (status, stdout, stderr). |
| 403 | auto runCommand = [&](const char* cmd, |
| 404 | const std::optional<std::string>& input = {}, |
| 405 | std::vector<const char*> env = {}) -> std::tuple<int, std::string, std::string> { |
| 406 | std::vector<const char*> arguments = {"/bin/sh", "-c", cmd, nullptr}; |
| 407 | WSLCProcessHandle process = nullptr; |
| 408 | THROW_IF_FAILED(g_api->WSLCCreateProcess( |
| 409 | Session->SessionId, arguments[0], arguments.data(), env.empty() ? nullptr : env.data(), &process, nullptr)); |
| 410 | auto releaseProcess = wil::scope_exit([&]() { g_api->WSLCReleaseProcess(process); }); |
| 411 | |
| 412 | wil::unique_handle stdinHandle; |
| 413 | wil::unique_handle stdoutHandle; |
| 414 | wil::unique_handle stderrHandle; |
| 415 | wil::unique_handle exitEvent; |
| 416 | THROW_IF_FAILED(g_api->WSLCProcessGetFd(process, WSLCProcessFdStdin, &stdinHandle)); |
| 417 | THROW_IF_FAILED(g_api->WSLCProcessGetFd(process, WSLCProcessFdStdout, &stdoutHandle)); |
| 418 | THROW_IF_FAILED(g_api->WSLCProcessGetFd(process, WSLCProcessFdStderr, &stderrHandle)); |
| 419 | THROW_IF_FAILED(g_api->WSLCProcessGetExitEvent(process, &exitEvent)); |
| 420 | |
| 421 | std::string out; |
| 422 | std::string err; |
| 423 | |
| 424 | MultiHandleWait io; |
| 425 | io.AddHandle(std::make_unique<ReadHandle>( |
| 426 | std::move(stdoutHandle), [&out](const auto& span) { out.append(span.begin(), span.end()); })); |
| 427 | |
| 428 | io.AddHandle(std::make_unique<ReadHandle>( |
| 429 | std::move(stderrHandle), [&err](const auto& span) { err.append(span.begin(), span.end()); })); |
| 430 | |
| 431 | io.AddHandle(std::make_unique<EventHandle>(std::move(exitEvent))); |
| 432 | |
| 433 | if (input.has_value()) |
| 434 | { |
| 435 | io.AddHandle(std::make_unique<WriteHandle>(std::move(stdinHandle), std::vector<char>(input->begin(), input->end()))); |
| 436 | } |
| 437 | else |
| 438 | { |
| 439 | stdinHandle.reset(); |
| 440 | } |
| 441 | |
| 442 | io.Run(60000ms); |
| 443 | |
| 444 | int status = 0; |
| 445 | THROW_IF_FAILED(g_api->WSLCProcessGetExitCode(process, &status)); |
| 446 | g_logfile << "Command: '" << cmd << "', status=" << status << ", stdout: " << out << ", stderr: " << err << std::endl; |
| 447 | |
| 448 | return {status, out, err}; |
| 449 | }; |
| 450 | |
| 451 | // Test process creation (output & exit code validated by the test code). |
| 452 | { |
| 453 | runCommand("echo -n stdout-ok && echo -n stderr-ok >&2"); |
| 454 | runCommand("cat", "stdin-ok"); |
| 455 | runCommand("exit 12"); |
| 456 | runCommand("echo -n $ENV", {}, {"ENV=env-ok", nullptr}); |
| 457 | } |
| 458 | |
| 459 | // Validate that trying to execute a non-existent file fails with the expected error code. |
| 460 | { |
| 461 | WSLCProcessHandle process = nullptr; |
| 462 | int errnoValue = 0; |
| 463 | std::vector<const char*> args = {"does-not-exist", nullptr}; |
| 464 | |
| 465 | auto hr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, &errnoValue); |
| 466 | g_logfile << "WSLCCreateProcess(does-not-exist): " << std::hex << hr << ", errno=" << std::dec << errnoValue << std::endl; |
| 467 | } |
| 468 | |
| 469 | // Validate various error paths |
| 470 | { |
| 471 | std::vector<const char*> args = {"/bin/sh", "-c", "sleep 9999", nullptr}; |
| 472 | WSLCProcessHandle process = nullptr; |
| 473 | THROW_IF_FAILED(g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr)); |
| 474 | auto releaseProcess = wil::scope_exit([&]() { g_api->WSLCReleaseProcess(process); }); |
| 475 | |
| 476 | // Validate that getting an fd that doesn't exist fails with the expected error code. |
| 477 | HANDLE dummy = nullptr; |
| 478 | g_logfile << "WSLCProcessGetFd(999): " << g_api->WSLCProcessGetFd(process, static_cast<WSLCProcessFd>(999), &dummy) << std::endl; |
| 479 | int exitCode = -1; |
| 480 | |
| 481 | g_logfile << "WSLCProcessGetExitCode(<running>): " << g_api->WSLCProcessGetExitCode(process, &exitCode) << std::endl; |
| 482 | } |
| 483 | |
| 484 | const auto testFolder = L"C:\\"; |
| 485 | constexpr auto testFileName = L"plugin-test.txt"; |
| 486 | constexpr auto rwMountpoint = "/mnt/wsl-plugin/plugin-rw-test"; |
| 487 | constexpr auto roMountpoint = "/mnt/wsl-plugin/plugin-ro-test"; |
| 488 | |
| 489 | // Validate rw mounts. |
| 490 | { |
| 491 | auto rwCleanup = wil::scope_exit_log( |
| 492 | WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove(std::wstring(testFolder) + testFileName); }); |
| 493 | |
| 494 | { |
| 495 | std::ofstream file(std::wstring(testFolder) + testFileName); |
| 496 | file << "Windows-content"; |
| 497 | } |
| 498 | |
| 499 | const auto deniedFilePath = std::wstring(testFolder) + L"plugin-denied.txt"; |
| 500 | { |
| 501 | wil::unique_hfile deniedFile{ |
| 502 | CreateFileW(deniedFilePath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr)}; |
| 503 | THROW_LAST_ERROR_IF(!deniedFile); |
| 504 | } |
| 505 | |
| 506 | auto deniedFileCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove(deniedFilePath); }); |
| 507 | |
| 508 | PACL originalAcl = nullptr; |
| 509 | wil::unique_hlocal originalDescriptor; |
| 510 | THROW_IF_WIN32_ERROR(GetNamedSecurityInfoW( |
| 511 | deniedFilePath.c_str(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &originalAcl, nullptr, &originalDescriptor)); |
| 512 | |
| 513 | auto restoreAcl = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { |
| 514 | THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW( |
| 515 | const_cast<LPWSTR>(deniedFilePath.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, originalAcl, nullptr)); |
| 516 | }); |
| 517 | |
| 518 | EXPLICIT_ACCESSW deniedAccess{}; |
| 519 | deniedAccess.grfAccessPermissions = FILE_READ_DATA; |
| 520 | deniedAccess.grfAccessMode = DENY_ACCESS; |
| 521 | deniedAccess.grfInheritance = NO_INHERITANCE; |
| 522 | deniedAccess.Trustee.TrusteeForm = TRUSTEE_IS_SID; |
| 523 | deniedAccess.Trustee.ptstrName = static_cast<LPWSTR>(Session->UserSid); |
| 524 | |
| 525 | wsl::windows::common::security::unique_acl deniedAcl; |
| 526 | THROW_IF_WIN32_ERROR(SetEntriesInAclW(1, &deniedAccess, originalAcl, &deniedAcl)); |
| 527 | THROW_IF_WIN32_ERROR(SetNamedSecurityInfoW( |
| 528 | const_cast<LPWSTR>(deniedFilePath.c_str()), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, deniedAcl.get(), nullptr)); |
| 529 | |
| 530 | { |
| 531 | wil::unique_handle impersonationToken; |
| 532 | THROW_LAST_ERROR_IF(!DuplicateTokenEx( |
| 533 | Session->UserToken, TOKEN_IMPERSONATE | TOKEN_QUERY, nullptr, SecurityImpersonation, TokenImpersonation, &impersonationToken)); |
| 534 | auto revert = wil::impersonate_token(impersonationToken.get()); |
| 535 | wil::unique_hfile deniedFile{ |
| 536 | CreateFileW(deniedFilePath.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; |
| 537 | const auto openError = GetLastError(); |
| 538 | THROW_HR_IF(E_UNEXPECTED, deniedFile || openError != ERROR_ACCESS_DENIED); |
| 539 | } |
| 540 | |
| 541 | // Mount read-write and verify the file can be read from Linux. |
| 542 | THROW_IF_FAILED(g_api->WSLCMountFolder(Session->SessionId, testFolder, rwMountpoint, false)); |
| 543 | |
| 544 | g_logfile << "WSLC RW folder mounted at: " << rwMountpoint << std::endl; |
| 545 | |
| 546 | auto readCmd = std::format("cat {}/{}", rwMountpoint, testFileName); |
| 547 | runCommand(readCmd.c_str()); |
| 548 | |
| 549 | auto deniedReadCmd = std::format("cat {}/plugin-denied.txt", rwMountpoint); |
| 550 | runCommand(deniedReadCmd.c_str()); |
| 551 | |
| 552 | THROW_IF_FAILED(g_api->WSLCUnmountFolder(Session->SessionId, rwMountpoint)); |
| 553 | } |
| 554 | |
| 555 | // Validate ro mounts. |
| 556 | { |
| 557 | THROW_IF_FAILED(g_api->WSLCMountFolder(Session->SessionId, L"C:\\", roMountpoint, TRUE)); |
| 558 | |
| 559 | g_logfile << "WSLC RO folder mounted at: " << roMountpoint << std::endl; |
| 560 | |
| 561 | // Attempt to write from Linux — should fail on a read-only mount. |
| 562 | auto writeCmd = std::format("echo fail > {}/should-not-exist.txt", roMountpoint); |
| 563 | runCommand(writeCmd.c_str()); |
| 564 | |
| 565 | THROW_IF_FAILED(g_api->WSLCUnmountFolder(Session->SessionId, roMountpoint)); |
| 566 | } |
| 567 | |
| 568 | // Validate that trying to mount a folder that doesn't exist fails with the expected error code. |
| 569 | g_logfile << "WSLCMountFolder(nonexistent): " << g_api->WSLCMountFolder(Session->SessionId, L"C:\\nonexistent", roMountpoint, TRUE) |
| 570 | << std::endl; |
| 571 | |
| 572 | // Validate that non-absolute mountpoints are rejected. |
| 573 | g_logfile << "WSLCMountFolder(relative): " << g_api->WSLCMountFolder(Session->SessionId, L"C:\\", "relative-mountpoint", TRUE) |
| 574 | << std::endl; |
| 575 | |
| 576 | g_logfile << "Test completed" << std::endl; |
| 577 | } |
| 578 | } |
| 579 | |
| 580 | HRESULT OnWslcSessionStopping(const WSLCSessionInformation* Session) |
| 581 | { |
| 582 | // Drain the stop-window thread first, then report what it observed. Logging from here rather than |
| 583 | // from that thread keeps the log single-writer and the expected output deterministic. Safe |
| 584 | // because this is the last event of the session, so the join cannot run inside a VM notification. |
| 585 | if (g_stopWindowCaller.joinable()) |
| 586 | { |
| 587 | g_stopWindowCaller.join(); |
| 588 | |
| 589 | g_logfile << "WSLC stop-window caller: " << (SUCCEEDED(g_stopWindowCallerResult) ? "ok" : "failed") << std::endl; |
| 590 | g_logfile << "WSLC leaked process died: " << (g_leakedProcessDied.load() ? "yes" : "no") << std::endl; |
| 591 | } |
| 592 | |
| 593 | // Close the duplicated exit event if the stop-window thread did not get far enough to claim it. |
| 594 | // The leaked process wrapper is deliberately not released: it holds a COM proxy marshalled to the |
| 595 | // OnWslcVmStopping callback's thread, and releasing it from this one risks the same |
| 596 | // RPC_E_WRONG_THREAD hazard that forced the exit event to be cached as a plain handle. It is one |
| 597 | // wrapper for the lifetime of a test process, so leaking it is the safer trade. |
| 598 | if (auto* exitEvent = g_leakedProcessExitEvent.exchange(nullptr); exitEvent != nullptr) |
| 599 | { |
| 600 | const wil::unique_handle owned{exitEvent}; |
| 601 | } |
| 602 | |
| 603 | g_logfile << "WSLC Session stopping, name=" << wsl::shared::string::WideToMultiByte(Session->DisplayName) |
| 604 | << ", id=" << Session->SessionId << std::endl; |
| 605 | |
| 606 | return S_OK; |
| 607 | } |
| 608 | |
| 609 | HRESULT OnWslcContainerStarted(const WSLCSessionInformation* Session, LPCSTR InspectJson) |
| 610 | try |
| 611 | { |
| 612 | auto container = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectContainer>(InspectJson); |
| 613 | |
| 614 | g_logfile << "WSLC Container started, session=" << Session->SessionId << ", id=" << container.Id << ", name=" << container.Name |
| 615 | << ", image=" << container.Config.Image << ", state=" << container.State.Status << std::endl; |
| 616 | |
| 617 | if (g_testType == PluginTestType::WslcContainerRejected) |
| 618 | { |
| 619 | g_logfile << "OnWslcContainerStarted: ERROR_ACCESS_DENIED" << std::endl; |
| 620 | return HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED); |
| 621 | } |
| 622 | |
| 623 | return S_OK; |
| 624 | } |
| 625 | CATCH_RETURN(); |
| 626 | |
| 627 | HRESULT OnWslcContainerStopping(const WSLCSessionInformation* Session, LPCSTR ContainerId) |
| 628 | { |
| 629 | g_logfile << "WSLC Container stopping, session=" << Session->SessionId << ", id=" << ContainerId << std::endl; |
| 630 | return S_OK; |
| 631 | } |
| 632 | |
| 633 | HRESULT OnWslcImageCreated(const WSLCSessionInformation* Session, LPCSTR InspectJson) |
| 634 | { |
| 635 | auto image = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectImage>(InspectJson); |
| 636 | auto name = (image.RepoTags.has_value() && !image.RepoTags->empty()) ? image.RepoTags->front() : "<none>"; |
| 637 | g_logfile << "WSLC Image created, session=" << Session->SessionId << ", id=" << image.Id << ", name=" << name << std::endl; |
| 638 | return S_OK; |
| 639 | } |
| 640 | |
| 641 | HRESULT OnWslcImageDeleted(const WSLCSessionInformation* Session, LPCSTR ImageId) |
| 642 | { |
| 643 | g_logfile << "WSLC Image deleted, session=" << Session->SessionId << ", id=" << ImageId << std::endl; |
| 644 | return S_OK; |
| 645 | } |
| 646 | |
| 647 | HRESULT OnWslcVmStarted(const WSLCSessionInformation* Session) |
| 648 | try |
| 649 | { |
| 650 | if (g_testType == PluginTestType::WslcSuccess) |
| 651 | { |
| 652 | // Run once: the checks are written against the first VM of the session. |
| 653 | static std::atomic<bool> done = false; |
| 654 | if (!done.exchange(true)) |
| 655 | { |
| 656 | RunWslcSuccessChecks(Session); |
| 657 | } |
| 658 | |
| 659 | return S_OK; |
| 660 | } |
| 661 | |
| 662 | if (g_testType == PluginTestType::WslcVmStopCommitted) |
| 663 | { |
| 664 | g_logfile << "WSLC VM started, session=" << Session->SessionId << std::endl; |
| 665 | return S_OK; |
| 666 | } |
| 667 | |
| 668 | // The VM-never-started test expects no VM hook to fire at all. Logging here is the diagnostic |
| 669 | // that makes a regression visible: any line from this hook fails the expected output. |
| 670 | if (g_testType == PluginTestType::WslcVmNeverStarted) |
| 671 | { |
| 672 | g_logfile << "WSLC VM started, session=" << Session->SessionId << std::endl; |
| 673 | return S_OK; |
| 674 | } |
| 675 | |
| 676 | // Only log/exercise for the dedicated VM-restart test so other WSLC plugin tests (which start |
| 677 | // and stop VMs incidentally) are not affected by extra log lines. |
| 678 | if (g_testType != PluginTestType::WslcVmRestart) |
| 679 | { |
| 680 | return S_OK; |
| 681 | } |
| 682 | |
| 683 | g_logfile << "WSLC VM started, session=" << Session->SessionId << std::endl; |
| 684 | |
| 685 | // Prove the VM is usable from within the started hook, and that calling back into the session |
| 686 | // (WSLCCreateProcess acquires a VM lease + the runtime lock) does not deadlock. |
| 687 | std::vector<const char*> args = {"/bin/true", nullptr}; |
| 688 | WSLCProcessHandle process = nullptr; |
| 689 | const auto hr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr); |
| 690 | g_logfile << "WSLC VM started reentrant WSLCCreateProcess: " << (SUCCEEDED(hr) ? "ok" : "failed") << std::endl; |
| 691 | if (SUCCEEDED(hr)) |
| 692 | { |
| 693 | g_api->WSLCReleaseProcess(process); |
| 694 | } |
| 695 | |
| 696 | // Also exercise a reentrant mount + unmount from the started hook; the session is alive here so |
| 697 | // both calls succeed, validating that mount management reentrant from OnVmStarted does not deadlock. |
| 698 | constexpr auto* mountpoint = "/test-plugin/vm-started-mount"; |
| 699 | const auto mountHr = g_api->WSLCMountFolder(Session->SessionId, L"C:\\", mountpoint, TRUE); |
| 700 | if (SUCCEEDED(mountHr)) |
| 701 | { |
| 702 | const auto unmountHr = g_api->WSLCUnmountFolder(Session->SessionId, mountpoint); |
| 703 | g_logfile << "WSLC VM started mount+unmount: " << (SUCCEEDED(unmountHr) ? "ok" : "failed") << std::endl; |
| 704 | } |
| 705 | else |
| 706 | { |
| 707 | g_logfile << "WSLC VM started mount+unmount: skipped" << std::endl; |
| 708 | } |
| 709 | |
| 710 | return S_OK; |
| 711 | } |
| 712 | CATCH_RETURN(); |
| 713 | |
| 714 | HRESULT OnWslcVmStopping(const WSLCSessionInformation* Session) |
| 715 | try |
| 716 | { |
| 717 | if (g_testType == PluginTestType::WslcVmNeverStarted) |
| 718 | { |
| 719 | g_logfile << "WSLC VM stopping, session=" << Session->SessionId << std::endl; |
| 720 | return S_OK; |
| 721 | } |
| 722 | |
| 723 | if (g_testType == PluginTestType::WslcVmStopCommitted) |
| 724 | { |
| 725 | // Only the idle teardown is interesting here. The session's final teardown races with the |
| 726 | // session-stopping notification, which is delivered independently, so logging it would make |
| 727 | // the expected output order-dependent on that race. |
| 728 | if (g_stopWindowCaller.joinable()) |
| 729 | { |
| 730 | return S_OK; |
| 731 | } |
| 732 | |
| 733 | g_logfile << "WSLC VM stopping, session=" << Session->SessionId << std::endl; |
| 734 | |
| 735 | // Deliberately leave a live process behind when this callback returns. The stop is committed |
| 736 | // before it is announced, so the VM goes away regardless and this process dies with it -- |
| 737 | // which is exactly what the callback was just told would happen. |
| 738 | // |
| 739 | // Created before the thread below is started so its exit event is published first: that thread |
| 740 | // claims the event and must not race ahead of it. |
| 741 | std::vector<const char*> args = {"/bin/sleep", "60", nullptr}; |
| 742 | WSLCProcessHandle leaked = nullptr; |
| 743 | const auto hr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &leaked, nullptr); |
| 744 | g_leakedProcess.store(leaked); |
| 745 | |
| 746 | // Cache the exit event while still on the thread the process proxy is marshalled to. |
| 747 | if (SUCCEEDED(hr)) |
| 748 | { |
| 749 | HANDLE exitEvent = nullptr; |
| 750 | if (SUCCEEDED(g_api->WSLCProcessGetExitEvent(leaked, &exitEvent))) |
| 751 | { |
| 752 | g_leakedProcessExitEvent.store(exitEvent); |
| 753 | } |
| 754 | } |
| 755 | |
| 756 | g_logfile << "WSLC VM stopping leaked process: " << (SUCCEEDED(hr) ? "ok" : "failed") << std::endl; |
| 757 | |
| 758 | // A call from a thread this plugin owns is served on the same terms as the callback itself: |
| 759 | // by the VM that is stopping, not by a future one and not by a new one. It must not block on |
| 760 | // the teardown. Results are logged when this thread is joined, so the output stays deterministic. |
| 761 | const auto sessionId = Session->SessionId; |
| 762 | g_stopWindowCaller = std::thread([sessionId]() { |
| 763 | std::vector<const char*> processArgs = {"/bin/true", nullptr}; |
| 764 | WSLCProcessHandle process = nullptr; |
| 765 | g_stopWindowCallerResult = g_api->WSLCCreateProcess(sessionId, processArgs[0], processArgs.data(), nullptr, &process, nullptr); |
| 766 | if (SUCCEEDED(g_stopWindowCallerResult)) |
| 767 | { |
| 768 | g_api->WSLCReleaseProcess(process); |
| 769 | } |
| 770 | |
| 771 | // The announced stop takes the VM away and the leaked process with it. Prove that |
| 772 | // directly instead of inferring it from the fact that a new VM started -- a process that |
| 773 | // outlived the stop is the exact symptom of an announced stop that did not happen. |
| 774 | if (auto* exitEvent = g_leakedProcessExitEvent.exchange(nullptr); exitEvent != nullptr) |
| 775 | { |
| 776 | // GetExitEvent is marshalled as an [out, system_handle(sh_event)] parameter, so this |
| 777 | // is a duplicate owned by this process. |
| 778 | const wil::unique_handle owned{exitEvent}; |
| 779 | g_leakedProcessDied = WaitForSingleObject(owned.get(), 30 * 1000) == WAIT_OBJECT_0; |
| 780 | } |
| 781 | }); |
| 782 | |
| 783 | // Give the thread time to issue its call inside the stop window. If it has not, the test still |
| 784 | // passes -- it just proves less. |
| 785 | std::this_thread::sleep_for(500ms); |
| 786 | |
| 787 | return S_OK; |
| 788 | } |
| 789 | |
| 790 | if (g_testType != PluginTestType::WslcVmRestart) |
| 791 | { |
| 792 | return S_OK; |
| 793 | } |
| 794 | |
| 795 | g_logfile << "WSLC VM stopping, session=" << Session->SessionId << std::endl; |
| 796 | |
| 797 | // Proves OnVmStopping doesn't deadlock a plugin that calls back in: on idle teardown these are |
| 798 | // served by the VM that is still stopping and succeed; on permanent teardown they fail cleanly. |
| 799 | std::vector<const char*> args = {"/bin/true", nullptr}; |
| 800 | WSLCProcessHandle process = nullptr; |
| 801 | const auto processHr = g_api->WSLCCreateProcess(Session->SessionId, args[0], args.data(), nullptr, &process, nullptr); |
| 802 | g_logfile << "WSLC VM stopping reentrant WSLCCreateProcess: " << (SUCCEEDED(processHr) ? "ok" : "failed") << std::endl; |
| 803 | if (SUCCEEDED(processHr)) |
| 804 | { |
| 805 | g_api->WSLCReleaseProcess(process); |
| 806 | } |
| 807 | |
| 808 | constexpr auto* mountpoint = "/test-plugin/vm-stopping-mount"; |
| 809 | const auto mountHr = g_api->WSLCMountFolder(Session->SessionId, L"C:\\", mountpoint, TRUE); |
| 810 | if (SUCCEEDED(mountHr)) |
| 811 | { |
| 812 | const auto unmountHr = g_api->WSLCUnmountFolder(Session->SessionId, mountpoint); |
| 813 | g_logfile << "WSLC VM stopping mount+unmount: " << (SUCCEEDED(unmountHr) ? "ok" : "failed") << std::endl; |
| 814 | } |
| 815 | else |
| 816 | { |
| 817 | g_logfile << "WSLC VM stopping mount+unmount: skipped" << std::endl; |
| 818 | } |
| 819 | |
| 820 | return S_OK; |
| 821 | } |
| 822 | CATCH_RETURN(); |
| 823 | |
| 824 | EXTERN_C __declspec(dllexport) HRESULT WSLPLUGINAPI_ENTRYPOINTV1(const WSLPluginAPIV1* Api, WSLPluginHooksV1* Hooks) |
| 825 | { |
| 826 | try |
| 827 | { |
| 828 | const auto key = OpenTestRegistryKey(KEY_READ); |
| 829 | |
| 830 | const std::wstring outputFile = ReadString(key.get(), nullptr, c_logFile); |
| 831 | g_logfile.open(outputFile); |
| 832 | THROW_HR_IF(E_UNEXPECTED, !g_logfile); |
| 833 | |
| 834 | g_testType = static_cast<PluginTestType>(ReadDword(key.get(), nullptr, c_testType, static_cast<DWORD>(PluginTestType::Invalid))); |
| 835 | THROW_HR_IF(E_INVALIDARG, static_cast<DWORD>(g_testType) <= 0 || static_cast<DWORD>(g_testType) > static_cast<DWORD>(PluginTestType::WslcVmNeverStarted)); |
| 836 | |
| 837 | g_logfile << "Plugin loaded. TestMode=" << static_cast<DWORD>(g_testType) << std::endl; |
| 838 | g_api = Api; |
| 839 | Hooks->OnVMStarted = &OnVmStarted; |
| 840 | Hooks->OnVMStopping = &OnVmStopping; |
| 841 | Hooks->OnDistributionStarted = &OnDistroStarted; |
| 842 | Hooks->OnDistributionStopping = &OnDistroStopping; |
| 843 | Hooks->OnDistributionRegistered = &OnDistributionRegistered; |
| 844 | Hooks->OnDistributionUnregistered = &OnDistributionUnregistered; |
| 845 | Hooks->OnSessionCreated = &OnWslcSessionCreated; |
| 846 | Hooks->OnSessionStopping = &OnWslcSessionStopping; |
| 847 | Hooks->ContainerStarted = &OnWslcContainerStarted; |
| 848 | Hooks->ContainerStopping = &OnWslcContainerStopping; |
| 849 | Hooks->ImageCreated = &OnWslcImageCreated; |
| 850 | Hooks->ImageDeleted = &OnWslcImageDeleted; |
| 851 | Hooks->WslcVmStarted = &OnWslcVmStarted; |
| 852 | Hooks->WslcVmStopping = &OnWslcVmStopping; |
| 853 | |
| 854 | if (g_testType == PluginTestType::FailToLoad) |
| 855 | { |
| 856 | g_logfile << "OnLoad: E_UNEXPECTED" << std::endl; |
| 857 | return E_UNEXPECTED; |
| 858 | } |
| 859 | else if (g_testType == PluginTestType::PluginRequiresUpdate) |
| 860 | { |
| 861 | g_logfile << "OnLoad: WSL_E_PLUGINREQUIRESUPDATE" << std::endl; |
| 862 | |
| 863 | WSL_PLUGIN_REQUIRE_VERSION(9999, 99, 99, Api); |
| 864 | } |
| 865 | } |
| 866 | catch (...) |
| 867 | { |
| 868 | const auto error = wil::ResultFromCaughtException(); |
| 869 | if (g_logfile) |
| 870 | { |
| 871 | g_logfile << "Failed to initialize plugin, " << error << std::endl; |
| 872 | } |
| 873 | |
| 874 | return error; |
| 875 | } |
| 876 | return S_OK; |
| 877 | } |