master
cpp 898 lines 31.5 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 ContainerService.cpp
8
9 Abstract:
10
11 This file contains the ContainerService implementation
12
13 --*/
14
15 #include <precomp.h>
16 #include "ContainerService.h"
17 #include "ConsoleService.h"
18 #include "ImageService.h"
19 #include "ImageProgressCallback.h"
20 #include "WarningCallback.h"
21 #include <wslutil.h>
22 #include <HandleConsoleProgressBar.h>
23 #include <WSLCProcessLauncher.h>
24 #include <WSLCContainerEntry.h>
25 #include <ConsoleState.h>
26 #include <CommandLine.h>
27 #include <WSLCUserSettings.h>
28 #include <filesystem>
29 #include <unordered_map>
30 #include <wslc.h>
31
32 namespace wsl::windows::wslc::services {
33 namespace mount = wsl::windows::common::mount;
34
35 using wsl::windows::common::ClientRunningWSLCProcess;
36 using wsl::windows::common::wslc_schema::InspectContainer;
37 using namespace wsl::windows::common::wslutil;
38 using namespace wsl::shared;
39 using namespace wsl::windows::wslc::models;
40 using namespace std::chrono_literals;
41
42 static void SetContainerArguments(WSLCProcessOptions& options, std::vector<const char*>& argsStorage)
43 {
44 options.CommandLine = {.Values = argsStorage.data(), .Count = static_cast<ULONG>(argsStorage.size())};
45 }
46
47 static bool SupportsNetworkAliases(std::string_view network)
48 {
49 // Aliases are only supported for user-defined networks, not built-in or container-sourced network modes.
50 return network != "bridge" && network != "host" && network != "none" && !network.starts_with("container:");
51 }
52
53 static void PullImage(Terminal& terminal, Session& session, const std::string& image)
54 {
55 ImageProgressCallback callback(terminal, Terminal::Level::Info);
56 ImageService imageService;
57 imageService.Pull(terminal, session, image, &callback);
58 }
59
60 static wsl::windows::common::RunningWSLCContainer CreateInternal(Terminal& terminal, Session& session, const std::string& image, const ContainerOptions& options)
61 {
62 WarningCallback warningCallback(terminal);
63
64 if (options.Pull == PullPolicy::Always)
65 {
66 PullImage(terminal, session, image);
67 }
68
69 auto processFlags = WSLCProcessFlagsNone;
70 WI_SetFlagIf(processFlags, WSLCProcessFlagsStdin, options.Interactive);
71 WI_SetFlagIf(processFlags, WSLCProcessFlagsTty, options.TTY);
72
73 auto containerFlags = WSLCContainerFlagsNone;
74 WI_SetFlagIf(containerFlags, WSLCContainerFlagsRm, options.Remove);
75 WI_SetFlagIf(containerFlags, WSLCContainerFlagsPublishAll, options.PublishAll);
76 WI_SetFlagIf(containerFlags, WSLCContainerFlagsGpu, options.Gpu);
77
78 std::string networkMode = options.Networks.empty() ? std::string("bridge") : options.Networks.front().Name;
79
80 wsl::windows::common::WSLCContainerLauncher containerLauncher(
81 image, options.Name, options.Arguments, options.EnvironmentVariables, std::move(networkMode), processFlags);
82
83 for (size_t i = 1; i < options.Networks.size(); ++i)
84 {
85 const auto& network = options.Networks[i];
86 THROW_HR_WITH_USER_ERROR_IF(
87 E_INVALIDARG,
88 Localization::MessageWslcAliasRequiresUserDefinedNetwork(),
89 !network.Aliases.empty() && !SupportsNetworkAliases(network.Name));
90
91 containerLauncher.AddAdditionalNetwork(network.Name, network.Aliases);
92 }
93
94 if (!options.NetworkAliases.empty())
95 {
96 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcAliasRequiresUserDefinedNetwork(), options.Networks.empty());
97
98 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcAliasAmbiguousWithMultipleNetworks(), options.Networks.size() > 1);
99
100 const auto& primary = options.Networks.front().Name;
101 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcAliasRequiresUserDefinedNetwork(), !SupportsNetworkAliases(primary));
102
103 for (const auto& alias : options.NetworkAliases)
104 {
105 containerLauncher.AddPrimaryNetworkAlias(alias);
106 }
107 }
108
109 if (options.IpAddress.has_value())
110 {
111 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcIpRequiresUserDefinedNetwork(), options.Networks.empty());
112
113 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcIpAmbiguousWithMultipleNetworks(), options.Networks.size() > 1);
114
115 const auto& primary = options.Networks.front().Name;
116 THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcIpRequiresUserDefinedNetwork(), !SupportsNetworkAliases(primary));
117
118 containerLauncher.SetPrimaryNetworkIpAddress(std::string(options.IpAddress.value()));
119 }
120
121 if (!options.Networks.empty())
122 {
123 const auto& primary = options.Networks.front();
124 THROW_HR_WITH_USER_ERROR_IF(
125 E_INVALIDARG,
126 Localization::MessageWslcAliasRequiresUserDefinedNetwork(),
127 !primary.Aliases.empty() && !SupportsNetworkAliases(primary.Name));
128
129 for (const auto& alias : primary.Aliases)
130 {
131 containerLauncher.AddPrimaryNetworkAlias(alias);
132 }
133 }
134
135 const auto defaultBindingAddress = settings::User().Get<settings::Setting::SessionDefaultBindingAddress>();
136
137 // Set port options if provided
138 for (const auto& port : options.Ports)
139 {
140 auto portMapping = PublishPort::Parse(port);
141
142 const int protocol = portMapping.PortProtocol() == PublishPort::Protocol::UDP ? IPPROTO_UDP : IPPROTO_TCP;
143 const int family = (portMapping.HostIP().has_value() && portMapping.HostIP()->IsIPv6()) ? AF_INET6 : AF_INET;
144 std::optional<std::string> bindAddress;
145 if (portMapping.HostIP().has_value())
146 {
147 bindAddress = portMapping.HostIP()->IP();
148 }
149 else if (!defaultBindingAddress.empty())
150 {
151 // No explicit host IP: apply the configured default binding address (IPv4 only,
152 // since IPv6 bindings are always explicit). When unset, AddPort falls back to loopback.
153 bindAddress = defaultBindingAddress;
154 }
155
156 auto containerPort = portMapping.ContainerPort();
157 for (uint16_t i = 0; i < containerPort.Count(); ++i)
158 {
159 auto currentContainerPort = static_cast<uint16_t>(containerPort.Start() + i);
160 auto currentHostPort = portMapping.HostPort().IsEphemeral() ? static_cast<uint16_t>(WSLC_EPHEMERAL_PORT)
161 : static_cast<uint16_t>(portMapping.HostPort().Start() + i);
162 containerLauncher.AddPort(currentHostPort, currentContainerPort, family, protocol, bindAddress);
163 }
164 }
165
166 for (const auto& mountSpec : options.Mounts)
167 {
168 containerLauncher.AddMount(mountSpec);
169 }
170
171 containerLauncher.SetContainerFlags(containerFlags);
172
173 if (options.StopSignal != WSLCSignalNone)
174 {
175 containerLauncher.SetDefaultStopSignal(options.StopSignal);
176 }
177
178 if (options.StopTimeout.has_value())
179 {
180 containerLauncher.SetStopTimeout(options.StopTimeout.value());
181 }
182
183 if (options.ShmSize.has_value())
184 {
185 containerLauncher.SetShmSize(options.ShmSize.value());
186 }
187
188 if (options.HealthCmd.has_value())
189 {
190 containerLauncher.SetHealthCmd(std::string(options.HealthCmd.value()));
191 }
192
193 if (options.HealthInterval.has_value())
194 {
195 containerLauncher.SetHealthInterval(options.HealthInterval.value());
196 }
197
198 if (options.HealthTimeout.has_value())
199 {
200 containerLauncher.SetHealthTimeout(options.HealthTimeout.value());
201 }
202
203 if (options.HealthStartPeriod.has_value())
204 {
205 containerLauncher.SetHealthStartPeriod(options.HealthStartPeriod.value());
206 }
207
208 if (options.HealthRetries.has_value())
209 {
210 containerLauncher.SetHealthRetries(options.HealthRetries.value());
211 }
212
213 if (options.NoHealthcheck)
214 {
215 containerLauncher.SetNoHealthcheck();
216 }
217
218 if (options.MemoryBytes.has_value())
219 {
220 containerLauncher.SetMemoryLimit(options.MemoryBytes.value());
221 }
222
223 if (options.NanoCpus.has_value())
224 {
225 containerLauncher.SetNanoCpus(options.NanoCpus.value());
226 }
227
228 for (const auto& [name, soft, hard] : options.Ulimits)
229 {
230 containerLauncher.AddUlimit(name, soft, hard);
231 }
232
233 if (!options.Entrypoint.empty())
234 {
235 auto entrypoints = options.Entrypoint;
236 containerLauncher.SetEntrypoint(std::move(entrypoints));
237 }
238
239 if (options.User.has_value())
240 {
241 auto user = options.User.value();
242 containerLauncher.SetUser(std::move(user));
243 }
244
245 if (!options.WorkingDirectory.empty())
246 {
247 containerLauncher.SetWorkingDirectory(std::string(options.WorkingDirectory));
248 }
249
250 if (options.Hostname.has_value())
251 {
252 containerLauncher.SetHostname(std::string(options.Hostname.value()));
253 }
254
255 if (options.Domainname.has_value())
256 {
257 containerLauncher.SetDomainname(std::string(options.Domainname.value()));
258 }
259
260 if (!options.DnsServers.empty())
261 {
262 containerLauncher.SetDnsServers(std::vector<std::string>(options.DnsServers));
263 }
264
265 if (!options.DnsSearchDomains.empty())
266 {
267 containerLauncher.SetDnsSearchDomains(std::vector<std::string>(options.DnsSearchDomains));
268 }
269
270 if (!options.DnsOptions.empty())
271 {
272 containerLauncher.SetDnsOptions(std::vector<std::string>(options.DnsOptions));
273 }
274
275 for (const auto& [key, value] : options.Labels)
276 {
277 containerLauncher.AddLabel(key, value);
278 }
279
280 auto [result, runningContainer] = containerLauncher.CreateNoThrow(*session.Get(), &warningCallback);
281 if (result == WSLC_E_IMAGE_NOT_FOUND && options.Pull == PullPolicy::Missing)
282 {
283 terminal.Info(L"{}\n", Localization::WSLCCLI_ImageNotFoundPulling(wsl::shared::string::MultiByteToWide(image)));
284 PullImage(terminal, session, image);
285 return containerLauncher.Create(*session.Get(), &warningCallback);
286 }
287
288 THROW_IF_FAILED(result);
289 ASSERT(runningContainer);
290 return std::move(*runningContainer);
291 }
292
293 static PortInformation PortInformationFromWSLCPortMapping(const WSLCPortMapping& mapping)
294 {
295 return PortInformation{
296 .HostPort = mapping.HostPort,
297 .ContainerPort = mapping.ContainerPort,
298 .Protocol = static_cast<int>(mapping.Protocol),
299 .BindingAddress = mapping.BindingAddress,
300 };
301 }
302
303 int ContainerService::Attach(Terminal& terminal, Session& session, const std::string& id)
304 {
305 [[maybe_unused]] auto operation = session.BeginContainerOperation();
306 wil::com_ptr<IWSLCContainer> container;
307 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
308
309 wil::com_ptr<IWSLCProcess> process;
310 THROW_IF_FAILED(container->GetInitProcess(&process));
311
312 WSLCProcessFlags processFlags{};
313 THROW_IF_FAILED(process->GetFlags(&processFlags));
314
315 ClientRunningWSLCProcess runningProcess(std::move(process), processFlags);
316
317 COMOutputHandle stdinLogs{};
318 COMOutputHandle stdoutLogs{};
319 COMOutputHandle stderrLogs{};
320 THROW_IF_FAILED(container->Attach(nullptr, &stdinLogs, &stdoutLogs, &stderrLogs));
321
322 if (!stdoutLogs.Empty())
323 {
324 // Non-TTY process - relay separate stdout/stderr streams
325 WI_ASSERT(!stderrLogs.Empty());
326 ConsoleService::RelayNonTtyProcess(stdinLogs.Release(), stdoutLogs.Release(), stderrLogs.Release());
327 }
328 else
329 {
330 // TTY process - relay using interactive TTY handling
331 WI_ASSERT(stderrLogs.Empty());
332 wsl::windows::common::ConsoleState console;
333 if (!ConsoleService::RelayInteractiveTty(console, runningProcess, stdinLogs.Release().Get(), true))
334 {
335 terminal.Info(L"[detached]\n");
336 return 0; // Exit early if user detached
337 }
338 }
339
340 // Wait for the container process to exit
341 return runningProcess.Wait();
342 }
343
344 // The invariant state name. This is what "container list --format json" reports.
345 std::wstring ContainerService::ContainerStateName(WSLCContainerState state)
346 {
347 switch (state)
348 {
349 case WSLCContainerState::WslcContainerStateCreated:
350 return L"created";
351 case WSLCContainerState::WslcContainerStateRunning:
352 return L"running";
353 case WSLCContainerState::WslcContainerStateDeleted:
354 return L"stopped";
355 case WSLCContainerState::WslcContainerStateExited:
356 return L"exited";
357 case WSLCContainerState::WslcContainerStateInvalid:
358 return L"invalid";
359 default:
360 THROW_HR(E_UNEXPECTED);
361 }
362 }
363
364 std::wstring ContainerService::LocalizedContainerStateName(WSLCContainerState state)
365 {
366 switch (state)
367 {
368 case WSLCContainerState::WslcContainerStateCreated:
369 return Localization::WSLCCLI_ContainerStateCreated();
370 case WSLCContainerState::WslcContainerStateRunning:
371 return Localization::WSLCCLI_ContainerStateRunning();
372 case WSLCContainerState::WslcContainerStateDeleted:
373 return Localization::WSLCCLI_ContainerStateStopped();
374 case WSLCContainerState::WslcContainerStateExited:
375 return Localization::WSLCCLI_ContainerStateExited();
376 case WSLCContainerState::WslcContainerStateInvalid:
377 return Localization::WSLCCLI_ContainerStateInvalid();
378 default:
379 THROW_HR(E_UNEXPECTED);
380 }
381 }
382
383 std::wstring ContainerService::ContainerStateToString(WSLCContainerState state, LONGLONG stateChangedAt, FormatType format)
384 {
385 const auto invariant = format == FormatType::Json;
386 auto stateString = invariant ? ContainerStateName(state) : LocalizedContainerStateName(state);
387 if (stateChangedAt == 0 || state == WSLCContainerState::WslcContainerStateInvalid)
388 {
389 return stateString;
390 }
391
392 const auto relative = invariant ? wsl::windows::common::timestamp::FormatInvariantRelativeTime(stateChangedAt)
393 : wsl::windows::common::timestamp::FormatRelativeTime(stateChangedAt);
394
395 return std::format(L"{} {}", stateString, relative);
396 }
397
398 // Reports whether a code point is printable using the same rule as Go's unicode.IsPrint, which docker relies on when
399 // quoting: letters, marks, numbers, punctuation, symbols and the ASCII space.
400 static bool IsPrintable(UChar32 codePoint)
401 {
402 constexpr auto printableMask = U_GC_L_MASK | U_GC_M_MASK | U_GC_N_MASK | U_GC_P_MASK | U_GC_S_MASK;
403 return codePoint == U' ' || (U_GET_GC_MASK(codePoint) & printableMask) != 0;
404 }
405
406 // Appends a code point that has no printable representation, mirroring the escapes Go's strconv.Quote emits.
407 static void AppendEscape(std::wstring& quoted, UChar32 codePoint)
408 {
409 switch (codePoint)
410 {
411 case L'\a':
412 quoted += L"\\a";
413 return;
414 case L'\b':
415 quoted += L"\\b";
416 return;
417 case L'\f':
418 quoted += L"\\f";
419 return;
420 case L'\n':
421 quoted += L"\\n";
422 return;
423 case L'\r':
424 quoted += L"\\r";
425 return;
426 case L'\t':
427 quoted += L"\\t";
428 return;
429 case L'\v':
430 quoted += L"\\v";
431 return;
432 default:
433 break;
434 }
435
436 if (codePoint < L' ' || codePoint == 0x7F)
437 {
438 quoted += std::format(L"\\x{:02x}", static_cast<unsigned int>(codePoint));
439 }
440 else if (U_IS_SURROGATE(codePoint))
441 {
442 // An unpaired surrogate is not a valid code point, and Go substitutes the replacement character.
443 quoted += L"\\ufffd";
444 }
445 else if (codePoint < 0x10000)
446 {
447 quoted += std::format(L"\\u{:04x}", static_cast<unsigned int>(codePoint));
448 }
449 else
450 {
451 quoted += std::format(L"\\U{:08x}", static_cast<unsigned int>(codePoint));
452 }
453 }
454
455 std::wstring ContainerService::FormatCommand(const std::string& command, bool truncate)
456 {
457 constexpr size_t c_maxDisplayWidth = 20;
458
459 auto wide = wsl::shared::string::MultiByteToWide(command);
460 if (truncate)
461 {
462 wide = wsl::windows::common::string::Ellipsis(wide, c_maxDisplayWidth);
463 }
464
465 // Quoting happens after truncation, so the result can exceed c_maxDisplayWidth. This matches docker, which truncates
466 // the command first and quotes the truncated value.
467 const auto length = static_cast<int32_t>(wide.size());
468 std::wstring quoted{L'"'};
469 for (int32_t index = 0; index < length;)
470 {
471 const auto start = index;
472 UChar32 codePoint{};
473 U16_NEXT(wide.data(), index, length, codePoint);
474
475 if (codePoint == L'"' || codePoint == L'\\')
476 {
477 quoted += L'\\';
478 quoted += static_cast<wchar_t>(codePoint);
479 }
480 else if (IsPrintable(codePoint))
481 {
482 quoted.append(wide, start, static_cast<size_t>(index - start));
483 }
484 else
485 {
486 AppendEscape(quoted, codePoint);
487 }
488 }
489
490 quoted += L'"';
491 return quoted;
492 }
493
494 std::wstring ContainerService::FormatMounts(const std::string& mounts, bool truncate)
495 {
496 constexpr size_t c_maxDisplayWidth = 15;
497
498 auto wide = wsl::shared::string::MultiByteToWide(mounts);
499 if (!truncate || wide.empty())
500 {
501 return wide;
502 }
503
504 std::vector<std::wstring> shortened;
505 for (const auto& mount : wsl::shared::string::SplitPreserveEmpty(std::wstring_view{wide}, L','))
506 {
507 shortened.emplace_back(wsl::windows::common::string::Ellipsis(mount, c_maxDisplayWidth));
508 }
509
510 return wsl::shared::string::Join(shortened, L',');
511 }
512
513 std::wstring ContainerService::FormatStatus(const std::string& status, WSLCContainerState state, LONGLONG stateChangedAt, FormatType format)
514 {
515 if (!status.empty())
516 {
517 return wsl::shared::string::MultiByteToWide(status);
518 }
519
520 return ContainerStateToString(state, stateChangedAt, format);
521 }
522
523 std::string ContainerService::FormatHealthStatus(const std::string& status)
524 {
525 const auto open = status.find('(');
526 if (open == std::string::npos || status.back() != ')')
527 {
528 return {};
529 }
530
531 constexpr std::string_view c_healthPrefix = "health: ";
532 auto health = std::string_view{status}.substr(open + 1, status.size() - open - 2);
533 if (health.starts_with(c_healthPrefix))
534 {
535 health.remove_prefix(c_healthPrefix.size());
536 }
537
538 if (health == "healthy" || health == "unhealthy" || health == "starting")
539 {
540 return std::string{health};
541 }
542
543 return {};
544 }
545
546 std::wstring ContainerService::FormatPorts(WSLCContainerState state, const std::vector<PortInformation>& ports)
547 {
548 if (state != WslcContainerStateRunning || ports.empty())
549 {
550 return L"";
551 }
552
553 std::wstring result;
554 for (size_t i = 0; i < ports.size(); ++i)
555 {
556 const auto& port = ports[i];
557
558 std::wstring hostIp = wsl::shared::string::MultiByteToWide(port.BindingAddress);
559
560 std::wstring protocol = (port.Protocol == IPPROTO_TCP) ? L"tcp"
561 : (port.Protocol == IPPROTO_UDP) ? L"udp"
562 : std::format(L"{}", port.Protocol);
563
564 if (i > 0)
565 {
566 result += L", ";
567 }
568
569 result += std::format(
570 L"{}:{}->{}/{}", (hostIp.find(L':') != std::wstring::npos) ? std::format(L"[{}]", hostIp) : hostIp, port.HostPort, port.ContainerPort, protocol);
571 }
572
573 return result;
574 }
575
576 int ContainerService::Run(Terminal& terminal, Session& session, const std::string& image, ContainerOptions runOptions)
577 {
578 // Reserve the CID file (fails if it already exists) before creating the container so a
579 // container isn't created when the caller-requested path can't be written. The file is
580 // removed automatically if we don't reach Commit() below.
581 CidFile cidFile(runOptions.CidFile);
582
583 // Create the container
584 auto runningContainer = CreateInternal(terminal, session, image, runOptions);
585 auto& container = runningContainer.Get();
586
587 WSLCContainerId containerId{};
588 THROW_IF_FAILED(container.GetId(containerId));
589
590 // Start the created container
591 WSLCContainerStartFlags startFlags{};
592 WI_SetFlagIf(startFlags, WSLCContainerStartFlagsAttach, !runOptions.Detach);
593
594 const bool attach = WI_IsFlagSet(startFlags, WSLCContainerStartFlagsAttach);
595
596 wsl::windows::common::ConsoleState console;
597 WSLCProcessStartOptions startOptions{};
598 if (runOptions.TTY)
599 {
600
601 const auto size = console.GetWindowSize();
602 startOptions.TtyRows = size.Y;
603 startOptions.TtyColumns = size.X;
604 }
605
606 WarningCallback warningCallback(terminal);
607 THROW_IF_FAILED(container.Start(startFlags, &startOptions, &warningCallback)); // TODO: detach keys
608
609 // Disable auto-delete only after successful start
610 runningContainer.SetDeleteOnClose(false);
611 cidFile.Commit(containerId);
612
613 // Handle attach if requested
614 if (attach)
615 {
616 return ConsoleService::AttachToCurrentConsole(terminal, console, runningContainer.GetInitProcess());
617 }
618
619 terminal.Output(L"{}\n", wsl::shared::string::MultiByteToWide(containerId));
620 return 0;
621 }
622
623 CreateContainerResult ContainerService::Create(Terminal& terminal, Session& session, const std::string& image, ContainerOptions runOptions)
624 {
625 CidFile cidFile(runOptions.CidFile);
626 auto runningContainer = CreateInternal(terminal, session, image, runOptions);
627 runningContainer.SetDeleteOnClose(false);
628 auto& container = runningContainer.Get();
629 WSLCContainerId id{};
630 THROW_IF_FAILED(container.GetId(id));
631 cidFile.Commit(id);
632 return {.Id = id};
633 }
634
635 int ContainerService::Start(Terminal& terminal, Session& session, const std::string& id, bool attach)
636 {
637 [[maybe_unused]] auto operation = session.BeginContainerOperation();
638 wil::com_ptr<IWSLCContainer> container;
639 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
640 WSLCContainerStartFlags flags = attach ? WSLCContainerStartFlagsAttach : WSLCContainerStartFlagsNone;
641
642 wsl::windows::common::ConsoleState console;
643 WSLCProcessStartOptions startOptions{};
644 const auto size = console.GetWindowSize();
645 startOptions.TtyRows = size.Y;
646 startOptions.TtyColumns = size.X;
647
648 WarningCallback warningCallback(terminal);
649 THROW_IF_FAILED_EXCEPT(container->Start(flags, &startOptions, &warningCallback), WSLC_E_CONTAINER_IS_RUNNING);
650
651 if (!attach)
652 {
653 return 0;
654 }
655
656 wil::com_ptr<IWSLCProcess> process;
657 THROW_IF_FAILED(container->GetInitProcess(&process));
658
659 WSLCProcessFlags processFlags{};
660 THROW_IF_FAILED(process->GetFlags(&processFlags));
661 ClientRunningWSLCProcess runningProcess(std::move(process), processFlags);
662
663 return ConsoleService::AttachToCurrentConsole(terminal, console, std::move(runningProcess), true);
664 }
665
666 void ContainerService::Stop(Session& session, const std::string& id, StopContainerOptions options)
667 {
668 [[maybe_unused]] auto operation = session.BeginContainerOperation();
669 wil::com_ptr<IWSLCContainer> container;
670 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
671 THROW_IF_FAILED_EXCEPT(container->Stop(options.Signal, options.Timeout), WSLC_E_CONTAINER_NOT_RUNNING);
672 }
673
674 void ContainerService::Restart(Terminal& terminal, Session& session, const std::string& id, StopContainerOptions options)
675 {
676 [[maybe_unused]] auto operation = session.BeginContainerOperation();
677 wil::com_ptr<IWSLCContainer> container;
678 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
679
680 WarningCallback warningCallback(terminal);
681 THROW_IF_FAILED(container->Restart(options.Signal, options.Timeout, &warningCallback));
682 }
683
684 void ContainerService::Kill(Session& session, const std::string& id, WSLCSignal signal)
685 {
686 [[maybe_unused]] auto operation = session.BeginContainerOperation();
687 wil::com_ptr<IWSLCContainer> container;
688 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
689 THROW_IF_FAILED(container->Kill(signal));
690 }
691
692 void ContainerService::Delete(Session& session, const std::string& id, bool force, bool deleteVolumes)
693 {
694 [[maybe_unused]] auto operation = session.BeginContainerOperation();
695 wil::com_ptr<IWSLCContainer> container;
696 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
697
698 auto flags = WSLCDeleteFlagsNone;
699 WI_SetFlagIf(flags, WSLCDeleteFlagsForce, force);
700 WI_SetFlagIf(flags, WSLCDeleteFlagsDeleteVolumes, deleteVolumes);
701 THROW_IF_FAILED(container->Delete(flags));
702 }
703
704 std::vector<ContainerInformation> ContainerService::List(
705 Session& session, bool all, int limit, const std::vector<std::pair<std::string, std::string>>& filters)
706 {
707 std::vector<WSLCFilter> filterEntries;
708 filterEntries.reserve(filters.size());
709 for (const auto& [key, value] : filters)
710 {
711 filterEntries.push_back({.Key = key.c_str(), .Value = value.c_str()});
712 }
713
714 WSLCListContainersOptions options{};
715 options.Flags = all ? WSLCListContainersFlagsAll : WSLCListContainersFlagsNone;
716 options.Limit = limit;
717 options.Filters = filterEntries.data();
718 options.FiltersCount = static_cast<ULONG>(filterEntries.size());
719
720 wsl::windows::common::wslc::unique_container_entry_array containers;
721 wil::unique_cotaskmem_array_ptr<WSLCContainerPortMapping> ports;
722 THROW_IF_FAILED(
723 session.Get()->ListContainers(&options, &containers, containers.size_address<ULONG>(), &ports, ports.size_address<ULONG>()));
724
725 std::vector<ContainerInformation> result;
726
727 for (const auto& current : containers)
728 {
729 ContainerInformation entry;
730 entry.Name = current.Name;
731 entry.Image = current.Image;
732 entry.Command = current.Command == nullptr ? "" : current.Command;
733 entry.Status = current.Status == nullptr ? "" : current.Status;
734 entry.Labels = current.Labels == nullptr ? "" : current.Labels;
735 entry.Networks = current.Networks == nullptr ? "" : current.Networks;
736 entry.Mounts = current.Mounts == nullptr ? "" : current.Mounts;
737 entry.LocalVolumes = current.LocalVolumes;
738 entry.State = current.State;
739 entry.Id = current.Id;
740 entry.StateChangedAt = current.StateChangedAt;
741 entry.CreatedAt = current.CreatedAt;
742
743 for (const auto& port : ports)
744 {
745 if (strcmp(port.Id, current.Id) == 0)
746 {
747 entry.Ports.push_back(PortInformationFromWSLCPortMapping(port.PortMapping));
748 }
749 }
750
751 result.emplace_back(std::move(entry));
752 }
753
754 return result;
755 }
756
757 int ContainerService::Exec(Terminal& terminal, Session& session, const std::string& id, ContainerOptions options)
758 {
759 [[maybe_unused]] auto operation = session.BeginContainerOperation();
760 wil::com_ptr<IWSLCContainer> container;
761 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
762
763 auto execFlags = WSLCProcessFlagsNone;
764 WI_SetFlagIf(execFlags, WSLCProcessFlagsStdin, options.Interactive);
765 WI_SetFlagIf(execFlags, WSLCProcessFlagsTty, options.TTY);
766
767 auto processLauncher = wsl::windows::common::WSLCProcessLauncher({}, options.Arguments, options.EnvironmentVariables, execFlags);
768
769 wsl::windows::common::ConsoleState console;
770 if (options.TTY)
771 {
772 const auto size = console.GetWindowSize();
773 processLauncher.SetTtySize(size.Y, size.X);
774 }
775
776 if (options.User.has_value())
777 {
778 auto user = options.User.value();
779 processLauncher.SetUser(std::move(user));
780 }
781 if (!options.WorkingDirectory.empty())
782 {
783 processLauncher.SetWorkingDirectory(std::move(options.WorkingDirectory));
784 }
785
786 return ConsoleService::AttachToCurrentConsole(terminal, console, processLauncher.Launch(*container));
787 }
788
789 InspectContainer ContainerService::Inspect(Session& session, const std::string& id, bool size)
790 {
791 [[maybe_unused]] auto operation = session.BeginContainerOperation();
792 wil::com_ptr<IWSLCContainer> container;
793 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
794 wil::unique_cotaskmem_ansistring output;
795 THROW_IF_FAILED(container->Inspect(size ? TRUE : FALSE, &output));
796 return wsl::shared::FromJson<InspectContainer>(output.get());
797 }
798
799 void ContainerService::Export(Session& session, const std::string& id, const std::wstring& outputPath)
800 {
801 wil::unique_hfile outputFile{
802 CreateFileW(outputPath.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
803 THROW_LAST_ERROR_IF(!outputFile);
804
805 Export(session, id, outputFile.get());
806 }
807
808 void ContainerService::Export(Session& session, const std::string& id, HANDLE outputHandle)
809 {
810 [[maybe_unused]] auto operation = session.BeginContainerOperation();
811
812 wil::com_ptr<IWSLCContainer> container;
813 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
814
815 wsl::windows::common::HandleConsoleProgressBar progressBar(
816 outputHandle, Localization::MessageWslcExportInProgress(), wsl::windows::common::HandleConsoleProgressBar::Format::FileSize);
817
818 THROW_IF_FAILED(container->Export(ToCOMInputHandle(outputHandle)));
819 }
820
821 void ContainerService::CopyToContainer(Session& session, const std::string& id, const std::string& destPath, HANDLE inputHandle, ULONGLONG contentSize)
822 {
823 [[maybe_unused]] auto operation = session.BeginContainerOperation();
824
825 wil::com_ptr<IWSLCContainer> container;
826 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
827
828 THROW_IF_FAILED(container->UploadArchive(ToCOMInputHandle(inputHandle), destPath.c_str(), contentSize));
829 }
830
831 void ContainerService::CopyFromContainer(Session& session, const std::string& id, const std::string& srcPath, HANDLE outputHandle)
832 {
833 [[maybe_unused]] auto operation = session.BeginContainerOperation();
834
835 wil::com_ptr<IWSLCContainer> container;
836 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
837
838 THROW_IF_FAILED(container->DownloadArchive(srcPath.c_str(), ToCOMInputHandle(outputHandle)));
839 }
840
841 void ContainerService::Logs(Session& session, const std::string& id, bool follow, bool timestamps, LONGLONG since, LONGLONG until, ULONGLONG tail)
842 {
843 [[maybe_unused]] auto operation = session.BeginContainerOperation();
844 wil::com_ptr<IWSLCContainer> container;
845 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
846
847 COMOutputHandle stdoutHandle;
848 COMOutputHandle stderrHandle;
849 WSLCLogsFlags flags = WSLCLogsFlagsNone;
850 WI_SetFlagIf(flags, WSLCLogsFlagsFollow, follow);
851 WI_SetFlagIf(flags, WSLCLogsFlagsTimestamps, timestamps);
852
853 THROW_IF_FAILED(container->Logs(flags, &stdoutHandle, &stderrHandle, since, until, tail));
854
855 // Container output is UTF-8.
856 wsl::windows::common::ConsoleState console;
857 console.SetOutputCodePageUtf8();
858
859 wsl::windows::common::io::MultiHandleWait io;
860 io.AddHandle(std::make_unique<wsl::windows::common::io::RelayHandle<wsl::windows::common::io::ReadHandle>>(
861 stdoutHandle.Release(), GetStdHandle(STD_OUTPUT_HANDLE)));
862
863 if (!stderrHandle.Empty()) // This handle is only used for non-tty processes.
864 {
865 io.AddHandle(std::make_unique<wsl::windows::common::io::RelayHandle<wsl::windows::common::io::ReadHandle>>(
866 stderrHandle.Release(), GetStdHandle(STD_ERROR_HANDLE)));
867 }
868
869 // TODO: Handle ctrl-c.
870 io.Run({});
871 }
872
873 wsl::windows::common::docker_schema::ContainerStats ContainerService::Stats(Session& session, const std::string& id)
874 {
875 [[maybe_unused]] auto operation = session.BeginContainerOperation();
876 wil::com_ptr<IWSLCContainer> container;
877 THROW_IF_FAILED(session.Get()->OpenContainer(id.c_str(), &container));
878 wil::unique_cotaskmem_ansistring output;
879 THROW_IF_FAILED(container->Stats(&output));
880 return wsl::shared::FromJson<wsl::windows::common::docker_schema::ContainerStats>(output.get());
881 }
882
883 PruneContainersResult ContainerService::Prune(Session& session)
884 {
885 PruneResult result;
886 THROW_IF_FAILED(session.Get()->PruneContainers(nullptr, 0, &result.result));
887
888 PruneContainersResult pruneResult;
889 pruneResult.SpaceReclaimed = result.result.SpaceReclaimed;
890 pruneResult.PrunedContainers.reserve(result.result.ContainersCount);
891 for (ULONG i = 0; i < result.result.ContainersCount; i++)
892 {
893 pruneResult.PrunedContainers.push_back(result.result.Containers[i]);
894 }
895
896 return pruneResult;
897 }
898 } // namespace wsl::windows::wslc::services