diff --git a/infra/postgres/docker-compose.yml b/infra/postgres/docker-compose.yml index 1546188..44a9f85 100644 --- a/infra/postgres/docker-compose.yml +++ b/infra/postgres/docker-compose.yml @@ -14,9 +14,9 @@ services: test: - CMD-SHELL - pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" - interval: 5s + interval: 30s timeout: 5s - retries: 12 + retries: 5 start_period: 5s volumes: diff --git a/scripts/check-voidbot-operations.ps1 b/scripts/check-voidbot-operations.ps1 index d4f8bc6..5783639 100644 --- a/scripts/check-voidbot-operations.ps1 +++ b/scripts/check-voidbot-operations.ps1 @@ -180,10 +180,57 @@ function Get-Json { return $response | ConvertFrom-Json } +function Invoke-CommandWithTimeout { + param( + [Parameter(Mandatory = $true)] + [string] $FilePath, + [Parameter(Mandatory = $false)] + [string[]] $ArgumentList = @(), + [Parameter(Mandatory = $false)] + [int] $TimeoutSeconds = 15 + ) + + $stdoutPath = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()) + $stderrPath = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()) + + try { + $process = Start-Process -FilePath $FilePath -ArgumentList $ArgumentList -NoNewWindow -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -PassThru + + if (-not $process.WaitForExit($TimeoutSeconds * 1000)) { + try { + $process.Kill() + } catch { + } + + throw "Command timed out after $TimeoutSeconds second(s): $FilePath $($ArgumentList -join ' ')" + } + + $stdout = if (Test-Path -LiteralPath $stdoutPath) { + Get-Content -LiteralPath $stdoutPath -Raw + } else { + "" + } + + $stderr = if (Test-Path -LiteralPath $stderrPath) { + Get-Content -LiteralPath $stderrPath -Raw + } else { + "" + } + + return [PSCustomObject]@{ + ExitCode = $process.ExitCode + StdOut = $stdout + StdErr = $stderr + } + } finally { + Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue + } +} + function Test-DockerAvailable { try { - & docker version *> $null - return $LASTEXITCODE -eq 0 + $result = Invoke-CommandWithTimeout -FilePath "docker.exe" -ArgumentList @("version") -TimeoutSeconds 15 + return $result.ExitCode -eq 0 } catch { return $false } @@ -196,13 +243,13 @@ function Get-DockerContainerHealth { ) try { - $status = & docker inspect --format "{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}" $ContainerName 2>$null + $result = Invoke-CommandWithTimeout -FilePath "docker.exe" -ArgumentList @("inspect", "--format", "{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}", $ContainerName) -TimeoutSeconds 20 - if ($LASTEXITCODE -ne 0) { + if ($result.ExitCode -ne 0) { return $null } - return $status.Trim() + return $result.StdOut.Trim() } catch { return $null } @@ -480,13 +527,18 @@ try { } try { - $tableList = & docker exec voidbot-postgres psql -U $dsn.username -d $dsn.database -tAc "select table_name from information_schema.tables where table_schema = 'public';" + $tableResult = Invoke-CommandWithTimeout -FilePath "docker.exe" -ArgumentList @("exec", "voidbot-postgres", "psql", "-U", $dsn.username, "-d", $dsn.database, "-tAc", "select table_name from information_schema.tables where table_schema = 'public';") -TimeoutSeconds 20 - if ($LASTEXITCODE -ne 0) { - throw "psql table listing failed." + if ($tableResult.ExitCode -ne 0) { + $stderr = $tableResult.StdErr.Trim() + if ([string]::IsNullOrWhiteSpace($stderr)) { + throw "psql table listing failed." + } + + throw "psql table listing failed: $stderr" } - $tables = @($tableList -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $tables = @($tableResult.StdOut -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }) foreach ($requiredTable in @("jobs", "audit_events", "interaction_memory_events")) { if ($tables -contains $requiredTable) { Add-Check -Name "postgres.table.$requiredTable" -Status "passed" -Detail "Postgres table $requiredTable exists." diff --git a/scripts/install-offsite-backup-task.ps1 b/scripts/install-offsite-backup-task.ps1 index e96d7fc..1bad989 100644 --- a/scripts/install-offsite-backup-task.ps1 +++ b/scripts/install-offsite-backup-task.ps1 @@ -67,9 +67,9 @@ if (-not ($config.ContainsKey("OFFSITE_BACKUP_SSH_TARGET")) -or [string]::IsNull } $runAt = [DateTime]::ParseExact($taskTimeValue, "HH:mm", [System.Globalization.CultureInfo]::InvariantCulture) -$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$syncScript`"" +$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$syncScript`"" $trigger = New-ScheduledTaskTrigger -Daily -At $runAt -$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries +$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Hours 6) $principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Limited $description = "Runs VoidBot offsite backup sync to the Qwen box. Interactive logon required so the SSH key material is available." @@ -79,6 +79,8 @@ Write-Host "Installed scheduled task: $taskNameValue" Write-Host "Schedule: daily at $taskTimeValue" Write-Host "Task runs as: $($env:USERDOMAIN)\$($env:USERNAME)" Write-Host "Logon mode: Interactive" +Write-Host "Window mode: Hidden" +Write-Host "Execution time limit: 6 hours" if ($RunNow) { Start-ScheduledTask -TaskName $taskNameValue diff --git a/scripts/install-operations-watchdog-task.ps1 b/scripts/install-operations-watchdog-task.ps1 index 0c45d6b..817023b 100644 --- a/scripts/install-operations-watchdog-task.ps1 +++ b/scripts/install-operations-watchdog-task.ps1 @@ -76,9 +76,9 @@ if (-not ($config.ContainsKey("DISCORD_OWNER_ID")) -or [string]::IsNullOrWhiteSp } $startAt = (Get-Date).AddMinutes(5) -$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$checkScript`" -NotifyOwner -FailOnIssues" +$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$checkScript`" -NotifyOwner -FailOnIssues" $trigger = New-ScheduledTaskTrigger -Once -At $startAt -RepetitionInterval (New-TimeSpan -Minutes $intervalMinutesValue) -RepetitionDuration (New-TimeSpan -Days 3650) -$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries +$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Minutes 15) $principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Limited $description = "Runs the VoidBot operations watchdog on a repeating interval and DMs the owner when the stack or backup path drifts into the swamp." @@ -88,6 +88,8 @@ Write-Host "Installed scheduled task: $taskNameValue" Write-Host "Schedule: every $intervalMinutesValue minute(s), starting at $($startAt.ToString('yyyy-MM-dd HH:mm'))" Write-Host "Task runs as: $($env:USERDOMAIN)\$($env:USERNAME)" Write-Host "Logon mode: Interactive" +Write-Host "Window mode: Hidden" +Write-Host "Execution time limit: 15 minutes" if ($RunNow) { Start-ScheduledTask -TaskName $taskNameValue