Keep the site up when the cleanup job is killed

The cleanup job stops the server, puts the files back and starts it again. On
a slow disk that took minutes, and on 2026-10-28 the job was killed while the
server was down: the task of the server only starts it again when it failed,
never when it was stopped, so the site was down until the box was power-cycled.

The job now puts the home directory, the four texts and the permissions of the
installation back while the server runs, and only stops it for the protected
copy, which cannot be replaced while the server holds it open.

A new task, "KiK watch the server", starts the server every two minutes when
nothing listens on port 22 and the cleanup job is not running.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Zebreus 2026-10-28 11:55:37 +01:00
commit 5a885e3781

View file

@ -30,6 +30,7 @@ $binDir = Join-Path $installDir 'bin'
$sshdBat = Join-Path $installDir 'run-sshd.bat'
$restoreTask = 'KiK restore shell files'
$sshdTask = 'KiK sshd'
$watchTask = 'KiK watch the server'
$service = 'msys2-sshd'
# The programs the journey needs, kept out of the user's reach. sshd-session and sshd-auth are the per-connection
@ -215,60 +216,8 @@ if ($restart) {
$runtimeBroken = (Invoke-Bounded "$msys\usr\bin\pacman.exe" @('-Qkk', 'msys2-runtime') 120) -ne 0
}
if ($restart) {
Stop-Sshd
Remove-Item "$kik\msys-2.0.dll" -Force -ErrorAction SilentlyContinue
if ($runtimeBroken) {
# the package is put back while the server is stopped: the file cannot be replaced while it is mapped.
# The pattern is quoted, or the runtime expands it to the files of the working directory.
Invoke-Bounded "$msys\usr\bin\pacman.exe" @('-S', '--noconfirm', '--overwrite', "'*'", 'msys2-runtime') 300 | Out-Null
Write-Log 'restored the runtime of the installation'
}
}
try {
# The protected login path: the copy the sshd and the login shell run from. The user cannot write it, so even
# after they replace or remove their own copies in /usr/bin the server and the journey keep working. The trusted
# originals are in bin\; anything missing from opt/kik is put back.
if (Test-Path -LiteralPath $binDir) {
if (-not (Test-Path -LiteralPath $kik)) { New-Item -ItemType Directory -Path $kik | Out-Null }
foreach ($file in Get-ChildItem -LiteralPath $binDir -File -Force | Where-Object { $_.Name -ne 'msys-2.0.dll' }) {
$target = Join-Path $kik $file.Name
try {
if (-not (Test-Path -LiteralPath $target) -or (Get-Item -LiteralPath $target -Force).Length -ne $file.Length) {
Copy-Item -LiteralPath $file.FullName -Destination $target -Force
Write-Log "restored opt/kik/$($file.Name)"
}
} catch {
# a running server holds its programs open; they are replaced in one of the next runs
Write-Log "failed opt/kik/$($file.Name) : $_"
}
}
New-Item -ItemType Directory -Force -Path "$kik\dev\shm", "$kik\dev\mqueue" | Out-Null
Set-LockedAcl $kik $true 'ReadAndExecute'
# /dev is scratch: the user may write it so POSIX shared memory works, the rest of opt/kik stays read-only
if ($sid) { icacls "$kik\dev" /grant "*${sid}:(OI)(CI)M" | Out-Null }
}
# The account database and the ssh keys and config the SYSTEM server reads: the user may read passwd (programs
# map ids through it) but change none of it, and has no business in /etc/ssh at all.
$rootShell = '/opt/kik/zsh.exe'
$pw = "$msys\etc\passwd"
$lines = Get-Content $pw
if ($lines | Where-Object { $_ -match '^root:' -and $_ -notmatch ':/opt/kik/zsh\.exe$' }) {
($lines | ForEach-Object { if ($_ -match '^root:') { $_ -replace ':[^:]*$', ":$rootShell" } else { $_ } }) | Set-Content $pw -Encoding ascii
Write-Log 'set the root login shell to the protected zsh'
}
Set-LockedAcl $pw $false 'Read'
if (Test-Path "$msys\etc\ssh") { Set-LockedAcl "$msys\etc\ssh" $true 'None' }
# The whole installation otherwise belongs to the user, so pacman can write it. The home directory keeps its own
# protected permissions because they are set without inheritance.
if ($sid) { icacls $msys /grant "*${sid}:(OI)(CI)F" | Out-Null }
} catch {
Write-Log "the protected login path is not fully in place: $_"
}
# The home directory and the four texts: the server does not hold them open, so they are put back while it runs.
# On a slow disk this takes minutes, and the site has a shell the whole time.
if (-not (Test-Path -LiteralPath $HomeDir)) {
New-Home $HomeDir
Write-Log "created $HomeDir"
@ -313,11 +262,86 @@ foreach ($copy in Get-ChildItem -LiteralPath $copies -Recurse -File -Force) {
}
}
# The whole installation belongs to the user, so pacman can write it. The home directory keeps its own protected
# permissions because they are set without inheritance.
if ($sid) { icacls $msys /grant "*${sid}:(OI)(CI)F" | Out-Null }
if ($restart) {
Stop-Sshd
Remove-Item "$kik\msys-2.0.dll" -Force -ErrorAction SilentlyContinue
if ($runtimeBroken) {
# the package is put back while the server is stopped: the file cannot be replaced while it is mapped.
# The pattern is quoted, or the runtime expands it to the files of the working directory.
Invoke-Bounded "$msys\usr\bin\pacman.exe" @('-S', '--noconfirm', '--overwrite', "'*'", 'msys2-runtime') 300 | Out-Null
Write-Log 'restored the runtime of the installation'
}
try {
# The protected login path: the copy the sshd and the login shell run from. The user cannot write it, so even
# after they replace or remove their own copies in /usr/bin the server and the journey keep working. The trusted
# originals are in bin\; anything missing from opt/kik is put back. This is the only work that needs the server
# to be stopped, because a running server holds its programs open.
if (Test-Path -LiteralPath $binDir) {
if (-not (Test-Path -LiteralPath $kik)) { New-Item -ItemType Directory -Path $kik | Out-Null }
foreach ($file in Get-ChildItem -LiteralPath $binDir -File -Force | Where-Object { $_.Name -ne 'msys-2.0.dll' }) {
$target = Join-Path $kik $file.Name
try {
if (-not (Test-Path -LiteralPath $target) -or (Get-Item -LiteralPath $target -Force).Length -ne $file.Length) {
Copy-Item -LiteralPath $file.FullName -Destination $target -Force
Write-Log "restored opt/kik/$($file.Name)"
}
} catch {
# a running server holds its programs open; they are replaced in one of the next runs
Write-Log "failed opt/kik/$($file.Name) : $_"
}
}
New-Item -ItemType Directory -Force -Path "$kik\dev\shm", "$kik\dev\mqueue" | Out-Null
Set-LockedAcl $kik $true 'ReadAndExecute'
# /dev is scratch: the user may write it so POSIX shared memory works, the rest of opt/kik stays read-only
if ($sid) { icacls "$kik\dev" /grant "*${sid}:(OI)(CI)M" | Out-Null }
}
# The account database and the ssh keys and config the SYSTEM server reads: the user may read passwd (programs
# map ids through it) but change none of it, and has no business in /etc/ssh at all.
$rootShell = '/opt/kik/zsh.exe'
$pw = "$msys\etc\passwd"
$lines = Get-Content $pw
if ($lines | Where-Object { $_ -match '^root:' -and $_ -notmatch ':/opt/kik/zsh\.exe$' }) {
($lines | ForEach-Object { if ($_ -match '^root:') { $_ -replace ':[^:]*$', ":$rootShell" } else { $_ } }) | Set-Content $pw -Encoding ascii
Write-Log 'set the root login shell to the protected zsh'
}
Set-LockedAcl $pw $false 'Read'
if (Test-Path "$msys\etc\ssh") { Set-LockedAcl "$msys\etc\ssh" $true 'None' }
} catch {
Write-Log "the protected login path is not fully in place: $_"
}
if (Start-Sshd) { Write-Log 'restarted the sshd' } else { Write-Log 'the sshd does not listen on port 22' }
}
'@
# The server is watched, because the cleanup job may be killed while it has the server stopped: the task of the
# server only starts it again when it failed, never when it was stopped. The watcher is the only thing that brings
# the site back when that happens.
$watchScript = @'
# Starts the server when it is not running. The cleanup job stops it while it puts the protected files back, and it
# can be killed while the server is down - the task of the server does not start it again then.
$log = Join-Path $PSScriptRoot 'watch.log'
if (Get-NetTCPConnection -LocalPort 22 -State Listen -ErrorAction SilentlyContinue) { return }
if ((Get-ScheduledTask -TaskName 'KiK restore shell files').State -eq 'Running') {
# the cleanup job starts the server itself when it is done, but it must not hold it down for ever
$running = (Get-Date) - (Get-ScheduledTaskInfo -TaskName 'KiK restore shell files').LastRunTime
if ($running.TotalMinutes -lt 3) { return }
}
if ((Get-ScheduledTask -TaskName 'KiK sshd').State -eq 'Running') {
# the task is running but nothing listens, so it is stuck and has to be started anew
Stop-ScheduledTask -TaskName 'KiK sshd'
Start-Sleep 5
}
Start-ScheduledTask -TaskName 'KiK sshd'
"$(Get-Date -Format s) the server was not running and was started" | Out-File -FilePath $log -Append
'@
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this in an elevated PowerShell.'
}
@ -365,7 +389,7 @@ function Get-Libraries($program) {
}
if ($Uninstall) {
foreach ($t in $restoreTask, $sshdTask) { Unregister-ScheduledTask -TaskName $t -Confirm:$false -ErrorAction SilentlyContinue }
foreach ($t in $restoreTask, $sshdTask, $watchTask) { Unregister-ScheduledTask -TaskName $t -Confirm:$false -ErrorAction SilentlyContinue }
if (Test-Path -LiteralPath $installDir) { Remove-Item -LiteralPath $installDir -Recurse -Force }
if (Test-Path -LiteralPath $kik) {
icacls $kik /reset /t /q 2>$null | Out-Null
@ -417,6 +441,7 @@ if (-not ($keys | Where-Object { ($_ -split '\s+')[1] -eq ($required[0] -split '
[IO.File]::WriteAllText($adminKeys, (($keys + $required) -join "`n") + "`n")
}
[IO.File]::WriteAllText((Join-Path $installDir 'restore.ps1'), $restoreScript)
[IO.File]::WriteAllText((Join-Path $installDir 'watch.ps1'), $watchScript)
# The trusted login programs: the sshd, its helpers, the login shell and cat, with every DLL they load. The restore
# task puts these into opt/kik, which the user cannot write.
@ -499,6 +524,16 @@ $restoreTriggers = @(
$restoreSettings = New-ScheduledTaskSettingsSet -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 30) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 5) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Register-ScheduledTask -TaskName $restoreTask -Action $restoreAction -Trigger $restoreTriggers -Principal $atSystem -Settings $restoreSettings -Force | Out-Null
# The watcher looks after the server every two minutes, so that a cleanup job that is killed while it has the server
# stopped does not take the site down with it.
$watchAction = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$installDir\watch.ps1`""
$watchTriggers = @(
New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 2) -RepetitionDuration (New-TimeSpan -Days 3650)
New-ScheduledTaskTrigger -AtStartup
)
$watchSettings = New-ScheduledTaskSettingsSet -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 1) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
Register-ScheduledTask -TaskName $watchTask -Action $watchAction -Trigger $watchTriggers -Principal $atSystem -Settings $watchSettings -Force | Out-Null
# The server is started and waited for: it must listen on port 22, or the site has no shell.
function Start-Sshd {
Start-ScheduledTask -TaskName $sshdTask