Add experimental implementation for pull & run with related API changes (#14050)

* Add API flags for creating & opening persistent sessions

* Properly handle session name conflicts

* Format

* Format

* Save state

* Working end to end pull

* Clean up console cursor logic

* Save state

* Save state

* Implement run

* Merge

* Correctly handle error messages

* Improve error handling

* Add test coverage
This commit is contained in:
Blue 2026-01-14 16:47:23 +00:00 committed by GitHub
parent 23bda7c6d6
commit f90c8e8e52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 714 additions and 138 deletions

View File

@ -308,7 +308,7 @@ public:
#ifdef WIN32
ArgumentParser(const std::wstring& CommandLine, LPCWSTR Name, int StartIndex = 1, bool ignoreUnknownArgs = false) :
m_startIndex(StartIndex), m_name(Name), m_ignoreUnknownArgs(ignoreUnknownArgs)
m_parseIndex(StartIndex), m_name(Name), m_ignoreUnknownArgs(ignoreUnknownArgs)
{
m_argv.reset(CommandLineToArgvW(std::wstring(CommandLine).c_str(), &m_argc));
THROW_LAST_ERROR_IF(!m_argv);
@ -317,7 +317,7 @@ public:
#else
ArgumentParser(int argc, const char* const* argv, bool ignoreUnknownArgs = false) :
m_argc(argc), m_argv(argv), m_startIndex(1), m_ignoreUnknownArgs(ignoreUnknownArgs)
m_argc(argc), m_argv(argv), m_parseIndex(1), m_ignoreUnknownArgs(ignoreUnknownArgs)
{
}
@ -354,13 +354,13 @@ public:
m_arguments.emplace_back(std::move(match), BuildParseMethod(std::forward<T>(Output)), true);
}
void Parse() const
void Parse()
{
int argumentPosition = 0;
bool stopParameters = false;
for (size_t i = m_startIndex; i < m_argc; i++)
for (; m_parseIndex < m_argc; m_parseIndex++)
{
if (!stopParameters && wsl::shared::string::IsEqual(m_argv[i], TEXT("--")))
if (!stopParameters && wsl::shared::string::IsEqual(m_argv[m_parseIndex], TEXT("--")))
{
stopParameters = true;
continue;
@ -370,9 +370,10 @@ public:
int offset = 0;
// Special case for short argument with multiple values like -abc
if (!stopParameters && m_argv[i][0] == '-' && m_argv[i][1] != '-' && m_argv[i][1] != '\0' && m_argv[i][2] != '\0')
if (!stopParameters && m_argv[m_parseIndex][0] == '-' && m_argv[m_parseIndex][1] != '-' &&
m_argv[m_parseIndex][1] != '\0' && m_argv[m_parseIndex][2] != '\0')
{
for (const auto* arg = &m_argv[i][1]; *arg != '\0'; arg++)
for (const auto* arg = &m_argv[m_parseIndex][1]; *arg != '\0'; arg++)
{
foundMatch = false;
for (const auto& e : m_arguments)
@ -397,23 +398,26 @@ public:
{
for (const auto& e : m_arguments)
{
if (e.Matches(stopParameters ? nullptr : m_argv[i], m_argv[i][0] == '-' && m_argv[i][1] != '\0' && !stopParameters ? -1 : argumentPosition))
if (e.Matches(
stopParameters ? nullptr : m_argv[m_parseIndex],
m_argv[m_parseIndex][0] == '-' && m_argv[m_parseIndex][1] != '\0' && !stopParameters ? -1 : argumentPosition))
{
const TChar* value = nullptr;
if (e.Positional)
{
value = m_argv[i]; // Positional arguments directly receive argv[i]
value = m_argv[m_parseIndex]; // Positional arguments directly receive argv[i]
}
else if (i + 1 < m_argc)
else if (m_parseIndex + 1 < m_argc)
{
value = m_argv[i + 1];
value = m_argv[m_parseIndex + 1];
}
offset = e.Consume(value);
if (offset < 0)
{
WI_ASSERT(value == nullptr);
THROW_USER_ERROR(wsl::shared::Localization::MessageMissingArgument(m_argv[i], m_name ? m_name : m_argv[0]));
THROW_USER_ERROR(
wsl::shared::Localization::MessageMissingArgument(m_argv[m_parseIndex], m_name ? m_name : m_argv[0]));
}
if (e.Positional) // Positional arguments can't consume extra arguments.
@ -421,7 +425,7 @@ public:
offset = 0;
}
i += offset;
m_parseIndex += offset;
foundMatch = true;
break;
@ -436,16 +440,32 @@ public:
break;
}
THROW_USER_ERROR(wsl::shared::Localization::MessageInvalidCommandLine(m_argv[i], m_name ? m_name : m_argv[0]));
THROW_USER_ERROR(wsl::shared::Localization::MessageInvalidCommandLine(m_argv[m_parseIndex], m_name ? m_name : m_argv[0]));
}
if (i < m_argc && m_argv[i - offset][0] != '-')
if (m_parseIndex < m_argc && m_argv[m_parseIndex - offset][0] != '-')
{
argumentPosition++;
}
}
}
size_t ParseIndex() const noexcept
{
return m_parseIndex;
}
size_t Argc() const noexcept
{
return m_argc;
}
const auto* Argv(size_t Index) const noexcept
{
WI_ASSERT(Index < static_cast<size_t>(m_argc));
return m_argv[Index];
}
private:
template <typename T>
static std::function<int(const TChar*)> BuildParseMethod(T&& Output)
@ -540,7 +560,7 @@ private:
#endif
int m_startIndex{};
int m_parseIndex{};
const TChar* m_name{};
bool m_ignoreUnknownArgs{false};
};

View File

@ -367,6 +367,11 @@ LXSS_ERROR_INFO* ClientExecutionContext::OutError() noexcept
return &m_outError;
}
void wsl::windows::common::SetErrorMessage(std::string&& message)
{
return SetErrorMessage(wsl::shared::string::MultiByteToWide(message));
}
void wsl::windows::common::SetErrorMessage(std::wstring&& message)
{
if (g_currentContext == nullptr || message.empty())

View File

@ -182,6 +182,7 @@ private:
void EnableContextualizedErrors(bool service);
void SetErrorMessage(std::wstring&& message);
void SetErrorMessage(std::string&& message);
void SetEventLog(HANDLE eventLog);

View File

@ -112,7 +112,7 @@ std::pair<HRESULT, std::optional<RunningWSLAContainer>> WSLAContainerLauncher::C
// TODO: Support volumes, ports, flags, shm size, container networking mode, etc.
wil::com_ptr<IWSLAContainer> container;
auto result = Session.CreateContainer(&options, &container);
auto result = Session.CreateContainer(&options, &container, nullptr);
if (FAILED(result))
{
return std::pair<HRESULT, std::optional<RunningWSLAContainer>>(result, std::optional<RunningWSLAContainer>{});

View File

@ -112,17 +112,9 @@ struct ShellExecOptions
}
};
bool IsInteractiveConsole()
{
const HANDLE stdinHandle = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode{};
return GetFileType(stdinHandle) == FILE_TYPE_CHAR && GetConsoleMode(stdinHandle, &mode);
}
void PromptForKeyPress()
{
if (IsInteractiveConsole())
if (wsl::windows::common::wslutil::IsInteractiveConsole())
{
wsl::windows::common::wslutil::PrintMessage(wsl::shared::Localization::MessagePressAnyKeyToExit());
LOG_IF_WIN32_BOOL_FALSE(FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE)));
@ -1649,7 +1641,7 @@ int WslaShell(_In_ std::wstring_view commandLine)
}
else
{
THROW_IF_FAILED(session->PullImage(containerImage.c_str(), nullptr, nullptr));
THROW_IF_FAILED(session->PullImage(containerImage.c_str(), nullptr, nullptr, nullptr));
std::vector<WSLA_PROCESS_FD> fds;
@ -1692,7 +1684,7 @@ int WslaShell(_In_ std::wstring_view commandLine)
}
container.emplace();
THROW_IF_FAILED(session->CreateContainer(&containerOptions, &container.value()));
THROW_IF_FAILED(session->CreateContainer(&containerOptions, &container.value(), nullptr));
THROW_IF_FAILED((*container)->Start());
wil::com_ptr<IWSLAProcess> createdProcess;
@ -1729,7 +1721,7 @@ int WslaShell(_In_ std::wstring_view commandLine)
});
// Required because ReadFile() blocks if stdin is a tty.
if (IsInteractiveConsole())
if (wsl::windows::common::wslutil::IsInteractiveConsole())
{
inputThread = std::thread{[&]() {
wsl::windows::common::relay::StandardInputRelay(Stdin, process->GetStdHandle(0).get(), []() {}, exitEvent.get());

View File

@ -1016,12 +1016,8 @@ IOHandleStatus OverlappedIOHandle::GetState() const
return State;
}
EventHandle::EventHandle(HANDLE Handle, std::function<void()>&& OnSignalled) : Handle(Handle), OnSignalled(std::move(OnSignalled))
{
}
EventHandle::EventHandle(wil::unique_event&& Handle, std::function<void()>&& OnSignalled) :
OwnedHandle(std::move(Handle)), Handle(OwnedHandle.get()), OnSignalled(std::move(OnSignalled))
EventHandle::EventHandle(HandleWrapper&& Handle, std::function<void()>&& OnSignalled) :
Handle(std::move(Handle)), OnSignalled(std::move(OnSignalled))
{
}
@ -1038,7 +1034,7 @@ void EventHandle::Collect()
HANDLE EventHandle::GetHandle() const
{
return Handle;
return Handle.Get();
}
ReadHandle::ReadHandle(HandleWrapper&& MovedHandle, std::function<void(const gsl::span<char>& Buffer)>&& OnRead) :

View File

@ -170,6 +170,18 @@ struct HandleWrapper
{
}
HandleWrapper(
wil::unique_socket&& handle, std::function<void()>&& OnClose = []() {}) :
OwnedHandle((HANDLE)handle.release()), Handle(OwnedHandle.get()), OnClose(std::move(OnClose))
{
}
HandleWrapper(
wil::unique_event&& handle, std::function<void()>&& OnClose = []() {}) :
OwnedHandle(handle.release()), Handle(OwnedHandle.get()), OnClose(std::move(OnClose))
{
}
HandleWrapper(
SOCKET handle, std::function<void()>&& OnClose = []() {}) :
Handle(reinterpret_cast<HANDLE>(handle)), OnClose(std::move(OnClose))
@ -237,15 +249,13 @@ public:
NON_COPYABLE(EventHandle)
NON_MOVABLE(EventHandle)
EventHandle(wil::unique_event&& EventHandle, std::function<void()>&& OnSignalled);
EventHandle(HANDLE EventHandle, std::function<void()>&& OnSignalled);
EventHandle(HandleWrapper&& EventHandle, std::function<void()>&& OnSignalled = []() {});
void Schedule() override;
void Collect() override;
HANDLE GetHandle() const override;
private:
wil::unique_event OwnedHandle;
HANDLE Handle;
HandleWrapper Handle;
std::function<void()> OnSignalled;
};

View File

@ -16,6 +16,7 @@ Abstract:
#include "wslutil.h"
#include "WslPluginApi.h"
#include "wslinstallerservice.h"
#include "wslaservice.h"
#include "ConsoleProgressBar.h"
#include "ExecutionContext.h"
@ -145,7 +146,8 @@ static const std::map<HRESULT, LPCWSTR> g_commonErrors{
X_WIN32(ERROR_BAD_PATHNAME),
X(WININET_E_TIMEOUT),
X_WIN32(ERROR_INVALID_SID),
X_WIN32(ERROR_INVALID_STATE)};
X_WIN32(ERROR_INVALID_STATE),
X(WSLA_E_IMAGE_NOT_FOUND)};
#undef X
@ -1197,6 +1199,14 @@ void wsl::windows::common::wslutil::InitializeWil()
}
}
bool wsl::windows::common::wslutil::IsInteractiveConsole()
{
const HANDLE stdinHandle = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode{};
return GetFileType(stdinHandle) == FILE_TYPE_CHAR && GetConsoleMode(stdinHandle, &mode);
}
bool wsl::windows::common::wslutil::IsRunningInMsix()
{
UINT32 dummy{};
@ -1590,4 +1600,32 @@ catch (...)
{
LOG_CAUGHT_EXCEPTION();
return nullptr;
}
wsl::windows::common::wslutil::WSLAErrorDetails::~WSLAErrorDetails()
{
Reset();
}
void wsl::windows::common::wslutil::WSLAErrorDetails::Reset()
{
CoTaskMemFree(Error.UserErrorMessage);
Error = {};
}
void wsl::windows::common::wslutil::WSLAErrorDetails::ThrowIfFailed(HRESULT Result)
{
if (SUCCEEDED(Result))
{
return;
}
if (Error.UserErrorMessage != nullptr)
{
THROW_HR_WITH_USER_ERROR(Result, Error.UserErrorMessage);
}
else
{
THROW_HR(Result);
}
}

View File

@ -19,6 +19,7 @@ Abstract:
#include "SubProcess.h"
#include <winrt/windows.management.deployment.h>
#include "JsonUtils.h"
#include "wslaservice.h"
namespace wsl::windows::common {
struct Error;
@ -64,6 +65,17 @@ struct GitHubRelease
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(GitHubRelease, name, assets, created_at);
};
struct WSLAErrorDetails
{
~WSLAErrorDetails();
void Reset();
void ThrowIfFailed(HRESULT Result);
WSLA_ERROR_INFO Error{};
};
template <typename T>
void AssertValidPrintfArg()
{
@ -143,6 +155,8 @@ std::vector<BYTE> HashFile(HANDLE File, DWORD Algorithm);
void InitializeWil();
bool IsInteractiveConsole();
bool IsRunningInMsix();
bool IsVhdFile(_In_ const std::filesystem::path& path);

View File

@ -151,4 +151,23 @@ struct StartExec
NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(StartExec, Tty, Detach, ConsoleSize);
};
struct CreateImageProgressDetails
{
uint64_t current{};
uint64_t total{};
std::string unit;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CreateImageProgressDetails, current, total, unit);
};
struct CreateImageProgress
{
std::string status;
std::string id;
CreateImageProgressDetails progressDetail;
NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CreateImageProgress, status, id, progressDetail);
};
} // namespace wsl::windows::common::docker_schema

View File

@ -24,11 +24,40 @@ Abstract:
using namespace wsl::shared;
namespace wslutil = wsl::windows::common::wslutil;
using wsl::windows::common::ClientRunningWSLAProcess;
using wsl::windows::common::Context;
using wsl::windows::common::ExecutionContext;
using wsl::windows::common::WSLAProcessLauncher;
using wsl::windows::common::relay::EventHandle;
using wsl::windows::common::relay::MultiHandleWait;
using wsl::windows::common::relay::RelayHandle;
using wsl::windows::common::wslutil::WSLAErrorDetails;
class ChangeTerminalMode
{
public:
NON_COPYABLE(ChangeTerminalMode);
NON_MOVABLE(ChangeTerminalMode);
ChangeTerminalMode(HANDLE Console, bool CursorVisible) : m_console(Console)
{
THROW_IF_WIN32_BOOL_FALSE(GetConsoleCursorInfo(Console, &m_originalCursorInfo));
CONSOLE_CURSOR_INFO newCursorInfo = m_originalCursorInfo;
newCursorInfo.bVisible = CursorVisible;
THROW_IF_WIN32_BOOL_FALSE(SetConsoleCursorInfo(Console, &newCursorInfo));
}
~ChangeTerminalMode()
{
LOG_IF_WIN32_BOOL_FALSE(SetConsoleCursorInfo(m_console, &m_originalCursorInfo));
}
private:
HANDLE m_console{};
CONSOLE_CURSOR_INFO m_originalCursorInfo{};
};
// Report an operation failure with localized context and HRESULT details.
static int ReportError(const std::wstring& context, HRESULT hr)
{
auto errorString = wsl::windows::common::wslutil::ErrorCodeToString(hr);
@ -284,7 +313,350 @@ static int RunListCommand(std::wstring_view commandLine)
return 0;
}
// Print localized usage message to stderr.
DEFINE_ENUM_FLAG_OPERATORS(WSLASessionFlags);
static wil::com_ptr<IWSLASession> OpenCLISession()
{
wil::com_ptr<IWSLAUserSession> userSession;
THROW_IF_FAILED(CoCreateInstance(__uuidof(WSLAUserSession), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&userSession)));
wsl::windows::common::security::ConfigureForCOMImpersonation(userSession.get());
auto dataFolder = std::filesystem::path(wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr)) / "wsla";
// TODO: Have a configuration file for those.
WSLA_SESSION_SETTINGS settings{};
settings.DisplayName = L"wsla-cli";
settings.CpuCount = 4;
settings.MemoryMb = 2024;
settings.BootTimeoutMs = 30 * 1000;
settings.StoragePath = dataFolder.c_str();
settings.MaximumStorageSizeMb = 10000; // 10GB.
settings.NetworkingMode = WSLANetworkingModeNAT;
wil::com_ptr<IWSLASession> session;
THROW_IF_FAILED(userSession->CreateSession(&settings, WSLASessionFlagsPersistent | WSLASessionFlagsOpenExisting, &session));
wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
return session;
}
static void PullImpl(IWSLASession& Session, const std::string& Image)
{
HANDLE Stdout = GetStdHandle(STD_OUTPUT_HANDLE);
// Configure console for interactive usage.
DWORD OriginalOutputMode{};
UINT OriginalOutputCP = GetConsoleOutputCP();
THROW_LAST_ERROR_IF(!::GetConsoleMode(Stdout, &OriginalOutputMode));
DWORD OutputMode = OriginalOutputMode;
WI_SetAllFlags(OutputMode, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN);
THROW_IF_WIN32_BOOL_FALSE(::SetConsoleMode(Stdout, OutputMode));
THROW_LAST_ERROR_IF(!::SetConsoleOutputCP(CP_UTF8));
// TODO: Handle terminal resizes.
class DECLSPEC_UUID("7A1D3376-835A-471A-8DC9-23653D9962D0") Callback
: public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IProgressCallback, IFastRundown>
{
public:
auto MoveToLine(SHORT Line, bool Revert = true)
{
if (Line > 0)
{
wprintf(L"\033[%iA", Line);
}
return wil::scope_exit([Line = Line]() {
if (Line > 1)
{
wprintf(L"\033[%iB", Line - 1);
}
});
}
HRESULT OnProgress(LPCSTR Status, LPCSTR Id, ULONGLONG Current, ULONGLONG Total) override
try
{
if (Id == nullptr || *Id == '\0') // Print all 'global' statuses on their own line
{
wprintf(L"%hs\n", Status);
m_currentLine++;
return S_OK;
}
auto info = Info();
auto it = m_statuses.find(Id);
if (it == m_statuses.end())
{
// If this is the first time we see this ID, create a new line for it.
m_statuses.emplace(Id, m_currentLine);
wprintf(L"%ls\n", GenerateStatusLine(Status, Id, Current, Total, info).c_str());
m_currentLine++;
}
else
{
auto revert = MoveToLine(m_currentLine - it->second);
wprintf(L"%ls\n", GenerateStatusLine(Status, Id, Current, Total, info).c_str());
}
return S_OK;
}
CATCH_RETURN();
private:
static CONSOLE_SCREEN_BUFFER_INFO Info()
{
CONSOLE_SCREEN_BUFFER_INFO info{};
THROW_IF_WIN32_BOOL_FALSE(GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info));
return info;
}
std::wstring GenerateStatusLine(LPCSTR Status, LPCSTR Id, ULONGLONG Current, ULONGLONG Total, const CONSOLE_SCREEN_BUFFER_INFO& Info)
{
std::wstring line;
if (Total != 0)
{
line = std::format(L"{} '{}': {}%", Status, Id, Current * 100 / Total);
}
else if (Current != 0)
{
line = std::format(L"{} '{}': {}s", Status, Id, Current);
}
else
{
line = std::format(L"{} '{}'", Status, Id);
}
// Erase any previously written char on that line.
while (line.size() < Info.dwSize.X)
{
line += L' ';
}
return line;
}
std::map<std::string, SHORT> m_statuses;
SHORT m_currentLine = 0;
ChangeTerminalMode m_terminalMode{GetStdHandle(STD_OUTPUT_HANDLE), false};
};
wil::com_ptr<IWSLASession> session = OpenCLISession();
Callback callback;
WSLAErrorDetails error{};
auto result = session->PullImage(Image.c_str(), nullptr, &callback, &error.Error);
error.ThrowIfFailed(result);
}
static int Pull(std::wstring_view commandLine)
{
ArgumentParser parser(std::wstring{commandLine}, L"wsladiag", 2);
std::string image;
parser.AddPositionalArgument(Utf8String{image}, 0);
parser.Parse();
THROW_HR_IF(E_INVALIDARG, image.empty());
PullImpl(*OpenCLISession(), image);
return 0;
}
static int InteractiveShell(ClientRunningWSLAProcess&& Process, bool Tty)
{
HANDLE Stdout = GetStdHandle(STD_OUTPUT_HANDLE);
HANDLE Stdin = GetStdHandle(STD_INPUT_HANDLE);
auto exitEvent = Process.GetExitEvent();
if (Tty)
{
// Save original console modes so they can be restored on exit.
DWORD OriginalInputMode{};
DWORD OriginalOutputMode{};
UINT OriginalOutputCP = GetConsoleOutputCP();
THROW_LAST_ERROR_IF(!::GetConsoleMode(Stdin, &OriginalInputMode));
THROW_LAST_ERROR_IF(!::GetConsoleMode(Stdout, &OriginalOutputMode));
auto restoreConsoleMode = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
SetConsoleMode(Stdin, OriginalInputMode);
SetConsoleMode(Stdout, OriginalOutputMode);
SetConsoleOutputCP(OriginalOutputCP);
});
// Configure console for interactive usage.
DWORD InputMode = OriginalInputMode;
WI_SetAllFlags(InputMode, (ENABLE_WINDOW_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT));
WI_ClearAllFlags(InputMode, (ENABLE_ECHO_INPUT | ENABLE_INSERT_MODE | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT));
THROW_IF_WIN32_BOOL_FALSE(::SetConsoleMode(Stdin, InputMode));
DWORD OutputMode = OriginalOutputMode;
WI_SetAllFlags(OutputMode, ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN);
THROW_IF_WIN32_BOOL_FALSE(::SetConsoleMode(Stdout, OutputMode));
THROW_LAST_ERROR_IF(!::SetConsoleOutputCP(CP_UTF8));
auto processTty = Process.GetStdHandle(WSLAFDTty);
// TODO: Study a single thread for both handles.
// Create a thread to relay stdin to the pipe.
std::thread inputThread([&]() {
auto updateTerminal = [&Stdout, &Process, &processTty]() {
CONSOLE_SCREEN_BUFFER_INFOEX info{};
info.cbSize = sizeof(info);
THROW_IF_WIN32_BOOL_FALSE(GetConsoleScreenBufferInfoEx(Stdout, &info));
LOG_IF_FAILED(Process.Get().ResizeTty(
info.srWindow.Bottom - info.srWindow.Top + 1, info.srWindow.Right - info.srWindow.Left + 1));
};
wsl::windows::common::relay::StandardInputRelay(Stdin, processTty.get(), updateTerminal, exitEvent.get());
});
auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
exitEvent.SetEvent();
inputThread.join();
});
// Relay the contents of the pipe to stdout.
wsl::windows::common::relay::InterruptableRelay(processTty.get(), Stdout);
// Wait for the process to exit.
THROW_LAST_ERROR_IF(WaitForSingleObject(exitEvent.get(), INFINITE) != WAIT_OBJECT_0);
}
else
{
wsl::windows::common::relay::MultiHandleWait io;
// Create a thread to relay stdin to the pipe.
std::thread inputThread;
auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
if (inputThread.joinable())
{
exitEvent.SetEvent();
inputThread.join();
}
});
// Required because ReadFile() blocks if stdin is a tty.
if (wsl::windows::common::wslutil::IsInteractiveConsole())
{
// TODO: Will output CR instead of LF's which can confuse the linux app.
// Consider a custom relay logic to fix this.
inputThread = std::thread{
[&]() { wsl::windows::common::relay::InterruptableRelay(Stdin, Process.GetStdHandle(0).get(), exitEvent.get()); }};
}
else
{
io.AddHandle(std::make_unique<RelayHandle>(GetStdHandle(STD_INPUT_HANDLE), Process.GetStdHandle(0)));
}
io.AddHandle(std::make_unique<RelayHandle>(Process.GetStdHandle(1), GetStdHandle(STD_OUTPUT_HANDLE)));
io.AddHandle(std::make_unique<RelayHandle>(Process.GetStdHandle(2), GetStdHandle(STD_ERROR_HANDLE)));
io.AddHandle(std::make_unique<EventHandle>(exitEvent.get()));
io.Run({});
}
int exitCode = Process.GetExitCode();
return exitCode;
}
static int Run(std::wstring_view commandLine)
{
ArgumentParser parser(std::wstring{commandLine}, L"wsladiag", 2, true);
bool interactive{};
bool tty{};
std::string image;
parser.AddPositionalArgument(Utf8String{image}, 0);
parser.AddArgument(interactive, L"--interactive", 'i');
parser.AddArgument(tty, L"--tty", 't');
parser.Parse();
THROW_HR_IF(E_INVALIDARG, image.empty());
auto session = OpenCLISession();
WSLA_CONTAINER_OPTIONS options{};
options.Image = image.c_str();
std::vector<WSLA_PROCESS_FD> fds;
HANDLE Stdout = GetStdHandle(STD_OUTPUT_HANDLE);
HANDLE Stdin = GetStdHandle(STD_INPUT_HANDLE);
if (tty)
{
CONSOLE_SCREEN_BUFFER_INFOEX Info{};
Info.cbSize = sizeof(Info);
THROW_IF_WIN32_BOOL_FALSE(::GetConsoleScreenBufferInfoEx(Stdout, &Info));
fds.emplace_back(WSLA_PROCESS_FD{.Fd = 0, .Type = WSLAFdTypeTerminalInput});
fds.emplace_back(WSLA_PROCESS_FD{.Fd = 1, .Type = WSLAFdTypeTerminalOutput});
fds.emplace_back(WSLA_PROCESS_FD{.Fd = 2, .Type = WSLAFdTypeTerminalControl});
options.InitProcessOptions.TtyColumns = Info.srWindow.Right - Info.srWindow.Left + 1;
options.InitProcessOptions.TtyRows = Info.srWindow.Bottom - Info.srWindow.Top + 1;
}
else
{
if (interactive)
{
fds.emplace_back(WSLA_PROCESS_FD{.Fd = 0, .Type = WSLAFdTypeDefault});
}
fds.emplace_back(WSLA_PROCESS_FD{.Fd = 1, .Type = WSLAFdTypeDefault});
fds.emplace_back(WSLA_PROCESS_FD{.Fd = 2, .Type = WSLAFdTypeDefault});
}
std::vector<std::string> argsStorage;
std::vector<const char*> args;
for (size_t i = parser.ParseIndex(); i < parser.Argc(); i++)
{
argsStorage.emplace_back(wsl::shared::string::WideToMultiByte(parser.Argv(i)));
}
for (const auto& e : argsStorage)
{
args.emplace_back(e.c_str());
}
options.InitProcessOptions.CommandLine = args.data();
options.InitProcessOptions.CommandLineCount = static_cast<ULONG>(args.size());
options.InitProcessOptions.Fds = fds.data();
options.InitProcessOptions.FdsCount = static_cast<ULONG>(fds.size());
wil::com_ptr<IWSLAContainer> container;
WSLAErrorDetails error{};
auto result = session->CreateContainer(&options, &container, &error.Error);
if (result == WSLA_E_IMAGE_NOT_FOUND)
{
wslutil::PrintMessage(std::format(L"Image '{}' not found, pulling", image), stderr);
PullImpl(*session.get(), image);
error.Reset();
result = session->CreateContainer(&options, &container, &error.Error);
}
error.ThrowIfFailed(result);
THROW_IF_FAILED(container->Start()); // TODO: Error message
wil::com_ptr<IWSLAProcess> process;
THROW_IF_FAILED(container->GetInitProcess(&process));
return InteractiveShell(ClientRunningWSLAProcess(std::move(process), std::move(fds)), tty);
}
static void PrintUsage()
{
wslutil::PrintMessage(Localization::MessageWsladiagUsage(), stderr);
@ -324,21 +696,30 @@ int wsladiag_main(std::wstring_view commandLine)
PrintUsage();
return 0;
}
if (verb == L"list")
else if (verb == L"list")
{
return RunListCommand(commandLine);
}
if (verb == L"shell")
else if (verb == L"shell")
{
return RunShellCommand(commandLine);
}
else if (verb == L"pull")
{
return Pull(commandLine);
}
else if (verb == L"run")
{
return Run(commandLine);
}
else
{
wslutil::PrintMessage(Localization::MessageWslaUnknownCommand(verb.c_str()), stderr);
PrintUsage();
// Unknown verb - show usage and fail.
wslutil::PrintMessage(Localization::MessageWslaUnknownCommand(verb.c_str()), stderr);
PrintUsage();
return 1;
// Unknown verb - show usage and fail.
return 1;
}
}
int wmain(int, wchar_t**)
@ -360,15 +741,16 @@ int wmain(int, wchar_t**)
if (FAILED(result))
{
if (auto reported = context.ReportedError())
if (const auto& reported = context.ReportedError())
{
auto strings = wsl::windows::common::wslutil::ErrorToString(*reported);
wslutil::PrintMessage(strings.Message.empty() ? strings.Code : strings.Message, stderr);
auto errorMessage = strings.Message.empty() ? strings.Code : strings.Message;
wslutil::PrintMessage(Localization::MessageErrorCode(errorMessage, wslutil::ErrorCodeToString(result)), stderr);
}
else
{
// Fallback for errors without context
wslutil::PrintMessage(wslutil::GetErrorString(result), stderr);
wslutil::PrintMessage(Localization::MessageErrorCode("", wslutil::ErrorCodeToString(result)), stderr);
}
}

View File

@ -38,15 +38,10 @@ DockerHTTPClient::DockerHTTPClient(wsl::shared::SocketChannel&& Channel, HANDLE
{
}
uint32_t DockerHTTPClient::PullImage(const char* Name, const char* Tag, const OnImageProgress& Callback)
std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::PullImage(const char* Name, const char* Tag)
{
auto [code, _] = SendRequest(
verb::post,
std::format("http://localhost/images/create?fromImage=library/{}&tag={}", Name, Tag),
{},
[Callback](const gsl::span<char>& span) { Callback(std::string{span.data(), span.size()}); });
return code;
auto url = std::format("http://localhost/images/create?fromImage=library/{}&tag={}", Name, Tag);
return SendRequestImpl(verb::post, url, {}, {});
}
std::unique_ptr<DockerHTTPClient::HTTPRequestContext> DockerHTTPClient::LoadImage(uint64_t ContentLength)
@ -77,10 +72,15 @@ std::vector<docker_schema::Image> DockerHTTPClient::ListImages()
return Transaction<docker_schema::EmptyRequest, std::vector<docker_schema::Image>>(verb::get, "http://localhost/images/json");
}
docker_schema::CreatedContainer DockerHTTPClient::CreateContainer(const docker_schema::CreateContainer& Request)
docker_schema::CreatedContainer DockerHTTPClient::CreateContainer(const docker_schema::CreateContainer& Request, const std::optional<std::string>& Name)
{
// TODO: Url escaping.
return Transaction<docker_schema::CreateContainer>(verb::post, "http://localhost/containers/create", Request);
std::string url = "http://localhost/containers/create";
if (Name.has_value())
{
url += std::format("?name={}", Name.value());
}
return Transaction<docker_schema::CreateContainer>(verb::post, url, Request);
}
void DockerHTTPClient::ResizeContainerTty(const std::string& Id, ULONG Rows, ULONG Columns)

View File

@ -37,7 +37,7 @@ public:
}
template <typename T = docker_schema::ErrorResponse>
T DockerMessage()
T DockerMessage() const
{
return wsl::shared::FromJson<T>(m_response.c_str());
}
@ -60,7 +60,6 @@ class DockerHTTPClient
public:
using OnResponseBytes = std::function<void(gsl::span<char>)>;
using OnImageProgress = std::function<void(const std::string&)>;
struct HTTPRequestContext
{
@ -80,7 +79,7 @@ public:
DockerHTTPClient(wsl::shared::SocketChannel&& Channel, HANDLE ExitingEvent, GUID VmId, ULONG ConnectTimeoutMs);
// Container management.
common::docker_schema::CreatedContainer CreateContainer(const common::docker_schema::CreateContainer& Request);
common::docker_schema::CreatedContainer CreateContainer(const common::docker_schema::CreateContainer& Request, const std::optional<std::string>& Name);
void StartContainer(const std::string& Id);
void StopContainer(const std::string& Id, int Signal, ULONG TimeoutSeconds);
void DeleteContainer(const std::string& Id);
@ -90,7 +89,7 @@ public:
void ResizeContainerTty(const std::string& Id, ULONG Rows, ULONG Columns);
// Image management.
uint32_t PullImage(const char* Name, const char* Tag, const OnImageProgress& Callback);
std::unique_ptr<HTTPRequestContext> PullImage(const char* Name, const char* Tag);
std::unique_ptr<HTTPRequestContext> ImportImage(const std::string& Repo, const std::string& Tag, uint64_t ContentLength);
std::unique_ptr<HTTPRequestContext> LoadImage(uint64_t ContentLength);
void TagImage(const std::string& Id, const std::string& Repo, const std::string& Tag);

View File

@ -136,7 +136,7 @@ WSLAContainerImpl::WSLAContainerImpl(
ContainerEventTracker& EventTracker,
DockerHTTPClient& DockerClient) :
m_parentVM(parentVM),
m_name(Options.Name),
m_name(Options.Name == nullptr ? "" : Options.Name), // TODO: get name from docker.
m_image(Options.Image),
m_id(std::move(Id)),
m_mountedVolumes(std::move(volumes)),
@ -227,6 +227,11 @@ const std::string& WSLAContainerImpl::Image() const noexcept
return m_image;
}
const std::string& WSLAContainerImpl::Name() const noexcept
{
return m_name;
}
IWSLAContainer& WSLAContainerImpl::ComWrapper()
{
return *m_comWrapper.Get();
@ -555,13 +560,6 @@ std::unique_ptr<WSLAContainerImpl> WSLAContainerImpl::Create(
for (DWORD i = 0; i < containerOptions.InitProcessOptions.EnvironmentCount; i++)
{
THROW_HR_IF_MSG(
E_INVALIDARG,
containerOptions.InitProcessOptions.Environment[i][0] == '-',
"Invalid environment string at index: %i: %hs",
i,
containerOptions.InitProcessOptions.Environment[i]);
request.Env.push_back(containerOptions.InitProcessOptions.Environment[i]);
}
@ -604,23 +602,16 @@ std::unique_ptr<WSLAContainerImpl> WSLAContainerImpl::Create(
}
// Send the request to docker.
try
{
auto result = DockerClient.CreateContainer(request);
auto result =
DockerClient.CreateContainer(request, containerOptions.Name != nullptr ? containerOptions.Name : std::optional<std::string>{});
// N.B. mappedPorts is explicitly copied because it's referenced in errorCleanup, so it can't be moved.
auto container = std::make_unique<WSLAContainerImpl>(
&parentVM, containerOptions, std::move(result.Id), std::move(volumes), std::vector<PortMapping>(*mappedPorts), std::move(OnDeleted), EventTracker, DockerClient);
// N.B. mappedPorts is explicitly copied because it's referenced in errorCleanup, so it can't be moved.
auto container = std::make_unique<WSLAContainerImpl>(
&parentVM, containerOptions, std::move(result.Id), std::move(volumes), std::vector<PortMapping>(*mappedPorts), std::move(OnDeleted), EventTracker, DockerClient);
errorCleanup.release();
errorCleanup.release();
return container;
}
catch (const DockerHTTPException& e)
{
// TODO: propagate error message to caller.
THROW_HR_MSG(E_FAIL, "Failed to create container: %hs ", e.what());
}
return container;
}
const std::string& WSLAContainerImpl::ID() const noexcept

View File

@ -71,6 +71,7 @@ public:
IWSLAContainer& ComWrapper();
const std::string& Image() const noexcept;
const std::string& Name() const noexcept;
WSLA_CONTAINER_STATE State() noexcept;
void OnProcessReleased(DockerExecProcessControl* process);

View File

@ -275,11 +275,14 @@ try
}
CATCH_LOG();
HRESULT WSLASession::PullImage(LPCSTR ImageUri, const WSLA_REGISTRY_AUTHENTICATION_INFORMATION* RegistryAuthenticationInformation, IProgressCallback* ProgressCallback)
HRESULT WSLASession::PullImage(
LPCSTR ImageUri,
const WSLA_REGISTRY_AUTHENTICATION_INFORMATION* RegistryAuthenticationInformation,
IProgressCallback* ProgressCallback,
WSLA_ERROR_INFO* Error)
try
{
UNREFERENCED_PARAMETER(RegistryAuthenticationInformation);
UNREFERENCED_PARAMETER(ProgressCallback);
RETURN_HR_IF_NULL(E_POINTER, ImageUri);
@ -287,13 +290,73 @@ try
std::lock_guard lock{m_lock};
auto callback = [&](const std::string& content) {
WSL_LOG("ImagePullProgress", TraceLoggingValue(ImageUri, "Image"), TraceLoggingValue(content.c_str(), "Content"));
auto requestContext = m_dockerClient->PullImage(repo.c_str(), tag.c_str());
relay::MultiHandleWait io;
std::optional<boost::beast::http::status> pullResult;
auto onHttpResponse = [&](const boost::beast::http::message<false, boost::beast::http::buffer_body>& response) {
WSL_LOG("PullHttpResponse", TraceLoggingValue(static_cast<int>(response.result()), "StatusCode"));
pullResult = response.result();
};
auto code = m_dockerClient->PullImage(repo.c_str(), tag.c_str(), callback);
std::string errorJson;
auto onChunk = [&](const gsl::span<char>& Content) {
if (pullResult.has_value() && pullResult.value() != boost::beast::http::status::ok)
{
// If the status code is an error, then this is an error message, not a progress update.
errorJson.append(Content.data(), Content.size());
return;
}
THROW_HR_IF_MSG(E_FAIL, code != 200, "Failed to pull image: %hs", ImageUri);
std::string contentString{Content.begin(), Content.end()};
WSL_LOG("ImagePullProgress", TraceLoggingValue(ImageUri, "Image"), TraceLoggingValue(contentString.c_str(), "Content"));
if (ProgressCallback == nullptr)
{
return;
}
auto parsed = wsl::shared::FromJson<docker_schema::CreateImageProgress>(contentString.c_str());
THROW_IF_FAILED(ProgressCallback->OnProgress(
parsed.status.c_str(), parsed.id.c_str(), parsed.progressDetail.current, parsed.progressDetail.total));
};
auto onCompleted = [&]() { io.Cancel(); };
io.AddHandle(std::make_unique<relay::EventHandle>(m_sessionTerminatingEvent.get(), [&]() { THROW_HR(E_ABORT); }));
io.AddHandle(std::make_unique<relay::EventHandle>(m_sessionTerminatingEvent.get(), [&]() { THROW_HR(E_ABORT); }));
io.AddHandle(std::make_unique<DockerHTTPClient::DockerHttpResponseHandle>(
*requestContext, std::move(onHttpResponse), std::move(onChunk), std::move(onCompleted)));
io.Run({});
THROW_HR_IF(E_ABORT, m_sessionTerminatingEvent.is_signaled());
THROW_HR_IF(E_UNEXPECTED, !pullResult.has_value());
if (pullResult.value() != boost::beast::http::status::ok)
{
std::string errorMessage;
if (static_cast<int>(pullResult.value()) >= 400 && static_cast<int>(pullResult.value()) < 500)
{
// pull failed, parse the error message.
errorMessage = wsl::shared::FromJson<docker_schema::ErrorResponse>(errorJson.c_str()).message;
if (Error != nullptr)
{
Error->UserErrorMessage = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(errorMessage.c_str()).release();
}
}
if (pullResult.value() == boost::beast::http::status::not_found)
{
THROW_HR_MSG(WSLA_E_IMAGE_NOT_FOUND, "%hs", errorMessage.c_str());
}
THROW_HR_MSG(E_FAIL, "Image import failed: %hs", errorMessage.c_str());
}
return S_OK;
}
@ -432,52 +495,72 @@ HRESULT WSLASession::DeleteImage(LPCWSTR Image)
return E_NOTIMPL;
}
HRESULT WSLASession::CreateContainer(const WSLA_CONTAINER_OPTIONS* containerOptions, IWSLAContainer** Container)
HRESULT WSLASession::CreateContainer(const WSLA_CONTAINER_OPTIONS* containerOptions, IWSLAContainer** Container, WSLA_ERROR_INFO* Error)
try
{
RETURN_HR_IF_NULL(E_POINTER, containerOptions);
// Validate that Image and Name are not null.
// Validate that Image is not null.
RETURN_HR_IF(E_INVALIDARG, containerOptions->Image == nullptr);
RETURN_HR_IF(E_INVALIDARG, containerOptions->Name == nullptr);
std::lock_guard lock{m_lock};
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
// Validate that no container with the same name already exists.
auto it = m_containers.find(containerOptions->Name);
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), it != m_containers.end());
// Validate that name & images are within length limits.
RETURN_HR_IF(E_INVALIDARG, strlen(containerOptions->Name) > WSLA_MAX_CONTAINER_NAME_LENGTH);
RETURN_HR_IF(E_INVALIDARG, containerOptions->Name != nullptr && strlen(containerOptions->Name) > WSLA_MAX_CONTAINER_NAME_LENGTH);
RETURN_HR_IF(E_INVALIDARG, strlen(containerOptions->Image) > WSLA_MAX_IMAGE_NAME_LENGTH);
// TODO: Log entrance into the function.
auto [container, inserted] = m_containers.emplace(
containerOptions->Name,
WSLAContainerImpl::Create(
try
{
auto& it = m_containers.emplace_back(WSLAContainerImpl::Create(
*containerOptions,
*m_virtualMachine,
std::bind(&WSLASession::OnContainerDeleted, this, std::placeholders::_1),
m_eventTracker.value(),
m_dockerClient.value()));
WI_ASSERT(inserted);
THROW_IF_FAILED(it->ComWrapper().QueryInterface(__uuidof(IWSLAContainer), (void**)Container));
THROW_IF_FAILED(container->second->ComWrapper().QueryInterface(__uuidof(IWSLAContainer), (void**)Container));
return S_OK;
}
catch (const DockerHTTPException& e)
{
std::string errorMessage;
if ((e.StatusCode() >= 400 && e.StatusCode() < 500))
{
errorMessage = e.DockerMessage<docker_schema::ErrorResponse>().message;
}
return S_OK;
if (Error != nullptr)
{
Error->UserErrorMessage = wil::make_unique_ansistring<wil::unique_cotaskmem_ansistring>(errorMessage.c_str()).release();
}
if (e.StatusCode() == 404)
{
THROW_HR_MSG(WSLA_E_IMAGE_NOT_FOUND, "%hs", errorMessage.c_str());
}
else if (e.StatusCode() == 409)
{
THROW_WIN32_MSG(ERROR_ALREADY_EXISTS, "%hs", errorMessage.c_str());
}
return E_FAIL;
}
}
CATCH_RETURN();
HRESULT WSLASession::OpenContainer(LPCSTR Name, IWSLAContainer** Container)
try
{
// TODO: Rethink name / id usage here.
std::lock_guard lock{m_lock};
auto it = m_containers.find(Name);
auto it = std::ranges::find_if(m_containers, [Name](const auto& e) { return e->Name() == Name; });
RETURN_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), it == m_containers.end(), "Container not found: '%hs'", Name);
THROW_IF_FAILED(it->second->ComWrapper().QueryInterface(__uuidof(IWSLAContainer), (void**)Container));
THROW_IF_FAILED((*it)->ComWrapper().QueryInterface(__uuidof(IWSLAContainer), (void**)Container));
return S_OK;
}
@ -494,11 +577,11 @@ try
auto output = wil::make_unique_cotaskmem<WSLA_CONTAINER[]>(m_containers.size());
size_t index = 0;
for (const auto& [name, container] : m_containers)
for (const auto& e : m_containers)
{
THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, container->Image().c_str()) != 0);
THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Name, name.c_str()) != 0);
container->GetState(&output[index].State);
THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Image, e->Image().c_str()) != 0);
THROW_HR_IF(E_UNEXPECTED, strcpy_s(output[index].Name, e->Name().c_str()) != 0);
e->GetState(&output[index].State);
index++;
}
@ -638,7 +721,7 @@ CATCH_RETURN();
void WSLASession::OnContainerDeleted(const WSLAContainerImpl* Container)
{
std::lock_guard lock{m_lock};
WI_VERIFY(std::erase_if(m_containers, [Container](const auto& e) { return e.second.get() == Container; }) == 1);
WI_VERIFY(std::erase_if(m_containers, [Container](const auto& e) { return e.get() == Container; }) == 1);
}
HRESULT WSLASession::GetImplNoRef(_Out_ WSLASession** Session)

View File

@ -50,14 +50,15 @@ public:
IFACEMETHOD(PullImage)(
_In_ LPCSTR ImageUri,
_In_ const WSLA_REGISTRY_AUTHENTICATION_INFORMATION* RegistryAuthenticationInformation,
_In_ IProgressCallback* ProgressCallback) override;
_In_ IProgressCallback* ProgressCallback,
_Inout_opt_ WSLA_ERROR_INFO* ErrorInfo) override;
IFACEMETHOD(LoadImage)(_In_ ULONG ImageHandle, _In_ IProgressCallback* ProgressCallback, _In_ ULONGLONG ContentLength) override;
IFACEMETHOD(ImportImage)(_In_ ULONG ImageHandle, _In_ LPCSTR ImageName, _In_ IProgressCallback* ProgressCallback, _In_ ULONGLONG ContentLength) override;
IFACEMETHOD(ListImages)(_Out_ WSLA_IMAGE_INFORMATION** Images, _Out_ ULONG* Count) override;
IFACEMETHOD(DeleteImage)(_In_ LPCWSTR Image) override;
// Container management.
IFACEMETHOD(CreateContainer)(_In_ const WSLA_CONTAINER_OPTIONS* Options, _Out_ IWSLAContainer** Container) override;
IFACEMETHOD(CreateContainer)(_In_ const WSLA_CONTAINER_OPTIONS* Options, _Out_ IWSLAContainer** Container, _Inout_opt_ WSLA_ERROR_INFO* Error) override;
IFACEMETHOD(OpenContainer)(_In_ LPCSTR Name, _In_ IWSLAContainer** Container) override;
IFACEMETHOD(ListContainers)(_Out_ WSLA_CONTAINER** Images, _Out_ ULONG* Count) override;
@ -100,7 +101,7 @@ private:
std::thread m_containerdThread;
std::wstring m_displayName;
std::filesystem::path m_storageVhdPath;
std::map<std::string, std::unique_ptr<WSLAContainerImpl>> m_containers;
std::vector<std::unique_ptr<WSLAContainerImpl>> m_containers;
wil::unique_event m_sessionTerminatingEvent{wil::EventOptions::ManualReset};
std::recursive_mutex m_lock;
};

View File

@ -111,7 +111,7 @@ interface ITerminationCallback : IUnknown
]
interface IProgressCallback : IUnknown
{
HRESULT OnProgress(ULONG Progress, ULONG Total);
HRESULT OnProgress(LPCSTR Status, LPCSTR Id, ULONGLONG Current, ULONGLONG Total);
};
struct WSLA_REGISTRY_AUTHENTICATION_INFORMATION
@ -180,7 +180,7 @@ enum WSLA_CONTAINER_FLAGS
struct WSLA_CONTAINER_OPTIONS
{
LPCSTR Image;
LPCSTR Name;
[unique] LPCSTR Name;
struct WSLA_PROCESS_OPTIONS InitProcessOptions;
[unique, size_is(VolumesCount)] struct WSLA_VOLUME* Volumes;
ULONG VolumesCount;
@ -219,6 +219,12 @@ enum WSLA_PROCESS_STATE
WslaProcessStateSignalled = 3
};
// TODO: Design for localization.
typedef struct _WSLA_ERROR_INFO{
[string] LPSTR UserErrorMessage;
ULONG WarningsPipe;
} WSLA_ERROR_INFO;
[
uuid(1AD163CD-393D-4B33-83A2-8A3F3F23E608),
pointer_default(unique),
@ -299,14 +305,14 @@ interface IWSLAContainer : IUnknown
interface IWSLASession : IUnknown
{
// Image management.
HRESULT PullImage([in] LPCSTR ImageUri, [in, unique] const struct WSLA_REGISTRY_AUTHENTICATION_INFORMATION* RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback);
HRESULT PullImage([in] LPCSTR ImageUri, [in, unique] const struct WSLA_REGISTRY_AUTHENTICATION_INFORMATION* RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback, [in, out, unique, optional] WSLA_ERROR_INFO* ErrorInfo);
HRESULT LoadImage([in] ULONG ImageHandle, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength);
HRESULT ImportImage([in] ULONG ImageHandle, [in] LPCSTR ImageName, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength);
HRESULT ListImages([out, size_is(, *Count)] struct WSLA_IMAGE_INFORMATION** Images, [out] ULONG* Count);
HRESULT DeleteImage([in] LPCWSTR Image);
// Container management.
HRESULT CreateContainer([in] const struct WSLA_CONTAINER_OPTIONS* Options, [out] IWSLAContainer** Container);
HRESULT CreateContainer([in] const struct WSLA_CONTAINER_OPTIONS* Options, [out] IWSLAContainer** Container, [in, out, unique, optional] WSLA_ERROR_INFO* ErrorInfo);
HRESULT OpenContainer([in] LPCSTR Name, [out] IWSLAContainer** Container);
HRESULT ListContainers([out, size_is(, *Count)] struct WSLA_CONTAINER** Images, [out] ULONG* Count);
@ -357,4 +363,7 @@ interface IWSLAUserSession : IUnknown
HRESULT OpenSessionByName([in] LPCWSTR DisplayName, [out] IWSLASession** Session);
// TODO: Do we need 'TerminateSession()' ?
}
}
cpp_quote("#define WSLA_E_BASE (0x0600)")
cpp_quote("#define WSLA_E_IMAGE_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLA_E_BASE + 1) /* 0x80040601 */")

View File

@ -28,6 +28,7 @@ using wsl::windows::common::WSLAContainerLauncher;
using wsl::windows::common::WSLAProcessLauncher;
using wsl::windows::common::relay::OverlappedIOHandle;
using wsl::windows::common::relay::WriteHandle;
using wsl::windows::common::wslutil::WSLAErrorDetails;
DEFINE_ENUM_FLAG_OPERATORS(WSLAFeatureFlags);
@ -53,8 +54,8 @@ class WSLATests
storagePath = std::filesystem::current_path() / "test-storage";
auto session = CreateSession();
VERIFY_SUCCEEDED(session->PullImage("debian:latest", nullptr, nullptr));
VERIFY_SUCCEEDED(session->PullImage("python:3.12-alpine", nullptr, nullptr));
VERIFY_SUCCEEDED(session->PullImage("debian:latest", nullptr, nullptr, nullptr));
VERIFY_SUCCEEDED(session->PullImage("python:3.12-alpine", nullptr, nullptr, nullptr));
return true;
}
@ -296,12 +297,28 @@ class WSLATests
auto session = CreateSession(settings);
VERIFY_SUCCEEDED(session->PullImage("hello-world:latest", nullptr, nullptr));
{
VERIFY_SUCCEEDED(session->PullImage("hello-world:linux", nullptr, nullptr, nullptr));
// Verify that the image is in the list of images.
ExpectImagePresent(*session, "hello-world:latest");
// Verify that the image is in the list of images.
ExpectImagePresent(*session, "hello-world:linux");
WSLAContainerLauncher launcher("hello-world:linux", "wsla-pull-image-container");
// TODO: Check that the image can actually be used to start a container.
auto container = launcher.Launch(*session);
auto result = container.GetInitProcess().WaitAndCaptureOutput();
VERIFY_ARE_EQUAL(0, result.Code);
VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos);
}
{
std::string expectedError =
"pull access denied for does-not, repository does not exist or may require 'docker login'";
WSLAErrorDetails error;
VERIFY_ARE_EQUAL(session->PullImage("does-not:exist", nullptr, nullptr, &error.Error), WSLA_E_IMAGE_NOT_FOUND);
VERIFY_ARE_EQUAL(expectedError, error.Error.UserErrorMessage);
}
}
// TODO: Test that invalid tars are correctly handled.
@ -326,7 +343,7 @@ class WSLATests
// Verify that the image is in the list of images.
ExpectImagePresent(*session, "hello-world:latest");
WSLAContainerLauncher launcher("hello-world:latest", "wsla-import-image-container");
WSLAContainerLauncher launcher("hello-world:latest", "wsla-load-image-container");
auto container = launcher.Launch(*session);
auto result = container.GetInitProcess().WaitAndCaptureOutput();
@ -1336,11 +1353,10 @@ class WSLATests
VERIFY_ARE_EQUAL(hresult, E_INVALIDARG);
}
// TODO: Add logic to detect when starting the container fails, and enable this test case.
{
WSLAContainerLauncher launcher("invalid-image-name", "dummy", "/bin/cat");
auto [hresult, container] = launcher.LaunchNoThrow(*session);
VERIFY_ARE_EQUAL(hresult, E_FAIL); // TODO: Have a nicer error code when the image is not found.
VERIFY_ARE_EQUAL(hresult, WSLA_E_IMAGE_NOT_FOUND);
}
// Test null image name
@ -1352,7 +1368,7 @@ class WSLATests
options.InitProcessOptions.CommandLineCount = 0;
wil::com_ptr<IWSLAContainer> container;
auto hr = session->CreateContainer(&options, &container);
auto hr = session->CreateContainer(&options, &container, nullptr);
VERIFY_ARE_EQUAL(hr, E_INVALIDARG);
}
@ -1365,8 +1381,7 @@ class WSLATests
options.InitProcessOptions.CommandLineCount = 0;
wil::com_ptr<IWSLAContainer> container;
auto hr = session->CreateContainer(&options, &container);
VERIFY_ARE_EQUAL(hr, E_INVALIDARG);
VERIFY_SUCCEEDED(session->CreateContainer(&options, &container, nullptr));
}
}