Skip to content
8 changes: 8 additions & 0 deletions docs/SETUP_ENGINE_REDESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,14 @@ absent or unregister succeeds. To replace such a legacy distro, uninstall it
first, using `--uninstall --confirm-destructive` and the same distro name, then
rerun setup with a supported new name.

Automatic rollback of the WSL-create step has cleanup authority only after that
execution starts creating the distro. If creation refuses pre-existing state,
rollback preserves the distro, install directory, ownership marker, and empty
parent directory. A reused step starts with no authority from an earlier attempt.
Once creation starts, the existing live-registration ownership and
unregister-failure preservation gates still apply. Explicit uninstall remains a
separate, destructive-consent path.

```json
{
"DistroName": "OpenClawGateway",
Expand Down
59 changes: 54 additions & 5 deletions src/OpenClaw.SetupEngine/CreateWslInstanceStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,26 @@ public sealed class CreateWslInstanceStep : SetupStep
TimeSpan.FromSeconds(90),
];

private readonly IWslRegistrationInspector _registrationInspector;
private bool _createdRegistrationThisAttempt;

public override string Id => "wsl-create";
public override string DisplayName => "Create WSL instance";
public override bool CanRetry => false;

public CreateWslInstanceStep()
: this(new WindowsWslRegistrationInspector())
{
}

internal CreateWslInstanceStep(IWslRegistrationInspector registrationInspector)
{
_registrationInspector = registrationInspector ?? throw new ArgumentNullException(nameof(registrationInspector));
}

public override async Task<StepResult> ExecuteAsync(SetupContext ctx, CancellationToken ct)
{
_createdRegistrationThisAttempt = false;
var distro = ctx.DistroName!;
var baseDistro = ctx.Config.BaseDistro.Trim();

Expand Down Expand Up @@ -76,6 +90,7 @@ or ArgumentException

Directory.CreateDirectory(Path.GetDirectoryName(installPath)!);

_createdRegistrationThisAttempt = true;
var installArgs = WslInstallSupport.BuildDirectInstallArgs(baseDistro, distro, installPath);
ctx.Logger.Info($"Installing fresh WSL distro with arguments: {string.Join(' ', installArgs)}");
var install = await ctx.Commands.RunAsync(
Expand Down Expand Up @@ -208,7 +223,7 @@ private static async Task<bool> IsDistroRegisteredAsync(SetupContext ctx, string
return false;
}

private static async Task<string> CleanupPartialInstall(SetupContext ctx, string distro, string installPath, CancellationToken ct)
private async Task<string> CleanupPartialInstall(SetupContext ctx, string distro, string installPath, CancellationToken ct)
{
var cleanupErrors = new List<string>();
var installPathExists = Directory.Exists(installPath) || File.Exists(installPath);
Expand All @@ -221,14 +236,39 @@ private static async Task<string> CleanupPartialInstall(SetupContext ctx, string
var distroExists = registrationStateKnown && WslInstallSupport.ContainsDistro(list.Stdout, distro);
var canDeleteInstallPath = registrationStateKnown && !distroExists;

if (!registrationStateKnown)
if (ctx.IsUninstalling)
{
if (!(registrationStateKnown && !distroExists))
canDeleteInstallPath = await TryUnregisterPartialInstall(ctx, distro, cleanupErrors, ct);
}
else if (!registrationStateKnown)
{
ctx.Logger.Warn($"Partial install cleanup could not list WSL distros (exit {list.ExitCode}); refusing to unregister '{distro}'");
}
else if (distroExists && _createdRegistrationThisAttempt)
{
ctx.Logger.Warn($"Partial install cleanup could not list WSL distros (exit {list.ExitCode}); attempting best-effort unregister for '{distro}' before deleting app-owned files");
canDeleteInstallPath = await TryUnregisterPartialInstall(ctx, distro, cleanupErrors, ct);
if (!ManagedDistroOwnership.HasRegisteredDistroEvidence(
ctx.DataDir,
ctx.LocalDataDir,
distro,
installPath,
_registrationInspector,
out var registrationFailure))
{
ctx.Logger.Warn(
$"Refusing to unregister '{distro}' because the live WSL registration is not this install: {registrationFailure}");
cleanupErrors.Add(
$"refusing to unregister '{distro}' because the live WSL registration is not this install: {registrationFailure}");
}
else
{
canDeleteInstallPath = await TryUnregisterPartialInstall(ctx, distro, cleanupErrors, ct);
}
}
else if (distroExists)
{
canDeleteInstallPath = await TryUnregisterPartialInstall(ctx, distro, cleanupErrors, ct);
ctx.Logger.Warn($"Refusing to unregister '{distro}' because this install did not create it");
cleanupErrors.Add($"refusing to unregister '{distro}' because this install did not create it");
}

if (!canDeleteInstallPath)
Expand Down Expand Up @@ -307,6 +347,15 @@ public override async Task RollbackAsync(SetupContext ctx, CancellationToken ct)
if (!DistroInstallPathPolicy.TryGetManagedInstallPath(ctx.LocalDataDir, distro, out var vhdDir, out var pathError))
throw new IOException($"[Uninstall] Refusing WSL rollback filesystem cleanup: {pathError}");

// Execute's partial cleanup is reached only after creation starts; pipeline
// rollback also runs when Execute refuses pre-existing state.
if (!ctx.IsUninstalling && !_createdRegistrationThisAttempt)
{
ctx.Logger.Warn(
$"Preserving WSL distro '{distro}', install path, ownership marker, and parent directory because this attempt did not start creating it.");
return;
}

var cleanupError = await CleanupPartialInstall(ctx, distro, vhdDir, ct);
if (cleanupError.Length > 0)
throw new IOException($"[Uninstall] Refusing unsafe WSL rollback cleanup.{cleanupError}");
Expand Down
Loading
Loading