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)
{