master
cpp 3,645 lines 130 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WSLCContainer.cpp
8
9 Abstract:
10
11 Contains the implementation of WSLCContainer.
12 N.B. This class is designed to allow multiple container operations to run in parallel.
13 Operations that don't change the state of the container must be const qualified, and acquire a shared lock on m_lock.
14 Operations that do change the container's state must acquire m_lock exclusively.
15 Operations that interact with processes inside the container or the init process must acquire m_processesLock.
16 m_lock must always be acquired before m_processesLock
17
18 --*/
19
20 #include "precomp.h"
21 #include "WSLCContainer.h"
22 #include "WSLCExecutionContext.h"
23 #include "WSLCProcess.h"
24 #include "WSLCProcessIO.h"
25 #include "WSLCVolumes.h"
26 #include "APICompat.h"
27 #include "MountSpecParsing.h"
28 #include <unordered_set>
29
30 namespace apicompat = wsl::windows::common::apicompat;
31
32 using wsl::windows::common::COMServiceExecutionContext;
33 using wsl::windows::common::docker_schema::ErrorResponse;
34 using wsl::windows::common::io::DockerIORelayHandle;
35 using wsl::windows::common::io::HandleWrapper;
36 using wsl::windows::common::io::HTTPChunkBasedReadHandle;
37 using wsl::windows::common::io::OverlappedIOHandle;
38 using wsl::windows::common::io::ReadHandle;
39 using wsl::windows::common::io::RelayHandle;
40 using wsl::windows::service::wslc::ContainerPortMapping;
41 using wsl::windows::service::wslc::DockerEventTracker;
42 using wsl::windows::service::wslc::DockerHTTPClient;
43 using wsl::windows::service::wslc::DockerHTTPException;
44 using wsl::windows::service::wslc::IORelay;
45 using wsl::windows::service::wslc::IWSLCVolume;
46 using wsl::windows::service::wslc::NetworkEntry;
47 using wsl::windows::service::wslc::RelayedProcessIO;
48 using wsl::windows::service::wslc::TypedHandle;
49 using wsl::windows::service::wslc::unique_com_disconnect;
50 using wsl::windows::service::wslc::VMPortMapping;
51 using wsl::windows::service::wslc::WSLCContainer;
52 using wsl::windows::service::wslc::WSLCContainerImpl;
53 using wsl::windows::service::wslc::WSLCContainerMetadata;
54 using wsl::windows::service::wslc::WSLCContainerMetadataLabel;
55 using wsl::windows::service::wslc::WSLCContainerMetadataV1;
56 using wsl::windows::service::wslc::WSLCExecutionContext;
57 using wsl::windows::service::wslc::WSLCSession;
58 using wsl::windows::service::wslc::WSLCVirtualMachine;
59 using wsl::windows::service::wslc::WSLCVolumeMount;
60 using wsl::windows::service::wslc::WSLCVolumes;
61
62 using namespace wsl::windows::common::io;
63 using namespace wsl::windows::common::docker_schema;
64 using namespace wsl::windows::common::wslutil;
65 using namespace std::chrono_literals;
66 using wsl::shared::Localization;
67
68 namespace wslc_schema = wsl::windows::common::wslc_schema;
69
70 using DockerInspectContainer = wsl::windows::common::docker_schema::InspectContainer;
71 using WslcInspectContainer = wsl::windows::common::wslc_schema::InspectContainer;
72
73 namespace {
74
75 void ValidateStopTimeout(LONG TimeoutSeconds, bool allowDefault)
76 {
77 THROW_HR_WITH_USER_ERROR_IF(
78 E_INVALIDARG,
79 Localization::MessageWslcInvalidStopTimeout(TimeoutSeconds),
80 TimeoutSeconds < 0 && TimeoutSeconds != WSLC_STOP_TIMEOUT_NONE && (!allowDefault || TimeoutSeconds != WSLC_STOP_TIMEOUT_DEFAULT));
81 }
82
83 std::vector<std::string> StringArrayToVector(const WSLCStringArray& array)
84 {
85 if (array.Count == 0)
86 {
87 return {};
88 }
89
90 THROW_HR_IF_NULL_MSG(E_INVALIDARG, array.Values, "StringArray.Values is null with Count=%lu", array.Count);
91
92 std::vector<std::string> result;
93 result.reserve(array.Count);
94 for (ULONG i = 0; i < array.Count; i += 1)
95 {
96 THROW_HR_IF_NULL_MSG(E_INVALIDARG, array.Values[i], "StringArray.Values[%lu] is null", i);
97 result.emplace_back(array.Values[i]);
98 }
99
100 return result;
101 }
102
103 // Parses a Docker ExposedPorts key (e.g. "8080/tcp", "5432/udp") into port number and protocol.
104 std::pair<uint16_t, int> ParseExposedPortKey(const std::string& key)
105 {
106 auto slashPos = key.find('/');
107 THROW_HR_IF_MSG(E_INVALIDARG, slashPos == std::string::npos, "Invalid exposed port format: %hs", key.c_str());
108
109 auto portStr = std::string_view(key.c_str(), slashPos);
110
111 uint16_t port{};
112 auto result = std::from_chars(portStr.data(), portStr.data() + portStr.size(), port);
113 if (result.ec != std::errc{} || result.ptr != portStr.data() + portStr.size() || port == 0)
114 {
115 THROW_HR_MSG(E_INVALIDARG, "Invalid port number in exposed port: %hs", key.c_str());
116 }
117
118 auto protoStr = key.substr(slashPos + 1);
119 int protocol{};
120 if (protoStr == "tcp")
121 {
122 protocol = IPPROTO_TCP;
123 }
124 else if (protoStr == "udp")
125 {
126 protocol = IPPROTO_UDP;
127 }
128 else
129 {
130 THROW_HR_MSG(E_INVALIDARG, "Unsupported protocol in exposed port: %hs", key.c_str());
131 }
132
133 return {static_cast<uint16_t>(port), protocol};
134 }
135
136 // Temporary solution to allocate an ephemeral port.
137 // TODO: Remove once the port relay can allocate ephemeral ports.
138 uint16_t AllocateEphemeralPort(int family, const char* address)
139 {
140 wil::unique_socket sock(::socket(family, SOCK_STREAM, IPPROTO_TCP));
141 THROW_LAST_ERROR_IF(!sock);
142
143 SOCKADDR_INET addr{};
144 addr.si_family = static_cast<ADDRESS_FAMILY>(family);
145
146 if (family == AF_INET)
147 {
148 THROW_HR_IF_MSG(E_INVALIDARG, inet_pton(AF_INET, address, &addr.Ipv4.sin_addr) != 1, "Failed to parse ip address: %hs", address);
149 }
150 else if (family == AF_INET6)
151 {
152 THROW_HR_IF_MSG(E_INVALIDARG, inet_pton(AF_INET6, address, &addr.Ipv6.sin6_addr) != 1, "Failed to parse ip address: %hs", address);
153 }
154 else
155 {
156 THROW_HR_MSG(E_UNEXPECTED, "Unexpected address family: %i", family);
157 }
158
159 THROW_LAST_ERROR_IF(bind(sock.get(), reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == SOCKET_ERROR);
160
161 int addrLen = sizeof(addr);
162 THROW_LAST_ERROR_IF(getsockname(sock.get(), reinterpret_cast<sockaddr*>(&addr), &addrLen) == SOCKET_ERROR);
163
164 uint16_t port = (family == AF_INET6) ? ntohs(addr.Ipv6.sin6_port) : ntohs(addr.Ipv4.sin_port);
165 THROW_HR_IF_MSG(E_UNEXPECTED, port == 0, "OS returned ephemeral port 0");
166
167 return port;
168 }
169
170 constexpr std::string_view c_containerNetworkPrefix = "container:";
171
172 bool NetworkModeAllocatesVmPorts(std::string_view mode) noexcept
173 {
174 return mode != "host" && mode != "none" && !mode.starts_with(c_containerNetworkPrefix);
175 }
176
177 bool NetworkSupportsAliases(std::string_view mode) noexcept
178 {
179 return mode != "bridge" && NetworkModeAllocatesVmPorts(mode);
180 }
181
182 // Reject `<prefix>:<value>` strings whose prefix isn't `container:`. Docker treats colon-prefixed
183 // modes (`service:`, `ns:`, ...) as special, but WSLC only supports `container:`. Surface the
184 // rejection here so both Create() and Open() recovery paths share the same gate.
185 void RejectUnsupportedNetworkModes(std::string_view mode)
186 {
187 if (mode.starts_with(c_containerNetworkPrefix))
188 {
189 return;
190 }
191
192 const auto colon = mode.find(':');
193 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcInvalidNetworkMode(std::string{mode}), colon != std::string_view::npos);
194 }
195
196 std::string ResolveNetworkMode(LPCSTR networkMode, bool hasRequestedPorts, const std::unordered_map<std::string, NetworkEntry>& sessionNetworks, DockerHTTPClient& dockerClient)
197 {
198 const std::string_view mode = (networkMode == nullptr || *networkMode == '\0') ? std::string_view{"bridge"} : networkMode;
199
200 // Reject `service:foo` and similar unsupported colon-prefixed modes before any further processing.
201 RejectUnsupportedNetworkModes(mode);
202
203 // N.B. Docker validates incompatible combinations (e.g. host/none/container: with additional networks)
204 // and returns clear error messages, so we don't duplicate that validation here.
205
206 if (mode == "host")
207 {
208 return "host";
209 }
210
211 if (mode == "none")
212 {
213 THROW_HR_IF_MSG(E_INVALIDARG, hasRequestedPorts, "Port mappings are not supported without networking");
214 return "none";
215 }
216
217 if (mode.starts_with(c_containerNetworkPrefix))
218 {
219 const std::string target{mode.substr(c_containerNetworkPrefix.size())};
220 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcContainerModeRequiresTarget(), target.empty());
221 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcContainerModeNoPorts(), hasRequestedPorts);
222
223 try
224 {
225 return std::format("container:{}", dockerClient.InspectContainer(target).Id);
226 }
227 catch (const DockerHTTPException& e)
228 {
229 THROW_HR_WITH_USER_ERROR_IF(
230 WSLC_E_CONTAINER_NOT_FOUND, Localization::MessageWslcContainerModeTargetNotFound(target), e.StatusCode() == 404);
231 throw;
232 }
233 }
234
235 // User-defined network: bridge is the built-in one and bypasses the session lookup.
236 if (mode != "bridge")
237 {
238 THROW_HR_WITH_USER_ERROR_IF(
239 WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(std::string{mode}), !sessionNetworks.contains(std::string{mode}));
240 }
241 return std::string{mode};
242 }
243
244 // Unknown Settings keys are rejected rather than silently dropped, so callers get a clear error.
245 EndpointConfig ResolveEndpointConfig(const KeyValuePair* settings, ULONG count, std::string_view networkName)
246 {
247 EndpointConfig config{};
248 if (count == 0)
249 {
250 return config;
251 }
252
253 THROW_HR_IF_MSG(E_INVALIDARG, settings == nullptr, "Settings is null with SettingsCount=%lu", count);
254
255 auto parsed = ParseKeyMultiValuePairs(settings, count);
256
257 static constexpr std::array knownKeys{"Aliases", "IPAddress", "Links", "LinkLocalIPs", "DriverOpts"};
258 for (const auto& [key, _] : parsed)
259 {
260 THROW_HR_WITH_USER_ERROR_IF(
261 E_INVALIDARG,
262 Localization::MessageWslcEndpointSettingUnknown(key, std::string{networkName}),
263 std::find(knownKeys.begin(), knownKeys.end(), key) == knownKeys.end());
264 }
265
266 auto isBlank = [](const std::string& value) {
267 return value.empty() || std::all_of(value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch); });
268 };
269
270 if (auto it = parsed.find("Aliases"); it != parsed.end())
271 {
272 for (const auto& alias : it->second)
273 {
274 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcAliasEmpty(), isBlank(alias));
275 }
276
277 config.Aliases = std::move(it->second);
278 }
279
280 if (auto it = parsed.find("IPAddress"); it != parsed.end())
281 {
282 THROW_HR_WITH_USER_ERROR_IF(
283 E_INVALIDARG, Localization::MessageWslcIpAddressSingleValue(std::string{networkName}), it->second.size() != 1);
284
285 const auto& address = it->second.front();
286 in_addr parsedAddress{};
287 ParseIpv4Address(address.c_str(), parsedAddress);
288
289 EndpointIPAMConfig ipam{};
290 ipam.IPv4Address = address;
291 config.IPAMConfig = std::move(ipam);
292 }
293
294 if (auto it = parsed.find("Links"); it != parsed.end())
295 {
296 for (const auto& link : it->second)
297 {
298 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcLinkEmpty(), isBlank(link));
299 }
300 config.Links = std::move(it->second);
301 }
302
303 if (auto it = parsed.find("LinkLocalIPs"); it != parsed.end())
304 {
305 for (const auto& address : it->second)
306 {
307 in_addr parsedAddress{};
308 ParseIpv4Address(address.c_str(), parsedAddress);
309 }
310 if (!config.IPAMConfig.has_value())
311 {
312 config.IPAMConfig = EndpointIPAMConfig{};
313 }
314 config.IPAMConfig->LinkLocalIPs = std::move(it->second);
315 }
316
317 if (auto it = parsed.find("DriverOpts"); it != parsed.end())
318 {
319 std::map<std::string, std::string> driverOpts;
320 for (const auto& entry : it->second)
321 {
322 const auto separator = entry.find('=');
323 THROW_HR_WITH_USER_ERROR_IF(
324 E_INVALIDARG, Localization::MessageWslcDriverOptInvalid(entry), separator == std::string::npos || separator == 0);
325
326 auto key = entry.substr(0, separator);
327 auto value = entry.substr(separator + 1);
328 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcDriverOptInvalid(entry), isBlank(key));
329 THROW_HR_WITH_USER_ERROR_IF(
330 E_INVALIDARG, Localization::MessageWslcDriverOptDuplicate(key), !driverOpts.try_emplace(key, std::move(value)).second);
331 }
332 config.DriverOpts = std::move(driverOpts);
333 }
334
335 return config;
336 }
337
338 std::map<std::string, EndpointConfig> ResolveEndpoints(
339 const WSLCNetworkConnection* connections, ULONG count, std::string_view resolvedMode, const std::unordered_map<std::string, NetworkEntry>& sessionNetworks)
340 {
341 std::map<std::string, EndpointConfig> resolved;
342 if (count == 0)
343 {
344 return resolved;
345 }
346
347 THROW_HR_IF_MSG(E_INVALIDARG, connections == nullptr, "Networks is null with NetworksCount=%lu", count);
348
349 for (ULONG i = 0; i < count; i++)
350 {
351 const char* raw = connections[i].NetworkName;
352 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcNetworkNameRequired(), !raw || !*raw);
353
354 std::string name{raw};
355 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcDuplicateNetwork(name), name == resolvedMode);
356
357 auto [it, inserted] = resolved.try_emplace(name);
358 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcDuplicateNetwork(name), !inserted);
359
360 auto config = ResolveEndpointConfig(connections[i].Settings, connections[i].SettingsCount, name);
361 THROW_HR_WITH_USER_ERROR_IF(
362 E_INVALIDARG, Localization::MessageWslcAliasRequiresUserDefinedNetwork(), config.Aliases.has_value() && !NetworkSupportsAliases(name));
363
364 if (name != "bridge")
365 {
366 THROW_HR_WITH_USER_ERROR_IF(
367 WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(name), !sessionNetworks.contains(name));
368 }
369
370 it->second = std::move(config);
371 }
372 return resolved;
373 }
374
375 // Builds the port-mapping list from caller-supplied requests.
376 std::vector<ContainerPortMapping> BuildPortMappings(std::vector<_WSLCPortMapping>& requestedPorts, std::string_view primary, WSLCVirtualMachine& vm)
377 {
378 std::vector<ContainerPortMapping> ports;
379 ports.reserve(requestedPorts.size());
380
381 const bool allocateVmPorts = NetworkModeAllocatesVmPorts(primary);
382 for (auto& e : requestedPorts)
383 {
384 // Pre-allocate a concrete host port whenever the wslrelay relay path will be used: that path
385 // maps the host port verbatim and has no ephemeral writeback (unlike the virtioNet path), so
386 // an unresolved WSLC_EPHEMERAL_PORT (0) would otherwise be mapped as port 0 and fail.
387 if (e.HostPort == WSLC_EPHEMERAL_PORT && vm.UseWslRelayPortForwarding())
388 {
389 e.HostPort = AllocateEphemeralPort(e.Family, e.BindingAddress);
390 }
391
392 auto& entry = ports.emplace_back(VMPortMapping::FromWSLCPortMapping(e), e.ContainerPort);
393 if (allocateVmPorts)
394 {
395 entry.VmMapping.AssignVmPort(vm.AllocatePort(e.Family, e.Protocol));
396 }
397 }
398 return ports;
399 }
400
401 void UnmountVolumes(std::vector<WSLCVolumeMount>& volumes, WSLCVirtualMachine& parentVM)
402 {
403 for (auto& volume : volumes)
404 {
405 if (volume.Mounted)
406 {
407 auto result = parentVM.UnmountWindowsFolder(volume.ParentVMPath.c_str());
408 if (SUCCEEDED(result))
409 {
410 volume.Mounted = false;
411 }
412 else
413 {
414 LOG_HR(result);
415 EMIT_USER_WARNING(wsl::shared::Localization::MessageWslcVolumeUnmountFailed(
416 volume.HostPath, wsl::windows::common::wslutil::GetErrorString(result)));
417 }
418 }
419 }
420 }
421
422 auto MountVolumes(std::vector<WSLCVolumeMount>& volumes, WSLCVirtualMachine& parentVM)
423 {
424 auto errorCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&volumes, &parentVM]() { UnmountVolumes(volumes, parentVM); });
425
426 for (auto& volume : volumes)
427 {
428 std::error_code error;
429 const auto sourceExists = std::filesystem::exists(volume.HostPath, error);
430 if (error)
431 {
432 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcBindSourcePathError(volume.HostPath, error.message()));
433 }
434
435 if (!sourceExists)
436 {
437 if (!volume.CreateSourceIfMissing)
438 {
439 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcBindSourcePathNotFound(volume.HostPath));
440 }
441
442 auto result = wil::CreateDirectoryDeepNoThrow(volume.HostPath.c_str());
443 if (FAILED(result))
444 {
445 THROW_HR_WITH_USER_ERROR(
446 result, Localization::MessageWslcFailedToMountVolume(volume.HostPath, wsl::windows::common::wslutil::GetErrorString(result)));
447 }
448 }
449
450 auto result = parentVM.MountWindowsFolder(volume.HostPath.c_str(), volume.ParentVMPath.c_str(), volume.ReadOnly);
451 THROW_IF_FAILED_MSG(result, "Failed to mount %ls -> %hs", volume.HostPath.c_str(), volume.ParentVMPath.c_str());
452 volume.Mounted = true;
453 }
454
455 return std::move(errorCleanup);
456 }
457
458 WSLCContainerState DockerStateToWSLCState(ContainerState state)
459 {
460 // TODO: Handle other states like Paused, Restarting, etc.
461 switch (state)
462 {
463 case ContainerState::Created:
464 return WSLCContainerState::WslcContainerStateCreated;
465 case ContainerState::Running:
466 return WSLCContainerState::WslcContainerStateRunning;
467 case ContainerState::Exited:
468 case ContainerState::Dead:
469 return WSLCContainerState::WslcContainerStateExited;
470 case ContainerState::Removing:
471 return WSLCContainerState::WslcContainerStateDeleted;
472 default:
473 return WSLCContainerState::WslcContainerStateInvalid;
474 }
475 }
476
477 std::string WSLCStateToEventAction(WSLCContainerState state)
478 {
479 switch (state)
480 {
481 case WslcContainerStateRunning:
482 return "start";
483 case WslcContainerStateExited:
484 return "stop";
485 case WslcContainerStateDeleted:
486 return "destroy";
487 default:
488 WI_ASSERT(false);
489 return "unknown";
490 }
491 }
492 std::string CleanContainerName(const std::string& name)
493 {
494 // Docker container names have a leading '/', strip it.
495 if (!name.empty() && name[0] == '/')
496 {
497 return name.substr(1);
498 }
499
500 return name;
501 }
502
503 std::string ExtractContainerName(const std::vector<std::string>& names, const std::string& id)
504 {
505 if (names.empty())
506 {
507 return id;
508 }
509
510 return CleanContainerName(names[0]);
511 }
512
513 std::string FormatPortEndpoint(const ContainerPortMapping& portMapping)
514 {
515 auto addr = portMapping.VmMapping.BindingAddressString();
516 return std::format(
517 "{}:{}/{}",
518 portMapping.VmMapping.IsIPv6() ? std::format("[{}]", addr) : addr,
519 portMapping.VmMapping.HostPort(),
520 portMapping.ProtocolString());
521 }
522
523 WSLCContainerMetadataV1 ParseContainerMetadata(const std::string& json)
524 {
525 auto wrapper = wsl::shared::FromJson<WSLCContainerMetadata>(json.c_str());
526 THROW_HR_IF(E_UNEXPECTED, !wrapper.V1.has_value());
527
528 return wrapper.V1.value();
529 }
530
531 std::string SerializeContainerMetadata(const WSLCContainerMetadataV1& metadata)
532 {
533 WSLCContainerMetadata wrapper;
534 wrapper.V1 = metadata;
535
536 return wsl::shared::ToJson(wrapper);
537 }
538
539 std::map<std::string, std::string> StripInternalLabels(std::map<std::string, std::string> labels)
540 {
541 labels.erase(WSLCContainerMetadataLabel);
542 return labels;
543 }
544
545 std::map<std::string, std::string> StripInternalLabels(std::optional<std::map<std::string, std::string>>&& labels)
546 {
547 return StripInternalLabels(std::move(labels).value_or(std::map<std::string, std::string>{}));
548 }
549
550 // Validate every mount representation as one collection before preparing VM shares or calling Docker.
551 // Docker handles duplicate destinations differently across Binds, Mounts, and Tmpfs and can create named volumes while processing the request.
552 // This service-boundary check gives every caller consistent duplicate semantics and keeps invalid requests side-effect free.
553 std::vector<wsl::windows::common::mount::Spec> ConvertAndValidateMounts(const WSLCContainerOptions& containerOptions)
554 {
555 namespace mount = wsl::windows::common::mount;
556
557 THROW_HR_IF(E_INVALIDARG, containerOptions.MountsCount > 0 && containerOptions.Mounts == nullptr);
558
559 std::vector<mount::Spec> mounts;
560 mounts.reserve(containerOptions.MountsCount);
561 for (ULONG i = 0; i < containerOptions.MountsCount; ++i)
562 {
563 const auto& value = containerOptions.Mounts[i];
564 THROW_HR_IF_NULL_MSG(E_INVALIDARG, value.Target, "Mount at index %lu has null Target", i);
565 THROW_HR_IF_MSG(
566 E_INVALIDARG,
567 WI_IsAnyFlagSet(value.Flags, ~WSLCMountSpecFlagsValid),
568 "Mount at index %lu has invalid flags: 0x%x",
569 i,
570 value.Flags);
571
572 switch (value.Type)
573 {
574 case WSLCMountTypeBind:
575 case WSLCMountTypeVolume:
576 case WSLCMountTypeTmpfs:
577 break;
578
579 default:
580 THROW_HR_MSG(E_INVALIDARG, "Mount at index %lu has invalid type: %d", i, value.Type);
581 }
582
583 const auto type = value.Type;
584 THROW_HR_IF_MSG(
585 E_INVALIDARG,
586 type != WSLCMountTypeBind && WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsCreateSourceIfMissing),
587 "Mount at index %lu specifies create-source-if-missing for a non-bind mount",
588 i);
589 THROW_HR_IF_MSG(
590 E_INVALIDARG,
591 type != WSLCMountTypeTmpfs && value.TmpfsOptions != nullptr,
592 "Mount at index %lu specifies tmpfs options for a non-tmpfs mount",
593 i);
594 THROW_HR_IF_MSG(
595 E_INVALIDARG,
596 value.TmpfsOptions != nullptr && WI_IsAnyFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsSize | WSLCMountSpecFlagsTmpfsMode),
597 "Mount at index %lu combines legacy and structured tmpfs options",
598 i);
599
600 mounts.push_back({
601 .MountType = type,
602 .Source = value.Source != nullptr ? value.Source : L"",
603 .Target = value.Target,
604 .ReadOnly = static_cast<bool>(value.ReadOnly),
605 .BindSource = WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsCreateSourceIfMissing) ? mount::BindSourcePolicy::CreateIfMissing
606 : mount::BindSourcePolicy::RequireExisting,
607 .TmpfsSizeBytes = WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsSize) ? std::optional<int64_t>{value.TmpfsSizeBytes} : std::nullopt,
608 .TmpfsMode = WI_IsFlagSet(value.Flags, WSLCMountSpecFlagsTmpfsMode) ? std::optional<uint32_t>{value.TmpfsMode} : std::nullopt,
609 .TmpfsOptions = value.TmpfsOptions != nullptr ? std::optional<std::string>{value.TmpfsOptions} : std::nullopt,
610 });
611 }
612
613 try
614 {
615 mount::ValidateMountCollection(mounts);
616 for (const auto& mount : mounts)
617 {
618 if (mount.MountType == WSLCMountTypeBind)
619 {
620 if (mount.BindSource == mount::BindSourcePolicy::CreateIfMissing)
621 {
622 continue;
623 }
624
625 std::error_code error;
626 const auto sourceExists = std::filesystem::exists(mount.Source, error);
627 if (error)
628 {
629 throw mount::MountValidationException(Localization::MessageWslcBindSourcePathError(mount.Source, error.message()));
630 }
631
632 if (!sourceExists)
633 {
634 throw mount::MountValidationException(Localization::MessageWslcBindSourcePathNotFound(mount.Source));
635 }
636 }
637 }
638 }
639 catch (const mount::MountException& ex)
640 {
641 if (ex.Error() == mount::ValidationError::DuplicateDestination)
642 {
643 THROW_HR_WITH_USER_ERROR(
644 E_INVALIDARG, Localization::WSLCCLI_DuplicateMountDestinationError(wsl::shared::string::MultiByteToWide(ex.Destination())));
645 }
646
647 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, ex.Reason());
648 }
649
650 std::unordered_set<std::string> destinations;
651 const auto addDestination = [&](const char* destination) {
652 THROW_HR_IF_NULL(E_INVALIDARG, destination);
653
654 const auto normalizedDestination = mount::NormalizeDestination(destination);
655 THROW_HR_WITH_USER_ERROR_IF(
656 E_INVALIDARG,
657 Localization::WSLCCLI_DuplicateMountDestinationError(wsl::shared::string::MultiByteToWide(normalizedDestination)),
658 !destinations.emplace(normalizedDestination).second);
659 };
660
661 for (const auto& mount : mounts)
662 {
663 addDestination(mount.Target.c_str());
664 }
665
666 THROW_HR_IF(E_INVALIDARG, containerOptions.VolumesCount > 0 && containerOptions.Volumes == nullptr);
667 for (ULONG i = 0; i < containerOptions.VolumesCount; ++i)
668 {
669 THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Volumes[i].HostPath, "Volumes[%lu].HostPath is null", i);
670 addDestination(containerOptions.Volumes[i].ContainerPath);
671 }
672
673 THROW_HR_IF(E_INVALIDARG, containerOptions.NamedVolumesCount > 0 && containerOptions.NamedVolumes == nullptr);
674 for (ULONG i = 0; i < containerOptions.NamedVolumesCount; ++i)
675 {
676 THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.NamedVolumes[i].Name, "NamedVolume at index %lu has null Name", i);
677 addDestination(containerOptions.NamedVolumes[i].ContainerPath);
678 }
679
680 THROW_HR_IF(E_INVALIDARG, containerOptions.TmpfsCount > 0 && containerOptions.Tmpfs == nullptr);
681 for (ULONG i = 0; i < containerOptions.TmpfsCount; ++i)
682 {
683 addDestination(containerOptions.Tmpfs[i].Destination);
684 }
685
686 return mounts;
687 }
688
689 struct PreparedBindMount
690 {
691 WSLCVolumeMount Volume;
692 std::string DockerSource;
693 };
694
695 enum class MissingBindSource
696 {
697 Create,
698 Reject,
699 };
700
701 PreparedBindMount PrepareBindMount(const std::wstring& source, const std::string& target, bool readOnly, MissingBindSource missingSource)
702 {
703 std::filesystem::path hostPath = source;
704 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessagePathNotAbsolute(source), !hostPath.is_absolute());
705
706 std::wstring sourceFilename;
707 {
708 std::error_code ec;
709 hostPath = wsl::windows::common::filesystem::GetCanonicalPath(hostPath, ec);
710 if (ec)
711 {
712 THROW_HR_WITH_USER_ERROR(HRESULT_FROM_WIN32(ec.value()), Localization::MessageWslcFailedToMountVolume(source, ec.message()));
713 }
714
715 if (std::filesystem::is_regular_file(hostPath))
716 {
717 sourceFilename = hostPath.filename().wstring();
718 hostPath = hostPath.parent_path();
719 }
720 }
721
722 GUID volumeId;
723 THROW_IF_FAILED(CoCreateGuid(&volumeId));
724 auto parentVMPath = std::format("/mnt/{}", wsl::shared::string::GuidToString<char>(volumeId));
725 auto dockerSource = sourceFilename.empty() ? parentVMPath : std::format("{}/{}", parentVMPath, sourceFilename);
726 return {
727 .Volume =
728 {
729 .HostPath = std::move(hostPath),
730 .ParentVMPath = std::move(parentVMPath),
731 .ContainerPath = target,
732 .ReadOnly = readOnly,
733 .SourceFilename = std::move(sourceFilename),
734 .CreateSourceIfMissing = missingSource == MissingBindSource::Create,
735 },
736 .DockerSource = std::move(dockerSource),
737 };
738 }
739
740 void ProcessNamedVolumes(const WSLCContainerOptions& containerOptions, wsl::windows::common::docker_schema::CreateContainer& request)
741 {
742 THROW_HR_IF(E_INVALIDARG, containerOptions.NamedVolumesCount > 0 && containerOptions.NamedVolumes == nullptr);
743
744 for (ULONG i = 0; i < containerOptions.NamedVolumesCount; i++)
745 {
746 const auto& nv = containerOptions.NamedVolumes[i];
747 THROW_HR_IF_NULL_MSG(E_INVALIDARG, nv.Name, "NamedVolume at index %lu has null Name", i);
748 THROW_HR_IF_NULL_MSG(E_INVALIDARG, nv.ContainerPath, "NamedVolume at index %lu has null ContainerPath", i);
749
750 wsl::windows::common::docker_schema::Mount mount{};
751 mount.Source = std::string(nv.Name);
752 mount.Target = std::string(nv.ContainerPath);
753 mount.Type = "volume";
754 mount.ReadOnly = static_cast<bool>(nv.ReadOnly);
755
756 request.HostConfig.Mounts.emplace_back(mount);
757 }
758 }
759
760 } // namespace
761
762 ContainerPortMapping::ContainerPortMapping(VMPortMapping&& VmMapping, uint16_t ContainerPort) :
763 VmMapping(std::move(VmMapping)), ContainerPort(ContainerPort)
764 {
765 }
766
767 ContainerPortMapping::ContainerPortMapping(ContainerPortMapping&& Other) :
768 VmMapping(std::move(Other.VmMapping)), ContainerPort(Other.ContainerPort)
769 {
770 }
771
772 ContainerPortMapping& ContainerPortMapping::operator=(ContainerPortMapping&& Other)
773 {
774 if (this != &Other)
775 {
776 VmMapping = std::move(Other.VmMapping);
777 ContainerPort = Other.ContainerPort;
778 }
779 return *this;
780 }
781
782 const char* ContainerPortMapping::ProtocolString() const
783 {
784 if (VmMapping.Protocol == IPPROTO_TCP)
785 {
786 return "tcp";
787 }
788 else
789 {
790 WI_ASSERT(VmMapping.Protocol == IPPROTO_UDP);
791 return "udp";
792 }
793 }
794
795 unique_com_disconnect::unique_com_disconnect(Microsoft::WRL::ComPtr<WSLCContainer>&& wrapper) noexcept :
796 m_wrapper(std::move(wrapper))
797 {
798 }
799
800 unique_com_disconnect::~unique_com_disconnect() noexcept
801 {
802 if (m_wrapper)
803 {
804 m_wrapper->Disconnect();
805 }
806 }
807
808 wsl::windows::service::wslc::WSLCPortMapping ContainerPortMapping::Serialize() const
809 {
810 return wsl::windows::service::wslc::WSLCPortMapping{
811 .HostPort = VmMapping.HostPort(),
812 .VmPort = VmMapping.VmPort ? VmMapping.VmPort->Port() : ContainerPort,
813 .ContainerPort = ContainerPort,
814 .Family = VmMapping.BindAddress.si_family,
815 .Protocol = VmMapping.Protocol,
816 .BindingAddress = VmMapping.BindingAddressString()};
817 }
818
819 WSLCContainerImpl::WSLCContainerImpl(
820 WSLCSession& wslcSession,
821 WSLCSessionRuntime& runtime,
822 IWSLCPluginNotifier* pluginNotifier,
823 std::string&& Id,
824 std::string&& Name,
825 std::string&& Image,
826 std::string NetworkMode,
827 std::vector<WSLCVolumeMount>&& volumes,
828 std::vector<std::string>&& namedVolumes,
829 std::vector<ContainerPortMapping>&& ports,
830 std::map<std::string, std::string>&& labels,
831 std::function<void(const WSLCContainerImpl*)>&& onDeleted,
832 EventStore& eventStore,
833 WSLCContainerState InitialState,
834 std::int64_t CreatedAt,
835 WSLCProcessFlags InitProcessFlags,
836 WSLCContainerFlags ContainerFlags) :
837 m_wslcSession(wslcSession),
838 m_pluginNotifier(pluginNotifier),
839 m_runtime(runtime),
840 m_name(std::move(Name)),
841 m_image(std::move(Image)),
842 m_networkMode(std::move(NetworkMode)),
843 m_id(std::move(Id)),
844 m_mountedVolumes(std::move(volumes)),
845 m_namedVolumes(std::move(namedVolumes)),
846 m_mappedPorts(std::move(ports)),
847 m_labels(std::move(labels)),
848 m_comWrapper(wil::MakeOrThrow<WSLCContainer>(wslcSession, std::move(onDeleted))),
849 m_containerEvents(runtime.Events().RegisterContainerStateUpdates(
850 m_id, std::bind(&WSLCContainerImpl::OnEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))),
851 m_eventStore(eventStore),
852 m_state(InitialState),
853 m_createdAt(CreatedAt),
854 m_initProcessFlags(InitProcessFlags),
855 m_containerFlags(ContainerFlags)
856 {
857 // Acquire the activity hold up front for a container recovered in the running state, so it keeps
858 // the VM alive even before any client opens its wrapper. A merely-created (never-started)
859 // container does not pin the VM: its metadata survives teardown and the VM restarts on next use.
860 if (m_state == WslcContainerStateRunning)
861 {
862 m_activityHold = ActivityRef(m_wslcSession.Runtime().IdleStateShared());
863 }
864 }
865
866 WSLCContainerImpl::~WSLCContainerImpl()
867 {
868 // Destructors are implicitly noexcept, so any escaping exception terminates the session host.
869 // Everything below touches VM-scoped state that may already be gone.
870 try
871 {
872 WSL_LOG(
873 "~WSLCContainerImpl",
874 TraceLoggingValue(m_name.c_str(), "Name"),
875 TraceLoggingValue(m_id.c_str(), "Id"),
876 TraceLoggingValue((int)m_state, "State"));
877
878 // Snapshot and clear process references under the lock.
879 // Callbacks are then invoked without holding m_lock.
880 decltype(m_processes) processes;
881 decltype(m_initProcessControl) initProcessControl = nullptr;
882
883 {
884 auto lock = m_lock.lock_exclusive();
885 std::lock_guard processesLock{m_processesLock};
886 initProcessControl = std::exchange(m_initProcessControl, nullptr);
887 processes = std::exchange(m_processes, {});
888 }
889
890 if (initProcessControl)
891 {
892 initProcessControl->OnContainerReleased();
893 }
894
895 for (auto& process : processes)
896 {
897 if (auto control = process.lock())
898 {
899 control->OnContainerReleased();
900 }
901 }
902
903 m_containerEvents.Reset();
904
905 // Release resources under m_lock, but extract the COM wrapper so Disconnect()
906 // can be called without holding m_lock. Calling Disconnect() under m_lock can
907 // deadlock if an in-flight COM caller is waiting for m_lock.
908 unique_com_disconnect wrapper;
909 {
910 auto lock = m_lock.lock_exclusive();
911 wrapper = ReleaseResources();
912 }
913 }
914 CATCH_LOG()
915 }
916
917 void WSLCContainerImpl::Initialize()
918 {
919 // N.B. this must be done here because weak_from_this() is only valid after the constructor returns.
920 m_comWrapper->Initialize(weak_from_this());
921 }
922
923 void WSLCContainerImpl::SetExitCode(int ExitCode) noexcept
924 {
925 std::lock_guard processesLock{m_processesLock};
926 if (m_initProcessControl != nullptr)
927 {
928 m_initProcessControl->SetExitCode(ExitCode);
929 }
930 }
931
932 void WSLCContainerImpl::SignalInitProcessExit() noexcept
933 {
934 std::lock_guard processesLock{m_processesLock};
935 if (m_initProcessControl != nullptr)
936 {
937 m_initProcessControl->SignalExit();
938 }
939 }
940
941 const std::string& WSLCContainerImpl::Image() const noexcept
942 {
943 return m_image;
944 }
945
946 const std::string& WSLCContainerImpl::Name() const noexcept
947 {
948 return m_name;
949 }
950
951 std::vector<wsl::windows::service::wslc::WSLCPortMapping> WSLCContainerImpl::GetPorts() const
952 {
953 auto lock = m_lock.lock_shared();
954 if (m_state != WslcContainerStateRunning)
955 {
956 return {};
957 }
958
959 std::vector<wsl::windows::service::wslc::WSLCPortMapping> result;
960 result.reserve(m_mappedPorts.size());
961 for (const auto& port : m_mappedPorts)
962 {
963 result.push_back(port.Serialize());
964 }
965 return result;
966 }
967
968 void WSLCContainerImpl::GetStateChangedAt(LONGLONG* Result)
969 {
970 auto lock = m_lock.lock_shared();
971 *Result = m_stateChangedAt;
972 }
973
974 void WSLCContainerImpl::GetCreatedAt(LONGLONG* Result)
975 {
976 auto lock = m_lock.lock_shared();
977 *Result = m_createdAt;
978 }
979
980 void WSLCContainerImpl::CopyTo(IWSLCContainer** Container) const
981 {
982 auto lock = m_lock.lock_shared();
983
984 THROW_HR_IF_MSG(RPC_E_DISCONNECTED, m_comWrapper == nullptr, "Container '%hs' is being released", m_id.c_str());
985
986 THROW_IF_FAILED(m_comWrapper.CopyTo(Container));
987 }
988
989 void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* Stdout, WSLCHandle* Stderr) const
990 {
991 auto lock = m_lock.lock_shared();
992
993 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_RUNNING, Localization::MessageWslcContainerNotRunning(m_id.c_str()), m_state != WslcContainerStateRunning);
994
995 wil::shared_socket ioHandle;
996
997 try
998 {
999 ioHandle = wil::shared_socket{
1000 m_runtime.Docker().AttachContainer(m_id, DetachKeys == nullptr ? std::nullopt : std::optional<std::string>(DetachKeys))};
1001 }
1002 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to attach to container '%hs'", m_id.c_str());
1003
1004 // If this is a TTY process, the PTY handle can be returned directly.
1005 if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty))
1006 {
1007 *Stdin = common::wslutil::ToCOMOutputHandle(
1008 reinterpret_cast<HANDLE>(ioHandle.get()), GENERIC_READ | GENERIC_WRITE | SYNCHRONIZE, WSLCHandleTypeSocket);
1009
1010 return;
1011 }
1012
1013 // Otherwise the stream is multiplexed and needs to be relayed.
1014 // TODO: Consider skipping stdin if the stdin flag isn't set.
1015 auto [stdinRead, stdinWrite] = common::wslutil::OpenAnonymousPipe(LX_RELAY_BUFFER_SIZE, true, true);
1016 auto [stdoutRead, stdoutWrite] = common::wslutil::OpenAnonymousPipe(LX_RELAY_BUFFER_SIZE, true, true);
1017 auto [stderrRead, stderrWrite] = common::wslutil::OpenAnonymousPipe(LX_RELAY_BUFFER_SIZE, true, true);
1018
1019 std::vector<std::unique_ptr<OverlappedIOHandle>> handles;
1020
1021 // This is required for docker to know when stdin is closed.
1022 auto onInputComplete = [ioHandle]() { LOG_LAST_ERROR_IF(shutdown(ioHandle.get(), SD_SEND) == SOCKET_ERROR); };
1023
1024 handles.emplace_back(std::make_unique<RelayHandle<ReadHandle>>(
1025 HandleWrapper{std::move(stdinRead), std::move(onInputComplete)}, HandleWrapper{ioHandle}));
1026
1027 handles.emplace_back(std::make_unique<DockerIORelayHandle>(
1028 HandleWrapper{ioHandle}, std::move(stdoutWrite), std::move(stderrWrite), DockerIORelayHandle::Format::Raw));
1029
1030 m_runtime.Relay()->AddHandles(std::move(handles));
1031
1032 *Stdin = common::wslutil::ToCOMOutputHandle(reinterpret_cast<HANDLE>(stdinWrite.get()), GENERIC_WRITE | SYNCHRONIZE, WSLCHandleTypePipe);
1033
1034 *Stdout = common::wslutil::ToCOMOutputHandle(reinterpret_cast<HANDLE>(stdoutRead.get()), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe);
1035
1036 *Stderr = common::wslutil::ToCOMOutputHandle(reinterpret_cast<HANDLE>(stderrRead.get()), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe);
1037 }
1038
1039 void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions)
1040 {
1041 StartPhase(Flags, StartOptions, false);
1042 }
1043
1044 void WSLCContainerImpl::StartPhase(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, bool RestartPhase)
1045 {
1046 std::shared_ptr<StateTransition> transition;
1047 auto lifecycleLock = m_lifecycleLock.lock_shared();
1048 auto lock = m_lock.lock_exclusive();
1049
1050 WaitForConflictingTransitionToComplete(lock, lifecycleLock, std::nullopt, !RestartPhase);
1051
1052 // A Delete() that raced a restart may have already moved the container to the Deleted state.
1053 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_DELETED, Localization::MessageWslcContainerDeleted(m_id), m_state == WslcContainerStateDeleted);
1054
1055 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_IS_RUNNING, Localization::MessageWslcContainerIsRunning(m_id), m_state == WslcContainerStateRunning);
1056
1057 THROW_HR_IF_MSG(
1058 HRESULT_FROM_WIN32(ERROR_INVALID_STATE),
1059 m_state != WslcContainerStateCreated && m_state != WslcContainerStateExited,
1060 "Cannot start container '%hs', state %i",
1061 m_id.c_str(),
1062 m_state);
1063
1064 std::optional<std::string> detachKeys;
1065
1066 if (StartOptions != nullptr)
1067 {
1068 detachKeys = StartOptions->DetachKeys != nullptr ? std::optional<std::string>(StartOptions->DetachKeys) : std::nullopt;
1069
1070 THROW_HR_IF_MSG(
1071 E_INVALIDARG,
1072 WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty) && (StartOptions->TtyColumns == 0 || StartOptions->TtyRows == 0),
1073 "Invalid tty size: %lu:%lu",
1074 StartOptions->TtyRows,
1075 StartOptions->TtyColumns);
1076 }
1077
1078 // Attach to the container's init process so no IO is lost.
1079 std::unique_ptr<WSLCProcessIO> io;
1080
1081 try
1082 {
1083 if (WI_IsFlagSet(Flags, WSLCContainerStartFlagsAttach))
1084 {
1085 if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty))
1086 {
1087 io = std::make_unique<TTYProcessIO>(TypedHandle{m_runtime.Docker().AttachContainer(m_id, detachKeys), WSLCHandleTypeSocket});
1088 }
1089 else
1090 {
1091 io = CreateRelayedProcessIO(wil::shared_socket{m_runtime.Docker().AttachContainer(m_id, detachKeys)}, m_initProcessFlags);
1092 }
1093 }
1094 }
1095 catch (const DockerHTTPException& e)
1096 {
1097 // N.B. This can happen if 'DetachKeys' is invalid.
1098 THROW_DOCKER_USER_ERROR_MSG(e, "Failed to attach to container '%hs' during start", m_id.c_str());
1099 }
1100
1101 auto control = std::make_unique<DockerContainerProcessControl>(*this, m_runtime.Docker());
1102
1103 {
1104 std::lock_guard processesLock{m_processesLock};
1105 m_initProcessControl = control.get();
1106 m_initProcess = wil::MakeOrThrow<WSLCProcess>(std::move(control), std::move(io), m_initProcessFlags);
1107 }
1108
1109 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() mutable {
1110 std::lock_guard processesLock{m_processesLock};
1111 m_initProcess.Reset();
1112 m_initProcessControl = nullptr;
1113 });
1114
1115 // Refuse to start if any referenced named volume is in a failed state.
1116 std::vector<std::string> unavailableVolumes;
1117 for (const auto& volumeName : m_namedVolumes)
1118 {
1119 const auto [code, message] = m_runtime.Volumes().GetVolumeStatus(volumeName);
1120 if (FAILED(code))
1121 {
1122 EMIT_USER_WARNING(Localization::MessageWslcVolumeNotAvailableReason(volumeName, message));
1123 unavailableVolumes.push_back(volumeName);
1124 }
1125 }
1126
1127 THROW_HR_WITH_USER_ERROR_IF(
1128 WSLC_E_VOLUME_NOT_AVAILABLE,
1129 Localization::MessageWslcVolumeNotAvailable(wsl::shared::string::Join(unavailableVolumes, ',')),
1130 !unavailableVolumes.empty());
1131
1132 // A restart keeps its ports and mounts across both phases, so re-acquiring them here would collide
1133 // with the container's own reservations. Release them if the start does not land, since an exited
1134 // container must not keep holding them.
1135 auto resourceCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { ReleaseRuntimeResources(); });
1136
1137 if (!m_runtimeResourcesHeld)
1138 {
1139 MountVolumes(m_mountedVolumes, m_runtime.Vm()).release();
1140 MapPorts();
1141 m_runtimeResourcesHeld = true;
1142 }
1143
1144 try
1145 {
1146 m_runtime.Docker().StartContainer(m_id, detachKeys);
1147 }
1148 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to start container '%hs'", m_id.c_str());
1149
1150 if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty) && StartOptions != nullptr)
1151 {
1152 try
1153 {
1154 m_runtime.Docker().ResizeContainerTty(m_id, StartOptions->TtyRows, StartOptions->TtyColumns);
1155 }
1156 CATCH_LOG();
1157 }
1158
1159 auto inspectJson = InspectLockHeld();
1160 const auto pluginResult = m_pluginNotifier->OnContainerStarted(inspectJson.c_str());
1161 if (FAILED(pluginResult))
1162 {
1163 // Forward the COM error message, if available.
1164 auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo();
1165
1166 LOG_HR_MSG(pluginResult, "Plugin rejected start of container '%hs' (0x%x)", m_id.c_str(), pluginResult);
1167 try
1168 {
1169 m_runtime.Docker().StopContainer(m_id.c_str(), {}, {});
1170 }
1171 catch (...)
1172 {
1173 LOG_CAUGHT_EXCEPTION();
1174 EMIT_USER_WARNING(wsl::shared::Localization::MessageWslcContainerStopAfterPluginRejectionFailed(
1175 wsl::shared::string::MultiByteToWide(m_id)));
1176 }
1177
1178 if (comError.has_value() && comError->Message)
1179 {
1180 THROW_HR_WITH_USER_ERROR(pluginResult, comError->Message.get());
1181 }
1182 else
1183 {
1184 THROW_HR(pluginResult);
1185 }
1186 }
1187
1188 transition = StartTransition(TransitionKind::Start, ContainerEvent::Start);
1189
1190 resourceCleanup.release();
1191 cleanup.release();
1192
1193 lock.reset();
1194 lifecycleLock.reset();
1195 AttachToTransition(transition);
1196 }
1197
1198 void WSLCContainerImpl::WaitForConflictingTransitionToComplete(
1199 wil::rwlock_release_exclusive_scope_exit& lock, wil::rwlock_release_shared_scope_exit& lifecycleLock, std::optional<TransitionKind> kind, bool waitForRestart)
1200 {
1201 while (true)
1202 {
1203 // A restart spans two transitions, so waiting on the one in flight is not enough.
1204 if (waitForRestart && m_restart)
1205 {
1206 auto restart = m_restart;
1207 lock.reset();
1208 lifecycleLock.reset();
1209 WaitForCompletionEvent(restart->Completed.get());
1210 }
1211 else if (m_transition && (!kind.has_value() || m_transition->Kind != kind.value()))
1212 {
1213 auto transition = m_transition;
1214 lock.reset();
1215 lifecycleLock.reset();
1216 WaitForTransitionCompletion(transition);
1217 }
1218 else
1219 {
1220 return;
1221 }
1222
1223 lifecycleLock = m_lifecycleLock.lock_shared();
1224 lock = m_lock.lock_exclusive();
1225 }
1226 }
1227
1228 __requires_exclusive_lock_held(m_lock) std::shared_ptr<WSLCContainerImpl::StateTransition> WSLCContainerImpl::StartTransition(
1229 TransitionKind kind, ContainerEvent expectedEvent)
1230 {
1231 auto transition = std::make_shared<StateTransition>(kind, expectedEvent);
1232 WI_ASSERT(!m_transition);
1233 m_transition = transition;
1234 return transition;
1235 }
1236
1237 void WSLCContainerImpl::WaitForCompletionEvent(HANDLE Event) const
1238 {
1239 auto io = m_wslcSession.CreateIOContext();
1240 io.AddHandle(std::make_unique<EventHandle>(Event));
1241 io.Run({});
1242 }
1243
1244 void WSLCContainerImpl::WaitForTransitionCompletion(const std::shared_ptr<StateTransition>& transition) const
1245 {
1246 WaitForCompletionEvent(transition->Completed.get());
1247
1248 WI_ASSERT(transition->Completed.is_signaled());
1249 }
1250
1251 void WSLCContainerImpl::AttachToTransition(const std::shared_ptr<StateTransition>& transition) const
1252 {
1253 WaitForTransitionCompletion(transition);
1254
1255 unique_com_disconnect wrapper;
1256
1257 // Take ownership of the deferred COM disconnect after OnEvent leaves its critical section.
1258 {
1259 auto lock = m_lock.lock_exclusive();
1260 wrapper = std::move(transition->Wrapper);
1261 }
1262
1263 if (transition->Exception)
1264 {
1265 std::rethrow_exception(transition->Exception);
1266 }
1267 }
1268
1269 __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::CompleteTransition(const std::shared_ptr<StateTransition>& transition, std::exception_ptr exception) noexcept
1270 {
1271 WI_ASSERT(m_transition == transition);
1272 transition->Exception = std::move(exception);
1273 m_transition.reset();
1274 transition->Completed.SetEvent();
1275 }
1276
1277 void WSLCContainerImpl::RecordEvent(std::string&& Action, std::int64_t Time, std::optional<int> ExitCode) noexcept
1278 try
1279 {
1280 auto attributes = StripInternalLabels(m_labels);
1281 attributes["name"] = m_name;
1282 attributes["image"] = m_image;
1283
1284 if (ExitCode.has_value())
1285 {
1286 attributes["exitCode"] = std::to_string(ExitCode.value());
1287 }
1288
1289 m_eventStore.Record("container", std::move(Action), m_id, std::move(attributes), Time);
1290 }
1291 CATCH_LOG()
1292
1293 void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional<int> exitCode, std::int64_t eventTime) noexcept
1294 {
1295 // Either owner may disconnect the COM wrapper, so both must outlive m_lock.
1296 unique_com_disconnect comWrapper;
1297 std::shared_ptr<StateTransition> transition;
1298
1299 if (event == ContainerEvent::Kill)
1300 {
1301 RecordEvent("kill", eventTime);
1302 return;
1303 }
1304
1305 {
1306 auto lifecycleLock = m_lifecycleLock.lock_exclusive();
1307 auto lock = m_lock.lock_exclusive();
1308 transition = m_transition;
1309
1310 if (event == ContainerEvent::Start)
1311 {
1312 // Only WSLC should start the container, so if we receive a start event, it must be expected by a transition.
1313 // Otherwise the container was started externally. Log if the container was started externally.
1314 if (transition && transition->ExpectedEvent == ContainerEvent::Start)
1315 {
1316 WI_ASSERT(m_state == WslcContainerStateCreated || m_state == WslcContainerStateExited);
1317 CommitState(WslcContainerStateRunning, eventTime);
1318 CompleteTransition(transition);
1319 }
1320 else
1321 {
1322 WSL_LOG("UnexpectedContainerStart", TraceLoggingValue(m_id.c_str(), "Id"));
1323 }
1324 }
1325 else if (event == ContainerEvent::Stop)
1326 {
1327 WI_ASSERT(exitCode.has_value());
1328 OnStopped(exitCode.value(), eventTime);
1329 }
1330 else if (event == ContainerEvent::Destroy)
1331 {
1332 if (m_state != WslcContainerStateDeleted)
1333 {
1334 CommitState(WslcContainerStateDeleted, eventTime);
1335 comWrapper = ReleaseResources();
1336 }
1337
1338 // Signal init exit after the state transition and resource cleanup so awaiters observe Deleted.
1339 SignalInitProcessExit();
1340
1341 if (transition)
1342 {
1343 WI_ASSERT(transition->ExpectedEvent == ContainerEvent::Destroy);
1344
1345 // Let a COM caller waiting on this transition perform the disconnect, avoiding a deadlock with OnEvent.
1346 transition->Wrapper = std::move(comWrapper);
1347
1348 CompleteTransition(transition);
1349 }
1350 }
1351
1352 WSL_LOG(
1353 "ContainerEvent",
1354 TraceLoggingValue(m_name.c_str(), "Name"),
1355 TraceLoggingValue(m_id.c_str(), "Id"),
1356 TraceLoggingValue((int)event, "Event"));
1357 }
1358 }
1359
1360 void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
1361 {
1362 StopPhase(Signal, TimeoutSeconds, Kill, false);
1363 }
1364
1365 void WSLCContainerImpl::StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill, bool RestartPhase)
1366 {
1367 std::shared_ptr<StateTransition> transition;
1368
1369 {
1370 auto lifecycleLock = m_lifecycleLock.lock_shared();
1371 auto lock = m_lock.lock_exclusive();
1372
1373 // Kill is the escape hatch when a restart's stop phase is stuck, so it must not wait on the very
1374 // restart it is meant to unblock. Landing between the phases finds the container exited, which is
1375 // turned away below like any other kill of a stopped container.
1376 WaitForConflictingTransitionToComplete(lock, lifecycleLock, TransitionKind::Stop, !RestartPhase && !Kill);
1377
1378 // A Delete() that raced a restart may have already moved the container to the Deleted state.
1379 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_DELETED, Localization::MessageWslcContainerDeleted(m_id), m_state == WslcContainerStateDeleted);
1380
1381 transition = m_transition;
1382 WI_ASSERT(!transition || transition->Kind == TransitionKind::Stop);
1383
1384 // There can be an active stop transition post observing the exited state for cases where additional work needs to be done
1385 // after the container stopped: e.g. auto remove, restart, etc. Therefore, if there is an active stop transition, we still
1386 // need to attach to it below. This check simply skips creating a new transition once the state is already exited.
1387 if (!transition && m_state != WslcContainerStateRunning)
1388 {
1389 if (m_state == WslcContainerStateExited && !Kill)
1390 {
1391 return;
1392 }
1393
1394 THROW_HR_WITH_USER_ERROR_MSG(
1395 WSLC_E_CONTAINER_NOT_RUNNING,
1396 Localization::MessageWslcContainerNotRunning(m_id),
1397 "Cannot stop container '%hs', state: %i",
1398 m_id.c_str(),
1399 m_state);
1400 }
1401 // This check ensures WSLC does not call into docker if it has already observed the exited state. This prevents
1402 // conflicting with scenarios where work needs to be done after the container exits.
1403 else if (m_state == WslcContainerStateRunning)
1404 {
1405 std::optional<WSLCSignal> SignalArg;
1406
1407 if (Signal != WSLCSignalNone)
1408 {
1409 SignalArg = Signal;
1410 }
1411
1412 ValidateStopTimeout(TimeoutSeconds, true);
1413
1414 // Don't wait for the container to stop if we're not sending SIGKILL, since it may not stop the container.
1415 // N.B. If the signal was SIGTERM for instance, we'll receive the stop notification via OnEvent().
1416 bool waitForStop = !Kill || (SignalArg.value_or(WSLCSignalSIGKILL) == WSLCSignalSIGKILL);
1417 const auto generation = m_stateGeneration;
1418
1419 lock.reset();
1420 lifecycleLock.reset();
1421
1422 try
1423 {
1424 if (Kill)
1425 {
1426 m_runtime.Docker().SignalContainer(m_id, SignalArg);
1427 }
1428 else
1429 {
1430 std::optional<LONG> TimeoutArg;
1431
1432 if (TimeoutSeconds != WSLC_STOP_TIMEOUT_DEFAULT)
1433 {
1434 TimeoutArg = TimeoutSeconds;
1435 }
1436
1437 m_runtime.Docker().StopContainer(m_id, SignalArg, TimeoutArg);
1438 }
1439 }
1440 catch (const DockerHTTPException& e)
1441 {
1442 // HTTP 304 is returned when the container is already stopped.
1443 if (Kill || e.StatusCode() != 304)
1444 {
1445 lock = m_lock.lock_exclusive();
1446
1447 // A force delete can win the locks released above, so the container may be gone rather than stuck.
1448 THROW_HR_WITH_USER_ERROR_IF(
1449 WSLC_E_CONTAINER_DELETED,
1450 Localization::MessageWslcContainerDeleted(m_id),
1451 m_state == WslcContainerStateDeleted || (m_transition && m_transition->ExpectedEvent == ContainerEvent::Destroy));
1452
1453 THROW_DOCKER_USER_ERROR_MSG(e, "Failed to %hs container '%hs'", Kill ? "kill" : "stop", m_id.c_str());
1454 }
1455 }
1456
1457 if (waitForStop)
1458 {
1459 lock = m_lock.lock_exclusive();
1460 transition = m_transition;
1461
1462 // The container can exit and start again while the locks are released, so an unchanged generation is
1463 // the only proof that the stop event this call is waiting for is still to come.
1464 if (m_stateGeneration == generation)
1465 {
1466 if (!transition)
1467 {
1468 transition = StartTransition(TransitionKind::Stop, ContainerEvent::Stop);
1469 }
1470 }
1471 // The run already ended: keep waiting on the work it triggered (e.g. auto-remove), never on a start
1472 // that raced in behind it.
1473 else if (transition && transition->Kind == TransitionKind::Start)
1474 {
1475 transition.reset();
1476 }
1477 }
1478 else
1479 {
1480 transition.reset();
1481 }
1482 }
1483 }
1484
1485 if (transition)
1486 {
1487 AttachToTransition(transition);
1488 }
1489 }
1490
1491 void WSLCContainerImpl::Restart(WSLCSignal Signal, LONG TimeoutSeconds)
1492 {
1493 // The stop phase is skipped when the container is not running, so it cannot be the only validation.
1494 ValidateStopTimeout(TimeoutSeconds, true);
1495
1496 bool wasRunning{};
1497 auto restart = std::make_shared<RestartTransaction>();
1498
1499 {
1500 auto lifecycleLock = m_lifecycleLock.lock_shared();
1501 auto lock = m_lock.lock_exclusive();
1502 WaitForConflictingTransitionToComplete(lock, lifecycleLock);
1503
1504 wasRunning = m_state == WslcContainerStateRunning;
1505
1506 // N.B. Stop() and Start() each take m_lock, so it cannot be held across both phases. m_restart
1507 // stands them down until the start phase commits Running instead.
1508 m_restart = restart;
1509 }
1510
1511 // N.B. Nothing between here and the cleanup below may throw — nothing clears m_restart until it is armed.
1512 bool succeeded = false;
1513 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, restart, &succeeded]() {
1514 // N.B. Signalled last so a waiter cannot observe the restart as complete before the failure
1515 // cleanup below has published its delete.
1516 auto release = wil::scope_exit([&restart]() { restart->Completed.SetEvent(); });
1517
1518 std::shared_ptr<StateTransition> transition;
1519
1520 {
1521 auto lifecycleLock = m_lifecycleLock.lock_shared();
1522 auto lock = m_lock.lock_exclusive();
1523
1524 // CommitState() clears this once the start phase lands, so a later restart may already own it.
1525 if (m_restart == restart)
1526 {
1527 m_restart.reset();
1528 }
1529
1530 if (!succeeded)
1531 {
1532 transition = OnFailedRestartExclusiveLockHeld();
1533 }
1534 }
1535
1536 if (transition)
1537 {
1538 AttachToTransition(transition);
1539 }
1540 });
1541
1542 if (wasRunning)
1543 {
1544 StopPhase(Signal, TimeoutSeconds, false, true);
1545 }
1546
1547 StartPhase(WSLCContainerStartFlagsNone, nullptr, true);
1548 succeeded = true;
1549 }
1550
1551 // N.B. Runs with m_restart already cleared, so the delete below is no longer suppressed by OnStopped().
1552 __requires_exclusive_lock_held(m_lock) std::shared_ptr<WSLCContainerImpl::StateTransition> WSLCContainerImpl::OnFailedRestartExclusiveLockHeld()
1553 {
1554 // The start phase waits for the start event after Docker has accepted the start, so it can throw
1555 // on a container that is coming up. Leave that container alone; it still owns its resources.
1556 if (m_transition || m_state == WslcContainerStateRunning)
1557 {
1558 return nullptr;
1559 }
1560
1561 // The stop phase held these back for a start phase that never landed.
1562 if (m_runtimeResourcesHeld)
1563 {
1564 ReleaseRuntimeResources();
1565 }
1566
1567 if (WI_IsFlagClear(m_containerFlags, WSLCContainerFlagsRm) || m_state != WslcContainerStateExited)
1568 {
1569 return nullptr;
1570 }
1571
1572 // N.B. Requested here rather than through Delete() so the removal shares the scope that clears
1573 // m_restart, which is what stops a released Start() from bringing the container back up first.
1574 RequestDeleteExclusiveLockHeld(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes);
1575 return StartTransition(TransitionKind::Delete, ContainerEvent::Destroy);
1576 }
1577
1578 __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exitCode, std::int64_t stopTime)
1579 {
1580 auto transition = m_transition;
1581
1582 // A Stop while expecting Start should not occur normally: Docker emits start before die, and the event stream processes
1583 // them serially. It would indicate external manipulation. Ignoring it avoids applying an old exit code to the newly
1584 // staged init process.
1585 if (transition && (transition->ExpectedEvent == ContainerEvent::Start))
1586 {
1587 WSL_LOG("UnexpectedContainerExit", TraceLoggingValue(m_id.c_str(), "Id"), TraceLoggingValue(exitCode, "ExitCode"));
1588 return;
1589 }
1590
1591 SetExitCode(exitCode);
1592
1593 // Notify plugin manager that the container is stopping. Errors are ignored.
1594 if (m_state == WslcContainerStateRunning)
1595 {
1596 try
1597 {
1598 LOG_IF_FAILED(m_pluginNotifier->OnContainerStopping(m_id.c_str()));
1599 }
1600 CATCH_LOG();
1601 }
1602
1603 ReleaseProcesses();
1604
1605 // A restart's start phase relies on the container's ports and mounts still being held.
1606 if (!m_restart)
1607 {
1608 ReleaseRuntimeResources();
1609 }
1610
1611 // Ignore duplicate or late Stop events so they do not overwrite an already committed state.
1612 if (m_state == WslcContainerStateRunning)
1613 {
1614 CommitState(WslcContainerStateExited, stopTime, exitCode);
1615 }
1616
1617 std::exception_ptr transitionException;
1618
1619 // Docker delete request is already sent.
1620 if (transition && transition->ExpectedEvent == ContainerEvent::Destroy)
1621 {
1622 return;
1623 }
1624
1625 // Stop with Rm must initiate Delete.
1626 if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm) && !m_restart)
1627 {
1628 try
1629 {
1630 m_runtime.Docker().DeleteContainer(m_id, true, true);
1631
1632 if (transition)
1633 {
1634 transition->ExpectedEvent = ContainerEvent::Destroy;
1635 }
1636 else
1637 {
1638 transition = StartTransition(TransitionKind::Delete, ContainerEvent::Destroy);
1639 }
1640
1641 return;
1642 }
1643 catch (...)
1644 {
1645 transitionException = std::current_exception();
1646 LOG_CAUGHT_EXCEPTION_MSG("Failed to remove container '%hs'", m_id.c_str());
1647 }
1648 }
1649
1650 SignalInitProcessExit();
1651
1652 if (transition)
1653 {
1654 CompleteTransition(transition, std::move(transitionException));
1655 }
1656 }
1657
1658 void WSLCContainerImpl::RecoverPorts(const common::docker_schema::ContainerInfo& dockerContainer)
1659 {
1660 auto lock = m_lock.lock_exclusive();
1661
1662 // Re-register VM-scoped port reservations against the restarted VM using the numbers recorded at
1663 // create time, restoring bridge-mode forwarding when the stopped container starts again.
1664 const bool allocateVmPorts = NetworkModeAllocatesVmPorts(m_networkMode);
1665 if (!allocateVmPorts)
1666 {
1667 return;
1668 }
1669
1670 auto metadataIt = dockerContainer.Labels.find(WSLCContainerMetadataLabel);
1671 if (metadataIt == dockerContainer.Labels.end())
1672 {
1673 return;
1674 }
1675
1676 auto metadata = ParseContainerMetadata(metadataIt->second.c_str());
1677
1678 std::vector<ContainerPortMapping> ports;
1679 ports.reserve(metadata.Ports.size());
1680 for (const auto& e : metadata.Ports)
1681 {
1682 auto& inserted = ports.emplace_back(ContainerPortMapping{VMPortMapping::FromContainerMetaData(e), e.ContainerPort});
1683
1684 auto allocation = m_runtime.Vm().TryAllocatePort(e.VmPort, e.Family, e.Protocol);
1685
1686 THROW_HR_IF_MSG(
1687 HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), !allocation, "Port %hu is in use, cannot recover container %hs", e.VmPort, m_id.c_str());
1688
1689 inserted.VmMapping.AssignVmPort(allocation);
1690 }
1691
1692 m_mappedPorts = std::move(ports);
1693 }
1694
1695 void WSLCContainerImpl::Delete(WSLCDeleteFlags Flags)
1696 {
1697 std::shared_ptr<StateTransition> transition;
1698 auto lifecycleLock = m_lifecycleLock.lock_shared();
1699 auto lock = m_lock.lock_exclusive();
1700
1701 // N.B. Unlike Start() and Stop(), this deliberately does not stand down for an in-flight restart.
1702 // A remove that lands between the two phases takes effect, and the restart's start phase fails.
1703 WaitForConflictingTransitionToComplete(lock, lifecycleLock, std::nullopt, false);
1704
1705 RequestDeleteExclusiveLockHeld(Flags);
1706 transition = StartTransition(TransitionKind::Delete, ContainerEvent::Destroy);
1707
1708 lock.reset();
1709 lifecycleLock.reset();
1710 AttachToTransition(transition);
1711 }
1712
1713 __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::RequestDeleteExclusiveLockHeld(WSLCDeleteFlags Flags)
1714 {
1715 // Validate that the container is not running or already deleted.
1716 THROW_HR_WITH_USER_ERROR_IF(
1717 WSLC_E_CONTAINER_IS_RUNNING,
1718 Localization::MessageWslcCannotRemoveRunningContainer(m_id),
1719 m_state == WslcContainerStateRunning && WI_IsFlagClear(Flags, WSLCDeleteFlagsForce));
1720
1721 THROW_HR_IF_MSG(
1722 HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_state == WslcContainerStateDeleted, "Container %hs is already deleted", m_id.c_str());
1723
1724 WI_ASSERT(m_state != WslcContainerStateInvalid);
1725
1726 try
1727 {
1728 m_runtime.Docker().DeleteContainer(m_id, WI_IsFlagSet(Flags, WSLCDeleteFlagsForce), WI_IsFlagSet(Flags, WSLCDeleteFlagsDeleteVolumes));
1729 }
1730 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to delete container '%hs'", m_id.c_str());
1731 }
1732
1733 void WSLCContainerImpl::Export(WSLCHandle OutHandle) const
1734 {
1735 auto lock = m_lock.lock_shared();
1736
1737 // Validate that the container is not in the running state.
1738 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_IS_RUNNING, Localization::MessageWslcContainerIsRunning(m_id), m_state == WslcContainerStateRunning);
1739
1740 std::pair<uint32_t, wil::unique_socket> SocketCodePair;
1741 SocketCodePair = m_runtime.Docker().ExportContainer(m_id);
1742
1743 auto userHandle = m_wslcSession.OpenUserHandle(OutHandle);
1744
1745 wsl::windows::common::io::MultiHandleWait io = m_wslcSession.CreateIOContext();
1746
1747 std::string errorJson;
1748 auto accumulateError = [&](const gsl::span<char>& buffer) {
1749 // If the export failed, accumulate the error message.
1750 errorJson.append(buffer.data(), buffer.size());
1751 };
1752
1753 if (SocketCodePair.first != 200)
1754 {
1755 io.AddHandle(std::make_unique<ReadHandle>(HandleWrapper{std::move(SocketCodePair.second)}, std::move(accumulateError)));
1756 }
1757 else
1758 {
1759 io.AddHandle(std::make_unique<RelayHandle<HTTPChunkBasedReadHandle>>(
1760 HandleWrapper{std::move(SocketCodePair.second)}, userHandle.Get()));
1761 }
1762
1763 // Release the lock so the container can still be interacted with while the export is in progress.
1764 // Past this point, no member variables can be accessed.
1765 lock.reset();
1766
1767 io.Run({});
1768
1769 if (SocketCodePair.first != 200)
1770 {
1771 // Export failed, parse the error message.
1772 auto error = wsl::shared::FromJson<common::docker_schema::ErrorResponse>(errorJson.c_str());
1773 const auto errorMessage = FormatDockerEngineError(error.message);
1774
1775 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_FOUND, errorMessage, SocketCodePair.first == 404);
1776 THROW_HR_WITH_USER_ERROR(E_FAIL, errorMessage);
1777 }
1778 }
1779
1780 void WSLCContainerImpl::UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize) const
1781 {
1782 auto lock = m_lock.lock_shared();
1783
1784 std::optional<uint64_t> contentLength;
1785 if (ContentSize > 0)
1786 {
1787 contentLength = ContentSize;
1788 }
1789
1790 auto requestContext = m_runtime.Docker().PutArchive(m_id, DestPath, contentLength);
1791
1792 auto userHandle = m_wslcSession.OpenUserHandle(TarHandle);
1793
1794 auto io = m_wslcSession.CreateIOContext();
1795
1796 std::optional<std::string> pendingErrorJson;
1797 unsigned int httpStatusCode = 0;
1798 auto onHttpResponse = [&](const boost::beast::http::message<false, boost::beast::http::buffer_body>& response) {
1799 WSL_LOG("ContainerUploadArchiveHttpResponse", TraceLoggingValue(static_cast<int>(response.result()), "StatusCode"));
1800
1801 httpStatusCode = response.result_int();
1802 if (httpStatusCode != 200)
1803 {
1804 pendingErrorJson.emplace();
1805 }
1806 };
1807
1808 auto onProgress = [&](const gsl::span<char>& buffer) {
1809 if (pendingErrorJson.has_value())
1810 {
1811 pendingErrorJson->append(buffer.data(), buffer.size());
1812 }
1813 };
1814
1815 // Shutdown the Docker stream's write side when the input is fully read.
1816 auto onInputComplete = [socket = requestContext->stream.native_handle()]() {
1817 LOG_LAST_ERROR_IF(shutdown(socket, SD_SEND) == SOCKET_ERROR);
1818 };
1819
1820 io.AddHandle(std::make_unique<RelayHandle<ReadHandle>>(
1821 HandleWrapper{userHandle.Get(), std::move(onInputComplete)}, HandleWrapper{requestContext->stream.native_handle()}));
1822
1823 io.AddHandle(
1824 std::make_unique<DockerHTTPClient::DockerHttpResponseHandle>(*requestContext, std::move(onHttpResponse), std::move(onProgress)),
1825 wsl::windows::common::io::MultiHandleWait::CancelOnCompleted);
1826
1827 // Release the lock so the container can still be interacted with while the upload is in progress.
1828 lock.reset();
1829
1830 io.Run({});
1831
1832 if (pendingErrorJson.has_value())
1833 {
1834 auto error = wsl::shared::FromJson<ErrorResponse>(pendingErrorJson->c_str());
1835 const auto errorMessage = FormatDockerEngineError(error.message);
1836
1837 THROW_HR_WITH_USER_ERROR_IF(HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), errorMessage, httpStatusCode == 404);
1838 THROW_HR_WITH_USER_ERROR(E_FAIL, errorMessage);
1839 }
1840 }
1841
1842 void WSLCContainerImpl::DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle) const
1843 {
1844 auto lock = m_lock.lock_shared();
1845
1846 auto [statusCode, socket, isChunked] = m_runtime.Docker().GetArchive(m_id, SrcPath);
1847
1848 auto userHandle = m_wslcSession.OpenUserHandle(OutHandle);
1849
1850 wsl::windows::common::io::MultiHandleWait io = m_wslcSession.CreateIOContext();
1851
1852 std::string errorJson;
1853
1854 if (statusCode != 200)
1855 {
1856 io.AddHandle(std::make_unique<ReadHandle>(HandleWrapper{std::move(socket)}, [&](const gsl::span<char>& buffer) {
1857 errorJson.append(buffer.data(), buffer.size());
1858 }));
1859 }
1860 else
1861 {
1862 if (isChunked)
1863 {
1864 io.AddHandle(
1865 std::make_unique<RelayHandle<HTTPChunkBasedReadHandle>>(HandleWrapper{std::move(socket)}, userHandle.Get()),
1866 wsl::windows::common::io::MultiHandleWait::CancelOnCompleted);
1867 }
1868 else
1869 {
1870 io.AddHandle(
1871 std::make_unique<RelayHandle<ReadHandle>>(HandleWrapper{std::move(socket)}, userHandle.Get()),
1872 wsl::windows::common::io::MultiHandleWait::CancelOnCompleted);
1873 }
1874 }
1875
1876 lock.reset();
1877
1878 io.Run({});
1879
1880 if (statusCode != 200)
1881 {
1882 auto error = wsl::shared::FromJson<ErrorResponse>(errorJson.c_str());
1883 const auto errorMessage = FormatDockerEngineError(error.message);
1884
1885 THROW_HR_WITH_USER_ERROR_IF(HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND), errorMessage, statusCode == 404);
1886 THROW_HR_WITH_USER_ERROR(E_FAIL, errorMessage);
1887 }
1888 }
1889
1890 void WSLCContainerImpl::GetState(WSLCContainerState* Result)
1891 {
1892 auto lock = m_lock.lock_shared();
1893 *Result = m_state;
1894 }
1895
1896 WSLCContainerState WSLCContainerImpl::State() const noexcept
1897 {
1898 auto lock = m_lock.lock_shared();
1899 return m_state;
1900 }
1901
1902 void WSLCContainerImpl::GetInitProcess(IWSLCProcess** Process) const
1903 {
1904 auto lock = m_lock.lock_shared();
1905 std::lock_guard processesLock{m_processesLock};
1906
1907 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_initProcess);
1908 THROW_IF_FAILED(m_initProcess.CopyTo(__uuidof(IWSLCProcess), (void**)Process));
1909 }
1910
1911 void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, const WSLCProcessStartOptions* StartOptions, IWSLCProcess** Process)
1912 {
1913 THROW_HR_IF_MSG(E_INVALIDARG, Options->CommandLine.Count == 0, "Exec command line cannot be empty");
1914
1915 auto lock = m_lock.lock_shared();
1916
1917 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_NOT_RUNNING, Localization::MessageWslcContainerNotRunning(m_id), m_state != WslcContainerStateRunning);
1918
1919 if (StartOptions != nullptr)
1920 {
1921 THROW_HR_IF_MSG(
1922 E_INVALIDARG,
1923 WI_IsFlagSet(Options->Flags, WSLCProcessFlagsTty) && (StartOptions->TtyRows == 0 || StartOptions->TtyColumns == 0),
1924 "Invalid tty size: %lu:%lu",
1925 StartOptions->TtyRows,
1926 StartOptions->TtyColumns);
1927 }
1928
1929 common::docker_schema::CreateExec request{};
1930 request.AttachStdout = true;
1931 request.AttachStderr = true;
1932
1933 request.Cmd = StringArrayToVector(Options->CommandLine);
1934 request.Env = StringArrayToVector(Options->Environment);
1935
1936 if (Options->CurrentDirectory != nullptr)
1937 {
1938 request.WorkingDir = Options->CurrentDirectory;
1939 }
1940
1941 if (Options->User != nullptr)
1942 {
1943 request.User = Options->User;
1944 }
1945
1946 if (WI_IsFlagSet(Options->Flags, WSLCProcessFlagsTty))
1947 {
1948 request.Tty = true;
1949
1950 if (StartOptions != nullptr)
1951 {
1952 request.ConsoleSize = {StartOptions->TtyRows, StartOptions->TtyColumns};
1953 }
1954 }
1955
1956 if (WI_IsFlagSet(Options->Flags, WSLCProcessFlagsStdin))
1957 {
1958 request.AttachStdin = true;
1959 }
1960
1961 if (StartOptions != nullptr && StartOptions->DetachKeys != nullptr)
1962 {
1963 request.DetachKeys = StartOptions->DetachKeys;
1964 }
1965
1966 try
1967 {
1968 auto result = m_runtime.Docker().CreateExec(m_id, request);
1969
1970 // N.B. There's no way to delete a created exec instance, it is removed when the container is deleted.
1971
1972 auto stream = m_runtime.Docker().StartExec(
1973 result.Id, common::docker_schema::StartExec{.Tty = request.Tty, .ConsoleSize = request.ConsoleSize});
1974
1975 std::unique_ptr<WSLCProcessIO> io;
1976 if (request.Tty)
1977 {
1978 io = std::make_unique<TTYProcessIO>(TypedHandle{std::move(stream), WSLCHandleTypeSocket});
1979 }
1980 else
1981 {
1982 io = CreateRelayedProcessIO(wil::shared_socket{std::move(stream)}, Options->Flags);
1983 }
1984
1985 auto control = std::make_shared<DockerExecProcessControl>(*this, result.Id, m_runtime.Docker(), m_runtime.Events());
1986
1987 {
1988 std::lock_guard processesLock{m_processesLock};
1989
1990 // Drop entries for execs that have since been released, then store a non-owning weak
1991 // reference. The owning shared_ptr is moved into the COM WSLCProcess returned below.
1992 std::erase_if(m_processes, [](const auto& weak) { return weak.expired(); });
1993 m_processes.push_back(control);
1994 }
1995
1996 // Poll for the exec'd process to either be running, or failed.
1997 // This is required because StartExec() returns before the process is actually created, and if exec() fails, we'll never
1998 // get an exec_die notification, so this case needs to be caught before returning the process to the caller.
1999 //
2000 // N.B. Pid is 0 until runc forks the user process, so a transient {Running=true, Pid=0} response (seen e.g. on a
2001 // fast failure such as an invalid user/group) must not be treated as "running" or we'd wait forever.
2002
2003 // TODO: Configurable timeout.
2004 auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
2005
2006 do
2007 {
2008 auto state = m_runtime.Docker().InspectExec(result.Id);
2009 if (state.Running && state.Pid > 0)
2010 {
2011 control->SetPid(state.Pid);
2012 break; // Exec is running, exit.
2013 }
2014 else if (state.ExitCode.has_value())
2015 {
2016 control->SetExitCode(state.ExitCode.value());
2017 break; // Exec has exited, exit.
2018 }
2019 else if (std::chrono::steady_clock::now() > deadline)
2020 {
2021 THROW_HR_MSG(
2022 HRESULT_FROM_WIN32(ERROR_TIMEOUT),
2023 "Timed out waiting for exec state for '%hs'. Last state: %hs",
2024 result.Id.c_str(),
2025 wsl::shared::ToJson(state).c_str());
2026 }
2027
2028 } while (!control->GetExitEvent().wait(100));
2029
2030 auto process = wil::MakeOrThrow<WSLCProcess>(std::move(control), std::move(io), Options->Flags);
2031
2032 // The exec'd process wrapper is handed to the client and is not retained internally, so its
2033 // lifetime tracks the client's proxy. Bind a keep-alive token to it so the idle worker does
2034 // not tear the VM down (killing the process) while the client still holds the proxy.
2035 process->SetKeepAliveToken(m_wslcSession.CreateActivityToken());
2036
2037 THROW_IF_FAILED(process.CopyTo(__uuidof(IWSLCProcess), (void**)Process));
2038 }
2039 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to exec process in container %hs", m_id.c_str());
2040 }
2041
2042 WslcInspectContainer WSLCContainerImpl::BuildInspectContainer(const DockerInspectContainer& dockerInspect) const
2043 {
2044 WslcInspectContainer wslcInspect{};
2045
2046 wslcInspect.Id = dockerInspect.Id;
2047 wslcInspect.Name = dockerInspect.Name;
2048 wslcInspect.Created = dockerInspect.Created;
2049 wslcInspect.Image = dockerInspect.Image;
2050 wslcInspect.SizeRw = dockerInspect.SizeRw;
2051 wslcInspect.SizeRootFs = dockerInspect.SizeRootFs;
2052
2053 // Map container state.
2054 wslcInspect.State.Status = dockerInspect.State.Status;
2055 wslcInspect.State.Running = dockerInspect.State.Running;
2056 wslcInspect.State.ExitCode = dockerInspect.State.ExitCode;
2057 wslcInspect.State.StartedAt = dockerInspect.State.StartedAt;
2058 wslcInspect.State.FinishedAt = dockerInspect.State.FinishedAt;
2059
2060 if (dockerInspect.State.Health.has_value())
2061 {
2062 const auto& dockerHealth = dockerInspect.State.Health.value();
2063
2064 wslc_schema::Health health{};
2065 health.Status = dockerHealth.Status;
2066 health.FailingStreak = dockerHealth.FailingStreak;
2067
2068 health.Log.reserve(dockerHealth.Log.size());
2069 for (const auto& entry : dockerHealth.Log)
2070 {
2071 health.Log.push_back({entry.Start, entry.End, entry.ExitCode, entry.Output});
2072 }
2073
2074 wslcInspect.State.Health = std::move(health);
2075 }
2076
2077 wslcInspect.HostConfig.NetworkMode = dockerInspect.HostConfig.NetworkMode;
2078 wslcInspect.HostConfig.Memory = dockerInspect.HostConfig.Memory;
2079 wslcInspect.HostConfig.NanoCpus = dockerInspect.HostConfig.NanoCpus;
2080
2081 if (dockerInspect.HostConfig.Ulimits.has_value())
2082 {
2083 wslcInspect.HostConfig.Ulimits.reserve(dockerInspect.HostConfig.Ulimits->size());
2084 for (const auto& ulimit : dockerInspect.HostConfig.Ulimits.value())
2085 {
2086 wslcInspect.HostConfig.Ulimits.push_back({ulimit.Name, ulimit.Soft, ulimit.Hard});
2087 }
2088 }
2089
2090 wslcInspect.Config.Image = m_image;
2091 wslcInspect.Config.Env = dockerInspect.Config.Env;
2092 wslcInspect.Config.Cmd = dockerInspect.Config.Cmd;
2093 wslcInspect.Config.Entrypoint = dockerInspect.Config.Entrypoint;
2094 wslcInspect.Config.User = dockerInspect.Config.User;
2095 wslcInspect.Config.WorkingDir = dockerInspect.Config.WorkingDir;
2096 wslcInspect.Config.StopTimeout = dockerInspect.Config.StopTimeout;
2097
2098 if (dockerInspect.Config.Healthcheck.has_value())
2099 {
2100 const auto& dockerHealth = dockerInspect.Config.Healthcheck.value();
2101
2102 wslc_schema::HealthConfig health{};
2103 health.Test = dockerHealth.Test;
2104 health.Interval = dockerHealth.Interval;
2105 health.Timeout = dockerHealth.Timeout;
2106 health.StartPeriod = dockerHealth.StartPeriod;
2107 health.Retries = dockerHealth.Retries;
2108
2109 wslcInspect.Config.Healthcheck = std::move(health);
2110 }
2111
2112 // Map WSLC port mappings (Windows host ports only).
2113 for (const auto& e : m_mappedPorts)
2114 {
2115 auto portKey = std::format("{}/{}", e.ContainerPort, e.ProtocolString());
2116
2117 wslc_schema::InspectPortBinding portBinding{};
2118 portBinding.HostIp = e.VmMapping.BindingAddressString();
2119 portBinding.HostPort = std::to_string(e.VmMapping.HostPort());
2120
2121 wslcInspect.Ports[portKey].push_back(std::move(portBinding));
2122 }
2123
2124 // Map mounts without exposing Linux paths from the utility VM.
2125 wslcInspect.Mounts.reserve(
2126 m_mountedVolumes.size() + dockerInspect.Mounts.size() + dockerInspect.HostConfig.Tmpfs.size() +
2127 dockerInspect.HostConfig.Mounts.size());
2128 for (const auto& volume : m_mountedVolumes)
2129 {
2130 wslc_schema::InspectMount mountInfo{};
2131 mountInfo.Type = "bind";
2132
2133 // For file mounts, reconstruct the original host path from the parent directory and filename.
2134 if (volume.SourceFilename.empty())
2135 {
2136 mountInfo.Source = wsl::shared::string::WideToMultiByte(volume.HostPath);
2137 }
2138 else
2139 {
2140 std::filesystem::path fullPath(volume.HostPath);
2141 fullPath /= volume.SourceFilename;
2142 mountInfo.Source = fullPath.string();
2143 }
2144
2145 mountInfo.Destination = volume.ContainerPath;
2146 mountInfo.ReadWrite = !volume.ReadOnly;
2147 wslcInspect.Mounts.push_back(std::move(mountInfo));
2148 }
2149
2150 for (const auto& volume : dockerInspect.Mounts)
2151 {
2152 // This block covers non-vhd volumes. This includes:
2153 // - Guest volumes mounted via -v
2154 // - Volumes mounted as part of the image (via VOLUME)
2155 //
2156 // TODO: Return mounts once --mount is implemented.
2157
2158 if (volume.Type != "volume")
2159 {
2160 continue;
2161 }
2162
2163 wslc_schema::InspectMount mountInfo{};
2164 mountInfo.Type = volume.Type;
2165 mountInfo.Name = volume.Name;
2166 const auto structuredMount = std::ranges::find_if(dockerInspect.HostConfig.Mounts, [&](const auto& mount) {
2167 return mount.Type == "volume" && mount.Target == volume.Destination;
2168 });
2169 if (structuredMount != dockerInspect.HostConfig.Mounts.end())
2170 {
2171 mountInfo.Source = structuredMount->Source;
2172 }
2173 mountInfo.Destination = volume.Destination;
2174 mountInfo.ReadWrite = volume.RW;
2175
2176 wslcInspect.Mounts.push_back(std::move(mountInfo));
2177 }
2178
2179 // Map tmpfs mounts from Docker inspect data.
2180 for (const auto& entry : dockerInspect.HostConfig.Tmpfs)
2181 {
2182 wslc_schema::InspectMount mountInfo{};
2183 mountInfo.Type = "tmpfs";
2184 mountInfo.Destination = entry.first;
2185 // Tmpfs mounts are read-write by default. We currently do not parse tmpfs options
2186 // (e.g. "ro") for inspect output; Docker enforces actual mount behavior.
2187 mountInfo.ReadWrite = true;
2188 wslcInspect.Mounts.push_back(std::move(mountInfo));
2189 }
2190
2191 // Bind mounts are populated from m_mountedVolumes so their inspect source is the Windows host path.
2192 for (const auto& mount : dockerInspect.HostConfig.Mounts)
2193 {
2194 if (mount.Type == "tmpfs")
2195 {
2196 wslc_schema::InspectMount mountInfo{};
2197 mountInfo.Type = mount.Type;
2198 mountInfo.Source = mount.Source;
2199 mountInfo.Destination = mount.Target;
2200 mountInfo.ReadWrite = !mount.ReadOnly;
2201 wslcInspect.Mounts.push_back(std::move(mountInfo));
2202 }
2203 }
2204
2205 // Config.Labels is the Docker-shape location; top-level Labels is a legacy alias.
2206 wslcInspect.Config.Labels = m_labels;
2207 wslcInspect.Labels = m_labels;
2208
2209 // Map per-endpoint network settings from Docker inspect data.
2210 for (const auto& [name, endpoint] : dockerInspect.NetworkSettings.Networks)
2211 {
2212 wslc_schema::InspectEndpointSettings wslcEndpoint{};
2213 wslcEndpoint.IPAddress = endpoint.IPAddress;
2214 wslcEndpoint.Gateway = endpoint.Gateway;
2215 wslcEndpoint.MacAddress = endpoint.MacAddress;
2216 wslcEndpoint.IPPrefixLen = endpoint.IPPrefixLen;
2217 wslcEndpoint.Aliases = endpoint.Aliases.value_or(std::vector<std::string>{});
2218 wslcEndpoint.Links = endpoint.Links.value_or(std::vector<std::string>{});
2219 wslcEndpoint.DriverOpts = endpoint.DriverOpts.value_or(std::map<std::string, std::string>{});
2220 if (endpoint.IPAMConfig.has_value())
2221 {
2222 wslc_schema::InspectEndpointIPAMConfig ipam{};
2223 ipam.IPv4Address = endpoint.IPAMConfig->IPv4Address;
2224 ipam.LinkLocalIPs = endpoint.IPAMConfig->LinkLocalIPs.value_or(std::vector<std::string>{});
2225 wslcEndpoint.IPAMConfig = std::move(ipam);
2226 }
2227 wslcInspect.NetworkSettings.Networks[name] = std::move(wslcEndpoint);
2228 }
2229
2230 return wslcInspect;
2231 }
2232
2233 std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
2234 const WSLCContainerOptions& containerOptions,
2235 const std::string& containerName,
2236 WSLCSession& wslcSession,
2237 WSLCSessionRuntime& runtime,
2238 IWSLCPluginNotifier* pluginNotifier,
2239 const std::unordered_map<std::string, NetworkEntry>& sessionNetworks,
2240 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
2241 EventStore& eventStore)
2242 {
2243 auto& virtualMachine = runtime.Vm();
2244 auto& DockerClient = runtime.Docker();
2245 const auto mounts = ConvertAndValidateMounts(containerOptions);
2246
2247 common::docker_schema::CreateContainer request;
2248 request.Image = containerOptions.Image;
2249
2250 // TODO: Think about when 'StdinOnce' should be set.
2251 request.StdinOnce = true;
2252
2253 if (WI_IsFlagSet(containerOptions.InitProcessOptions.Flags, WSLCProcessFlagsTty))
2254 {
2255 request.Tty = true;
2256 }
2257
2258 if (WI_IsFlagSet(containerOptions.InitProcessOptions.Flags, WSLCProcessFlagsStdin))
2259 {
2260 request.OpenStdin = true;
2261 }
2262
2263 if (containerOptions.InitProcessOptions.CommandLine.Count > 0)
2264 {
2265 request.Cmd = StringArrayToVector(containerOptions.InitProcessOptions.CommandLine);
2266 }
2267
2268 if (containerOptions.Entrypoint.Count > 0)
2269 {
2270 request.Entrypoint = StringArrayToVector(containerOptions.Entrypoint);
2271 }
2272
2273 request.Env = StringArrayToVector(containerOptions.InitProcessOptions.Environment);
2274
2275 if (containerOptions.StopSignal != WSLCSignalNone)
2276 {
2277 request.StopSignal = std::to_string(containerOptions.StopSignal);
2278 }
2279
2280 if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsStopTimeout))
2281 {
2282 ValidateStopTimeout(containerOptions.StopTimeout, false);
2283
2284 request.StopTimeout = static_cast<int>(containerOptions.StopTimeout);
2285 }
2286
2287 if (containerOptions.InitProcessOptions.CurrentDirectory != nullptr)
2288 {
2289 request.WorkingDir = containerOptions.InitProcessOptions.CurrentDirectory;
2290 }
2291
2292 if (containerOptions.HostName != nullptr)
2293 {
2294 request.Hostname = containerOptions.HostName;
2295 }
2296
2297 if (containerOptions.DomainName != nullptr)
2298 {
2299 request.Domainname = containerOptions.DomainName;
2300 }
2301
2302 if (containerOptions.DnsServers.Count > 0)
2303 {
2304 THROW_HR_IF_NULL_MSG(
2305 E_INVALIDARG,
2306 containerOptions.DnsServers.Values,
2307 "DnsServers.Values is null with Count=%lu",
2308 containerOptions.DnsServers.Count);
2309
2310 request.HostConfig.Dns = StringArrayToVector(containerOptions.DnsServers);
2311 }
2312
2313 if (containerOptions.DnsSearchDomains.Count > 0)
2314 {
2315 THROW_HR_IF_NULL_MSG(
2316 E_INVALIDARG,
2317 containerOptions.DnsSearchDomains.Values,
2318 "DnsSearchDomains.Values is null with Count=%lu",
2319 containerOptions.DnsSearchDomains.Count);
2320
2321 request.HostConfig.DnsSearch = StringArrayToVector(containerOptions.DnsSearchDomains);
2322 }
2323
2324 if (containerOptions.DnsOptions.Count > 0)
2325 {
2326 THROW_HR_IF_NULL_MSG(
2327 E_INVALIDARG,
2328 containerOptions.DnsOptions.Values,
2329 "DnsOptions.Values is null with Count=%lu",
2330 containerOptions.DnsOptions.Count);
2331
2332 request.HostConfig.DnsOptions = StringArrayToVector(containerOptions.DnsOptions);
2333 }
2334
2335 if (containerOptions.InitProcessOptions.User != nullptr)
2336 {
2337 request.User = containerOptions.InitProcessOptions.User;
2338 }
2339
2340 request.HostConfig.Init = WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsInit);
2341
2342 request.HostConfig.Memory = containerOptions.MemoryBytes;
2343 request.HostConfig.NanoCpus = containerOptions.NanoCpus;
2344
2345 if (containerOptions.UlimitsCount > 0)
2346 {
2347 THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Ulimits, "Ulimits is null with UlimitsCount=%lu", containerOptions.UlimitsCount);
2348
2349 std::vector<wsl::windows::common::docker_schema::Ulimit> ulimits;
2350 ulimits.reserve(containerOptions.UlimitsCount);
2351
2352 for (ULONG i = 0; i < containerOptions.UlimitsCount; i++)
2353 {
2354 const auto& ulimit = containerOptions.Ulimits[i];
2355 THROW_HR_IF_NULL_MSG(E_INVALIDARG, ulimit.Name, "Ulimits[%lu].Name is null", i);
2356
2357 ulimits.push_back({ulimit.Name, ulimit.Soft, ulimit.Hard});
2358 }
2359
2360 request.HostConfig.Ulimits = std::move(ulimits);
2361 }
2362
2363 request.HostConfig.ShmSize = containerOptions.ShmSize;
2364
2365 if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsNoHealthCheck))
2366 {
2367 THROW_HR_IF_MSG(
2368 E_INVALIDARG,
2369 WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsHealthCheck),
2370 "WSLCContainerFlagsHealthCheck and WSLCContainerFlagsNoHealthCheck cannot be combined");
2371
2372 request.Healthcheck.emplace().Test = std::vector<std::string>{"NONE"};
2373 }
2374 else if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsHealthCheck))
2375 {
2376 common::docker_schema::HealthConfig health{};
2377
2378 if (containerOptions.HealthCmd != nullptr)
2379 {
2380 health.Test = std::vector<std::string>{"CMD-SHELL", containerOptions.HealthCmd};
2381 }
2382
2383 // N.B. '0' will use the default value from the image.
2384 if (containerOptions.HealthIntervalNs != 0)
2385 {
2386 health.Interval = containerOptions.HealthIntervalNs;
2387 }
2388
2389 if (containerOptions.HealthTimeoutNs != 0)
2390 {
2391 health.Timeout = containerOptions.HealthTimeoutNs;
2392 }
2393
2394 if (containerOptions.HealthStartPeriodNs != 0)
2395 {
2396 health.StartPeriod = containerOptions.HealthStartPeriodNs;
2397 }
2398
2399 if (containerOptions.HealthRetries != 0)
2400 {
2401 health.Retries = containerOptions.HealthRetries;
2402 }
2403
2404 request.Healthcheck = std::move(health);
2405 }
2406
2407 // Build bind mount list from container options.
2408 std::vector<WSLCVolumeMount> volumes;
2409 volumes.reserve(containerOptions.VolumesCount + mounts.size());
2410
2411 std::vector<std::string> binds;
2412 binds.reserve(containerOptions.VolumesCount);
2413
2414 for (ULONG i = 0; i < containerOptions.VolumesCount; i++)
2415 {
2416 auto volume = containerOptions.Volumes[i];
2417 auto prepared =
2418 PrepareBindMount(volume.HostPath, volume.ContainerPath, static_cast<bool>(volume.ReadOnly), MissingBindSource::Create);
2419 binds.push_back(std::format("{}:{}:{}", prepared.DockerSource, volume.ContainerPath, volume.ReadOnly ? "ro" : "rw"));
2420 volumes.push_back(std::move(prepared.Volume));
2421 }
2422
2423 // Process tmpfs mounts from container options.
2424 if (containerOptions.TmpfsCount > 0)
2425 {
2426 THROW_HR_IF_NULL_MSG(E_INVALIDARG, containerOptions.Tmpfs, "Tmpfs is null with TmpfsCount=%lu", containerOptions.TmpfsCount);
2427
2428 for (ULONG i = 0; i < containerOptions.TmpfsCount; i++)
2429 {
2430 const auto& tmpfs = containerOptions.Tmpfs[i];
2431
2432 THROW_HR_IF_NULL_MSG(E_INVALIDARG, tmpfs.Destination, "Tmpfs mount at index %lu has null destination", i);
2433
2434 request.HostConfig.Tmpfs[tmpfs.Destination] = tmpfs.Options != nullptr ? tmpfs.Options : "";
2435 }
2436 }
2437
2438 ProcessNamedVolumes(containerOptions, request);
2439
2440 for (const auto& mount : mounts)
2441 {
2442 common::docker_schema::Mount dockerMount{
2443 .Target = mount.Target,
2444 .ReadOnly = mount.ReadOnly,
2445 };
2446
2447 switch (mount.MountType)
2448 {
2449 case WSLCMountTypeBind:
2450 {
2451 // Docker's colon-delimited bind format cannot represent ':' in the target.
2452 const auto missingSource = mount.BindSource == wsl::windows::common::mount::BindSourcePolicy::CreateIfMissing
2453 ? MissingBindSource::Create
2454 : MissingBindSource::Reject;
2455 auto prepared = PrepareBindMount(mount.Source, mount.Target, mount.ReadOnly, missingSource);
2456 dockerMount.Source = std::move(prepared.DockerSource);
2457 dockerMount.Type = "bind";
2458 volumes.push_back(std::move(prepared.Volume));
2459 break;
2460 }
2461
2462 case WSLCMountTypeVolume:
2463 dockerMount.Source = wsl::shared::string::WideToMultiByte(mount.Source);
2464 dockerMount.Type = "volume";
2465 break;
2466
2467 case WSLCMountTypeTmpfs:
2468 if (mount.TmpfsOptions.has_value())
2469 {
2470 request.HostConfig.Tmpfs[mount.Target] = mount.TmpfsOptions.value();
2471 continue;
2472 }
2473
2474 dockerMount.Type = "tmpfs";
2475 if (mount.TmpfsSizeBytes.has_value() || mount.TmpfsMode.has_value())
2476 {
2477 dockerMount.TmpfsOptions = common::docker_schema::MountTmpfsOptions{
2478 .SizeBytes = mount.TmpfsSizeBytes.value_or(0),
2479 .Mode = mount.TmpfsMode.value_or(0),
2480 };
2481 }
2482 break;
2483 }
2484
2485 request.HostConfig.Mounts.push_back(std::move(dockerMount));
2486 }
2487
2488 request.HostConfig.Binds = std::move(binds);
2489
2490 // Configure GPU support if requested.
2491 if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsGpu))
2492 {
2493 THROW_HR_IF_MSG(
2494 HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED),
2495 !virtualMachine.FeatureEnabled(WslcFeatureFlagsGPU),
2496 "WSLCContainerFlagsGpu requires GPU support enabled on the session");
2497
2498 // Request the WSL GPU device via CDI.
2499 request.HostConfig.DeviceRequests = std::vector<common::docker_schema::DeviceRequest>{{"cdi", {LX_WSLC_GPU_CDI_DEVICE}}};
2500 }
2501
2502 // Prepare port mappings from container options.
2503 std::vector<_WSLCPortMapping> ports;
2504 for (ULONG i = 0; i < containerOptions.PortsCount; i++)
2505 {
2506 auto& port = ports.emplace_back();
2507 port.HostPort = containerOptions.Ports[i].HostPort;
2508 port.ContainerPort = containerOptions.Ports[i].ContainerPort;
2509 port.Family = containerOptions.Ports[i].Family;
2510 port.Protocol = containerOptions.Ports[i].Protocol;
2511 strcpy_s(port.BindingAddress, containerOptions.Ports[i].BindingAddress);
2512 }
2513
2514 // Append exposed ports from the image, if requested.
2515 if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsPublishAll))
2516 {
2517 auto imageInfo = DockerClient.InspectImage(containerOptions.Image);
2518
2519 // Use the resolved image ID so the container is created from the exact same image.
2520 request.Image = imageInfo.Id;
2521
2522 if (imageInfo.Config.has_value() && imageInfo.Config->ExposedPorts.has_value())
2523 {
2524 // The userspace wslrelay port relay only forwards TCP. When it's active, adding a UDP
2525 // mapping would fail the whole container (MapPort throws ERROR_NOT_SUPPORTED), so skip
2526 // UDP exposed ports and warn once. UDP is published normally on the virtioNet path.
2527 const bool relayForwarding = virtualMachine.UseWslRelayPortForwarding();
2528 bool warnedUdpSkipped = false;
2529
2530 for (const auto& [portKey, _] : imageInfo.Config->ExposedPorts.value())
2531 {
2532 auto [port, protocol] = ParseExposedPortKey(portKey);
2533
2534 if (relayForwarding && protocol == IPPROTO_UDP)
2535 {
2536 if (!warnedUdpSkipped)
2537 {
2538 EMIT_USER_WARNING(Localization::MessageWslcPublishAllUdpNotSupported());
2539 warnedUdpSkipped = true;
2540 }
2541
2542 continue;
2543 }
2544
2545 // Exposed ports carry only a port and protocol (tcp/udp), never an address family.
2546 // Mirror Docker's dual-stack default by publishing each exposed port on both the IPv4
2547 // and IPv6 loopback, while keeping wslc's loopback-only default binding convention.
2548 for (const auto& [family, address] : {std::pair{AF_INET, "127.0.0.1"}, std::pair{AF_INET6, "::1"}})
2549 {
2550 auto& createdPort = ports.emplace_back();
2551 createdPort.HostPort = WSLC_EPHEMERAL_PORT;
2552 createdPort.Family = family;
2553 createdPort.ContainerPort = port;
2554 createdPort.Protocol = protocol;
2555 strcpy_s(createdPort.BindingAddress, address);
2556 }
2557 }
2558 }
2559 }
2560
2561 auto networkMode = ResolveNetworkMode(containerOptions.ContainerNetwork.NetworkMode, !ports.empty(), sessionNetworks, DockerClient);
2562
2563 auto endpoints = ResolveEndpoints(
2564 containerOptions.ContainerNetwork.Networks, containerOptions.ContainerNetwork.NetworksCount, networkMode, sessionNetworks);
2565
2566 auto primaryConfig =
2567 ResolveEndpointConfig(containerOptions.ContainerNetwork.Settings, containerOptions.ContainerNetwork.SettingsCount, networkMode);
2568
2569 THROW_HR_WITH_USER_ERROR_IF(
2570 E_INVALIDARG,
2571 Localization::MessageWslcAliasRequiresUserDefinedNetwork(),
2572 primaryConfig.Aliases.has_value() && !NetworkSupportsAliases(networkMode));
2573
2574 const bool hasNonAliasEndpointSettings =
2575 primaryConfig.IPAMConfig.has_value() || primaryConfig.Links.has_value() || primaryConfig.DriverOpts.has_value();
2576 // N.B. NetworkModeAllocatesVmPorts is reused here as the "supports endpoint settings" predicate: modes
2577 // that lack a dedicated netns (host/none/container:*) also can't accept per-endpoint settings.
2578 THROW_HR_WITH_USER_ERROR_IF(
2579 E_INVALIDARG,
2580 Localization::MessageWslcEndpointSettingsRequireNetwork(networkMode),
2581 hasNonAliasEndpointSettings && !NetworkModeAllocatesVmPorts(networkMode));
2582
2583 auto mappedPorts = BuildPortMappings(ports, networkMode, virtualMachine);
2584
2585 request.HostConfig.NetworkMode = networkMode;
2586 request.NetworkingConfig.EndpointsConfig = std::move(endpoints);
2587
2588 const bool hasPrimaryEndpointSettings = primaryConfig.Aliases.has_value() || primaryConfig.IPAMConfig.has_value() ||
2589 primaryConfig.Links.has_value() || primaryConfig.DriverOpts.has_value();
2590 if (NetworkModeAllocatesVmPorts(networkMode) && (!request.NetworkingConfig.EndpointsConfig.empty() || hasPrimaryEndpointSettings))
2591 {
2592 request.NetworkingConfig.EndpointsConfig[networkMode] = std::move(primaryConfig);
2593 }
2594
2595 for (const auto& e : mappedPorts)
2596 {
2597 auto portKey = std::format("{}/{}", e.ContainerPort, e.ProtocolString());
2598 request.ExposedPorts[portKey] = {};
2599
2600 auto& portEntry = request.HostConfig.PortBindings[portKey];
2601
2602 // In host mode, VmPort is empty until the container starts.
2603 // In that networking mode, the host port always matches the vm port.
2604 auto hostPort = e.VmMapping.VmPort ? e.VmMapping.VmPort->Port() : e.VmMapping.HostPort();
2605
2606 // Use catch-all binding address based on the address family. :: binds all ipv6 interfaces, and 0:0:0:0 binds all ipv4 interfaces.
2607 portEntry.emplace_back(common::docker_schema::PortMapping{
2608 .HostIp = e.VmMapping.IsIPv6() ? "::" : "0.0.0.0", .HostPort = std::to_string(hostPort)});
2609 }
2610
2611 auto requestedLabels = ParseKeyValuePairs(containerOptions.Labels, containerOptions.LabelsCount, WSLCContainerMetadataLabel);
2612
2613 // Build WSLC metadata to store in a label for recovery on Open().
2614 WSLCContainerMetadataV1 metadata;
2615 metadata.Flags = containerOptions.Flags;
2616 metadata.InitProcessFlags = containerOptions.InitProcessOptions.Flags;
2617 metadata.Volumes = volumes;
2618
2619 for (const auto& e : mappedPorts)
2620 {
2621 metadata.Ports.emplace_back(e.Serialize());
2622 }
2623
2624 request.Labels[WSLCContainerMetadataLabel] = SerializeContainerMetadata(metadata);
2625 request.Labels.insert(requestedLabels.begin(), requestedLabels.end());
2626
2627 // Docker validates structured bind sources during container creation, so their VM paths must exist here.
2628 // Release the temporary shares before returning; Start remounts them for the container lifetime.
2629 auto result = [&]() {
2630 auto volumeCleanup = MountVolumes(volumes, virtualMachine);
2631 return DockerClient.CreateContainer(request, containerName);
2632 }();
2633
2634 // Surface any warnings returned by Docker (e.g., deprecated features, configuration issues).
2635 for (const auto& warning : result.Warnings)
2636 {
2637 EMIT_USER_WARNING(wsl::shared::string::MultiByteToWide(warning));
2638 }
2639
2640 // Clean up the Docker container if anything below fails.
2641 // N.B. The container ID is captured by value since it is moved into the WSLCContainerImpl constructor below.
2642 auto deleteOnFailure = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&DockerClient, containerId = result.Id]() {
2643 DockerClient.DeleteContainer(containerId, true, true);
2644 });
2645
2646 // Inspect the container to fetch its generated name (if needed) and Docker's authoritative Created timestamp.
2647 auto inspectData = DockerClient.InspectContainer(result.Id);
2648
2649 // Post-create verification: confirm every requested network is actually attached.
2650 // If Docker rejected any endpoint, throw here so deleteOnFailure cleans up the orphan container.
2651 // container:<id> mode shares the target's netns, so the mode string is not a network name and
2652 // won't appear in NetworkSettings.Networks. Skip the check for that mode.
2653 if (!networkMode.starts_with(c_containerNetworkPrefix))
2654 {
2655 THROW_HR_IF_MSG(
2656 E_UNEXPECTED,
2657 !inspectData.NetworkSettings.Networks.contains(networkMode),
2658 "Container was created but primary network '%hs' was not attached",
2659 networkMode.c_str());
2660 }
2661
2662 for (const auto& [name, _] : request.NetworkingConfig.EndpointsConfig)
2663 {
2664 // The primary network was auto-inserted into EndpointsConfig for Docker v1.44 compat
2665 // and is already verified above. Only check explicitly-requested additional networks here.
2666 if (name == networkMode)
2667 {
2668 continue;
2669 }
2670
2671 THROW_HR_IF_MSG(
2672 E_UNEXPECTED,
2673 !inspectData.NetworkSettings.Networks.contains(name),
2674 "Container was created but requested network '%hs' was not attached",
2675 name.c_str());
2676 }
2677
2678 // Collect the names of referenced docker named volumes so Start() can verify
2679 // they are available before running the container.
2680 std::vector<std::string> namedVolumes;
2681 namedVolumes.reserve(containerOptions.NamedVolumesCount + mounts.size());
2682 for (ULONG i = 0; i < containerOptions.NamedVolumesCount; i++)
2683 {
2684 namedVolumes.emplace_back(containerOptions.NamedVolumes[i].Name);
2685 }
2686
2687 for (const auto& mount : mounts)
2688 {
2689 if (mount.MountType == WSLCMountTypeVolume && !mount.Source.empty())
2690 {
2691 namedVolumes.emplace_back(wsl::shared::string::WideToMultiByte(mount.Source));
2692 }
2693 }
2694
2695 auto mergedLabels = StripInternalLabels(std::move(inspectData.Config.Labels));
2696 const auto createdAt = wsl::windows::common::timestamp::Rfc3339ToEpoch(inspectData.Created);
2697
2698 auto container = std::make_shared<WSLCContainerImpl>(
2699 wslcSession,
2700 runtime,
2701 pluginNotifier,
2702 std::move(result.Id),
2703 CleanContainerName(inspectData.Name),
2704 std::string(containerOptions.Image),
2705 std::move(networkMode),
2706 std::move(volumes),
2707 std::move(namedVolumes),
2708 std::move(mappedPorts),
2709 std::move(mergedLabels),
2710 std::move(OnDeleted),
2711 eventStore,
2712 WslcContainerStateCreated,
2713 createdAt,
2714 containerOptions.InitProcessOptions.Flags,
2715 containerOptions.Flags);
2716
2717 container->Initialize();
2718
2719 deleteOnFailure.release();
2720 return container;
2721 }
2722
2723 std::shared_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
2724 const common::docker_schema::ContainerInfo& dockerContainer,
2725 WSLCSession& wslcSession,
2726 WSLCSessionRuntime& runtime,
2727 IWSLCPluginNotifier* pluginNotifier,
2728 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
2729 EventStore& eventStore)
2730 {
2731 auto& virtualMachine = runtime.Vm();
2732 auto& DockerClient = runtime.Docker();
2733
2734 // Extract container name from Docker's names list.
2735 std::string name = ExtractContainerName(dockerContainer.Names, dockerContainer.Id);
2736
2737 // Collect the names of referenced docker named volumes.
2738 std::vector<std::string> namedVolumes;
2739 for (const auto& mount : dockerContainer.Mounts)
2740 {
2741 if (mount.Type == "volume" && !mount.Name.empty())
2742 {
2743 namedVolumes.push_back(mount.Name);
2744 }
2745 }
2746
2747 auto metadataIt = dockerContainer.Labels.find(WSLCContainerMetadataLabel);
2748
2749 THROW_HR_IF_MSG(
2750 E_INVALIDARG,
2751 metadataIt == dockerContainer.Labels.end(),
2752 "Cannot open WSLC container %hs: missing WSLC metadata label",
2753 dockerContainer.Id.c_str());
2754
2755 WI_ASSERT(dockerContainer.State != ContainerState::Running);
2756
2757 auto metadata = ParseContainerMetadata(metadataIt->second.c_str());
2758 auto labels = StripInternalLabels(dockerContainer.Labels);
2759
2760 // Docker treats empty NetworkMode as the default (bridge).
2761 std::string networkMode = dockerContainer.HostConfig.NetworkMode.empty() ? std::string{"bridge"} : dockerContainer.HostConfig.NetworkMode;
2762
2763 RejectUnsupportedNetworkModes(networkMode);
2764
2765 const bool allocateVmPorts = NetworkModeAllocatesVmPorts(networkMode);
2766
2767 // Re-register recovered VM ports in the allocation pool to prevent conflicts.
2768 std::vector<ContainerPortMapping> ports;
2769 for (const auto& e : metadata.Ports)
2770 {
2771 auto& inserted = ports.emplace_back(ContainerPortMapping{VMPortMapping::FromContainerMetaData(e), e.ContainerPort});
2772
2773 if (allocateVmPorts)
2774 {
2775 auto allocation = virtualMachine.TryAllocatePort(e.VmPort, e.Family, e.Protocol);
2776
2777 THROW_HR_IF_MSG(
2778 HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS),
2779 !allocation,
2780 "Port %hu is in use, cannot open container %hs",
2781 e.VmPort,
2782 dockerContainer.Id.c_str());
2783
2784 inserted.VmMapping.AssignVmPort(allocation);
2785 }
2786 }
2787
2788 auto container = std::make_shared<WSLCContainerImpl>(
2789 wslcSession,
2790 runtime,
2791 pluginNotifier,
2792 std::string(dockerContainer.Id),
2793 std::move(name),
2794 std::string(dockerContainer.Image),
2795 std::move(networkMode),
2796 std::move(metadata.Volumes),
2797 std::move(namedVolumes),
2798 std::move(ports),
2799 std::move(labels),
2800 std::move(OnDeleted),
2801 eventStore,
2802 DockerStateToWSLCState(dockerContainer.State),
2803 dockerContainer.Created,
2804 metadata.InitProcessFlags,
2805 metadata.Flags);
2806
2807 container->Initialize();
2808
2809 // Restore the state change timestamp from Docker inspect data.
2810 try
2811 {
2812 auto inspectData = DockerClient.InspectContainer(dockerContainer.Id);
2813 auto state = DockerStateToWSLCState(dockerContainer.State);
2814
2815 if (state == WslcContainerStateCreated)
2816 {
2817 // A created-but-never-started container has no StartedAt/FinishedAt; its state last
2818 // changed when it was created.
2819 container->m_stateChangedAt = dockerContainer.Created;
2820 }
2821 else
2822 {
2823 const auto& timestamp = (state == WslcContainerStateRunning) ? inspectData.State.StartedAt : inspectData.State.FinishedAt;
2824
2825 if (!timestamp.empty() && timestamp != c_unsetTimestamp)
2826 {
2827 container->m_stateChangedAt = wsl::windows::common::timestamp::Rfc3339ToEpoch(timestamp);
2828 }
2829 }
2830 }
2831 catch (...)
2832 {
2833 LOG_CAUGHT_EXCEPTION();
2834 EMIT_USER_WARNING(wsl::shared::Localization::MessageWslcContainerTimestampRecoveryFailed(
2835 wsl::shared::string::MultiByteToWide(dockerContainer.Id)));
2836 }
2837
2838 return container;
2839 }
2840
2841 const std::string& WSLCContainerImpl::ID() const noexcept
2842 {
2843 return m_id;
2844 }
2845
2846 void WSLCContainerImpl::Inspect(BOOL Size, LPSTR* Output) const
2847 {
2848 auto lock = m_lock.lock_shared();
2849
2850 try
2851 {
2852 *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(InspectLockHeld(!!Size).c_str()).release();
2853 }
2854 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to inspect container '%hs'", m_id.c_str());
2855 }
2856
2857 std::string WSLCContainerImpl::InspectLockHeld(bool Size) const
2858 {
2859 // Get Docker inspect data
2860 auto dockerInspect = m_runtime.Docker().InspectContainer(m_id, Size);
2861
2862 // Convert to WSLC schema
2863 auto wslcInspect = BuildInspectContainer(dockerInspect);
2864
2865 // Serialize WSLC schema to JSON
2866 return wsl::shared::ToJson(wslcInspect);
2867 }
2868
2869 void WSLCContainerImpl::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, LONGLONG Since, LONGLONG Until, ULONGLONG Tail) const
2870 {
2871 auto lock = m_lock.lock_shared();
2872
2873 wil::unique_socket socket;
2874 try
2875 {
2876 socket = m_runtime.Docker().ContainerLogs(m_id, Flags, Since, Until, Tail);
2877 }
2878 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to get logs from '%hs'", m_id.c_str());
2879
2880 if (WI_IsFlagSet(m_initProcessFlags, WSLCProcessFlagsTty))
2881 {
2882 // For tty processes, simply relay the HTTP chunks.
2883 auto [ttyRead, ttyWrite] = common::wslutil::OpenAnonymousPipe(0, true, true);
2884
2885 auto handle = std::make_unique<RelayHandle<HTTPChunkBasedReadHandle>>(std::move(socket), std::move(ttyWrite));
2886 m_runtime.Relay()->AddHandle(std::move(handle));
2887
2888 *Stdout = common::wslutil::ToCOMOutputHandle(ttyRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe);
2889 }
2890 else
2891 {
2892 // For non-tty process, stdout & stderr are multiplexed.
2893 auto [stdoutRead, stdoutWrite] = common::wslutil::OpenAnonymousPipe(0, true, true);
2894 auto [stderrRead, stderrWrite] = common::wslutil::OpenAnonymousPipe(0, true, true);
2895
2896 auto handle = std::make_unique<DockerIORelayHandle>(
2897 std::move(socket), std::move(stdoutWrite), std::move(stderrWrite), DockerIORelayHandle::Format::HttpChunked);
2898
2899 m_runtime.Relay()->AddHandle(std::move(handle));
2900
2901 *Stdout = common::wslutil::ToCOMOutputHandle(stdoutRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe);
2902 *Stderr = common::wslutil::ToCOMOutputHandle(stderrRead.get(), GENERIC_READ | SYNCHRONIZE, WSLCHandleTypePipe);
2903 }
2904 }
2905
2906 void WSLCContainerImpl::Stats(LPSTR* Output) const
2907 {
2908 auto lock = m_lock.lock_shared();
2909
2910 try
2911 {
2912 auto stats = m_runtime.Docker().ContainerStats(m_id);
2913
2914 // Always inject the authoritative id and name from this instance.
2915 // The response may omit them or use inconsistent casing.
2916 stats.id = m_id;
2917 stats.name = m_name;
2918
2919 std::string json = wsl::shared::ToJson(stats);
2920 *Output = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(json.c_str()).release();
2921 }
2922 CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to get stats for container '%hs'", m_id.c_str());
2923 }
2924
2925 std::unique_ptr<RelayedProcessIO> WSLCContainerImpl::CreateRelayedProcessIO(wil::shared_socket stream, WSLCProcessFlags flags)
2926 {
2927 // Create one pipe for each STD handle.
2928 std::vector<std::unique_ptr<OverlappedIOHandle>> ioHandles;
2929 std::map<ULONG, TypedHandle> fds;
2930
2931 // This is required for docker to know when stdin is closed.
2932 auto closeStdin = [stream]() { LOG_LAST_ERROR_IF(shutdown(stream.get(), SD_SEND) == SOCKET_ERROR); };
2933
2934 if (WI_IsFlagSet(flags, WSLCProcessFlagsStdin))
2935 {
2936 auto [stdinRead, stdinWrite] = common::wslutil::OpenAnonymousPipe(LX_RELAY_BUFFER_SIZE, true, true);
2937 ioHandles.emplace_back(std::make_unique<RelayHandle<ReadHandle>>(
2938 HandleWrapper{std::move(stdinRead), std::move(closeStdin)}, HandleWrapper{stream}));
2939
2940 fds.emplace(WSLCFDStdin, TypedHandle{wil::unique_handle{stdinWrite.release()}, WSLCHandleTypePipe});
2941 }
2942 else
2943 {
2944 // If stdin is not attached, close it now to make sure no one tries to write to it.
2945 closeStdin();
2946 }
2947
2948 auto [stdoutRead, stdoutWrite] = common::wslutil::OpenAnonymousPipe(LX_RELAY_BUFFER_SIZE, true, true);
2949 auto [stderrRead, stderrWrite] = common::wslutil::OpenAnonymousPipe(LX_RELAY_BUFFER_SIZE, true, true);
2950
2951 fds.emplace(WSLCFDStdout, TypedHandle{wil::unique_handle{stdoutRead.release()}, WSLCHandleTypePipe});
2952 fds.emplace(WSLCFDStderr, TypedHandle{wil::unique_handle{stderrRead.release()}, WSLCHandleTypePipe});
2953
2954 ioHandles.emplace_back(std::make_unique<DockerIORelayHandle>(
2955 HandleWrapper{stream}, std::move(stdoutWrite), std::move(stderrWrite), common::io::DockerIORelayHandle::Format::Raw));
2956
2957 m_runtime.Relay()->AddHandles(std::move(ioHandles));
2958
2959 return std::make_unique<RelayedProcessIO>(std::move(fds));
2960 }
2961
2962 void WSLCContainerImpl::MapPorts()
2963 {
2964 std::map<uint16_t, std::shared_ptr<VmPortAllocation>> allocatedPorts;
2965
2966 for (auto& e : m_mappedPorts)
2967 {
2968 // VmPort is empty when the container is using host mode.
2969 // In that case, allocate the VM ports to match the container ports.
2970 if (!e.VmMapping.VmPort)
2971 {
2972 // Reuse existing vm port allocation when possible.
2973 // This is required because the same container can bind the port number for different families or protocols.
2974 auto existing = allocatedPorts.find(e.ContainerPort);
2975 if (existing != allocatedPorts.end())
2976 {
2977 e.VmMapping.AssignVmPort(existing->second);
2978 }
2979 else
2980 {
2981 auto allocatedPort =
2982 m_runtime.Vm().TryAllocatePort(e.ContainerPort, e.VmMapping.BindAddress.si_family, e.VmMapping.Protocol);
2983
2984 THROW_HR_WITH_USER_ERROR_IF(
2985 HRESULT_FROM_WIN32(WSAEADDRINUSE), wsl::shared::Localization::MessageWslcPortInUse(FormatPortEndpoint(e), m_id), !allocatedPort);
2986
2987 e.VmMapping.AssignVmPort(allocatedPort);
2988
2989 allocatedPorts.emplace(e.ContainerPort, allocatedPort);
2990 }
2991 }
2992
2993 try
2994 {
2995 m_runtime.Vm().MapPort(e.VmMapping);
2996 }
2997 catch (...)
2998 {
2999 auto result = wil::ResultFromCaughtException();
3000 if (result == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS) || result == HRESULT_FROM_WIN32(WSAEADDRINUSE))
3001 {
3002 THROW_HR_WITH_USER_ERROR(
3003 HRESULT_FROM_WIN32(WSAEADDRINUSE), wsl::shared::Localization::MessageWslcPortInUse(FormatPortEndpoint(e), m_id));
3004 }
3005 throw;
3006 }
3007 }
3008 }
3009
3010 void WSLCContainerImpl::UnmapPorts()
3011 {
3012 for (auto& e : m_mappedPorts)
3013 {
3014 try
3015 {
3016 e.VmMapping.Unmap();
3017 }
3018 CATCH_LOG();
3019
3020 try
3021 {
3022 if (m_networkMode == "host")
3023 {
3024 e.VmMapping.VmPort.reset();
3025 }
3026 }
3027 CATCH_LOG();
3028 }
3029 }
3030
3031 __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::ReleaseProcesses()
3032 {
3033 // Snapshot under the lock, then notify outside it, pinning each control via lock() first
3034 decltype(m_processes) processes;
3035 {
3036 std::lock_guard processesLock{m_processesLock};
3037 processes = std::exchange(m_processes, {});
3038 }
3039
3040 // Notify all processes that the container has exited.
3041 // The exec callback isn't always sent to execed processes, so do this to avoid 'stuck' processes.
3042 for (auto& process : processes)
3043 {
3044 if (auto control = process.lock())
3045 {
3046 control->OnContainerReleased();
3047 }
3048 }
3049 }
3050
3051 __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::ReleaseRuntimeResources()
3052 {
3053 WSL_LOG("ReleaseRuntimeResources", TraceLoggingValue(m_id.c_str(), "ID"));
3054
3055 m_runtimeResourcesHeld = false;
3056
3057 // Release runtime resources (port relays, volume mounts) that were set up at Start().
3058 UnmapPorts();
3059
3060 // A VM that already exited (crash / external kill) has dropped every guest mount, so calling
3061 // UnmountWindowsFolder would only block on the RPC timeout and emit spurious unmount-failed
3062 // warnings. Mark the mounts inactive without touching the dead VM.
3063 //
3064 // The same applies when there is no VM at all: after a graceful idle teardown the VM object is
3065 // released without the exit event ever being signaled, so VmExited() is false while HasVm() is
3066 // false too. m_runtime.Vm() would throw on that state, and this runs from ~WSLCContainerImpl.
3067 if (m_runtime.VmExited() || !m_runtime.HasVm())
3068 {
3069 for (auto& volume : m_mountedVolumes)
3070 {
3071 volume.Mounted = false;
3072 }
3073 }
3074 else
3075 {
3076 UnmountVolumes(m_mountedVolumes, m_runtime.Vm());
3077 }
3078 }
3079
3080 __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::ReleaseResources()
3081 {
3082 WSL_LOG("ReleaseResources", TraceLoggingValue(m_id.c_str(), "ID"));
3083
3084 ReleaseRuntimeResources();
3085
3086 // Release VM port allocations back to the pool.
3087 for (auto& e : m_mappedPorts)
3088 {
3089 e.VmMapping.VmPort.reset();
3090 }
3091
3092 return PrepareDisconnectComWrapper();
3093 }
3094
3095 __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::PrepareDisconnectComWrapper()
3096 {
3097 if (m_comWrapper)
3098 {
3099 // Cache read-only properties in the COM wrapper before disconnecting,
3100 // so callers can still query state/process after the impl is gone.
3101 {
3102 std::lock_guard processesLock{m_processesLock};
3103 m_comWrapper->CacheState(m_id, m_name, m_state, m_initProcess);
3104 }
3105 }
3106
3107 return unique_com_disconnect{std::exchange(m_comWrapper, nullptr)};
3108 }
3109
3110 __requires_lock_held(m_lock) void WSLCContainerImpl::CommitState(WSLCContainerState State, std::int64_t Time, std::optional<int> ExitCode) noexcept
3111 {
3112 // N.B. A deleted container cannot transition back to any other state.
3113 WI_ASSERT(m_state != WslcContainerStateDeleted);
3114
3115 WSL_LOG(
3116 "ContainerStateChange",
3117 TraceLoggingValue(static_cast<int>(m_state), "PreviousState"),
3118 TraceLoggingValue(static_cast<int>(State), "NewState"),
3119 TraceLoggingValue(m_id.c_str(), "ID"));
3120
3121 m_state = State;
3122 m_stateGeneration++;
3123 m_stateChangedAt = Time;
3124
3125 RecordEvent(WSLCStateToEventAction(State), Time, ExitCode);
3126
3127 if (State == WslcContainerStateRunning)
3128 {
3129 // The restart's start phase landed, so a later exit must auto-delete an --rm container again.
3130 m_restart.reset();
3131 }
3132
3133 // Keep the VM alive while this container is Running and release the hold once it leaves that
3134 // state, even when no client holds the wrapper (e.g. a detached `run -d` container). Dropping
3135 // the hold on the transition out of Running is what lets an otherwise-idle VM be torn down; a
3136 // Created or Exited container does not pin the VM, since its metadata survives teardown.
3137 UpdateActivityHoldLockHeld();
3138 }
3139
3140 __requires_lock_held(m_lock) void WSLCContainerImpl::UpdateActivityHoldLockHeld() noexcept
3141 {
3142 const bool active = (m_state == WslcContainerStateRunning);
3143 if (active && !m_activityHold)
3144 {
3145 m_activityHold = ActivityRef(m_wslcSession.Runtime().IdleStateShared());
3146 }
3147 else if (!active && m_activityHold)
3148 {
3149 m_activityHold.reset();
3150 }
3151 }
3152
3153 WSLCContainer::WSLCContainer(WSLCSession& session, std::function<void(const WSLCContainerImpl*)>&& OnDeleted) :
3154 m_session(session), m_onDeleted(std::move(OnDeleted))
3155 {
3156 }
3157
3158 HRESULT WSLCContainer::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* Stdout, WSLCHandle* Stderr)
3159 try
3160 {
3161 WSLCExecutionContext context(&m_session);
3162
3163 RETURN_HR_IF_NULL(E_POINTER, Stdin);
3164 RETURN_HR_IF_NULL(E_POINTER, Stdout);
3165 RETURN_HR_IF_NULL(E_POINTER, Stderr);
3166
3167 *Stdin = {};
3168 *Stdout = {};
3169 *Stderr = {};
3170
3171 auto vmLease = m_session.Runtime().AcquireVmLease();
3172 return CallImpl(&WSLCContainerImpl::Attach, DetachKeys, Stdin, Stdout, Stderr);
3173 }
3174 CATCH_RETURN();
3175
3176 HRESULT WSLCContainer::GetState(WSLCContainerState* Result)
3177 {
3178 WSLCExecutionContext context(&m_session);
3179 RETURN_HR_IF_NULL(E_POINTER, Result);
3180
3181 *Result = WslcContainerStateInvalid;
3182 HRESULT hr = CallImpl(&WSLCContainerImpl::GetState, Result);
3183 if (SUCCEEDED(hr))
3184 {
3185 return S_OK;
3186 }
3187
3188 // PrepareDisconnectComWrapper() populates the cache before setting m_impl to null,
3189 // so if CallImpl failed with RPC_E_DISCONNECTED, the cache must be populated.
3190 if (hr == RPC_E_DISCONNECTED)
3191 {
3192 auto cacheLock = m_cacheLock.lock_shared();
3193 if (WI_VERIFY(m_cachedState.has_value()))
3194 {
3195 *Result = m_cachedState.value();
3196 return S_OK;
3197 }
3198 }
3199
3200 return hr;
3201 }
3202
3203 HRESULT WSLCContainer::GetInitProcess(IWSLCProcess** Process)
3204 {
3205 WSLCExecutionContext context(&m_session);
3206
3207 RETURN_HR_IF_NULL(E_POINTER, Process);
3208
3209 *Process = nullptr;
3210
3211 HRESULT hr = CallImpl(&WSLCContainerImpl::GetInitProcess, Process);
3212 if (SUCCEEDED(hr))
3213 {
3214 return S_OK;
3215 }
3216
3217 // PrepareDisconnectComWrapper() populates the cache before setting m_impl to null,
3218 // so if CallImpl failed with RPC_E_DISCONNECTED, the cache must be populated.
3219 if (hr == RPC_E_DISCONNECTED)
3220 {
3221 auto cacheLock = m_cacheLock.lock_shared();
3222 if (m_cachedInitProcess)
3223 {
3224 return m_cachedInitProcess.CopyTo(__uuidof(IWSLCProcess), (void**)Process);
3225 }
3226 }
3227
3228 return hr;
3229 }
3230
3231 HRESULT WSLCContainer::Exec(const WSLCProcessOptions* Options, const WSLCProcessStartOptions* StartOptions, IWSLCProcess** Process)
3232 try
3233 {
3234 WSLCExecutionContext context(&m_session);
3235
3236 RETURN_HR_IF_NULL(E_POINTER, Options);
3237 RETURN_HR_IF_NULL(E_POINTER, Process);
3238 RETURN_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Options->Flags, ~WSLCProcessFlagsValid), "Invalid flags: 0x%x", Options->Flags);
3239
3240 *Process = nullptr;
3241
3242 auto vmLease = m_session.Runtime().AcquireVmLease();
3243 return CallImpl(&WSLCContainerImpl::Exec, Options, StartOptions, Process);
3244 }
3245 CATCH_RETURN();
3246
3247 HRESULT WSLCContainer::Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds)
3248 try
3249 {
3250 WSLCExecutionContext context(&m_session);
3251
3252 // Hold a VM lease for the whole operation: --rm containers self-delete during Stop, which
3253 // disconnects the wrapper and drops activity. Without the lease, the idle worker can fire
3254 // during the post-stop destroy wait (up to 60s) and tear the VM down mid-call.
3255 auto vmLease = m_session.Runtime().AcquireVmLease();
3256 return CallImpl(&WSLCContainerImpl::Stop, Signal, TimeoutSeconds, false);
3257 }
3258 CATCH_RETURN();
3259
3260 HRESULT WSLCContainer::Kill(_In_ WSLCSignal Signal)
3261 try
3262 {
3263 WSLCExecutionContext context(&m_session);
3264
3265 // Hold a VM lease for the same reason as Stop(): --rm can self-delete and drop activity.
3266 auto vmLease = m_session.Runtime().AcquireVmLease();
3267 return CallImpl(&WSLCContainerImpl::Stop, Signal, {}, true);
3268 }
3269 CATCH_RETURN();
3270
3271 HRESULT WSLCContainer::Restart(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds, IWarningCallback* WarningCallback)
3272 try
3273 {
3274 WSLCExecutionContext context(&m_session, WarningCallback);
3275
3276 // Hold a VM lease across both phases: the container is not Running in between, so nothing else
3277 // keeps the VM alive.
3278 auto vmLease = m_session.Runtime().AcquireVmLease();
3279 return CallImpl(&WSLCContainerImpl::Restart, Signal, TimeoutSeconds);
3280 }
3281 CATCH_RETURN();
3282
3283 HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, IWarningCallback* WarningCallback)
3284 try
3285 {
3286 WSLCExecutionContext context(&m_session, WarningCallback);
3287
3288 THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCContainerStartFlagsValid), "Invalid flags: 0x%x", Flags);
3289
3290 auto vmLease = m_session.Runtime().AcquireVmLease();
3291 return CallImpl(&WSLCContainerImpl::Start, Flags, StartOptions);
3292 }
3293 CATCH_RETURN();
3294
3295 HRESULT WSLCContainer::Inspect(BOOL Size, LPSTR* Output)
3296 try
3297 {
3298 WSLCExecutionContext context(&m_session);
3299
3300 RETURN_HR_IF_NULL(E_POINTER, Output);
3301
3302 *Output = nullptr;
3303
3304 auto vmLease = m_session.Runtime().AcquireVmLease();
3305 return CallImpl(&WSLCContainerImpl::Inspect, Size, Output);
3306 }
3307 CATCH_RETURN();
3308
3309 HRESULT WSLCContainer::Stats(LPSTR* Output)
3310 try
3311 {
3312 WSLCExecutionContext context(&m_session);
3313
3314 RETURN_HR_IF(E_POINTER, Output == nullptr);
3315
3316 *Output = nullptr;
3317
3318 auto vmLease = m_session.Runtime().AcquireVmLease();
3319 return CallImpl(&WSLCContainerImpl::Stats, Output);
3320 }
3321 CATCH_RETURN();
3322
3323 HRESULT WSLCContainer::Delete(WSLCDeleteFlags Flags)
3324 try
3325 {
3326 WSLCExecutionContext context(&m_session);
3327
3328 THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCDeleteFlagsValid), "Invalid flags: 0x%x", Flags);
3329
3330 // Special case for Delete(): If deletion is successful, notify the WSLCSession that the container has been deleted.
3331 // Hold a VM lease across the whole operation: deleting a container makes it inactive and
3332 // can trigger an idle teardown. Without the lease the idle worker could take the session
3333 // lock exclusively and clear m_containers (destroying this container) concurrently, racing
3334 // the delete and inverting the container->session lock order.
3335 auto vmLease = m_session.Runtime().AcquireVmLease();
3336 auto [lock, impl] = LockImpl();
3337
3338 impl->Delete(Flags);
3339 m_onDeleted(impl.get());
3340
3341 return S_OK;
3342 }
3343 CATCH_RETURN();
3344
3345 void WSLCContainer::CacheState(const std::string& id, const std::string& name, WSLCContainerState state, const Microsoft::WRL::ComPtr<IWSLCProcess>& initProcess) noexcept
3346 try
3347 {
3348 auto cacheLock = m_cacheLock.lock_exclusive();
3349
3350 // CacheState must only be called once, during PrepareDisconnectComWrapper().
3351 WI_ASSERT(!m_cachedState.has_value());
3352
3353 m_cachedId = id;
3354 m_cachedName = name;
3355 m_cachedState = state;
3356 m_cachedInitProcess = initProcess;
3357 }
3358 CATCH_LOG();
3359
3360 HRESULT WSLCContainer::Export(WSLCHandle TarHandle)
3361 try
3362 {
3363 WSLCExecutionContext context(&m_session);
3364
3365 auto vmLease = m_session.Runtime().AcquireVmLease();
3366 return CallImpl(&WSLCContainerImpl::Export, TarHandle);
3367 }
3368 CATCH_RETURN();
3369
3370 HRESULT WSLCContainer::UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize)
3371 try
3372 {
3373 WSLCExecutionContext context(&m_session);
3374
3375 RETURN_HR_IF(E_POINTER, DestPath == nullptr);
3376 RETURN_HR_IF(E_INVALIDARG, DestPath[0] == '\0');
3377
3378 auto vmLease = m_session.Runtime().AcquireVmLease();
3379 return CallImpl(&WSLCContainerImpl::UploadArchive, TarHandle, DestPath, ContentSize);
3380 }
3381 CATCH_RETURN();
3382
3383 HRESULT WSLCContainer::DownloadArchive(LPCSTR SrcPath, WSLCHandle OutHandle)
3384 try
3385 {
3386 WSLCExecutionContext context(&m_session);
3387
3388 RETURN_HR_IF(E_POINTER, SrcPath == nullptr);
3389 RETURN_HR_IF(E_INVALIDARG, SrcPath[0] == '\0');
3390
3391 auto vmLease = m_session.Runtime().AcquireVmLease();
3392 return CallImpl(&WSLCContainerImpl::DownloadArchive, SrcPath, OutHandle);
3393 }
3394 CATCH_RETURN();
3395
3396 HRESULT WSLCContainer::Logs(WSLCLogsFlags Flags, WSLCHandle* Stdout, WSLCHandle* Stderr, LONGLONG Since, LONGLONG Until, ULONGLONG Tail)
3397 try
3398 {
3399 WSLCExecutionContext context(&m_session);
3400 RETURN_HR_IF(E_POINTER, Stdout == nullptr || Stderr == nullptr);
3401
3402 THROW_HR_IF_MSG(E_INVALIDARG, WI_IsAnyFlagSet(Flags, ~WSLCLogsFlagsValid), "Invalid flags: 0x%x", Flags);
3403
3404 *Stdout = {};
3405 *Stderr = {};
3406
3407 auto vmLease = m_session.Runtime().AcquireVmLease();
3408 return CallImpl(&WSLCContainerImpl::Logs, Flags, Stdout, Stderr, Since, Until, Tail);
3409 }
3410 CATCH_RETURN();
3411
3412 HRESULT WSLCContainer::GetId(WSLCContainerId Id)
3413 try
3414 {
3415 WSLCExecutionContext context(&m_session);
3416
3417 RETURN_HR_IF_NULL(E_POINTER, Id);
3418
3419 const auto hr = wil::ResultFromException([&] {
3420 auto [lock, impl] = LockImpl();
3421 WI_VERIFY(strcpy_s(Id, std::size<char>(WSLCContainerId{}), impl->ID().c_str()) == 0);
3422 });
3423
3424 RETURN_HR_IF(hr, hr != RPC_E_DISCONNECTED);
3425
3426 // PrepareDisconnectComWrapper() populates the cache before setting m_impl to null,
3427 // so if LockImpl failed with RPC_E_DISCONNECTED, the cache must be populated.
3428 auto cacheLock = m_cacheLock.lock_shared();
3429 if (WI_VERIFY(m_cachedId.has_value()))
3430 {
3431 WI_VERIFY(strcpy_s(Id, std::size<char>(WSLCContainerId{}), m_cachedId->c_str()) == 0);
3432 return S_OK;
3433 }
3434
3435 return hr;
3436 }
3437 CATCH_RETURN();
3438
3439 HRESULT WSLCContainer::GetName(LPSTR* Name)
3440 try
3441 {
3442 WSLCExecutionContext context(&m_session);
3443
3444 RETURN_HR_IF_NULL(E_POINTER, Name);
3445 *Name = nullptr;
3446
3447 const auto hr = wil::ResultFromException([&] {
3448 auto [lock, impl] = LockImpl();
3449 *Name = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(impl->Name().c_str()).release();
3450 });
3451
3452 RETURN_HR_IF(hr, hr != RPC_E_DISCONNECTED);
3453
3454 // PrepareDisconnectComWrapper() populates the cache before setting m_impl to null,
3455 // so if LockImpl failed with RPC_E_DISCONNECTED, the cache must be populated.
3456 auto cacheLock = m_cacheLock.lock_shared();
3457 if (WI_VERIFY(m_cachedName.has_value()))
3458 {
3459 *Name = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(m_cachedName->c_str()).release();
3460 return S_OK;
3461 }
3462
3463 return hr;
3464 }
3465 CATCH_RETURN();
3466
3467 void WSLCContainerImpl::GetLabels(WSLCLabelInformation** Labels, ULONG* Count) const
3468 {
3469 auto lock = m_lock.lock_shared();
3470
3471 if (m_labels.empty())
3472 {
3473 *Labels = nullptr;
3474 *Count = 0;
3475 return;
3476 }
3477
3478 // Build labels locally using RAII strings. If an allocation throws mid-loop,
3479 // the vector destructor frees everything already built.
3480 std::vector<std::pair<wil::unique_cotaskmem_ansistring, wil::unique_cotaskmem_ansistring>> localLabels;
3481 localLabels.reserve(m_labels.size());
3482
3483 for (const auto& [key, value] : m_labels)
3484 {
3485 localLabels.emplace_back(
3486 wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(key.c_str()),
3487 wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(value.c_str()));
3488 }
3489
3490 // All strings built successfully — allocate output array and transfer ownership.
3491 auto labelsArray = wil::make_unique_cotaskmem<WSLCLabelInformation[]>(localLabels.size());
3492 for (size_t i = 0; i < localLabels.size(); ++i)
3493 {
3494 labelsArray[i].Key = localLabels[i].first.release();
3495 labelsArray[i].Value = localLabels[i].second.release();
3496 }
3497
3498 *Count = static_cast<ULONG>(localLabels.size());
3499 *Labels = labelsArray.release();
3500 }
3501
3502 void WSLCContainerImpl::ConnectToNetwork(const WSLCNetworkConnectionOptions* Options)
3503 {
3504 THROW_HR_IF(E_POINTER, Options == nullptr);
3505
3506 THROW_HR_WITH_USER_ERROR_IF(
3507 E_INVALIDARG, Localization::MessageWslcNetworkNameRequired(), !Options->NetworkName || strlen(Options->NetworkName) == 0);
3508
3509 auto endpointConfig = ResolveEndpointConfig(Options->Settings, Options->SettingsCount, Options->NetworkName);
3510
3511 auto lock = m_lock.lock_shared();
3512
3513 THROW_HR_WITH_USER_ERROR_IF(
3514 E_INVALIDARG, Localization::MessageWslcNetworkModeNoAdditionalNetworks(m_networkMode), !NetworkModeAllocatesVmPorts(m_networkMode));
3515
3516 common::docker_schema::ContainerNetworkRequest request{};
3517 request.Container = m_id;
3518 request.EndpointConfig = std::move(endpointConfig);
3519
3520 try
3521 {
3522 m_runtime.Docker().ConnectContainerToNetwork(Options->NetworkName, request);
3523 }
3524 catch (const DockerHTTPException& e)
3525 {
3526 THROW_HR_WITH_USER_ERROR_IF(
3527 WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(Options->NetworkName), e.StatusCode() == 404);
3528 THROW_DOCKER_USER_ERROR_MSG(e, "Failed to connect container '%hs' to network '%hs'", m_id.c_str(), Options->NetworkName);
3529 }
3530
3531 WSL_LOG(
3532 "ContainerConnectedToNetwork",
3533 TraceLoggingValue(m_id.c_str(), "ContainerId"),
3534 TraceLoggingValue(Options->NetworkName, "NetworkName"));
3535 }
3536
3537 void WSLCContainerImpl::DisconnectFromNetwork(LPCSTR NetworkName)
3538 {
3539 THROW_HR_IF(E_POINTER, NetworkName == nullptr);
3540 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcNetworkNameRequired(), strlen(NetworkName) == 0);
3541
3542 auto lock = m_lock.lock_shared();
3543
3544 THROW_HR_WITH_USER_ERROR_IF(
3545 E_INVALIDARG, Localization::MessageWslcNetworkModeNoAdditionalNetworks(m_networkMode), !NetworkModeAllocatesVmPorts(m_networkMode));
3546
3547 common::docker_schema::ContainerNetworkRequest request{};
3548 request.Container = m_id;
3549
3550 try
3551 {
3552 m_runtime.Docker().DisconnectContainerFromNetwork(NetworkName, request);
3553 }
3554 catch (const DockerHTTPException& e)
3555 {
3556 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_NETWORK_NOT_FOUND, Localization::MessageWslcNetworkNotFound(NetworkName), e.StatusCode() == 404);
3557 THROW_DOCKER_USER_ERROR_MSG(e, "Failed to disconnect container '%hs' from network '%hs'", m_id.c_str(), NetworkName);
3558 }
3559
3560 WSL_LOG(
3561 "ContainerDisconnectedFromNetwork",
3562 TraceLoggingValue(m_id.c_str(), "ContainerId"),
3563 TraceLoggingValue(NetworkName, "NetworkName"));
3564 }
3565
3566 HRESULT WSLCContainer::GetLabels(WSLCLabelInformation** Labels, ULONG* Count)
3567 try
3568 {
3569 WSLCExecutionContext context(&m_session);
3570
3571 RETURN_HR_IF(E_POINTER, Labels == nullptr || Count == nullptr);
3572
3573 *Count = 0;
3574 *Labels = nullptr;
3575 return CallImpl(&WSLCContainerImpl::GetLabels, Labels, Count);
3576 }
3577 CATCH_RETURN();
3578
3579 HRESULT WSLCContainer::ConnectToNetwork(const WSLCNetworkConnectionOptions* Options)
3580 try
3581 {
3582 COMServiceExecutionContext context;
3583
3584 auto vmLease = m_session.Runtime().AcquireVmLease();
3585 return CallImpl(&WSLCContainerImpl::ConnectToNetwork, Options);
3586 }
3587 CATCH_RETURN();
3588
3589 HRESULT WSLCContainer::DisconnectFromNetwork(LPCSTR NetworkName)
3590 try
3591 {
3592 COMServiceExecutionContext context;
3593
3594 auto vmLease = m_session.Runtime().AcquireVmLease();
3595 return CallImpl(&WSLCContainerImpl::DisconnectFromNetwork, NetworkName);
3596 }
3597 CATCH_RETURN();
3598
3599 HRESULT WSLCContainer::InterfaceSupportsErrorInfo(REFIID riid)
3600 {
3601 return riid == __uuidof(IWSLCContainer) || riid == __uuidof(IWSLCCompatContainer) ? S_OK : S_FALSE;
3602 }
3603
3604 HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags)
3605 {
3606 return Start(Flags, nullptr, nullptr);
3607 }
3608
3609 HRESULT WSLCContainer::GetInitProcess(IWSLCCompatProcess** Process)
3610 try
3611 {
3612 RETURN_HR_IF_NULL(E_POINTER, Process);
3613 *Process = nullptr;
3614
3615 Microsoft::WRL::ComPtr<IWSLCProcess> process;
3616 RETURN_IF_FAILED(GetInitProcess(&process));
3617 RETURN_HR_IF_NULL(E_UNEXPECTED, process);
3618
3619 return process.CopyTo(Process);
3620 }
3621 CATCH_RETURN();
3622
3623 HRESULT WSLCContainer::Exec(const WSLCCompatProcessOptions* Options, IWSLCCompatProcess** Process)
3624 try
3625 {
3626 RETURN_HR_IF_NULL(E_POINTER, Options);
3627 RETURN_HR_IF_NULL(E_POINTER, Process);
3628 *Process = nullptr;
3629
3630 const auto options = apicompat::Convert(*Options);
3631
3632 Microsoft::WRL::ComPtr<IWSLCProcess> process;
3633 RETURN_IF_FAILED(Exec(&options, nullptr, &process));
3634 RETURN_HR_IF_NULL(E_UNEXPECTED, process);
3635
3636 return process.CopyTo(Process);
3637 }
3638 CATCH_RETURN();
3639
3640 HRESULT WSLCContainer::Inspect(LPSTR* Output)
3641 try
3642 {
3643 return Inspect(FALSE, Output);
3644 }
3645 CATCH_RETURN();