CLI: Add system command (#40438)

This commit is contained in:
David Bennett
2026-05-11 11:22:43 -07:00
committed by GitHub
parent 210b7640f7
commit bf3a2ef5ee
9 changed files with 161 additions and 55 deletions

View File

@@ -2461,6 +2461,12 @@ For privacy information about this product please visit https://aka.ms/privacy.<
<data name="WSLCCLI_RegistryCommandLongDesc" xml:space="preserve">
<value>Manage registry credentials, including logging in and out of container registries.</value>
</data>
<data name="WSLCCLI_SystemCommandDesc" xml:space="preserve">
<value>System-level commands</value>
</data>
<data name="WSLCCLI_SystemCommandLongDesc" xml:space="preserve">
<value>System-level management commands.</value>
</data>
<data name="WSLCCLI_ImageTagDesc" xml:space="preserve">
<value>Tag an image.</value>
</data>

View File

@@ -17,8 +17,8 @@ Abstract:
#include "ContainerCommand.h"
#include "ImageCommand.h"
#include "RegistryCommand.h"
#include "SessionCommand.h"
#include "SettingsCommand.h"
#include "SystemCommand.h"
#include "InspectCommand.h"
#include "VersionCommand.h"
#include "VolumeCommand.h"
@@ -33,8 +33,8 @@ std::vector<std::unique_ptr<Command>> RootCommand::GetCommands() const
commands.push_back(std::make_unique<ContainerCommand>(FullName()));
commands.push_back(std::make_unique<ImageCommand>(FullName()));
commands.push_back(std::make_unique<RegistryCommand>(FullName()));
commands.push_back(std::make_unique<SessionCommand>(FullName()));
commands.push_back(std::make_unique<SettingsCommand>(FullName()));
commands.push_back(std::make_unique<SystemCommand>(FullName()));
commands.push_back(std::make_unique<VolumeCommand>(FullName()));
commands.push_back(std::make_unique<ContainerAttachCommand>(FullName()));
commands.push_back(std::make_unique<ImageBuildCommand>(FullName()));

View File

@@ -0,0 +1,48 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
SystemCommand.cpp
Abstract:
Definition of System command tree.
--*/
#include "CLIExecutionContext.h"
#include "SystemCommand.h"
#include "SessionCommand.h"
using namespace wsl::windows::wslc::execution;
using namespace wsl::shared;
namespace wsl::windows::wslc {
std::vector<std::unique_ptr<Command>> SystemCommand::GetCommands() const
{
std::vector<std::unique_ptr<Command>> commands;
commands.push_back(std::make_unique<SessionCommand>(FullName()));
return commands;
}
std::vector<Argument> SystemCommand::GetArguments() const
{
return {};
}
std::wstring SystemCommand::ShortDescription() const
{
return Localization::WSLCCLI_SystemCommandDesc();
}
std::wstring SystemCommand::LongDescription() const
{
return Localization::WSLCCLI_SystemCommandLongDesc();
}
void SystemCommand::ExecuteInternal(CLIExecutionContext& context) const
{
OutputHelp();
}
} // namespace wsl::windows::wslc

View File

@@ -0,0 +1,34 @@
/*++
Copyright (c) Microsoft. All rights reserved.
Module Name:
SystemCommand.h
Abstract:
Declaration of System command tree.
--*/
#pragma once
#include "Command.h"
namespace wsl::windows::wslc {
// Root System Command
struct SystemCommand final : public Command
{
constexpr static std::wstring_view CommandName = L"system";
SystemCommand(const std::wstring& parent) : Command(CommandName, parent)
{
}
std::vector<Argument> GetArguments() const override;
std::wstring ShortDescription() const override;
std::wstring LongDescription() const override;
std::vector<std::unique_ptr<Command>> GetCommands() const override;
protected:
void ExecuteInternal(CLIExecutionContext& context) const override;
};
} // namespace wsl::windows::wslc

View File

@@ -25,22 +25,23 @@ COMMAND_LINE_TEST_CASE(L"-?", L"root", true)
COMMAND_LINE_TEST_CASE(L"--version", L"root", true)
COMMAND_LINE_TEST_CASE(L"-v", L"root", true)
// Session command tests
COMMAND_LINE_TEST_CASE(L"session list", L"list", true)
COMMAND_LINE_TEST_CASE(L"session list --verbose", L"list", true)
COMMAND_LINE_TEST_CASE(L"session list --verbose --help", L"list", true)
COMMAND_LINE_TEST_CASE(L"session list --notanarg", L"list", false)
COMMAND_LINE_TEST_CASE(L"session list extraarg", L"list", false)
COMMAND_LINE_TEST_CASE(L"session shell session1", L"shell", true)
COMMAND_LINE_TEST_CASE(L"session shell", L"shell", true)
COMMAND_LINE_TEST_CASE(L"session terminate session1", L"terminate", true)
COMMAND_LINE_TEST_CASE(L"session terminate", L"terminate", true)
COMMAND_LINE_TEST_CASE(L"session enter C:\\storage", L"enter", true)
COMMAND_LINE_TEST_CASE(L"session enter C:\\storage --name my-session", L"enter", true)
COMMAND_LINE_TEST_CASE(L"session enter --name my-session C:\\storage", L"enter", true)
COMMAND_LINE_TEST_CASE(L"session enter", L"enter", false) // Missing required storage-path
COMMAND_LINE_TEST_CASE(L"session enter C:\\storage --notanarg", L"enter", false) // Invalid argument
COMMAND_LINE_TEST_CASE(L"session enter --name my-session", L"enter", false) // Missing required positional before flag
// System command tests
COMMAND_LINE_TEST_CASE(L"system -?", L"system", true)
COMMAND_LINE_TEST_CASE(L"system session list", L"list", true)
COMMAND_LINE_TEST_CASE(L"system session list --verbose", L"list", true)
COMMAND_LINE_TEST_CASE(L"system session list --verbose --help", L"list", true)
COMMAND_LINE_TEST_CASE(L"system session list --notanarg", L"list", false)
COMMAND_LINE_TEST_CASE(L"system session list extraarg", L"list", false)
COMMAND_LINE_TEST_CASE(L"system session shell session1", L"shell", true)
COMMAND_LINE_TEST_CASE(L"system session shell", L"shell", true)
COMMAND_LINE_TEST_CASE(L"system session terminate session1", L"terminate", true)
COMMAND_LINE_TEST_CASE(L"system session terminate", L"terminate", true)
COMMAND_LINE_TEST_CASE(L"system session enter C:\\storage", L"enter", true)
COMMAND_LINE_TEST_CASE(L"system session enter C:\\storage --name my-session", L"enter", true)
COMMAND_LINE_TEST_CASE(L"system session enter --name my-session C:\\storage", L"enter", true)
COMMAND_LINE_TEST_CASE(L"system session enter", L"enter", false) // Missing required storage-path
COMMAND_LINE_TEST_CASE(L"system session enter C:\\storage --notanarg", L"enter", false) // Invalid argument
COMMAND_LINE_TEST_CASE(L"system session enter --name my-session", L"enter", false) // Missing required positional before flag
// Container command tests
COMMAND_LINE_TEST_CASE(L"container list", L"list", true)

View File

@@ -22,6 +22,7 @@ Abstract:
#include "RootCommand.h"
#include "ContainerCommand.h"
#include "SessionCommand.h"
#include "SystemCommand.h"
#include "VersionCommand.h"
using namespace wsl::windows::wslc;
@@ -65,10 +66,26 @@ class WSLCCLICommandUnitTests
}
}
// Test: Verify SessionCommand has subcommands
// Test: Verify SystemCommand has subcommands
TEST_METHOD(SystemCommand_HasSubcommands)
{
auto cmd = SystemCommand(L"system");
auto subcommands = cmd.GetCommands();
// Verify it has subcommands
VERIFY_IS_TRUE(subcommands.size() > 0);
LogComment(L"SystemCommand has " + std::to_wstring(subcommands.size()) + L" subcommands");
for (const auto& subcmd : subcommands)
{
VERIFY_IS_NOT_NULL(subcmd.get());
}
}
// Test: Verify SessionCommand has subcommands (now under system)
TEST_METHOD(SessionCommand_HasSubcommands)
{
auto cmd = SessionCommand(L"session");
auto cmd = SessionCommand(L"system session");
auto subcommands = cmd.GetCommands();
// Verify it has subcommands
@@ -85,7 +102,7 @@ class WSLCCLICommandUnitTests
// Test: Verify SessionEnterCommand has the expected arguments
TEST_METHOD(SessionEnterCommand_HasExpectedArguments)
{
auto cmd = SessionEnterCommand(L"session");
auto cmd = SessionEnterCommand(L"system session");
auto args = cmd.GetArguments();
// Should have 2 arguments: storage-path (positional, required) and name (value, optional)
@@ -107,7 +124,7 @@ class WSLCCLICommandUnitTests
// Test: Verify SessionEnterCommand descriptions are not empty
TEST_METHOD(SessionEnterCommand_HasDescriptions)
{
auto cmd = SessionEnterCommand(L"session");
auto cmd = SessionEnterCommand(L"system session");
VERIFY_IS_FALSE(cmd.ShortDescription().empty());
VERIFY_IS_FALSE(cmd.LongDescription().empty());

View File

@@ -83,7 +83,7 @@ class WSLCE2EGlobalTests
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify session list shows the admin session name
result = RunWslc(L"session list", ElevationType::Elevated);
result = RunWslc(L"system session list", ElevationType::Elevated);
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.Stdout.has_value());
@@ -98,7 +98,7 @@ class WSLCE2EGlobalTests
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify session list shows the non-admin session name
result = RunWslc(L"session list", ElevationType::NonElevated);
result = RunWslc(L"system session list", ElevationType::NonElevated);
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.Stdout.has_value());
@@ -244,17 +244,17 @@ class WSLCE2EGlobalTests
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify session list shows the admin session name
result = RunWslc(L"session list");
result = RunWslc(L"system session list");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.Stdout.has_value());
VERIFY_IS_TRUE(result.Stdout->find(adminName) != std::wstring::npos);
// Terminate the session
result = RunWslc(L"session terminate");
result = RunWslc(L"system session terminate");
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify session no longer shows up
result = RunWslc(L"session list");
result = RunWslc(L"system session list");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.Stdout.has_value());
VERIFY_IS_FALSE(result.Stdout->find(adminName) != std::wstring::npos);
@@ -266,17 +266,17 @@ class WSLCE2EGlobalTests
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify session list shows the non-elevated session name
result = RunWslc(L"session list");
result = RunWslc(L"system session list");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.Stdout.has_value());
VERIFY_IS_TRUE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
// Terminate the session
result = RunWslc(L"session terminate", ElevationType::NonElevated);
result = RunWslc(L"system session terminate", ElevationType::NonElevated);
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify session no longer shows up
result = RunWslc(L"session list");
result = RunWslc(L"system session list");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.Stdout.has_value());
VERIFY_IS_FALSE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
@@ -292,17 +292,17 @@ class WSLCE2EGlobalTests
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify session list shows the admin session name
result = RunWslc(L"session list");
result = RunWslc(L"system session list");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.Stdout.has_value());
VERIFY_IS_TRUE(result.Stdout->find(adminName) != std::wstring::npos);
// Terminate the session
result = RunWslc(std::format(L"session terminate {}", adminName));
result = RunWslc(std::format(L"system session terminate {}", adminName));
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify session no longer shows up
result = RunWslc(L"session list");
result = RunWslc(L"system session list");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.Stdout.has_value());
VERIFY_IS_FALSE(result.Stdout->find(adminName) != std::wstring::npos);
@@ -314,17 +314,17 @@ class WSLCE2EGlobalTests
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify session list shows the non-elevated session name
result = RunWslc(L"session list");
result = RunWslc(L"system session list");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.Stdout.has_value());
VERIFY_IS_TRUE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
// Terminate the session
result = RunWslc(std::format(L"session terminate {}", nonAdminName), ElevationType::NonElevated);
result = RunWslc(std::format(L"system session terminate {}", nonAdminName), ElevationType::NonElevated);
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify session no longer shows up
result = RunWslc(L"session list");
result = RunWslc(L"system session list");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.Stdout.has_value());
VERIFY_IS_FALSE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
@@ -342,22 +342,22 @@ class WSLCE2EGlobalTests
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify session list shows both sessions.
result = RunWslc(L"session list");
result = RunWslc(L"system session list");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.Stdout.has_value());
VERIFY_IS_TRUE(result.Stdout->find(adminName) != std::wstring::npos);
VERIFY_IS_TRUE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
// Attempt to terminate the admin session from the non-elevated process and fail.
result = RunWslc(std::format(L"session terminate {}", adminName), ElevationType::NonElevated);
result = RunWslc(std::format(L"system session terminate {}", adminName), ElevationType::NonElevated);
result.Verify({.Stderr = L"The requested operation requires elevation. \r\nError code: ERROR_ELEVATION_REQUIRED\r\n", .ExitCode = 1});
// Terminate the non-elevated session from the elevated process.
result = RunWslc(std::format(L"session terminate {}", nonAdminName), ElevationType::Elevated);
result = RunWslc(std::format(L"system session terminate {}", nonAdminName), ElevationType::Elevated);
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify non-elevated session no longer shows up
result = RunWslc(L"session list");
result = RunWslc(L"system session list");
result.Verify({.Stderr = L"", .ExitCode = 0});
VERIFY_IS_TRUE(result.Stdout.has_value());
VERIFY_IS_FALSE(result.Stdout->find(nonAdminName + L"\r\n") != std::wstring::npos);
@@ -382,7 +382,7 @@ class WSLCE2EGlobalTests
result.Verify({.Stdout = L"", .Stderr = L"Element not found. \r\nError code: ERROR_NOT_FOUND\r\n", .ExitCode = 1});
// Verify session list
result = RunWslc(L"session list");
result = RunWslc(L"system session list");
result.Verify({.Stderr = L"", .ExitCode = 0});
// Verify there is a session with the name of the test session in the session list output.
@@ -419,7 +419,7 @@ class WSLCE2EGlobalTests
Log::Comment(L"Testing elevated interactive session");
// Session shell should attach to the correct default session.
// Test should be elevated, therefore this should be the admin session.
auto session = RunWslcInteractive(L"session shell");
auto session = RunWslcInteractive(L"system session shell");
VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
session.ExpectStdout(VT::SESSION_PROMPT);
@@ -444,7 +444,7 @@ class WSLCE2EGlobalTests
Log::Comment(L"Testing non-elevated interactive session with explicit session name");
// Non-Elevated session shell should attach to the wslc by name also.
auto nonAdminName = GetExpectedDefaultSessionName(false);
auto session = RunWslcInteractive(std::format(L"session shell {}", nonAdminName), ElevationType::NonElevated);
auto session = RunWslcInteractive(std::format(L"system session shell {}", nonAdminName), ElevationType::NonElevated);
VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
session.ExpectStdout(VT::SESSION_PROMPT);
@@ -469,7 +469,7 @@ class WSLCE2EGlobalTests
Log::Comment(L"Testing elevated interactive session with explicit admin session name");
// Elevated session shell should attach to the wslc by name also.
auto adminName = GetExpectedDefaultSessionName(true);
auto session = RunWslcInteractive(std::format(L"session shell {}", adminName), ElevationType::Elevated);
auto session = RunWslcInteractive(std::format(L"system session shell {}", adminName), ElevationType::Elevated);
VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
session.ExpectStdout(VT::SESSION_PROMPT);
@@ -526,8 +526,8 @@ private:
{L"container", Localization::WSLCCLI_ContainerCommandDesc()},
{L"image", Localization::WSLCCLI_ImageCommandDesc()},
{L"registry", Localization::WSLCCLI_RegistryCommandDesc()},
{L"session", Localization::WSLCCLI_SessionCommandDesc()},
{L"settings", Localization::WSLCCLI_SettingsCommandDesc()},
{L"system", Localization::WSLCCLI_SystemCommandDesc()},
{L"volume", Localization::WSLCCLI_VolumeCommandDesc()},
{L"attach", Localization::WSLCCLI_ContainerAttachDesc()},
{L"build", Localization::WSLCCLI_ImageBuildDesc()},
@@ -579,4 +579,4 @@ private:
return options.str();
}
};
} // namespace WSLCE2ETests
} // namespace WSLCE2ETests

View File

@@ -394,7 +394,7 @@ void EnsureSessionIsTerminated(const std::wstring& sessionName)
targetSession = std::format(L"{}-{}", baseName, username);
}
auto listResult = RunWslc(L"session list");
auto listResult = RunWslc(L"system session list");
listResult.Verify({.Stderr = L"", .ExitCode = 0});
auto stdoutLines = listResult.GetStdoutLines();
@@ -403,7 +403,7 @@ void EnsureSessionIsTerminated(const std::wstring& sessionName)
// Check if the line ends with the target session name
if (line.size() >= targetSession.size() && line.compare(line.size() - targetSession.size(), targetSession.size(), targetSession) == 0)
{
auto result = RunWslc(std::format(L"session terminate \"{}\"", targetSession));
auto result = RunWslc(std::format(L"system session terminate \"{}\"", targetSession));
result.Verify({.Stdout = L"", .Stderr = L"", .ExitCode = 0});
break;
}
@@ -492,4 +492,4 @@ std::wstring GetPythonHttpServerScript(uint16_t port)
{
return std::format(L"python3 -m http.server {}", port);
}
} // namespace WSLCE2ETests
} // namespace WSLCE2ETests

View File

@@ -58,7 +58,7 @@ class WSLCE2ESessionEnterTests
RunWslc(L"image ls").Verify({.ExitCode = 0});
// Terminate the wslc session since we use its storage path in this test class.
RunWslc(L"session terminate");
RunWslc(L"system session terminate");
return true;
}
@@ -67,7 +67,7 @@ class WSLCE2ESessionEnterTests
constexpr auto sessionName = L"test-wslc-session-enter";
// Run an interactive session enter with an explicit name.
auto session = RunWslcInteractive(std::format(L"session enter \"{}\" --name {}", GetDefaultStoragePath(), sessionName));
auto session = RunWslcInteractive(std::format(L"system session enter \"{}\" --name {}", GetDefaultStoragePath(), sessionName));
VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
session.ExpectStdout(VT::SESSION_PROMPT);
@@ -80,7 +80,7 @@ class WSLCE2ESessionEnterTests
session.ExpectStdout(VT::SESSION_PROMPT);
// Verify the session appears in session list.
auto listResult = RunWslc(L"session list");
auto listResult = RunWslc(L"system session list");
listResult.Verify({.Stderr = L"", .ExitCode = S_OK});
VERIFY_IS_TRUE(listResult.Stdout.has_value());
VERIFY_IS_TRUE(listResult.Stdout->find(sessionName) != std::wstring::npos);
@@ -89,7 +89,7 @@ class WSLCE2ESessionEnterTests
VERIFY_ARE_EQUAL(session.Exit(), 0);
// Verify the session is no longer in the session list after exiting.
listResult = RunWslc(L"session list");
listResult = RunWslc(L"system session list");
listResult.Verify({.Stderr = L"", .ExitCode = S_OK});
VERIFY_IS_TRUE(listResult.Stdout.has_value());
VERIFY_IS_FALSE(listResult.Stdout->find(sessionName) != std::wstring::npos);
@@ -97,7 +97,7 @@ class WSLCE2ESessionEnterTests
WSLC_TEST_METHOD(WSLCE2E_SessionEnter_WithoutName_GeneratesGuid)
{
auto session = RunWslcInteractive(std::format(L"session enter \"{}\"", GetDefaultStoragePath()));
auto session = RunWslcInteractive(std::format(L"system session enter \"{}\"", GetDefaultStoragePath()));
VERIFY_IS_TRUE(session.IsRunning(), L"Session should be running");
session.ExpectStderr("Created session: ");
@@ -108,7 +108,7 @@ class WSLCE2ESessionEnterTests
WSLC_TEST_METHOD(WSLCE2E_SessionEnter_StoragePathNotFound)
{
auto result = RunWslc(L"session enter does-not-exist");
auto result = RunWslc(L"system session enter does-not-exist");
result.Verify({
.Stderr = L"The system cannot find the path specified. \r\nError code: ERROR_PATH_NOT_FOUND\r\n",
.ExitCode = 1,