Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/SETUP_ENGINE_REDESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ 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.

The Inno uninstall helper also binds its primary filesystem cleanup to the exact
generated local-data root, not the user-selected install folder or its basename.
After WSL reports the configured distro absent or unregister succeeds, an
uncertain custom install folder is preserved with an `artifactWarnings` entry in
`uninstall-gateway-result.json` and a warning in `uninstall-gateway-wsl.log` under
that folder. Reparse points at the app root, WSL root, or configured child stop
primary deletion. These helper checks do not establish ownership of other WSL
children or replace the signed-installer and native-WSL proof gates.

```json
{
"DistroName": "OpenClawGateway",
Expand Down
83 changes: 79 additions & 4 deletions installer.iss
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ var
LocalGatewayCleanupChoiceInitialized: Boolean;
LocalGatewayCleanupRequested: Boolean;
LocalGatewayCleanupSucceeded: Boolean;
LocalGatewayCleanupScriptPath: String;

#if vcRedist != ""
procedure InstallVCRuntime;
Expand Down Expand Up @@ -235,6 +236,8 @@ begin
Exit;
end;

LocalGatewayCleanupScriptPath := TempScriptPath;

Params :=
'-NoProfile -ExecutionPolicy Bypass -File ' + AddQuotes(TempScriptPath) +
' -AppRoot ' + AddQuotes(ExpandConstant('{app}')) +
Expand Down Expand Up @@ -300,15 +303,87 @@ begin
Log('User continued uninstall after local gateway cleanup failed; generated state will be preserved.');
end;

procedure DeleteGeneratedChild(const ChildName: String);
var
ChildPath: String;
begin
ChildPath := AddBackslash(ExpandConstant('{app}')) + ChildName;
if DirExists(ChildPath) then
begin
if not DelTree(ChildPath, True, True, True) then
Log('Generated directory could not be deleted: ' + ChildName);
end
else if FileExists(ChildPath) then
begin
if not DeleteFile(ChildPath) then
Log('Generated file could not be deleted: ' + ChildName);
end;
end;

procedure DeleteConfirmedDistroChild;
var
ResultCode: Integer;
Started: Boolean;
Params: String;
begin
if (LocalGatewayCleanupScriptPath = '') or (not FileExists(LocalGatewayCleanupScriptPath)) then
begin
Log('Ownership uncertain: local gateway cleanup script is unavailable; leaving WSL distro children in place.');
Exit;
end;

Params :=
'-NoProfile -ExecutionPolicy Bypass -File ' + AddQuotes(LocalGatewayCleanupScriptPath) +
' -RemoveConfirmedDistroChild' +
' -AppRoot ' + AddQuotes(ExpandConstant('{tmp}')) +
' -DataDirectoryName ' + AddQuotes('{#MyInstallDir}') +
' -DistroName ' + AddQuotes('{#MyDistroName}');

Log('Deleting only the confirmed {#MyDistroName} child under the generated-data root.');
Started :=
Exec(
ExpandConstant('{sys}\WindowsPowerShell\v1.0\powershell.exe'),
Params,
'',
SW_HIDE,
ewWaitUntilTerminated,
ResultCode);
if (not Started) or (ResultCode <> 0) then
Log('Confirmed distro child cleanup did not finish; leaving uncertain WSL children in place. Exit code: ' + IntToStr(ResultCode) + '.');
end;

procedure DeleteGeneratedAppState;
var
AppDir: String;
GeneratedRoot: String;
begin
if not LocalGatewayCleanupSucceeded then
Exit;

if DelTree(ExpandConstant('{app}'), True, True, True) then
Log('Deleted generated app state from {app}.')
else
Log('Generated app state in {app} could not be fully deleted; continuing uninstall.');
DeleteConfirmedDistroChild;

AppDir := RemoveBackslashUnlessRoot(ExpandConstant('{app}'));
GeneratedRoot := RemoveBackslashUnlessRoot(ExpandConstant('{localappdata}\{#MyInstallDir}'));
if CompareText(AppDir, GeneratedRoot) <> 0 then
begin
Log('Ownership uncertain: {app} is not the generated-data root; leaving generated children in place.');
Exit;
end;

DeleteGeneratedChild('Logs');
DeleteGeneratedChild('wsl-keepalive');
DeleteGeneratedChild('WebView2');
DeleteGeneratedChild('canvas');
DeleteGeneratedChild('native-cli');
DeleteGeneratedChild('setup-state.json');
DeleteGeneratedChild('run.marker');
DeleteGeneratedChild('exec-approvals.json');
DeleteGeneratedChild('exec-policy.json');
DeleteGeneratedChild('openclaw-tray.log');
DeleteGeneratedChild('uninstall-gateway-result.json');
DeleteGeneratedChild('uninstall-gateway-error.log');
DeleteGeneratedChild('uninstall-gateway-wsl.log');
Log('Deleted generated app state children from {app}.');
end;

procedure RemoveAppAutoStart;
Expand Down
125 changes: 118 additions & 7 deletions scripts/Uninstall-LocalGateway.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ param(
[string]$DataDirectoryName = 'OpenClawTray',
[string]$AutoStartName = 'OpenClawTray',
[string]$StartupTaskName = 'OpenClaw Companion',
[string]$DistroName = 'OpenClawGateway'
[string]$DistroName = 'OpenClawGateway',
[switch]$RemoveConfirmedDistroChild
)

$ErrorActionPreference = 'Stop'
Expand Down Expand Up @@ -616,19 +617,123 @@ function Test-DistroListed {
return $distros -contains $DistroName
}

function Test-SameFullPath {
param(
[string]$Left,
[string]$Right
)

try {
$leftFull = [System.IO.Path]::GetFullPath($Left).TrimEnd('\')
$rightFull = [System.IO.Path]::GetFullPath($Right).TrimEnd('\')
return [string]::Equals($leftFull, $rightFull, [System.StringComparison]::OrdinalIgnoreCase)
} catch {
return $false
}
}

function Remove-ConfirmedDistroChild {
$localDataDir = Resolve-LocalDataDir
if ([string]::IsNullOrWhiteSpace($localDataDir)) {
Write-GatewayLog 'Ownership uncertain: generated-data root could not be resolved; leaving WSL children in place.'
return
}

try {
$generatedRoot = [System.IO.Path]::GetFullPath($localDataDir).TrimEnd('\')
} catch {
Write-GatewayLog "Ownership uncertain: generated-data root '$localDataDir' is not a usable path; leaving WSL children in place."
return
}

$rootName = [System.IO.Path]::GetFileName($generatedRoot)
if (-not [string]::Equals($rootName, $DataDirectoryName, [System.StringComparison]::OrdinalIgnoreCase)) {
Write-GatewayLog "Ownership uncertain: generated-data root '$generatedRoot' is not '$DataDirectoryName'; leaving WSL children in place."
return
}

if (-not [string]::IsNullOrWhiteSpace($AppRoot)) {
try {
$appRootFull = [System.IO.Path]::GetFullPath($AppRoot).TrimEnd('\')
$appRootName = [System.IO.Path]::GetFileName($appRootFull)
if ([string]::Equals($appRootName, $DataDirectoryName, [System.StringComparison]::OrdinalIgnoreCase) -and
-not (Test-SameFullPath $appRootFull $generatedRoot)) {
Write-GatewayLog "Ownership uncertain: '$appRootFull' matches the data-directory basename but is not the generated-data root '$generatedRoot'; leaving it in place."
}
} catch {
Write-GatewayLog "Ownership uncertain: AppRoot '$AppRoot' is not a usable path; leaving it in place."
}
}

$wslRoot = Join-Path $generatedRoot 'wsl'
if (-not (Test-Path -LiteralPath $wslRoot -PathType Container)) {
Write-GatewayLog "No wsl directory under generated-data root '$generatedRoot'."
return
}

$wslItem = Get-Item -LiteralPath $wslRoot -Force -ErrorAction Stop
if (($wslItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
Write-GatewayLog "Ownership uncertain: '$wslRoot' is a reparse point; leaving it in place."
return
}

try {
$confirmed = [System.IO.Path]::GetFullPath((Join-Path $wslRoot $DistroName)).TrimEnd('\')
} catch {
Write-GatewayLog "Ownership uncertain: configured distro path under '$wslRoot' is not usable; leaving WSL children in place."
return
}

$confirmedParent = [System.IO.Path]::GetDirectoryName($confirmed)
if (-not (Test-SameFullPath $confirmedParent $wslRoot)) {
Write-GatewayLog "Ownership uncertain: '$confirmed' is not an immediate child of '$wslRoot'; leaving WSL children in place."
return
}

foreach ($child in @(Get-ChildItem -LiteralPath $wslRoot -Force)) {
$isReparse = ($child.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0
$isConfirmed =
$child.PSIsContainer -and
-not $isReparse -and
[string]::Equals($child.Name, $DistroName, [System.StringComparison]::OrdinalIgnoreCase) -and
(Test-SameFullPath $child.FullName $confirmed)

if ($isConfirmed) {
Remove-Item -LiteralPath $child.FullName -Recurse -Force -ErrorAction Stop
Write-GatewayLog "Deleted confirmed distro child '$($child.FullName)'."
continue
}

Write-GatewayLog "Ownership uncertain; leaving leftover '$($child.FullName)'."
}
}

function Remove-GatewayDirectory {
$gatewayDirectory = Join-Path $AppRoot "wsl\$DistroName"
$generatedRoot = Resolve-LocalDataDir
if (-not (Test-SameFullPath $AppRoot $generatedRoot)) {
Add-CleanupWarning "Ownership uncertain: AppRoot '$AppRoot' is not the generated-data root '$generatedRoot'; skipping filesystem cleanup there."
return
}

$wslRoot = Join-Path $AppRoot 'wsl'
$gatewayDirectory = [System.IO.Path]::GetFullPath((Join-Path $wslRoot $DistroName)).TrimEnd('\')
if (-not (Test-SameFullPath ([System.IO.Path]::GetDirectoryName($gatewayDirectory)) $wslRoot)) {
throw "Refusing to delete '$gatewayDirectory': it is not an immediate child of '$wslRoot'."
}

foreach ($path in @($AppRoot, $wslRoot, $gatewayDirectory)) {
if (-not (Test-Path -LiteralPath $path)) { continue }
$item = Get-Item -LiteralPath $path -Force -ErrorAction Stop
if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
throw "Refusing to recursively delete reparse point '$path'."
}
}

if (-not (Test-Path -LiteralPath $gatewayDirectory)) {
Write-GatewayLog "Gateway directory does not exist: $gatewayDirectory"
return
}

$gatewayItem = Get-Item -LiteralPath $gatewayDirectory -Force -ErrorAction Stop
if (($gatewayItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
throw "Refusing to recursively delete reparse point '$gatewayDirectory'."
}

$lastError = $null
for ($attempt = 1; $attempt -le 6; $attempt++) {
try {
Expand All @@ -650,6 +755,12 @@ function Remove-GatewayDirectory {

try {
Ensure-AppRoot
if ($RemoveConfirmedDistroChild) {
Write-GatewayLog "Removing only the confirmed distro child '$DistroName' under the generated-data root."
Remove-ConfirmedDistroChild
exit 0
}

Write-GatewayLog "Starting local gateway cleanup for $DistroName."

$script:WslPath = Get-WslExePath
Expand Down
Loading
Loading