Skip to content

Commit 31dffdb

Browse files
RomneyDajoelagnel
authored andcommitted
fix(setup): explain WSL platform install failures
After an elevated WSL install fails, run a best-effort GitHub quota diagnostic and distinguish likely quota exhaustion from other download failures. Diagnostic timeouts never replace the original failure. Offer Store, winget, and elevated PowerShell recovery routes, and allow the platform step to be retried. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Joel Fernandes <joelagnelf@nvidia.com>
1 parent 28d0f3e commit 31dffdb

3 files changed

Lines changed: 139 additions & 2 deletions

File tree

src/OpenClaw.SetupEngine/PreflightWslStep.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,10 @@ internal static async Task<StepResult> InstallWslPlatformAsync(SetupContext ctx,
221221
return StepResult.Terminal("WSL platform install requires a restart. Reboot Windows, then run setup again.");
222222

223223
if (process.ExitCode != 0)
224-
return StepResult.Fail($"WSL platform install failed with exit code {process.ExitCode}.");
224+
{
225+
GitHubApiQuota? quota = await WslPlatformInstallDiagnostics.QueryGitHubQuotaAsync(ct);
226+
return StepResult.Fail(WslPlatformInstallDiagnostics.DescribeFailure(process.ExitCode, quota));
227+
}
225228

226229
var probe = await ctx.Commands.RunAsync(
227230
WslConstants.WslExePath,
@@ -240,6 +243,10 @@ internal static async Task<StepResult> InstallWslPlatformAsync(SetupContext ctx,
240243
{
241244
return StepResult.Fail("WSL platform install was cancelled at the elevation prompt.");
242245
}
246+
catch (OperationCanceledException) when (ct.IsCancellationRequested)
247+
{
248+
throw;
249+
}
243250
catch (Exception ex)
244251
{
245252
return StepResult.Fail($"WSL platform install failed: {ex.Message}", ex);
@@ -266,7 +273,7 @@ internal EnsureWslPlatformStep(
266273

267274
public override string Id => "ensure-wsl-platform";
268275
public override string DisplayName => "Prepare WSL platform";
269-
public override bool CanRetry => false;
276+
public override bool CanRetry => true;
270277

271278
public override async Task<StepResult> ExecuteAsync(SetupContext ctx, CancellationToken ct)
272279
{
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System.Text.Json;
2+
3+
namespace OpenClaw.SetupEngine;
4+
5+
internal sealed record GitHubApiQuota(int Limit, int Remaining, DateTimeOffset ResetsAt)
6+
{
7+
public bool IsExhausted => Remaining <= 0;
8+
public int Used => Math.Max(0, Limit - Remaining);
9+
}
10+
11+
internal static class WslPlatformInstallDiagnostics
12+
{
13+
private const string RateLimitUrl = "https://api.github.com/rate_limit";
14+
private const string WslStoreProductId = "9P9TQF7MRM4R";
15+
16+
public static string SelfInstallInstructions =>
17+
"Install WSL yourself, then run setup again:" + Environment.NewLine +
18+
$" Microsoft Store: {WslInstallSupport.UpdateUrl}" + Environment.NewLine +
19+
$" Or run: winget install --id {WslStoreProductId} --source msstore" + Environment.NewLine +
20+
" Or, in elevated PowerShell: wsl --install --no-distribution" + Environment.NewLine +
21+
"Reboot if Windows asks for one.";
22+
23+
public static string DescribeFailure(int exitCode, GitHubApiQuota? quota)
24+
{
25+
string reason = quota is { IsExhausted: true }
26+
? $"The WSL installer may need GitHub, and this network's unauthenticated API quota " +
27+
$"is exhausted ({quota.Used}/{quota.Limit}) until {quota.ResetsAt.ToLocalTime():HH:mm}."
28+
: "The WSL download did not complete. A network, policy, or installer error may be blocking it.";
29+
30+
return $"WSL platform install failed with exit code {exitCode}. {reason}" +
31+
Environment.NewLine + Environment.NewLine + SelfInstallInstructions;
32+
}
33+
34+
public static async Task<GitHubApiQuota?> QueryGitHubQuotaAsync(CancellationToken ct)
35+
{
36+
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
37+
return await QueryGitHubQuotaAsync(http, ct);
38+
}
39+
40+
internal static async Task<GitHubApiQuota?> QueryGitHubQuotaAsync(HttpClient http, CancellationToken ct)
41+
{
42+
try
43+
{
44+
using var request = new HttpRequestMessage(HttpMethod.Get, RateLimitUrl);
45+
request.Headers.UserAgent.ParseAdd("OpenClawSetup");
46+
using var response = await http.SendAsync(request, ct);
47+
if (!response.IsSuccessStatusCode)
48+
return null;
49+
50+
await using var body = await response.Content.ReadAsStreamAsync(ct);
51+
using var json = await JsonDocument.ParseAsync(body, cancellationToken: ct);
52+
JsonElement core = json.RootElement.GetProperty("resources").GetProperty("core");
53+
return new(
54+
core.GetProperty("limit").GetInt32(),
55+
core.GetProperty("remaining").GetInt32(),
56+
DateTimeOffset.FromUnixTimeSeconds(core.GetProperty("reset").GetInt64()));
57+
}
58+
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
59+
{
60+
return null;
61+
}
62+
catch (Exception ex) when (ex is not OperationCanceledException)
63+
{
64+
return null;
65+
}
66+
}
67+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System.Net;
2+
3+
namespace OpenClaw.SetupEngine.Tests;
4+
5+
public class WslPlatformInstallDiagnosticsTests
6+
{
7+
[Theory]
8+
[InlineData(0, true)]
9+
[InlineData(10, false)]
10+
public void DescribeFailure_ExplainsCauseAndRecovery(int remaining, bool exhausted)
11+
{
12+
var quota = new GitHubApiQuota(60, remaining, DateTimeOffset.UtcNow.AddMinutes(10));
13+
14+
string message = WslPlatformInstallDiagnostics.DescribeFailure(1, quota);
15+
16+
Assert.Contains("exit code 1", message);
17+
Assert.Equal(exhausted, message.Contains("quota is exhausted", StringComparison.OrdinalIgnoreCase));
18+
Assert.Contains(WslInstallSupport.UpdateUrl, message);
19+
Assert.Contains("winget install --id 9P9TQF7MRM4R --source msstore", message);
20+
Assert.Contains("wsl --install --no-distribution", message);
21+
}
22+
23+
[Fact]
24+
public void DescribeFailure_UnknownQuota_DoesNotClaimRateLimit()
25+
{
26+
string message = WslPlatformInstallDiagnostics.DescribeFailure(5, quota: null);
27+
28+
Assert.Contains("network, policy, or installer error", message);
29+
Assert.DoesNotContain("quota is exhausted", message);
30+
}
31+
32+
[Fact]
33+
public async Task QueryGitHubQuota_TimeoutIsBestEffort()
34+
{
35+
using var http = new HttpClient(new DelayedHandler()) { Timeout = TimeSpan.FromMilliseconds(20) };
36+
37+
Assert.Null(await WslPlatformInstallDiagnostics.QueryGitHubQuotaAsync(http, CancellationToken.None));
38+
}
39+
40+
[Fact]
41+
public async Task QueryGitHubQuota_CallerCancellationPropagates()
42+
{
43+
using var http = new HttpClient(new DelayedHandler());
44+
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(20));
45+
46+
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
47+
WslPlatformInstallDiagnostics.QueryGitHubQuotaAsync(http, cts.Token));
48+
}
49+
50+
[Fact]
51+
public void EnsureWslPlatform_IsRetryable() => Assert.True(new EnsureWslPlatformStep().CanRetry);
52+
53+
private sealed class DelayedHandler : HttpMessageHandler
54+
{
55+
protected override async Task<HttpResponseMessage> SendAsync(
56+
HttpRequestMessage request,
57+
CancellationToken cancellationToken)
58+
{
59+
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
60+
return new(HttpStatusCode.OK);
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)