diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index b94a8d126d..8f710a7043 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2560,6 +2560,49 @@ For privacy information about this product please visit https://aka.ms/privacy.< Manage the lifecycle of WSL containers, including creating, starting, stopping, and removing them. {Locked="WSL"}Product names should not be translated + + Manage compose sessions. + + + Create and manage containers declared by a compose file. + + + Create a compose session. + + + Create the containers declared by a compose file. + + + Start and attach to a compose session. + + + Start all containers in a compose session, then attach interactively and display their output. + + + Start a compose session. + + + Start all containers in a compose session. + + + Attach to a compose session. + + + Attach interactively and display output from all containers in a compose session. + + + Stop a compose session. + + + Stop all containers in a compose session. + + + Path to the compose YAML file + + + The compose file '{0}' is invalid: {1}. + {Locked="{0}"}{Locked="{1}"}File paths, property names, and error details should not be translated + Attach to a container. diff --git a/msipackage/package.wix.in b/msipackage/package.wix.in index 1bf5a8dc18..a6e9fd5835 100644 --- a/msipackage/package.wix.in +++ b/msipackage/package.wix.in @@ -351,6 +351,14 @@ + + + + + + + + diff --git a/src/windows/common/WSLCContainerLauncher.cpp b/src/windows/common/WSLCContainerLauncher.cpp index 57a96e3d91..4d07b2340a 100644 --- a/src/windows/common/WSLCContainerLauncher.cpp +++ b/src/windows/common/WSLCContainerLauncher.cpp @@ -313,9 +313,10 @@ std::pair> WSLCContainerLauncher::L return std::make_pair(result, std::move(container)); } -std::pair> WSLCContainerLauncher::CreateNoThrow(IWSLCSession& Session, IWarningCallback* WarningCallback) +WSLCContainerLauncher::ContainerOptionsStorage WSLCContainerLauncher::CreateOptions() { - WSLCContainerOptions options{}; + ContainerOptionsStorage storage; + auto& options = storage.Options; options.Image = m_image.c_str(); if (!m_name.empty()) @@ -323,7 +324,7 @@ std::pair> WSLCContainerLauncher::C options.Name = m_name.c_str(); } - std::vector entrypointStorage; + auto& entrypointStorage = storage.EntrypointStorage; for (const auto& e : m_entrypoint) { @@ -331,6 +332,8 @@ std::pair> WSLCContainerLauncher::C } auto [processOptions, commandLinePtrs, environmentPtrs] = CreateProcessOptions(); + storage.CommandLineStorage = std::move(commandLinePtrs); + storage.EnvironmentStorage = std::move(environmentPtrs); options.InitProcessOptions = processOptions; options.Ports = m_ports.data(); options.PortsCount = static_cast(m_ports.size()); @@ -374,7 +377,7 @@ std::pair> WSLCContainerLauncher::C options.DomainName = m_domainname.c_str(); } - std::vector dnsServersStorage; + auto& dnsServersStorage = storage.DnsServersStorage; for (const auto& e : m_dnsServers) { dnsServersStorage.push_back(e.c_str()); @@ -385,7 +388,7 @@ std::pair> WSLCContainerLauncher::C options.DnsServers = {dnsServersStorage.data(), static_cast(dnsServersStorage.size())}; } - std::vector dnsSearchDomainsStorage; + auto& dnsSearchDomainsStorage = storage.DnsSearchDomainsStorage; for (const auto& e : m_dnsSearchDomains) { dnsSearchDomainsStorage.push_back(e.c_str()); @@ -396,7 +399,7 @@ std::pair> WSLCContainerLauncher::C options.DnsSearchDomains = {dnsSearchDomainsStorage.data(), static_cast(dnsSearchDomainsStorage.size())}; } - std::vector dnsOptionsStorage; + auto& dnsOptionsStorage = storage.DnsOptionsStorage; for (const auto& e : m_dnsOptions) { dnsOptionsStorage.push_back(e.c_str()); @@ -427,9 +430,9 @@ std::pair> WSLCContainerLauncher::C options.ContainerNetwork.NetworkMode = m_networkMode.c_str(); // Each additional network becomes an entry in NetworkingConfig.EndpointsConfig. - std::vector connections; + auto& connections = storage.NetworkConnections; connections.reserve(m_additionalNetworks.size()); - std::vector> connectionSettings; + auto& connectionSettings = storage.NetworkConnectionSettings; connectionSettings.reserve(m_additionalNetworks.size()); for (const auto& e : m_additionalNetworks) { @@ -451,7 +454,7 @@ std::pair> WSLCContainerLauncher::C options.ContainerNetwork.NetworksCount = static_cast(connections.size()); // Aliases for the primary endpoint. - std::vector primarySettings; + auto& primarySettings = storage.PrimaryNetworkSettings; primarySettings.reserve(m_primaryNetworkAliases.size()); for (const auto& alias : m_primaryNetworkAliases) { @@ -466,9 +469,14 @@ std::pair> WSLCContainerLauncher::C options.UlimitsCount = static_cast(m_ulimits.size()); options.Ulimits = m_ulimits.size() > 0 ? m_ulimits.data() : nullptr; - // TODO: Support volumes, ports, flags, container networking mode, etc. + return storage; +} + +std::pair> WSLCContainerLauncher::CreateNoThrow(IWSLCSession& Session, IWarningCallback* WarningCallback) +{ + auto storage = CreateOptions(); wil::com_ptr container; - auto result = Session.CreateContainer(&options, WarningCallback, &container); + auto result = Session.CreateContainer(&storage.Options, WarningCallback, &container); if (FAILED(result)) { return std::pair>(result, std::optional{}); diff --git a/src/windows/common/WSLCContainerLauncher.h b/src/windows/common/WSLCContainerLauncher.h index 0ad00956cd..f38d8d87fe 100644 --- a/src/windows/common/WSLCContainerLauncher.h +++ b/src/windows/common/WSLCContainerLauncher.h @@ -99,6 +99,27 @@ class WSLCContainerLauncher : private WSLCProcessLauncher using WSLCProcessLauncher::SetUser; using WSLCProcessLauncher::SetWorkingDirectory; +protected: + struct ContainerOptionsStorage + { + ContainerOptionsStorage() = default; + NON_COPYABLE(ContainerOptionsStorage); + DEFAULT_MOVABLE(ContainerOptionsStorage); + + WSLCContainerOptions Options{}; + std::vector EntrypointStorage; + std::vector CommandLineStorage; + std::vector EnvironmentStorage; + std::vector DnsServersStorage; + std::vector DnsSearchDomainsStorage; + std::vector DnsOptionsStorage; + std::vector NetworkConnections; + std::vector> NetworkConnectionSettings; + std::vector PrimaryNetworkSettings; + }; + + ContainerOptionsStorage CreateOptions(); + private: struct NetworkConnection { diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 52e997346f..9566547635 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -540,6 +540,20 @@ interface IWSLCContainer : IUnknown HRESULT DownloadArchive([in, string] LPCSTR SrcPath, [in] WSLCHandle OutHandle); } +[ + uuid(96DFF26B-422A-44F3-B675-23DA2BA32C77), + pointer_default(unique), + object +] +interface IWSLCComposeSession : IUnknown +{ + HRESULT GetConfigPath([out] LPWSTR* Path); + HRESULT ListContainers([out, size_is(, *Count)] WSLCContainerEntry** Containers, [out] ULONG* Count); + HRESULT Start(); + HRESULT Attach(); + HRESULT Stop([in] ULONG Timeout); +} + typedef struct _WSLCDeletedImageInformation { char Image[WSLC_MAX_IMAGE_NAME_LENGTH + 1]; @@ -757,6 +771,8 @@ interface IWSLCSession : IUnknown // RPC_E_DISCONNECTED. The client holds the returned token for the whole operation; releasing // it (or the client exiting) lets the VM idle-terminate again. HRESULT BeginContainerOperation([out] IUnknown** Operation); + + HRESULT CreateComposeSession([in] LPCWSTR Path, [out] IWSLCComposeSession** ComposeSession); } // diff --git a/src/windows/wslc/commands/ComposeCommand.cpp b/src/windows/wslc/commands/ComposeCommand.cpp new file mode 100644 index 0000000000..b7fcd3e493 --- /dev/null +++ b/src/windows/wslc/commands/ComposeCommand.cpp @@ -0,0 +1,185 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ComposeCommand.cpp + +Abstract: + + Implements the minimal compose command tree. + +--*/ + +#include "precomp.h" +#include "ArgumentConvertedTypes.h" +#include "ComposeCommand.h" +#include "ComposeService.h" +#include "SessionTasks.h" +#include "Task.h" + +using namespace wsl::windows::wslc::services; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; + +namespace wsl::windows::wslc { + +namespace { + + std::vector ComposePathArguments(bool IncludeTimeout = false) + { + std::vector arguments{ + Argument::Create(ArgType::Path, true, std::nullopt, Localization::WSLCCLI_ComposePathArgDescription()), + }; + if (IncludeTimeout) + { + arguments.emplace_back(Argument::Create(ArgType::Time)); + } + + return arguments; + } + + models::Session& ResolveComposeSession(CLIExecutionContext& Context) + { + Context << ResolveSession; + return Context.Data.Get(); + } + + std::wstring ComposePath(CLIExecutionContext& Context) + { + return std::filesystem::absolute(Context.Args.GetValue()).wstring(); + } + +} // namespace + +std::vector> ComposeCommand::GetCommands() const +{ + std::vector> commands; + commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); + return commands; +} + +std::wstring ComposeCommand::ShortDescription() const +{ + return Localization::WSLCCLI_ComposeCommandDesc(); +} + +std::wstring ComposeCommand::LongDescription() const +{ + return Localization::WSLCCLI_ComposeCommandLongDesc(); +} + +void ComposeCommand::ExecuteInternal(CLIExecutionContext& Context) const +{ + OutputHelp(Context.Terminal); +} + +std::vector ComposeCreateCommand::GetArguments() const +{ + return ComposePathArguments(); +} + +std::wstring ComposeCreateCommand::ShortDescription() const +{ + return Localization::WSLCCLI_ComposeCreateDesc(); +} + +std::wstring ComposeCreateCommand::LongDescription() const +{ + return Localization::WSLCCLI_ComposeCreateLongDesc(); +} + +void ComposeCreateCommand::ExecuteInternal(CLIExecutionContext& Context) const +{ + ComposeService::Create(ResolveComposeSession(Context), ComposePath(Context)); +} + +std::vector ComposeUpCommand::GetArguments() const +{ + return ComposePathArguments(); +} + +std::wstring ComposeUpCommand::ShortDescription() const +{ + return Localization::WSLCCLI_ComposeUpDesc(); +} + +std::wstring ComposeUpCommand::LongDescription() const +{ + return Localization::WSLCCLI_ComposeUpLongDesc(); +} + +void ComposeUpCommand::ExecuteInternal(CLIExecutionContext& Context) const +{ + Context.ExitCode = ComposeService::Up(Context.Terminal, ResolveComposeSession(Context), ComposePath(Context)); +} + +std::vector ComposeStartCommand::GetArguments() const +{ + return ComposePathArguments(); +} + +std::wstring ComposeStartCommand::ShortDescription() const +{ + return Localization::WSLCCLI_ComposeStartDesc(); +} + +std::wstring ComposeStartCommand::LongDescription() const +{ + return Localization::WSLCCLI_ComposeStartLongDesc(); +} + +void ComposeStartCommand::ExecuteInternal(CLIExecutionContext& Context) const +{ + ComposeService::Start(ResolveComposeSession(Context), ComposePath(Context)); +} + +std::vector ComposeAttachCommand::GetArguments() const +{ + return ComposePathArguments(); +} + +std::wstring ComposeAttachCommand::ShortDescription() const +{ + return Localization::WSLCCLI_ComposeAttachDesc(); +} + +std::wstring ComposeAttachCommand::LongDescription() const +{ + return Localization::WSLCCLI_ComposeAttachLongDesc(); +} + +void ComposeAttachCommand::ExecuteInternal(CLIExecutionContext& Context) const +{ + Context.ExitCode = ComposeService::Attach(Context.Terminal, ResolveComposeSession(Context), ComposePath(Context)); +} + +std::vector ComposeStopCommand::GetArguments() const +{ + return ComposePathArguments(true); +} + +std::wstring ComposeStopCommand::ShortDescription() const +{ + return Localization::WSLCCLI_ComposeStopDesc(); +} + +std::wstring ComposeStopCommand::LongDescription() const +{ + return Localization::WSLCCLI_ComposeStopLongDesc(); +} + +void ComposeStopCommand::ExecuteInternal(CLIExecutionContext& Context) const +{ + constexpr LONG c_defaultTimeout = 10; + const LONG timeout = Context.Args.Contains(ArgType::Time) ? Context.Args.GetValue() : c_defaultTimeout; + THROW_HR_IF(E_INVALIDARG, timeout < 0); + ComposeService::Stop(ResolveComposeSession(Context), ComposePath(Context), static_cast(timeout)); +} + +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/ComposeCommand.h b/src/windows/wslc/commands/ComposeCommand.h new file mode 100644 index 0000000000..cfb6016b3c --- /dev/null +++ b/src/windows/wslc/commands/ComposeCommand.h @@ -0,0 +1,111 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ComposeCommand.h + +Abstract: + + Declares the minimal compose command tree. + +--*/ + +#pragma once + +#include "Command.h" + +namespace wsl::windows::wslc { + +struct ComposeCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"compose"; + ComposeCommand(const std::wstring& Parent) : Command(CommandName, Parent) + { + } + + std::vector> GetCommands() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& Context) const override; +}; + +struct ComposeCreateCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"create"; + ComposeCreateCommand(const std::wstring& Parent) : Command(CommandName, Parent) + { + } + + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& Context) const override; +}; + +struct ComposeUpCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"up"; + ComposeUpCommand(const std::wstring& Parent) : Command(CommandName, Parent) + { + } + + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& Context) const override; +}; + +struct ComposeStartCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"start"; + ComposeStartCommand(const std::wstring& Parent) : Command(CommandName, Parent) + { + } + + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& Context) const override; +}; + +struct ComposeAttachCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"attach"; + ComposeAttachCommand(const std::wstring& Parent) : Command(CommandName, Parent) + { + } + + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& Context) const override; +}; + +struct ComposeStopCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"stop"; + ComposeStopCommand(const std::wstring& Parent) : Command(CommandName, Parent) + { + } + + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& Context) const override; +}; + +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/RootCommand.cpp b/src/windows/wslc/commands/RootCommand.cpp index 3de0751fa2..69f004b239 100644 --- a/src/windows/wslc/commands/RootCommand.cpp +++ b/src/windows/wslc/commands/RootCommand.cpp @@ -15,6 +15,7 @@ Module Name: // Include all commands that parent to the root. #include "ContainerCommand.h" +#include "ComposeCommand.h" #include "ImageCommand.h" #include "NetworkCommand.h" #include "RegistryCommand.h" @@ -32,6 +33,7 @@ std::vector> RootCommand::GetCommands() const { std::vector> commands; commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); diff --git a/src/windows/wslc/services/ComposeService.cpp b/src/windows/wslc/services/ComposeService.cpp new file mode 100644 index 0000000000..c995d054a2 --- /dev/null +++ b/src/windows/wslc/services/ComposeService.cpp @@ -0,0 +1,92 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ComposeService.cpp + +Abstract: + + Implements minimal compose CLI operations. + +--*/ + +#include "precomp.h" +#include "ComposeService.h" +#include "ConsoleService.h" +#include +#include + +namespace wsl::windows::wslc::services { + +wil::com_ptr ComposeService::Open(models::Session& Session, const std::wstring& Path) +{ + wil::com_ptr composeSession; + THROW_IF_FAILED(Session.Get()->CreateComposeSession(Path.c_str(), &composeSession)); + return composeSession; +} + +void ComposeService::Create(models::Session& Session, const std::wstring& Path) +{ + Open(Session, Path); +} + +int ComposeService::Up(Terminal& Terminal, models::Session& Session, const std::wstring& Path) +{ + Start(Session, Path); + return Attach(Terminal, Session, Path); +} + +void ComposeService::Start(models::Session& Session, const std::wstring& Path) +{ + THROW_IF_FAILED(Open(Session, Path)->Start()); +} + +int ComposeService::Attach(Terminal& Terminal, models::Session& Session, const std::wstring& Path) +{ + [[maybe_unused]] auto operation = Session.BeginContainerOperation(); + auto composeSession = Open(Session, Path); + + wil::unique_cotaskmem_array_ptr containers; + THROW_IF_FAILED(composeSession->ListContainers(&containers, containers.size_address())); + + common::io::MultiHandleWait io; + + for (const auto& e : containers) + { + wil::com_ptr container; + THROW_IF_FAILED(Session.Get()->OpenContainer(e.Id, &container)); + + wil::com_ptr process; + THROW_IF_FAILED(container->GetInitProcess(&process)); + + WSLCProcessFlags flags{}; + THROW_IF_FAILED(process->GetFlags(&flags)); + + wsl::windows::common::wslutil::COMOutputHandle stdinHandle; + wsl::windows::common::wslutil::COMOutputHandle stdoutHandle; + wsl::windows::common::wslutil::COMOutputHandle stderrHandle; + + THROW_IF_FAILED(container->Attach(nullptr, &stdinHandle, &stdoutHandle, &stderrHandle)); + + // TODO: Add support for stdin, tty processes, stop on ctrl-c. + + io.AddHandle(std::make_unique>( + stdoutHandle.Release(), GetStdHandle(STD_OUTPUT_HANDLE))); + + io.AddHandle(std::make_unique>( + stderrHandle.Release(), GetStdHandle(STD_ERROR_HANDLE))); + } + + io.Run({}); + + return 0; +} + +void ComposeService::Stop(models::Session& Session, const std::wstring& Path, ULONG Timeout) +{ + THROW_IF_FAILED(Open(Session, Path)->Stop(Timeout)); +} + +} // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/ComposeService.h b/src/windows/wslc/services/ComposeService.h new file mode 100644 index 0000000000..00651f53e3 --- /dev/null +++ b/src/windows/wslc/services/ComposeService.h @@ -0,0 +1,34 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ComposeService.h + +Abstract: + + Defines minimal compose CLI operations. + +--*/ + +#pragma once + +#include "SessionModel.h" +#include "Terminal.h" + +namespace wsl::windows::wslc::services { + +struct ComposeService +{ + static void Create(models::Session& Session, const std::wstring& Path); + static int Up(Terminal& Terminal, models::Session& Session, const std::wstring& Path); + static void Start(models::Session& Session, const std::wstring& Path); + static int Attach(Terminal& Terminal, models::Session& Session, const std::wstring& Path); + static void Stop(models::Session& Session, const std::wstring& Path, ULONG Timeout); + +private: + static wil::com_ptr Open(models::Session& Session, const std::wstring& Path); +}; + +} // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/ConsoleService.cpp b/src/windows/wslc/services/ConsoleService.cpp index 285b34b163..2dd859ed21 100644 --- a/src/windows/wslc/services/ConsoleService.cpp +++ b/src/windows/wslc/services/ConsoleService.cpp @@ -131,6 +131,11 @@ bool ConsoleService::RelayInteractiveTty(wsl::windows::common::ConsoleState& Con } void ConsoleService::RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_handle&& Stdout, wil::unique_handle&& Stderr) +{ + RelayNonTtyProcess(std::move(Stdin), std::move(Stdout), std::move(Stderr), GetStdHandle(STD_OUTPUT_HANDLE), GetStdHandle(STD_ERROR_HANDLE)); +} + +void ConsoleService::RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_handle&& Stdout, wil::unique_handle&& Stderr, HANDLE Output, HANDLE Error) { // Process output is UTF-8. wsl::windows::common::ConsoleState console; @@ -171,8 +176,15 @@ void ConsoleService::RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_ } } - io.AddHandle(std::make_unique>(std::move(Stdout), GetStdHandle(STD_OUTPUT_HANDLE))); - io.AddHandle(std::make_unique>(std::move(Stderr), GetStdHandle(STD_ERROR_HANDLE))); + if (Stdout) + { + io.AddHandle(std::make_unique>(std::move(Stdout), Output)); + } + + if (Stderr) + { + io.AddHandle(std::make_unique>(std::move(Stderr), Error)); + } io.Run({}); } diff --git a/src/windows/wslc/services/ConsoleService.h b/src/windows/wslc/services/ConsoleService.h index 5cf80611f8..ccdced18a7 100644 --- a/src/windows/wslc/services/ConsoleService.h +++ b/src/windows/wslc/services/ConsoleService.h @@ -27,5 +27,6 @@ class ConsoleService static bool RelayInteractiveTty( wsl::windows::common::ConsoleState& console, wsl::windows::common::ClientRunningWSLCProcess& process, HANDLE tty, bool triggerRefresh = false); static void RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_handle&& Stdout, wil::unique_handle&& Stderr); + static void RelayNonTtyProcess(wil::unique_handle&& Stdin, wil::unique_handle&& Stdout, wil::unique_handle&& Stderr, HANDLE Output, HANDLE Error); }; } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslcsession/CMakeLists.txt b/src/windows/wslcsession/CMakeLists.txt index 29e0b02992..aac018831e 100644 --- a/src/windows/wslcsession/CMakeLists.txt +++ b/src/windows/wslcsession/CMakeLists.txt @@ -8,8 +8,11 @@ set(SOURCES WSLCSessionReference.cpp # Session and container implementation + ComposeSpec.cpp + ServiceContainerLauncher.cpp WSLCSession.cpp WSLCSessionRuntime.cpp + WSLCComposeSession.cpp WSLCContainer.cpp WSLCVirtualMachine.cpp @@ -33,12 +36,15 @@ set(SOURCES ) set(HEADERS + ComposeSpec.h DockerEventTracker.h DockerHTTPClient.h IORelay.h OptionParser.h ServiceProcessLauncher.h + ServiceContainerLauncher.h WSLCContainer.h + WSLCComposeSession.h WSLCContainerMetadata.h WindowsCertStore.h WSLCProcess.h @@ -62,6 +68,7 @@ add_compile_definitions(__WRL_CLASSIC_COM__) add_compile_definitions(USE_COM_CONTEXT_DEF=1) target_link_libraries(wslcsession ${COMMON_LINK_LIBRARIES} + yaml-cpp common legacy_stdio_definitions VirtDisk.lib diff --git a/src/windows/wslcsession/ComposeSpec.cpp b/src/windows/wslcsession/ComposeSpec.cpp new file mode 100644 index 0000000000..e505cd600d --- /dev/null +++ b/src/windows/wslcsession/ComposeSpec.cpp @@ -0,0 +1,360 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ComposeSpec.cpp + +Abstract: + + Parses compose YAML files. + +--*/ + +#include "precomp.h" +#include "ComposeSpec.h" +#include + +using wsl::shared::Localization; + +namespace wsl::windows::service::wslc { + +namespace { + + [[noreturn]] void ThrowInvalidComposeFile(const std::filesystem::path& Path, std::wstring_view Details) + { + THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcComposeFileInvalid(Path.wstring(), Details)); + } + + std::vector ParseComposeStringList( + const std::filesystem::path& Path, const std::string& ServiceName, const YAML::Node& Node, std::string_view Property) + { + if (!Node) + { + return {}; + } + + if (Node.IsScalar()) + { + return {Node.as()}; + } + + if (!Node.IsSequence()) + { + ThrowInvalidComposeFile( + Path, + std::format( + L"the '{}' property for service '{}' must be a string or list", + wsl::shared::string::MultiByteToWide(std::string(Property)), + wsl::shared::string::MultiByteToWide(ServiceName))); + } + + std::vector result; + result.reserve(Node.size()); + for (const auto& value : Node) + { + if (!value.IsScalar()) + { + ThrowInvalidComposeFile( + Path, + std::format( + L"the '{}' property for service '{}' must contain only strings", + wsl::shared::string::MultiByteToWide(std::string(Property)), + wsl::shared::string::MultiByteToWide(ServiceName))); + } + + result.emplace_back(value.as()); + } + + return result; + } + + std::vector ParseComposeEnvironment(const std::filesystem::path& Path, const std::string& ServiceName, const YAML::Node& Node) + { + if (!Node) + { + return {}; + } + + if (Node.IsSequence()) + { + return ParseComposeStringList(Path, ServiceName, Node, "environment"); + } + + if (!Node.IsMap()) + { + ThrowInvalidComposeFile( + Path, std::format(L"the 'environment' property for service '{}' must be a map or list", wsl::shared::string::MultiByteToWide(ServiceName))); + } + + std::vector result; + result.reserve(Node.size()); + for (const auto& entry : Node) + { + if (!entry.first.IsScalar() || (!entry.second.IsScalar() && !entry.second.IsNull())) + { + ThrowInvalidComposeFile( + Path, std::format(L"the 'environment' property for service '{}' must contain scalar values", wsl::shared::string::MultiByteToWide(ServiceName))); + } + + const auto name = entry.first.as(); + const auto value = entry.second.IsNull() ? std::string{} : entry.second.as(); + result.emplace_back(std::format("{}={}", name, value)); + } + + return result; + } + + ComposeContainerDefinition::Volume ParseComposeVolume(const std::filesystem::path& Path, const std::string& ServiceName, const YAML::Node& Node) + { + if (!Node.IsScalar()) + { + ThrowInvalidComposeFile( + Path, + std::format( + L"the 'volumes' property for service '{}' must contain only short-syntax strings", + wsl::shared::string::MultiByteToWide(ServiceName))); + } + + auto value = Node.as(); + bool readOnly = false; + if (value.ends_with(":ro") || value.ends_with(":rw")) + { + readOnly = value.ends_with(":ro"); + value.resize(value.size() - 3); + } + + const auto separator = value.rfind(':'); + if (separator == std::string::npos || separator == 0 || separator + 1 == value.size()) + { + ThrowInvalidComposeFile( + Path, + std::format( + L"the '{}' volume for service '{}' must use source:destination[:ro|rw] syntax", + wsl::shared::string::MultiByteToWide(value), + wsl::shared::string::MultiByteToWide(ServiceName))); + } + + auto source = value.substr(0, separator); + auto destination = value.substr(separator + 1); + if (!destination.starts_with('/')) + { + ThrowInvalidComposeFile( + Path, + std::format( + L"the '{}' volume destination for service '{}' must be an absolute Linux path", + wsl::shared::string::MultiByteToWide(destination), + wsl::shared::string::MultiByteToWide(ServiceName))); + } + + const std::filesystem::path sourcePath = wsl::shared::string::MultiByteToWide(source); + const bool bindMount = sourcePath.is_absolute() || source.starts_with('.') || source.find('/') != std::string::npos || + source.find('\\') != std::string::npos; + if (!bindMount) + { + return { + .Name = std::move(source), + .ContainerPath = std::move(destination), + .ReadOnly = readOnly, + }; + } + + const auto resolvedPath = sourcePath.is_absolute() ? sourcePath : std::filesystem::absolute(Path.parent_path() / sourcePath); + return { + .HostPath = resolvedPath.lexically_normal().wstring(), + .ContainerPath = std::move(destination), + .ReadOnly = readOnly, + }; + } + + ComposeContainerDefinition::Port ParseComposePort(const std::filesystem::path& Path, const std::string& ServiceName, const YAML::Node& Node) + { + if (!Node.IsScalar()) + { + ThrowInvalidComposeFile( + Path, + std::format( + L"the 'ports' property for service '{}' must contain only host:container strings", + wsl::shared::string::MultiByteToWide(ServiceName))); + } + + const auto value = Node.as(); + const auto separator = value.find(':'); + if (separator == std::string::npos || separator == 0 || separator + 1 == value.size() || separator != value.rfind(':')) + { + ThrowInvalidComposeFile( + Path, + std::format( + L"the '{}' port for service '{}' must use host:container syntax", + wsl::shared::string::MultiByteToWide(value), + wsl::shared::string::MultiByteToWide(ServiceName))); + } + + const auto parsePort = [&](std::string_view text, bool allowZero) { + uint16_t port{}; + const auto result = std::from_chars(text.data(), text.data() + text.size(), port); + if (result.ec != std::errc{} || result.ptr != text.data() + text.size() || (!allowZero && port == 0)) + { + ThrowInvalidComposeFile( + Path, + std::format( + L"the '{}' port for service '{}' contains an invalid port number", + wsl::shared::string::MultiByteToWide(value), + wsl::shared::string::MultiByteToWide(ServiceName))); + } + + return port; + }; + + return { + .HostPort = parsePort(std::string_view{value}.substr(0, separator), true), + .ContainerPort = parsePort(std::string_view{value}.substr(separator + 1), false), + }; + } + + ComposeSpec ParseComposeFile(const std::filesystem::path& Path) + { + const auto root = YAML::LoadFile(Path.string()); + const auto services = root["services"]; + if (!services || !services.IsMap() || services.size() == 0) + { + ThrowInvalidComposeFile(Path, L"the file must contain a non-empty services map"); + } + + ComposeSpec spec; + spec.ProjectName = Path.stem().string(); // TODO: Implement this properly. + + spec.Containers.reserve(services.size()); + for (const auto& service : services) + { + if (!service.first.IsScalar() || !service.second.IsMap()) + { + ThrowInvalidComposeFile(Path, L"each service must be a map"); + } + + const auto serviceName = service.first.as(); + const auto& settings = service.second; + for (const auto& setting : settings) + { + const auto key = setting.first.as(); + if (key != "name" && key != "container_name" && key != "image" && key != "environment" && key != "working_dir" && + key != "command" && key != "volumes" && key != "ports") + { + ThrowInvalidComposeFile( + Path, std::format(L"the '{}' property is not supported", wsl::shared::string::MultiByteToWide(key))); + } + } + + const auto image = settings["image"]; + if (!image || !image.IsScalar()) + { + ThrowInvalidComposeFile( + Path, std::format(L"the '{}' service must specify an image", wsl::shared::string::MultiByteToWide(serviceName))); + } + + const auto nameNode = settings["name"] ? settings["name"] : settings["container_name"]; + const auto name = nameNode ? nameNode.as() : serviceName; + if (name.empty()) + { + ThrowInvalidComposeFile( + Path, std::format(L"the '{}' service has an empty name", wsl::shared::string::MultiByteToWide(serviceName))); + } + + const auto imageName = image.as(); + if (imageName.empty()) + { + ThrowInvalidComposeFile( + Path, std::format(L"the '{}' service has an empty image", wsl::shared::string::MultiByteToWide(serviceName))); + } + + ComposeContainerDefinition definition{ + .Name = name, + .Image = imageName, + .Environment = ParseComposeEnvironment(Path, serviceName, settings["environment"]), + }; + + const auto command = settings["command"]; + if (command && command.IsScalar()) + { + // TODO: Implement proper parsing + definition.Command = shared::string::Split(command.as(), ' '); + } + else + { + definition.Command = ParseComposeStringList(Path, serviceName, command, "command"); + } + + const auto workingDirectory = settings["working_dir"]; + if (workingDirectory) + { + if (!workingDirectory.IsScalar()) + { + ThrowInvalidComposeFile( + Path, std::format(L"the 'working_dir' property for service '{}' must be a string", wsl::shared::string::MultiByteToWide(serviceName))); + } + + definition.WorkingDirectory = workingDirectory.as(); + if (!definition.WorkingDirectory.starts_with('/')) + { + ThrowInvalidComposeFile( + Path, + std::format( + L"the working directory for service '{}' must be an absolute Linux path", + wsl::shared::string::MultiByteToWide(serviceName))); + } + } + + const auto volumes = settings["volumes"]; + if (volumes) + { + if (!volumes.IsSequence()) + { + ThrowInvalidComposeFile( + Path, std::format(L"the 'volumes' property for service '{}' must be a list", wsl::shared::string::MultiByteToWide(serviceName))); + } + + definition.Volumes.reserve(volumes.size()); + for (const auto& volume : volumes) + { + definition.Volumes.emplace_back(ParseComposeVolume(Path, serviceName, volume)); + } + } + + const auto ports = settings["ports"]; + if (ports) + { + if (!ports.IsSequence()) + { + ThrowInvalidComposeFile( + Path, std::format(L"the 'ports' property for service '{}' must be a list", wsl::shared::string::MultiByteToWide(serviceName))); + } + + definition.Ports.reserve(ports.size()); + for (const auto& port : ports) + { + definition.Ports.emplace_back(ParseComposePort(Path, serviceName, port)); + } + } + + spec.Containers.emplace_back(std::move(definition)); + } + + return spec; + } + +} // namespace + +ComposeSpec ComposeSpec::Parse(const std::filesystem::path& Path) +{ + try + { + return ParseComposeFile(Path); + } + catch (const YAML::Exception& exception) + { + ThrowInvalidComposeFile(Path, wsl::shared::string::MultiByteToWide(exception.what())); + } +} + +} // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/ComposeSpec.h b/src/windows/wslcsession/ComposeSpec.h new file mode 100644 index 0000000000..90c856cd54 --- /dev/null +++ b/src/windows/wslcsession/ComposeSpec.h @@ -0,0 +1,59 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ComposeSpec.h + +Abstract: + + Defines the parsed representation of a compose file. + +--*/ + +#pragma once + +#include +#include +#include +#include +#include + +namespace wsl::windows::service::wslc { + +struct ComposeContainerDefinition +{ + std::string Name; + std::string Image; + std::vector Command; + std::vector Environment; + std::string WorkingDirectory; + + struct Volume + { + std::optional HostPath; + std::string Name; + std::string ContainerPath; + bool ReadOnly{}; + }; + + struct Port + { + uint16_t HostPort{}; + uint16_t ContainerPort{}; + }; + + std::vector Volumes; + std::vector Ports; +}; + +struct ComposeSpec +{ + std::vector Containers; + std::string ProjectName; + + static ComposeSpec Parse(const std::filesystem::path& Path); +}; + +} // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/ServiceContainerLauncher.cpp b/src/windows/wslcsession/ServiceContainerLauncher.cpp new file mode 100644 index 0000000000..b2ed9ebf82 --- /dev/null +++ b/src/windows/wslcsession/ServiceContainerLauncher.cpp @@ -0,0 +1,29 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ServiceContainerLauncher.cpp + +Abstract: + + Implements the service-side container launcher. + +--*/ + +#include "precomp.h" +#include "ServiceContainerLauncher.h" +#include "WSLCSession.h" + +namespace wsl::windows::service::wslc { + +Microsoft::WRL::ComPtr ServiceContainerLauncher::Create(WSLCSession& Session) +{ + auto storage = CreateOptions(); + Microsoft::WRL::ComPtr container; + Session.CreateContainerImpl(&storage.Options, &container); + return container; +} + +} // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/ServiceContainerLauncher.h b/src/windows/wslcsession/ServiceContainerLauncher.h new file mode 100644 index 0000000000..12b3c0fbb5 --- /dev/null +++ b/src/windows/wslcsession/ServiceContainerLauncher.h @@ -0,0 +1,33 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ServiceContainerLauncher.h + +Abstract: + + Defines the service-side container launcher. + +--*/ + +#pragma once + +#include "WSLCContainerLauncher.h" + +namespace wsl::windows::service::wslc { + +class WSLCSession; + +class ServiceContainerLauncher : public common::WSLCContainerLauncher +{ +public: + NON_COPYABLE(ServiceContainerLauncher); + NON_MOVABLE(ServiceContainerLauncher); + using WSLCContainerLauncher::WSLCContainerLauncher; + + Microsoft::WRL::ComPtr Create(WSLCSession& Session); +}; + +} // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCComposeSession.cpp b/src/windows/wslcsession/WSLCComposeSession.cpp new file mode 100644 index 0000000000..1167d470dc --- /dev/null +++ b/src/windows/wslcsession/WSLCComposeSession.cpp @@ -0,0 +1,132 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCComposeSession.cpp + +Abstract: + + Implements the minimal compose session COM object. + +--*/ + +#include "precomp.h" +#include "WSLCComposeSession.h" +#include "WSLCSession.h" + +namespace wsl::windows::service::wslc { + +HRESULT WSLCComposeSession::RuntimeClassInitialize( + WSLCSession* Session, std::wstring ConfigPath, ComposeSpec Spec, std::vector> Containers) +{ + RETURN_HR_IF_NULL(E_INVALIDARG, Session); + RETURN_HR_IF(E_INVALIDARG, ConfigPath.empty()); + RETURN_HR_IF(E_INVALIDARG, Containers.empty()); + + m_session = Session; + m_configPath = std::move(ConfigPath); + m_spec = std::move(Spec); + m_containers = std::move(Containers); + return S_OK; +} + +HRESULT WSLCComposeSession::GetConfigPath(LPWSTR* Path) +try +{ + RETURN_HR_IF_NULL(E_POINTER, Path); + *Path = nullptr; + + std::lock_guard lock(m_lock); + *Path = wil::make_cotaskmem_string(m_configPath.c_str()).release(); + return S_OK; +} +CATCH_RETURN(); + +HRESULT WSLCComposeSession::ListContainers(WSLCContainerEntry** Containers, ULONG* Count) +try +{ + RETURN_HR_IF_NULL(E_POINTER, Containers); + RETURN_HR_IF_NULL(E_POINTER, Count); + *Containers = nullptr; + *Count = 0; + + std::lock_guard lock(m_lock); + auto result = wil::make_unique_cotaskmem(m_containers.size()); + for (size_t index = 0; index < m_containers.size(); ++index) + { + auto& entry = result[index]; + wil::unique_cotaskmem_ansistring name; + THROW_IF_FAILED(m_containers[index]->GetName(&name)); + THROW_IF_FAILED(m_containers[index]->GetId(entry.Id)); + THROW_IF_FAILED(m_containers[index]->GetState(&entry.State)); + THROW_HR_IF(E_UNEXPECTED, strcpy_s(entry.Name, name.get()) != 0); + + wil::unique_cotaskmem_ansistring inspect; + THROW_IF_FAILED(m_containers[index]->Inspect(&inspect)); + const auto json = nlohmann::json::parse(inspect.get()); + const auto image = json.value("Image", std::string{}); + THROW_HR_IF(E_UNEXPECTED, strcpy_s(entry.Image, image.c_str()) != 0); + } + + *Count = static_cast(m_containers.size()); + *Containers = result.release(); + return S_OK; +} +CATCH_RETURN(); + +HRESULT WSLCComposeSession::Start() +try +{ + std::lock_guard lock(m_lock); + + for (const auto& container : m_containers) + { + const HRESULT result = container->Delete(WSLCDeleteFlagsForce); + THROW_IF_FAILED_EXCEPT(result, RPC_E_DISCONNECTED); + } + + m_containers = m_session->CreateComposeContainers(m_spec); + for (const auto& container : m_containers) + { + const HRESULT result = container->Start(WSLCContainerStartFlagsNone, nullptr, nullptr); + THROW_IF_FAILED_EXCEPT(result, WSLC_E_CONTAINER_IS_RUNNING); + } + + return S_OK; +} +CATCH_RETURN(); + +HRESULT WSLCComposeSession::Attach() +{ + // TODO + return S_OK; +} + +HRESULT WSLCComposeSession::Stop(ULONG Timeout) +try +{ + THROW_HR_IF(E_INVALIDARG, Timeout > LONG_MAX); + + std::lock_guard lock(m_lock); + HRESULT firstFailure = S_OK; + for (const auto& container : m_containers) + { + const HRESULT result = container->Stop(WSLCSignalSIGTERM, static_cast(Timeout)); + if (FAILED(result) && result != WSLC_E_CONTAINER_NOT_RUNNING && SUCCEEDED(firstFailure)) + { + firstFailure = result; + } + } + + return firstFailure; +} +CATCH_RETURN(); + +HRESULT WSLCComposeSession::InterfaceSupportsErrorInfo(REFIID InterfaceId) +{ + return InterfaceId == __uuidof(IWSLCComposeSession) ? S_OK : S_FALSE; +} + +} // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCComposeSession.h b/src/windows/wslcsession/WSLCComposeSession.h new file mode 100644 index 0000000000..a4d5bc1e5e --- /dev/null +++ b/src/windows/wslcsession/WSLCComposeSession.h @@ -0,0 +1,48 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCComposeSession.h + +Abstract: + + Contains the minimal compose session implementation. + +--*/ + +#pragma once + +#include "ComposeSpec.h" +#include "wslc.h" +#include +#include + +namespace wsl::windows::service::wslc { + +class WSLCSession; + +class DECLSPEC_UUID("A8AA75A3-5B41-45AE-A847-1DF6CF35EAA0") WSLCComposeSession + : public Microsoft::WRL::RuntimeClass, IWSLCComposeSession, IFastRundown, ISupportErrorInfo> +{ +public: + HRESULT RuntimeClassInitialize(WSLCSession* Session, std::wstring ConfigPath, ComposeSpec Spec, std::vector> Containers); + + IFACEMETHOD(GetConfigPath)(_Out_ LPWSTR* Path) override; + IFACEMETHOD(ListContainers)(_Out_ WSLCContainerEntry** Containers, _Out_ ULONG* Count) override; + IFACEMETHOD(Start()) override; + IFACEMETHOD(Attach()) override; + IFACEMETHOD(Stop)(_In_ ULONG Timeout) override; + + IFACEMETHOD(InterfaceSupportsErrorInfo)(_In_ REFIID InterfaceId) override; + +private: + std::mutex m_lock; + WSLCSession* m_session{}; + std::wstring m_configPath; + ComposeSpec m_spec; + std::vector> m_containers; +}; + +} // namespace wsl::windows::service::wslc diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 43599b0992..bf6a388584 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -13,11 +13,14 @@ Module Name: --*/ #include "precomp.h" +#include "ComposeSpec.h" #include "WSLCSession.h" +#include "WSLCComposeSession.h" #include "WSLCExecutionContext.h" #include "WSLCContainer.h" #include "WSLCNetworkMetadata.h" #include "ContainerNameGenerator.h" +#include "ServiceContainerLauncher.h" #include "ServiceProcessLauncher.h" #include "WindowsCertStore.h" #include "WslCoreFilesystem.h" @@ -925,12 +928,19 @@ try RETURN_HR_IF_NULL(E_POINTER, Image); + auto runtime = m_runtime.Acquire(); + PullImageLockHeld(Image, RegistryAuthenticationInformation, ProgressCallback); + return S_OK; +} +CATCH_RETURN(); + +void WSLCSession::PullImageLockHeld(LPCSTR Image, LPCSTR RegistryAuthenticationInformation, IProgressCallback* ProgressCallback) +{ const auto reference = wslutil::ImageReference::Parse(Image); const auto& repo = reference.Repository; auto tagOrDigest = reference.TagOrDigest(); EnforceRegistryAllowlist(repo); - auto runtime = m_runtime.Acquire(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasDocker()); if (!tagOrDigest.has_value()) @@ -945,14 +955,11 @@ try registryAuth = std::string(RegistryAuthenticationInformation); } - auto requestContext = runtime.Docker().PullImage(repo.Name, tagOrDigest, registryAuth); + auto requestContext = m_runtime.Docker().PullImage(repo.Name, tagOrDigest, registryAuth); StreamImageOperation(*requestContext, Image, "Pull", ProgressCallback); OnImageCreated(Image); - - return S_OK; } -CATCH_RETURN(); HRESULT WSLCSession::BuildImage(const WSLCBuildImageOptions* Options, IProgressCallback* ProgressCallback, HANDLE CancelEvent) try @@ -2259,6 +2266,118 @@ try } CATCH_RETURN(); +HRESULT WSLCSession::CreateComposeSession(LPCWSTR Path, IWSLCComposeSession** ComposeSession) +try +{ + WSLCExecutionContext context(this); + RETURN_HR_IF_NULL(E_POINTER, Path); + RETURN_HR_IF_NULL(E_POINTER, ComposeSession); + *ComposeSession = nullptr; + + std::error_code error; + const auto configPath = std::filesystem::canonical(Path, error); + THROW_IF_WIN32_ERROR_MSG(error.value(), "Failed to resolve compose path %ls", Path); + + auto key = configPath.wstring(); + std::ranges::transform(key, key.begin(), [](wchar_t value) { return std::towlower(value); }); + + std::lock_guard composeLock(m_composeSessionsLock); + if (const auto existing = m_composeSessions.find(key); existing != m_composeSessions.end()) + { + // TODO: Check the state of the compose session before returning. + return existing->second.CopyTo(ComposeSession); + } + + const auto spec = ComposeSpec::Parse(configPath); + auto containers = CreateComposeContainers(spec); + + Microsoft::WRL::ComPtr composeSession; + THROW_IF_FAILED(Microsoft::WRL::MakeAndInitialize(&composeSession, this, configPath.wstring(), spec, std::move(containers))); + auto [entry, inserted] = m_composeSessions.emplace(std::move(key), std::move(composeSession)); + WI_ASSERT(inserted); + + return entry->second.CopyTo(ComposeSession); +} +CATCH_RETURN(); + +std::vector> WSLCSession::CreateComposeContainers(const ComposeSpec& Spec) +{ + std::vector> containers; + std::string networkName = Spec.ProjectName + "_default"; // TODO: Implement this properly. + + // Create a network for the compose session. + // TODO: open an existing network instead of deleting. + + auto networkCleanup = DeleteNetworkImpl(networkName.c_str()); + THROW_HR_IF_MSG( + networkCleanup, + FAILED(networkCleanup) && networkCleanup != WSLC_E_NETWORK_NOT_FOUND, + "Failed to delete network %hs", + networkName.c_str()); + + WSLCNetworkOptions networkOptions{}; + networkOptions.Name = networkName.c_str(); + THROW_IF_FAILED(CreateNetworkImpl(&networkOptions)); + + auto cleanup = wil::scope_exit([&] { + for (const auto& container : containers) + { + LOG_IF_FAILED(container->Delete(WSLCDeleteFlagsForce)); + } + + LOG_IF_FAILED(DeleteNetworkImpl(networkName.c_str())); + }); + + auto lease = AcquireLease(); + for (const auto& definition : Spec.Containers) + { + ServiceContainerLauncher launcher( + definition.Image, definition.Name, definition.Command, definition.Environment, networkName, WSLCProcessFlagsStdin); + if (!definition.WorkingDirectory.empty()) + { + launcher.SetWorkingDirectory(std::string{definition.WorkingDirectory}); + } + + for (const auto& port : definition.Ports) + { + launcher.AddPort(port.HostPort, port.ContainerPort, AF_INET); + } + + launcher.AddPrimaryNetworkAlias(definition.Name); + + for (const auto& volume : definition.Volumes) + { + if (volume.HostPath.has_value()) + { + launcher.AddVolume(*volume.HostPath, volume.ContainerPath, volume.ReadOnly); + } + else + { + launcher.AddNamedVolume(volume.Name, volume.ContainerPath, volume.ReadOnly); + } + } + + Microsoft::WRL::ComPtr container; + HRESULT result = wil::ResultFromException([&]() { container = launcher.Create(*this); }); + if (result == WSLC_E_IMAGE_NOT_FOUND) + { + // TODO: Wire the pull output to caller. + PullImageLockHeld(definition.Image.c_str(), nullptr, nullptr); + container = launcher.Create(*this); + } + else + { + THROW_IF_FAILED(result); + } + + containers.emplace_back(std::move(container)); + } + + cleanup.release(); + + return containers; +} + void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptions, IWSLCContainer** Container) { THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_runtime.HasVm()); @@ -2847,7 +2966,13 @@ HRESULT WSLCSession::CreateNetwork(const WSLCNetworkOptions* Options, IWarningCa try { WSLCExecutionContext context(this, WarningCallback); + return CreateNetworkImpl(Options); +} +CATCH_RETURN(); +HRESULT WSLCSession::CreateNetworkImpl(const WSLCNetworkOptions* Options) +try +{ RETURN_HR_IF_NULL(E_POINTER, Options); RETURN_HR_IF_NULL(E_POINTER, Options->Name); @@ -2975,7 +3100,13 @@ HRESULT WSLCSession::DeleteNetwork(LPCSTR Name) try { WSLCExecutionContext context(this); + return DeleteNetworkImpl(Name); +} +CATCH_RETURN(); +HRESULT WSLCSession::DeleteNetworkImpl(LPCSTR Name) +try +{ RETURN_HR_IF_NULL(E_POINTER, Name); std::string name = Name; ValidateName(name.c_str(), WSLC_MAX_NETWORK_NAME_LENGTH); diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index 0a302522cc..24c9956ad9 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -16,6 +16,7 @@ Module Name: #include "wslc.h" #include "WSLCCompat.h" +#include "WSLCComposeSession.h" #include "WSLCVirtualMachine.h" #include "WSLCContainer.h" #include "WSLCIdleState.h" @@ -33,6 +34,7 @@ Module Name: namespace wsl::windows::service::wslc { class WSLCSession; +class ServiceContainerLauncher; class UserHandle { @@ -84,6 +86,8 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession // WSLCContainer::Delete acquires a VmLease to keep the VM alive (and block idle // teardown) for the duration of a container deletion. friend class WSLCContainer; + friend class WSLCComposeSession; + friend class ServiceContainerLauncher; public: WSLCSession() : m_runtime(*this) @@ -155,6 +159,7 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession IFACEMETHOD(CreateContainer)(_In_ const WSLCContainerOptions* Options, _In_opt_ IWarningCallback* WarningCallback, _Out_ IWSLCContainer** Container) override; IFACEMETHOD(OpenContainer)(_In_ LPCSTR Id, _In_ IWSLCContainer** Container) override; IFACEMETHOD(BeginContainerOperation)(_Outptr_ IUnknown** Operation) override; + IFACEMETHOD(CreateComposeSession)(_In_ LPCWSTR Path, _Out_ IWSLCComposeSession** ComposeSession) override; IFACEMETHOD(ListContainers)( _In_opt_ const WSLCListContainersOptions* Options, _Out_ WSLCContainerEntry** Containers, @@ -320,6 +325,8 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession __requires_lock_held(m_userCOMCallbacksLock) void CancelUserCOMCallbacks(); void CreateContainerImpl(const WSLCContainerOptions* Options, IWSLCContainer** Container); + HRESULT CreateNetworkImpl(const WSLCNetworkOptions* Options); + HRESULT DeleteNetworkImpl(LPCSTR Name); void ConfigureStorage(const WSLCSessionInitSettings& Settings, PSID UserSid); @@ -332,6 +339,7 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession void OnImageCreated(const std::string& ImageNameOrId) noexcept; void OnImageDeleted(const std::string& ImageId) noexcept; + void PullImageLockHeld(LPCSTR Image, LPCSTR RegistryAuthenticationInformation, IProgressCallback* ProgressCallback); void OnContainerdExited(); void OnDockerdExited(); @@ -345,6 +353,7 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession DockerHTTPClient::HTTPRequestContext& Request, const WSLCHandle ImageHandle, IImageLoadCallback* LoadCallback = nullptr); void RecoverExistingContainers(); void RecoverExistingNetworks(); + std::vector> CreateComposeContainers(const ComposeSpec& Spec); void SaveImageImpl(std::pair& RequestCodePair, WSLCHandle OutputHandle, HANDLE CancelEvent); void StreamImageOperation(DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback); @@ -367,6 +376,8 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession // WSLCVolumes has its own internal srwlock and does not require the runtime lock. std::mutex m_containersLock; std::unordered_map> m_containers; + std::mutex m_composeSessionsLock; + std::unordered_map> m_composeSessions; std::mutex m_networksLock; std::unordered_map m_networks; wil::shared_event m_sessionTerminatingEvent{wil::EventOptions::ManualReset}; diff --git a/test/windows/CMakeLists.txt b/test/windows/CMakeLists.txt index b19e5f9d9d..5f509c0aa1 100644 --- a/test/windows/CMakeLists.txt +++ b/test/windows/CMakeLists.txt @@ -11,6 +11,7 @@ set(SOURCES PluginTests.cpp PolicyTests.cpp InstallerTests.cpp + WSLCComposeTests.cpp WSLCTests.cpp WslcSdkTests.cpp WslcSdkWinRtTests.cpp diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index e0b7577f98..05a4bb8e5b 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -3002,6 +3002,69 @@ void LoadTestImage(IWSLCSession& session, std::string_view imageName) THROW_IF_FAILED(session.LoadImage(wsl::windows::common::wslutil::ToCOMInputHandle(imageFile.get()), fileSize.QuadPart, nullptr, nullptr)); } +namespace wsl::test { + +void LoadTestImages(IWSLCSession& session, std::initializer_list imageNames) +{ + wil::unique_cotaskmem_array_ptr images; + THROW_IF_FAILED(session.ListImages(nullptr, &images, images.size_address())); + + std::set> loadedImages; + for (const auto& image : images) + { + loadedImages.emplace(image.Image); + } + + for (const auto imageName : imageNames) + { + if (!loadedImages.contains(imageName)) + { + ::LoadTestImage(session, imageName); + loadedImages.emplace(imageName); + } + } +} + +WSLCSessionSettings GetDefaultWSLCSessionSettings(LPCWSTR name, LPCWSTR storagePath, WSLCNetworkingMode networkingMode) +{ + WSLCSessionSettings settings{}; + settings.DisplayName = name; + settings.CpuCount = 4; + settings.MemoryMb = 2048; + settings.BootTimeoutMs = 30 * 1000; + settings.StoragePath = storagePath; + settings.MaximumStorageSizeMb = 1024 * 20; + settings.NetworkingMode = networkingMode; + + return settings; +} + +wil::com_ptr OpenSessionManager() +{ + wil::com_ptr sessionManager; + THROW_IF_FAILED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager))); + wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get()); + + return sessionManager; +} + +wil::com_ptr CreateSession(const WSLCSessionSettings& sessionSettings, WSLCSessionFlags flags) +{ + const auto sessionManager = OpenSessionManager(); + + wil::com_ptr session; + THROW_IF_FAILED(sessionManager->CreateSession(&sessionSettings, flags, nullptr, &session)); + wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); + + WSLCSessionState state{}; + THROW_IF_FAILED(session->GetState(&state)); + THROW_HR_IF(E_UNEXPECTED, state != WSLCSessionStateRunning); + + return session; +} + +} // namespace wsl::test + void ExpectHttpResponse(LPCWSTR Url, std::optional expectedCode, bool retry) { const winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter filter; diff --git a/test/windows/Common.h b/test/windows/Common.h index 599ac069b2..db138e399a 100644 --- a/test/windows/Common.h +++ b/test/windows/Common.h @@ -656,6 +656,16 @@ std::filesystem::path GetTestImagePath(std::string_view imageName); void LoadTestImage(IWSLCSession& session, std::string_view imageName); +namespace wsl::test { + +void LoadTestImages(IWSLCSession& session, std::initializer_list imageNames); + +WSLCSessionSettings GetDefaultWSLCSessionSettings(LPCWSTR name, LPCWSTR storagePath = nullptr, WSLCNetworkingMode networkingMode = WSLCNetworkingModeNone); +wil::com_ptr OpenSessionManager(); +wil::com_ptr CreateSession(const WSLCSessionSettings& sessionSettings, WSLCSessionFlags flags = WSLCSessionFlagsNone); + +} // namespace wsl::test + void ExpectHttpResponse(LPCWSTR Url, std::optional expectedCode, bool retry = false); std::optional GetHostAdapterIpv4(); diff --git a/test/windows/WSLCComposeTests.cpp b/test/windows/WSLCComposeTests.cpp new file mode 100644 index 0000000000..a4cc785869 --- /dev/null +++ b/test/windows/WSLCComposeTests.cpp @@ -0,0 +1,202 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCComposeTests.cpp + +Abstract: + + This file contains test cases for WSLC compose sessions. + +--*/ + +#include "precomp.h" +#include "Common.h" +#include "wslc.h" +#include "wslc_schema.h" +#include "wslutil.h" + +using wsl::test::CreateSession; +using wsl::test::GetDefaultWSLCSessionSettings; +using wsl::test::LoadTestImages; + +extern bool g_fastTestRun; + +class WSLCComposeTests +{ + WSLC_TEST_CLASS(WSLCComposeTests) + + std::filesystem::path m_storagePath; + wil::com_ptr m_defaultSession; + + TEST_CLASS_SETUP(TestClassSetup) + { + m_storagePath = std::filesystem::current_path() / "test-storage"; + + const auto settings = GetDefaultWSLCSessionSettings(L"wslc-compose-test", m_storagePath.c_str(), WSLCNetworkingModeConsomme); + m_defaultSession = CreateSession(settings); + + LoadTestImages(*m_defaultSession, {"alpine:latest", "python:3.12-alpine"}); + + wsl::windows::common::wslutil::PruneResult result; + VERIFY_SUCCEEDED(m_defaultSession->PruneContainers(nullptr, 0, &result.result)); + + return true; + } + + TEST_CLASS_CLEANUP(TestClassCleanup) + { + wsl::windows::common::wslutil::PruneResult result; + LOG_IF_FAILED(m_defaultSession->PruneContainers(nullptr, 0, &result.result)); + m_defaultSession.reset(); + + if (!g_fastTestRun && !m_storagePath.empty()) + { + std::error_code error; + std::filesystem::remove_all(m_storagePath, error); + if (error) + { + LogError("Failed to cleanup storage path %ws: %hs", m_storagePath.c_str(), error.message().c_str()); + } + } + + return true; + } + + TEST_METHOD(ComposeSessionBasicLifecycle) + { + const auto composePath = std::filesystem::current_path() / std::format("compose-{}.yaml", GetCurrentProcessId()); + const auto volumePath = std::filesystem::current_path() / std::format("compose-volume-{}", GetCurrentProcessId()); + auto cleanup = wil::scope_exit([&] { + std::error_code error; + std::filesystem::remove(composePath, error); + std::filesystem::remove_all(volumePath, error); + }); + + std::filesystem::create_directory(volumePath); + std::ofstream(volumePath / "content.txt") << "compose-volume"; + + { + std::ofstream composeFile(composePath); + composeFile << "services:\n" + " basic:\n" + " name: wslc-compose-basic\n" + " image: alpine:latest\n" + " environment:\n" + " COMPOSE_TEST: enabled\n" + " working_dir: /work\n" + " command: [\"/bin/sh\", \"-c\", \"while true; do echo $COMPOSE_TEST; sleep 1; done\"]\n" + " volumes:\n" + " - ./" + << volumePath.filename().string() + << ":/work:ro\n" + " ports:\n" + " - \"0:8080\"\n" + " secondary:\n" + " name: wslc-compose-secondary\n" + " image: alpine:latest\n" + " command: [\"/bin/sh\", \"-c\", \"while true; do echo secondary; sleep 1; done\"]\n"; + } + + wil::com_ptr composeSession; + VERIFY_SUCCEEDED(m_defaultSession->CreateComposeSession(composePath.c_str(), &composeSession)); + VERIFY_IS_NOT_NULL(composeSession.get()); + + wil::unique_cotaskmem_array_ptr containers; + VERIFY_SUCCEEDED(composeSession->ListContainers(&containers, containers.size_address())); + VERIFY_ARE_EQUAL(2u, containers.size()); + VERIFY_ARE_EQUAL(std::string("wslc-compose-basic"), std::string(containers[0].Name)); + VERIFY_ARE_EQUAL(WslcContainerStateCreated, containers[0].State); + + wil::com_ptr basicContainer; + VERIFY_SUCCEEDED(m_defaultSession->OpenContainer("wslc-compose-basic", &basicContainer)); + const auto inspect = InspectContainer(basicContainer.get()); + VERIFY_ARE_EQUAL(std::string("/work"), inspect.Config.WorkingDir); + VERIFY_IS_TRUE(inspect.Config.Cmd.has_value()); + VERIFY_ARE_EQUAL( + std::vector({"/bin/sh", "-c", "while true; do echo $COMPOSE_TEST; sleep 1; done"}), *inspect.Config.Cmd); + VERIFY_IS_TRUE(inspect.Config.Env.has_value()); + VERIFY_IS_TRUE(std::ranges::find(*inspect.Config.Env, std::string("COMPOSE_TEST=enabled")) != inspect.Config.Env->end()); + VERIFY_ARE_EQUAL(1u, inspect.Mounts.size()); + VERIFY_ARE_EQUAL(std::string("/work"), inspect.Mounts[0].Destination); + VERIFY_IS_FALSE(inspect.Mounts[0].ReadWrite); + VERIFY_IS_TRUE(inspect.Ports.contains("8080/tcp")); + VERIFY_ARE_EQUAL(1u, inspect.Ports.at("8080/tcp").size()); + VERIFY_ARE_EQUAL(std::string("127.0.0.1"), inspect.Ports.at("8080/tcp")[0].HostIp); + + const std::string basicContainerId = containers[0].Id; + const std::string secondaryContainerId = containers[1].Id; + wil::com_ptr secondaryContainer; + VERIFY_SUCCEEDED(m_defaultSession->OpenContainer("wslc-compose-secondary", &secondaryContainer)); + VERIFY_SUCCEEDED(basicContainer->Delete(WSLCDeleteFlagsForce)); + VERIFY_SUCCEEDED(secondaryContainer->Delete(WSLCDeleteFlagsForce)); + + VERIFY_SUCCEEDED(composeSession->Start()); + containers.reset(); + VERIFY_SUCCEEDED(composeSession->ListContainers(&containers, containers.size_address())); + VERIFY_ARE_EQUAL(WslcContainerStateRunning, containers[0].State); + VERIFY_ARE_EQUAL(WslcContainerStateRunning, containers[1].State); + VERIFY_ARE_NOT_EQUAL(basicContainerId, std::string(containers[0].Id)); + VERIFY_ARE_NOT_EQUAL(secondaryContainerId, std::string(containers[1].Id)); + + VERIFY_SUCCEEDED(composeSession->Attach()); + VERIFY_SUCCEEDED(composeSession->Stop(10)); + containers.reset(); + VERIFY_SUCCEEDED(composeSession->ListContainers(&containers, containers.size_address())); + VERIFY_ARE_EQUAL(WslcContainerStateExited, containers[0].State); + VERIFY_ARE_EQUAL(WslcContainerStateExited, containers[1].State); + } + + TEST_METHOD(ComposeContainerNameHttpRequest) + { + const auto composePath = std::filesystem::current_path() / std::format("compose-http-{}.yaml", GetCurrentProcessId()); + auto cleanup = wil::scope_exit([&] { + std::error_code error; + std::filesystem::remove(composePath, error); + }); + + { + std::ofstream composeFile(composePath); + composeFile + << "services:\n" + " server:\n" + " name: wslc-compose-http-server\n" + " image: python:3.12-alpine\n" + " command: [\"python3\", \"-m\", \"http.server\", \"8000\"]\n" + " client:\n" + " name: wslc-compose-http-client\n" + " image: python:3.12-alpine\n" + " command: [\"python3\", \"-c\", \"import time, urllib.request\\nwhile True:\\n try:\\n with " + "urllib.request.urlopen(\\\"http://wslc-compose-http-server:8000/\\\", timeout=10) as response:\\n " + "if response.status == 200:\\n break\\n except Exception:\\n pass\\n " + "time.sleep(1)\"]\n"; + } + + wil::com_ptr composeSession; + VERIFY_SUCCEEDED(m_defaultSession->CreateComposeSession(composePath.c_str(), &composeSession)); + auto stopCompose = wil::scope_exit([&] { LOG_IF_FAILED(composeSession->Stop(0)); }); + + VERIFY_SUCCEEDED(composeSession->Start()); + + wil::com_ptr clientContainer; + VERIFY_SUCCEEDED(m_defaultSession->OpenContainer("wslc-compose-http-client", &clientContainer)); + wsl::shared::retry::RetryWithTimeout( + [&]() { + const auto inspect = InspectContainer(clientContainer.get()); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_RETRY), inspect.State.Status != "exited"); + THROW_HR_IF(E_FAIL, inspect.State.ExitCode != 0); + }, + 100ms, + 120s); + } + +private: + static wsl::windows::common::wslc_schema::InspectContainer InspectContainer(IWSLCContainer* container) + { + wil::unique_cotaskmem_ansistring inspectJson; + THROW_IF_FAILED(container->Inspect(&inspectJson)); + return wsl::shared::FromJson(inspectJson.get()); + } +}; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 2a80855908..e6b03dce1f 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -35,6 +35,10 @@ using wsl::windows::common::WSLCProcessLauncher; using wsl::windows::common::io::OverlappedIOHandle; using wsl::windows::common::io::WriteHandle; using namespace wsl::windows::common::wslutil; +using wsl::test::CreateSession; +using wsl::test::GetDefaultWSLCSessionSettings; +using wsl::test::LoadTestImages; +using wsl::test::OpenSessionManager; using WSLCE2ETests::StartLocalRegistry; extern std::wstring g_testDataPath; @@ -59,38 +63,9 @@ class WSLCTests m_defaultSessionSettings = GetDefaultSessionSettings(c_testSessionName, true, WSLCNetworkingModeConsomme); m_defaultSession = CreateSession(m_defaultSessionSettings); - wil::unique_cotaskmem_array_ptr images; - VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, &images, images.size_address())); - - auto hasImage = [&](const std::string& imageName) { - return std::ranges::any_of( - images.get(), images.get() + images.size(), [&](const auto& e) { return e.Image == imageName; }); - }; - - if (!hasImage("debian:latest")) - { - LoadTestImage(*m_defaultSession, "debian:latest"); - } - - if (!hasImage("python:3.12-alpine")) - { - LoadTestImage(*m_defaultSession, "python:3.12-alpine"); - } - - if (!hasImage("hello-world:latest")) - { - LoadTestImage(*m_defaultSession, "hello-world:latest"); - } - - if (!hasImage("alpine:latest")) - { - LoadTestImage(*m_defaultSession, "alpine:latest"); - } - - if (!hasImage("wslc-registry:latest")) - { - LoadTestImage(*m_defaultSession, "wslc-registry:latest"); - } + LoadTestImages( + *m_defaultSession, + {"debian:latest", "python:3.12-alpine", "hello-world:latest", "alpine:latest", "wslc-registry:latest"}); PruneResult result; VERIFY_SUCCEEDED(m_defaultSession->PruneContainers(nullptr, 0, &result.result)); @@ -122,16 +97,7 @@ class WSLCTests WSLCSessionSettings GetDefaultSessionSettings(LPCWSTR Name, bool enableStorage = false, WSLCNetworkingMode networkingMode = WSLCNetworkingModeNone) { - WSLCSessionSettings settings{}; - settings.DisplayName = Name; - settings.CpuCount = 4; - settings.MemoryMb = 2048; - settings.BootTimeoutMs = 30 * 1000; - settings.StoragePath = enableStorage ? m_storagePath.c_str() : nullptr; - settings.MaximumStorageSizeMb = 1024 * 20; // 20GB. - settings.NetworkingMode = networkingMode; - - return settings; + return GetDefaultWSLCSessionSettings(Name, enableStorage ? m_storagePath.c_str() : nullptr, networkingMode); } auto ResetTestSession() @@ -141,15 +107,6 @@ class WSLCTests return wil::scope_exit([this]() { m_defaultSession = CreateSession(m_defaultSessionSettings); }); } - static wil::com_ptr OpenSessionManager() - { - wil::com_ptr sessionManager; - VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager))); - wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get()); - - return sessionManager; - } - // Returns true for the names the wslc CLI reserves for its default sessions. static bool IsCliSessionName(std::wstring_view Name) { @@ -182,22 +139,6 @@ class WSLCTests return names; } - wil::com_ptr CreateSession(const WSLCSessionSettings& sessionSettings, WSLCSessionFlags Flags = WSLCSessionFlagsNone) - { - const auto sessionManager = OpenSessionManager(); - - wil::com_ptr session; - - VERIFY_SUCCEEDED(sessionManager->CreateSession(&sessionSettings, Flags, nullptr, &session)); - wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); - - WSLCSessionState state{}; - VERIFY_SUCCEEDED(session->GetState(&state)); - VERIFY_ARE_EQUAL(state, WSLCSessionStateRunning); - - return session; - } - RunningWSLCContainer OpenContainer(IWSLCSession* session, const std::string& name) { wil::com_ptr rawContainer; diff --git a/test/windows/wslc/WSLCCLICommandUnitTests.cpp b/test/windows/wslc/WSLCCLICommandUnitTests.cpp index 59b8c1a3d4..223a6786e7 100644 --- a/test/windows/wslc/WSLCCLICommandUnitTests.cpp +++ b/test/windows/wslc/WSLCCLICommandUnitTests.cpp @@ -21,6 +21,7 @@ Module Name: #include "Command.h" #include "RootCommand.h" #include "ContainerCommand.h" +#include "ComposeCommand.h" #include "SessionCommand.h" #include "SystemCommand.h" #include "VersionCommand.h" @@ -148,6 +149,19 @@ class WSLCCLICommandUnitTests } } + TEST_METHOD(ComposeCommand_HasExpectedSubcommands) + { + auto cmd = ComposeCommand(L"root"); + auto subcommands = cmd.GetCommands(); + + VERIFY_ARE_EQUAL(5u, subcommands.size()); + VERIFY_ARE_EQUAL(ComposeCreateCommand::CommandName, subcommands[0]->Name()); + VERIFY_ARE_EQUAL(ComposeUpCommand::CommandName, subcommands[1]->Name()); + VERIFY_ARE_EQUAL(ComposeStartCommand::CommandName, subcommands[2]->Name()); + VERIFY_ARE_EQUAL(ComposeAttachCommand::CommandName, subcommands[3]->Name()); + VERIFY_ARE_EQUAL(ComposeStopCommand::CommandName, subcommands[4]->Name()); + } + // Test: Verify VersionCommand has the correct name TEST_METHOD(VersionCommand_HasCorrectName) {