Skip to content
Merged
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
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -618,8 +618,16 @@ it; a custom external Workspace is always preserved:

sqrbx-uninstall --purge

A second confirmation is required if the recorded Workspace is non-empty.
Pass `-y` (or `-Yes` on PowerShell) to skip all prompts for scripting.
The confirmation summary lists a non-empty nested Workspace, with its item count,
as something that will be deleted, and a second confirmation is required.
Pass `-y` (or `-Yes` on PowerShell) to skip prompts for scripting. Unattended
purge refuses, before removing anything, to delete a non-empty nested
Workspace unless you also pass `--delete-workspace` (`-DeleteWorkspace` on
PowerShell):

sqrbx-uninstall --purge --yes --delete-workspace

An empty Workspace or an external Workspace needs no extra flag.
Idempotent for a valid Install identity. Legacy resources require `--adopt`;
purging an adopted, unlabeled volume additionally requires `--force`.

Expand All @@ -628,6 +636,7 @@ purging an adopted, unlabeled volume additionally requires `--force`.
sqrbx-uninstall # keep ~/squarebox
sqrbx-uninstall -Purge # also remove ~/squarebox
sqrbx-uninstall -Yes # skip confirmations
sqrbx-uninstall -Purge -Yes -DeleteWorkspace # also delete a non-empty nested Workspace

**Broken-state recovery** (e.g. shell functions are missing, or after partial
install): run the script matching the adapter that created the Install identity
Expand Down
22 changes: 22 additions & 0 deletions docs/releases/v1.3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,25 @@ Managed home. After success, use only the target adapter for lifecycle work.
Native Windows OpenSSH-agent forwarding remains experimental. PowerShell keeps
the read-only SSH-directory fallback; Git Bash can forward an already available
Unix-compatible socket. Migration does not install a named-pipe relay.

## Box PID limit applies on recreation

v1.3.0 bounds each Box to 4096 processes (`--pids-limit=4096`, Compose
`pids_limit: 4096`). The limit is a container creation setting, so an existing
Box keeps running without it until that Box is recreated:

- Installer users: run `sqrbx-rebuild` or re-run the installer.
- Compose users: after updating `docker-compose.yml`, run `docker compose up -d`;
Compose detects the changed configuration and recreates the container.

The Workspace and Managed home are preserved across recreation.

## Purge no longer deletes a non-empty Workspace unattended

`uninstall.sh --purge --yes` and `uninstall.ps1 -Purge -Yes` previously removed
the default Workspace nested at `<install dir>/workspace` without warning. The
confirmation summary now lists a non-empty nested Workspace and its item count,
and unattended purge refuses before removing anything unless
`--delete-workspace` (`-DeleteWorkspace` on PowerShell) is also passed. Update
scripts that intentionally purge a populated default Workspace. An empty
Workspace, or an external Workspace (always preserved), needs no flag.
48 changes: 48 additions & 0 deletions tests/test-lifecycle-ownership.sh
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,54 @@ fi
grep -q -- '--force is required' "$TMP/force.out"
test -d "$TMP/custom"

# Purge must not silently delete a non-empty Workspace nested in the install
# directory. Unattended purge refuses before any destructive operation unless
# --delete-workspace is explicit; an empty nested Workspace needs no flag.
STATE="$TMP/custom/.squarebox/install-state"
reset_host_adapters() {
printf '# >>> squarebox >>>\nmanaged\n# <<< squarebox <<<\n' >"$TMP/home/.bashrc"
printf '# squarebox-install-id=test-install-123\nmanaged\n' >"$TMP/home/.squarebox-shell-init"
rm -f "$MOCK_STATE/"* "$MOCK_LOG"
}
cp -a "$TMP/custom" "$TMP/custom.pristine"
sed -i "s#^WORKSPACE_DIR=.*#WORKSPACE_DIR=$TMP/custom/workspace#" "$STATE"
mkdir -p "$TMP/custom/workspace"
printf code >"$TMP/custom/workspace/project.txt"
printf hidden >"$TMP/custom/workspace/.env"
reset_host_adapters
status=0
"$ROOT/uninstall.sh" --purge --yes --force >"$TMP/workspace-guard.out" 2>&1 || status=$?
[ "$status" = 1 ] || { echo "unattended purge did not refuse a non-empty nested Workspace (status $status)" >&2; exit 1; }
grep -qF "Workspace inside install directory (2 item(s)): $TMP/custom/workspace" "$TMP/workspace-guard.out"
grep -q -- 'Pass --delete-workspace' "$TMP/workspace-guard.out"
test -f "$TMP/custom/workspace/project.txt"
test -f "$TMP/custom/.squarebox/install-state"
test -f "$TMP/home/.squarebox-shell-init"
grep -qF '# >>> squarebox >>>' "$TMP/home/.bashrc"
! grep -q '^rm \|^rmi \|^volume rm ' "$MOCK_LOG" 2>/dev/null

status=0
"$ROOT/uninstall.sh" --yes --delete-workspace >"$TMP/workspace-flag.out" 2>&1 || status=$?
[ "$status" = 64 ] || { echo '--delete-workspace was accepted without --purge' >&2; exit 1; }
grep -q -- '--delete-workspace requires --purge' "$TMP/workspace-flag.out"
test -f "$TMP/home/.squarebox-shell-init"

reset_host_adapters
"$ROOT/uninstall.sh" --purge --yes --force --delete-workspace >"$TMP/workspace-delete.out"
test ! -e "$TMP/custom"
grep -q '^volume rm custom-home$' "$MOCK_LOG"

cp -a "$TMP/custom.pristine" "$TMP/custom"
sed -i "s#^WORKSPACE_DIR=.*#WORKSPACE_DIR=$TMP/custom/workspace#" "$STATE"
mkdir -p "$TMP/custom/workspace"
reset_host_adapters
"$ROOT/uninstall.sh" --purge --yes --force >"$TMP/workspace-empty.out"
test ! -e "$TMP/custom"
! grep -q 'Workspace inside install directory' "$TMP/workspace-empty.out"

mv "$TMP/custom.pristine" "$TMP/custom"
reset_host_adapters

export FAIL_SHARED_RELEASE_REMOVE=1
"$ROOT/uninstall.sh" --purge --yes --force >"$TMP/purge.out"
test ! -e "$TMP/custom"
Expand Down
36 changes: 35 additions & 1 deletion tests/test-lifecycle-powershell.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ foreach ($boundary in @('checkout', 'image-alias', 'managed-home-create', 'manag
Assert-True ($install.Contains('Malformed squarebox marker block') -and $uninstall.Contains('Malformed squarebox marker block')) 'profile marker validation is absent'
Assert-True ($install.Contains('[regex]::Replace($profileBlock')) 'profile interpolation can rescan inserted path placeholders'
Assert-True ($uninstall.Contains('Assert-PurgeCheckout')) 'purge does not revalidate checkout identity'
Assert-True ($uninstall.Contains('[switch]$DeleteWorkspace') -and $uninstall.Contains('$Yes -and -not $DeleteWorkspace')) 'unattended purge can delete a nested Workspace without -DeleteWorkspace'
Assert-True ($install.Contains('Get-BoundedJson') -and $install.Contains('MaximumRetryCount 3')) 'release metadata HTTP is unbounded or lacks retries'
Assert-True ($install.Contains('{{range .RepoDigests}}{{println .}}{{end}}') -and -not $install.Contains('index .RepoDigests 0')) 'PowerShell trusts the first repository digest instead of enumerating identities'
Assert-True ($install -match '\$repoDigestOutput = @\(\)\s+if \(-not \$Build\)') 'local builds still derive identity from unordered RepoDigests'
Expand Down Expand Up @@ -233,6 +234,7 @@ $mockBin = Join-Path $migrationRoot 'bin'
$mockRuntime = Join-Path $mockBin 'mock-runtime.ps1'
[IO.File]::WriteAllText($mockRuntime, @'
$command = $args -join ' '
if ($env:SQUAREBOX_TEST_RUNTIME_LOG) { Add-Content -LiteralPath $env:SQUAREBOX_TEST_RUNTIME_LOG -Value $command }
if ($command.Contains('{{.Id}}')) { Write-Output ('sha256:' + ('c' * 64)) }
elseif ($command.Contains('io.squarebox.install-id')) { Write-Output 'test-install-123' }
exit 0
Expand Down Expand Up @@ -302,7 +304,9 @@ try {
[IO.File]::WriteAllText($uninstallHarness, @'
$PROFILE.CurrentUserAllHosts = $env:SQUAREBOX_TEST_PROFILE_ALL
$PROFILE.CurrentUserCurrentHost = $env:SQUAREBOX_TEST_PROFILE_HOST
& $env:SQUAREBOX_TEST_UNINSTALL -InstallDir $env:SQUAREBOX_TEST_INSTALL_DIR -Yes
$purge = $env:SQUAREBOX_TEST_PURGE -ceq '1'
$deleteWorkspace = $env:SQUAREBOX_TEST_DELETE_WORKSPACE -ceq '1'
& $env:SQUAREBOX_TEST_UNINSTALL -InstallDir $env:SQUAREBOX_TEST_INSTALL_DIR -Yes -Purge:$purge -DeleteWorkspace:$deleteWorkspace
exit $LASTEXITCODE
'@, [Text.UTF8Encoding]::new($false))
$oldUserProfile = $env:USERPROFILE
Expand All @@ -315,9 +319,39 @@ exit $LASTEXITCODE
& pwsh -NoProfile -File $uninstallHarness
Assert-True ($LASTEXITCODE -eq 0) 'PowerShell uninstaller rejected migrated Install state'
Assert-True (-not ((Get-Content $profileAll) -ccontains '# squarebox-install-id=test-install-123')) 'target uninstaller did not remove migrated adapter'

# Unattended purge must refuse a non-empty nested Workspace before any
# destructive operation unless -DeleteWorkspace is explicit.
$finalState = @{}; Get-Content $migrationState | ForEach-Object { $key, $value = $_ -split '=', 2; $finalState[$key] = $value }
$nestedWorkspace = $finalState.WORKSPACE_DIR
[IO.Directory]::CreateDirectory($nestedWorkspace) | Out-Null
$projectFile = Join-Path $nestedWorkspace 'project.txt'
[IO.File]::WriteAllText($projectFile, 'code', [Text.UTF8Encoding]::new($false))
$runtimeLog = Join-Path $migrationRoot 'runtime.log'
$env:SQUAREBOX_TEST_RUNTIME_LOG = $runtimeLog
$env:SQUAREBOX_TEST_PURGE = '1'
$guardOutput = (& pwsh -NoProfile -File $uninstallHarness *>&1) -join "`n"
Assert-True ($LASTEXITCODE -ne 0) 'unattended purge deleted a non-empty nested Workspace without -DeleteWorkspace'
Assert-True ($guardOutput.Contains('Workspace inside install directory (1 item(s))')) 'purge summary does not list the nested Workspace'
Assert-True ($guardOutput.Contains('Pass -DeleteWorkspace')) 'Workspace refusal does not name -DeleteWorkspace'
Assert-True (Test-Path -LiteralPath $projectFile -PathType Leaf) 'Workspace refusal removed project files'
Assert-True (Test-Path -LiteralPath $migrationState -PathType Leaf) 'Workspace refusal removed Install identity'
$guardCalls = if (Test-Path -LiteralPath $runtimeLog) { @(Get-Content -LiteralPath $runtimeLog) } else { @() }
Assert-True (-not ($guardCalls | Where-Object { $_ -match '^(rm|rmi|volume rm) ' })) 'Workspace refusal ran a destructive runtime command'

& git -C $migrationInstall init -q
Assert-True ($LASTEXITCODE -eq 0) 'unable to initialize purge checkout fixture'
& git -C $migrationInstall remote add origin 'https://github.com/SquareWaveSystems/squarebox.git'
Assert-True ($LASTEXITCODE -eq 0) 'unable to set purge checkout fixture origin'
$env:SQUAREBOX_TEST_DELETE_WORKSPACE = '1'
& pwsh -NoProfile -File $uninstallHarness
Assert-True ($LASTEXITCODE -eq 0) 'purge with -DeleteWorkspace failed'
Assert-True (-not (Test-Path -LiteralPath $migrationInstall)) 'purge with -DeleteWorkspace kept the install directory'
Assert-True (@(Get-Content -LiteralPath $runtimeLog) -ccontains 'volume rm custom-home') 'purge with -DeleteWorkspace kept the Managed home'
} finally {
$env:USERPROFILE = $oldUserProfile
Remove-Item Env:SQUAREBOX_TEST_PROFILE_ALL, Env:SQUAREBOX_TEST_PROFILE_HOST, Env:SQUAREBOX_TEST_UNINSTALL, Env:SQUAREBOX_TEST_INSTALL_DIR -ErrorAction SilentlyContinue
Remove-Item Env:SQUAREBOX_TEST_RUNTIME_LOG, Env:SQUAREBOX_TEST_PURGE, Env:SQUAREBOX_TEST_DELETE_WORKSPACE -ErrorAction SilentlyContinue
}
$global:LASTEXITCODE = 0
} finally {
Expand Down
4 changes: 4 additions & 0 deletions tests/test-lifecycle-static.sh
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ grep -q '\$LegacyStarshipBlob' install.ps1
grep -Fq '.install-state.$([guid]::NewGuid' install.ps1
grep -q 'Recorded Workspace contains' uninstall.ps1
grep -q "Read-Host 'Continue? \[y/N\]'" uninstall.ps1
grep -q -- '--delete-workspace' uninstall.sh
grep -q 'DeleteWorkspace' uninstall.ps1
grep -q 'Workspace inside install directory' uninstall.sh
grep -q 'Workspace inside install directory' uninstall.ps1

# FORMAT=1 is deliberately adapter-native. Both readers accept CRLF, but a
# Git-Bash C:/... path is not promised to be interchangeable with a native
Expand Down
37 changes: 26 additions & 11 deletions uninstall.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[CmdletBinding()]
param(
[switch]$Purge,
[switch]$DeleteWorkspace,
[Alias('y')][switch]$Yes,
[switch]$Adopt,
[switch]$Force,
Expand All @@ -15,6 +16,7 @@ $ErrorActionPreference = 'Stop'
$Repo = 'https://github.com/SquareWaveSystems/squarebox.git'
$UserHome = if ($IsWindows -and $env:USERPROFILE) { $env:USERPROFILE } else { $HOME }
function Abort([string]$Message) { Write-Host "Error: $Message" -ForegroundColor Red; exit 1 }
if ($DeleteWorkspace -and -not $Purge) { Abort '-DeleteWorkspace requires -Purge.' }
$StateFields = @(
'FORMAT', 'INSTALL_ID', 'RUNTIME', 'INSTALL_DIR', 'WORKSPACE_DIR', 'GIT_CONFIG_DIR',
'HOME_VOLUME', 'CONTAINER_NAME', 'IMAGE_ALIAS', 'IMAGE_REPOSITORY', 'IMAGE_REF',
Expand Down Expand Up @@ -275,6 +277,16 @@ foreach ($path in $ProfilePaths) {
}
}
$HasProfile = $ProfileBlocks.Count -gt 0
# A Workspace nested in the install directory is deleted by purge. Count it
# once so the summary, the -Yes guard, and the interactive prompt agree.
$PathComparison = if ($IsWindows) { [StringComparison]::OrdinalIgnoreCase } else { [StringComparison]::Ordinal }
$WorkspaceInside = ($WorkspaceDir + [IO.Path]::DirectorySeparatorChar).StartsWith(
$InstallDir + [IO.Path]::DirectorySeparatorChar, $PathComparison)
$WorkspaceCount = 0
if ($Purge -and (Test-Path -LiteralPath $WorkspaceDir -PathType Container)) {
try { $WorkspaceCount = @(Get-ChildItem -Force -LiteralPath $WorkspaceDir -ErrorAction Stop).Count }
catch { Abort "Unable to inspect recorded Workspace '$WorkspaceDir'; nothing was removed." }
}
Write-Host 'squarebox uninstall'
Write-Host '==================='
Write-Host "Install identity: $(if ($InstallId) { $InstallId } else { 'legacy adoption' })"
Expand All @@ -287,9 +299,16 @@ if ($ContainerOwned) { Write-Host " - Managed Box: $ContainerName"; $Anything =
if ($ImageOwned) { Write-Host " - Recorded image alias: $ImageAlias"; $Anything = $true }
if ($HasProfile) { Write-Host " - PowerShell adapter(s): $($ProfilePaths -join ', ')"; $Anything = $true }
if ($Purge -and (Test-Path $InstallDir)) { Write-Host " - Recorded install directory: $InstallDir"; $Anything = $true }
if ($WorkspaceInside -and $WorkspaceCount -gt 0) { Write-Host " - Workspace inside install directory ($WorkspaceCount item(s)): $WorkspaceDir" }
if ($Purge -and $VolumeOwned) { Write-Host " - Managed home: $HomeVolume"; $Anything = $true }
if (-not $Anything) { Write-Host ' (nothing)'; exit 0 }

# Unattended purge must never silently delete user project files. Refuse before
# any destructive operation unless deletion was requested explicitly.
if ($WorkspaceInside -and $WorkspaceCount -gt 0 -and $Yes -and -not $DeleteWorkspace) {
Abort "-Purge would delete the Workspace at $WorkspaceDir ($WorkspaceCount item(s)). Pass -DeleteWorkspace to delete it, or move it outside $InstallDir first. Nothing was removed."
}

if ($Purge -and $VolumeOwned -and ($HomeVolumeAdopted -or -not $State) -and -not $Force) {
Abort "'$HomeVolume' is an adopted unlabeled volume; -Force is required to purge it."
}
Expand All @@ -301,16 +320,12 @@ if (-not $Yes) {
if ([Console]::IsInputRedirected) { Abort 'stdin is not a terminal; pass -Yes.' }
if ((Read-Host 'Proceed? [y/N]') -notmatch '^[yY]([eE][sS])?$') { Write-Host 'Aborted.'; exit 1 }
}
if ($Purge -and -not $Yes -and (Test-Path -LiteralPath $WorkspaceDir -PathType Container)) {
$workspaceCount = @(Get-ChildItem -Force -LiteralPath $WorkspaceDir -ErrorAction Stop).Count
if ($workspaceCount -gt 0) {
Write-Warning "Recorded Workspace contains $workspaceCount item(s): $WorkspaceDir"
$comparison = if ($IsWindows) { [StringComparison]::OrdinalIgnoreCase } else { [StringComparison]::Ordinal }
if ($WorkspaceDir.StartsWith($InstallDir + [IO.Path]::DirectorySeparatorChar, $comparison)) {
Write-Host 'It will be removed with the install directory.'
} else { Write-Host 'It is outside the install directory and will be preserved.' }
if ((Read-Host 'Continue? [y/N]') -notmatch '^[yY]([eE][sS])?$') { Write-Host 'Aborted.'; exit 1 }
}
if ($WorkspaceCount -gt 0 -and -not $Yes) {
Write-Warning "Recorded Workspace contains $WorkspaceCount item(s): $WorkspaceDir"
if ($WorkspaceInside) {
Write-Host 'It will be removed with the install directory.'
} else { Write-Host 'It is outside the install directory and will be preserved.' }
if ((Read-Host 'Continue? [y/N]') -notmatch '^[yY]([eE][sS])?$') { Write-Host 'Aborted.'; exit 1 }
}

if ($Purge) { Assert-PurgeCheckout }
Expand Down Expand Up @@ -362,7 +377,7 @@ Write-Host 'Uninstall complete.'
if (-not $Purge) {
Write-Host "Preserved install identity and Workspace at $InstallDir."
if ($VolumeOwned) { Write-Host "Preserved Managed home $HomeVolume." }
} elseif ((Test-Path $WorkspaceDir) -and -not $WorkspaceDir.StartsWith($InstallDir + [IO.Path]::DirectorySeparatorChar)) {
} elseif ((Test-Path $WorkspaceDir) -and -not $WorkspaceInside) {
Write-Host "Preserved external Workspace $WorkspaceDir."
}
Write-Host 'Start a new PowerShell session to drop loaded functions.'
Loading
Loading