From a9ad12cc6c9fdbf9d9d9a4f7bcb1b672f8f86e22 Mon Sep 17 00:00:00 2001 From: Karen Lai <7976322+karkarl@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:38:42 -0700 Subject: [PATCH] fix(setup): recover uninitialized WSL automatically Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fa07d03-b4a0-4474-b9f2-2300ddb1b3e1 --- docs/ONBOARDING_WIZARD.md | 2 +- src/OpenClaw.SetupEngine/PreflightWslStep.cs | 31 +++- .../SetupStepsTests.cs | 165 +++++++++++++++++- 3 files changed, 192 insertions(+), 6 deletions(-) diff --git a/docs/ONBOARDING_WIZARD.md b/docs/ONBOARDING_WIZARD.md index 10e5ab14a..177294f7e 100644 --- a/docs/ONBOARDING_WIZARD.md +++ b/docs/ONBOARDING_WIZARD.md @@ -24,7 +24,7 @@ The setup flow no longer configures remote/manual gateways inline. The Welcome p Displays the OpenClaw icon, app title, and a brief description. If an app-owned local WSL gateway already exists, the primary CTA reads **Install new WSL Gateway** and confirmation warns that the current OpenClaw WSL gateway and distro will be deleted. If only an external gateway exists, the CTA remains **Set up locally** and confirmation explains that the external connection remains available in Connections. ### Local setup progress -Installs and connects a new app-owned `OpenClawGateway` WSL instance from a clean WSL baseline. Setup does not export from or mutate an existing user Ubuntu distro; if WSL cannot create the named app-owned distro directly, setup fails with an actionable update message. When replacing an app-owned local gateway, the removal step is shown as part of progress and can be retried on failure. +Installs and connects a new app-owned `OpenClawGateway` WSL instance from a clean WSL baseline. If the WSL platform is missing or its optional component is not initialized, setup requests administrator approval to install it, re-inspects readiness, and reports when a Windows restart is required. Setup does not export from or mutate an existing user Ubuntu distro; if WSL cannot create the named app-owned distro directly, setup fails with an actionable update message. When replacing an app-owned local gateway, the removal step is shown as part of progress and can be retried on failure. The managed distro is locked down and is not intended to be a normal interactive Ubuntu profile. For editing `openclaw.json` as the `openclaw` user and using root for protected-file administration, see [Managing the locked-down WSL gateway](WSL_GATEWAY_ADMIN.md). diff --git a/src/OpenClaw.SetupEngine/PreflightWslStep.cs b/src/OpenClaw.SetupEngine/PreflightWslStep.cs index 83d777438..aa3914fa1 100644 --- a/src/OpenClaw.SetupEngine/PreflightWslStep.cs +++ b/src/OpenClaw.SetupEngine/PreflightWslStep.cs @@ -115,6 +115,14 @@ public static async Task InspectAsync( } var combined = $"{status.Stdout}\n{status.Stderr}"; + if (LooksPlatformInstallRequired(status)) + { + return new( + WslViabilityKind.Installable, + "WSL is not initialized yet.", + "Setup can request administrator approval to initialize and verify it before continuing."); + } + if (WslInstallSupport.TryGetEnvironmentIssue(combined, out var message)) { logger.Warn($"WSL environment issue detected: {NormalizeWslOutput(combined).Trim()}"); @@ -147,10 +155,20 @@ internal static bool LooksUnavailable(CommandResult result) var text = NormalizeWslOutput($"{result.Stdout}\n{result.Stderr}"); return text.Contains("aka.ms/wslinstall", StringComparison.OrdinalIgnoreCase) || text.Contains("Windows Subsystem for Linux has no installed distributions", StringComparison.OrdinalIgnoreCase) + || LooksPlatformInstallRequired(text) || text.Contains("not recognized", StringComparison.OrdinalIgnoreCase) || text.Contains("not installed", StringComparison.OrdinalIgnoreCase); } + private static bool LooksPlatformInstallRequired(CommandResult result) => + LooksPlatformInstallRequired(NormalizeWslOutput($"{result.Stdout}\n{result.Stderr}")); + + private static bool LooksPlatformInstallRequired(string text) => + text.Contains("WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED", StringComparison.OrdinalIgnoreCase) + || text.Contains("0x8007019e", StringComparison.OrdinalIgnoreCase) + || text.Contains("requires the Windows Subsystem for Linux Optional Component", StringComparison.OrdinalIgnoreCase) + || text.Contains("Optional components needed to run WSL are not installed", StringComparison.OrdinalIgnoreCase); + private static bool LooksTooOldForVersionCommand(CommandResult result) { var text = NormalizeWslOutput($"{result.Stdout}\n{result.Stderr}"); @@ -294,8 +312,15 @@ public override async Task ExecuteAsync(SetupContext ctx, Cancellati viability = await WslViabilityInspector.InspectAsync(ctx.Commands, ctx.Logger, ct); ctx.WslViability = viability; - return viability.Kind == WslViabilityKind.Ready - ? StepResult.Ok("WSL platform installed and verified.") - : StepResult.Terminal(viability.Description); + if (viability.Kind == WslViabilityKind.Ready) + return StepResult.Ok("WSL platform installed and verified."); + if (viability.Kind == WslViabilityKind.Installable) + { + return StepResult.Terminal( + "WSL platform installation completed, but Windows must be restarted before WSL is ready. " + + "Reboot Windows, then run setup again."); + } + + return StepResult.Terminal(viability.Description); } } diff --git a/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs b/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs index d0cee91ea..ece50dfb1 100644 --- a/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs +++ b/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs @@ -1276,6 +1276,149 @@ public async Task EnsureWslPlatform_InstallsOnlyAfterReadOnlyPreflight() Assert.Equal(3, commands.Calls.Count); } + [Fact] + public async Task EnsureWslPlatform_LeavesReadyWslUnchanged() + { + var installCalls = 0; + var commands = new FakeCommandRunner(args => args switch + { + ["--version"] => Ok("WSL version: 2.7.3.0\n"), + ["--status"] => Ok("Default Version: 2\n"), + _ => Fail($"unexpected args: {string.Join(' ', args)}"), + }); + var ctx = CreateContext(commands: commands); + var step = new EnsureWslPlatformStep((_, _) => + { + installCalls++; + return Task.FromResult(StepResult.Ok("initialized")); + }); + + var result = await step.ExecuteAsync(ctx, CancellationToken.None); + + Assert.Equal(StepOutcome.Success, result.Outcome); + Assert.Equal("WSL platform is ready.", result.Message); + Assert.Equal(0, installCalls); + Assert.Equal(WslViabilityKind.Ready, ctx.WslViability?.Kind); + Assert.Equal(2, commands.Calls.Count); + } + + [Fact] + public async Task PreflightWsl_UninitializedPlatformIsInstallable() + { + var commands = new FakeCommandRunner(args => args switch + { + ["--version"] => Ok("WSL version: 2.7.3.0\n"), + ["--status"] => new CommandResult( + 1, + "", + "This application requires the Windows Subsystem for Linux Optional Component.\n" + + "Install it by running: wsl.exe --install --no-distribution\n" + + "Error code: Wsl/WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED", + TimeSpan.Zero, + TimedOut: false), + _ => Fail($"unexpected args: {string.Join(' ', args)}"), + }); + var ctx = CreateContext(commands: commands); + + var result = await new PreflightWslStep().ExecuteAsync(ctx, CancellationToken.None); + + Assert.Equal(StepOutcome.Success, result.Outcome); + Assert.Equal(WslViabilityKind.Installable, ctx.WslViability?.Kind); + Assert.Contains("not initialized", result.Message); + Assert.DoesNotContain(commands.Calls, call => call.Arguments.Contains("--install")); + } + + [Fact] + public async Task EnsureWslPlatform_InitializesPlatformAndReinspectsReadiness() + { + var initialized = false; + var installCalls = 0; + var commands = new FakeCommandRunner(args => args switch + { + ["--version"] => Ok("WSL version: 2.7.3.0\n"), + ["--status"] when !initialized => new CommandResult( + 1, + "", + "Error code: Wsl/WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED", + TimeSpan.Zero, + TimedOut: false), + ["--status"] => Ok("Default Version: 2\n"), + _ => Fail($"unexpected args: {string.Join(' ', args)}"), + }); + var ctx = CreateContext(commands: commands); + var step = new EnsureWslPlatformStep((_, _) => + { + installCalls++; + initialized = true; + return Task.FromResult(StepResult.Ok("initialized")); + }); + + var result = await step.ExecuteAsync(ctx, CancellationToken.None); + + Assert.Equal(StepOutcome.Success, result.Outcome); + Assert.Equal("WSL platform installed and verified.", result.Message); + Assert.Equal(1, installCalls); + Assert.Equal(WslViabilityKind.Ready, ctx.WslViability?.Kind); + Assert.Equal(4, commands.Calls.Count); + } + + [Fact] + public async Task EnsureWslPlatform_RequiresRestartWhenInitializationIsStillPending() + { + var installCalls = 0; + var commands = new FakeCommandRunner(args => args switch + { + ["--version"] => Ok("WSL version: 2.7.3.0\n"), + ["--status"] => new CommandResult( + 1, + "", + "Error code: Wsl/WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED", + TimeSpan.Zero, + TimedOut: false), + _ => Fail($"unexpected args: {string.Join(' ', args)}"), + }); + var ctx = CreateContext(commands: commands); + var step = new EnsureWslPlatformStep((_, _) => + { + installCalls++; + return Task.FromResult(StepResult.Ok("initialized")); + }); + + var result = await step.ExecuteAsync(ctx, CancellationToken.None); + + Assert.Equal(StepOutcome.FailedTerminal, result.Outcome); + Assert.Contains("restarted", result.Message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Reboot Windows", result.Message); + Assert.Equal(1, installCalls); + Assert.Equal(WslViabilityKind.Installable, ctx.WslViability?.Kind); + Assert.Equal(4, commands.Calls.Count); + } + + [Fact] + public async Task EnsureWslPlatform_PropagatesElevationCancellationWithoutReinspection() + { + var commands = new FakeCommandRunner(args => args switch + { + ["--version"] => Ok("WSL version: 2.7.3.0\n"), + ["--status"] => new CommandResult( + 1, + "", + "Error code: Wsl/WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED", + TimeSpan.Zero, + TimedOut: false), + _ => Fail($"unexpected args: {string.Join(' ', args)}"), + }); + var ctx = CreateContext(commands: commands); + var step = new EnsureWslPlatformStep((_, _) => + Task.FromResult(StepResult.Fail("WSL platform install was cancelled at the elevation prompt."))); + + var result = await step.ExecuteAsync(ctx, CancellationToken.None); + + Assert.Equal(StepOutcome.Failed, result.Outcome); + Assert.Contains("elevation prompt", result.Message); + Assert.Equal(2, commands.Calls.Count); + } + [Fact] public async Task PreflightWsl_UnclassifiedStatusFailureFailsClosed() { @@ -1855,10 +1998,16 @@ public async Task PreflightWsl_FailsTerminalWhenVirtualizationDisabledInFirmware if (args is ["--version"]) return Ok("WSL version: 2.7.3.0\n"); if (args is ["--status"]) - return Ok( + { + return new CommandResult( + 1, + "", "WSL2 is unable to start since virtualization is not enabled on this machine. " + "Please ensure the 'Virtual Machine Platform' optional component is enabled " - + "and virtualization is turned on in your computer's firmware settings."); + + "and virtualization is turned on in your computer's firmware settings.", + TimeSpan.Zero, + TimedOut: false); + } return Ok(); }); var ctx = CreateContext(commands: commands); @@ -2970,6 +3119,18 @@ public void ExistingConfigDetector_TreatsUnavailableWslAsNoDistro() Assert.False(ExistingConfigDetector.InterpretDistroList(result, "OpenClawGateway")); } + [Theory] + [InlineData("Error code: Wsl/WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED")] + [InlineData("This application requires the Windows Subsystem for Linux Optional Component.")] + [InlineData("Optional components needed to run WSL are not installed.")] + [InlineData("Error: 0x8007019e")] + public void ExistingConfigDetector_TreatsUninitializedWslAsNoDistro(string error) + { + var result = new CommandResult(1, "", error, TimeSpan.Zero, TimedOut: false); + + Assert.False(ExistingConfigDetector.InterpretDistroList(result, "OpenClawGateway")); + } + [Theory] [InlineData(true, 1, "")] [InlineData(false, 1, "unexpected failure")]