The task now runs scheduling\run-sync.ps1 instead of an inline cmd.exe loop: every provider is attempted, a run that dies without the app's own report pushes a high-priority FAILED with the exception class only, and each run's output is appended to cache\logs\scheduled-sync.log because Task Scheduler keeps none. Verified under PowerShell 7.6 on Linux with a stand-in cmd.exe; not yet run under Windows PowerShell 5.1 or Task Scheduler. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019rwKTmug58sEsJ72AuWsEi
151 lines
6.3 KiB
PowerShell
151 lines
6.3 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Run `ai-chat-exporter sync` once per provider - the action of the scheduled
|
|
task that Register-AiChatSyncTask.ps1 creates.
|
|
|
|
.DESCRIPTION
|
|
The Windows counterpart to run-sync.sh, and there for the same reason.
|
|
|
|
Every provider is attempted even after one fails, and the exit code is
|
|
non-zero if any failed - Task Scheduler reports only the last action's
|
|
result, so one action per provider would hide an earlier failure.
|
|
|
|
The app pushes its own ntfy result, but only from the end of a run it
|
|
finished. A crash, a non-zero exit before the sync starts (the terms-of-
|
|
service gate, a cache error) or a launcher that can't build its venv sends
|
|
nothing, and because each provider pushes separately, the providers that did
|
|
succeed still send "OK" - so a broken one looks like a quiet day. This
|
|
script pushes a FAILED notification for any run that exited non-zero without
|
|
the app having reported it. (Its "Sync completed with failures" banner
|
|
prints right after its push.)
|
|
|
|
The push carries the provider, the exit code and, for a crash, the
|
|
exception's class name - never its message. Same counts-only rule as
|
|
src/notify.py: on a public ntfy topic anyone who guesses the name can read
|
|
it, and exception text can carry conversation titles.
|
|
|
|
Task Scheduler keeps no output, so each run's output is appended to
|
|
cache\logs\scheduled-sync.log (rolled over to .1 past 1 MB). That is where
|
|
the traceback is.
|
|
|
|
.EXAMPLE
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\scheduling\run-sync.ps1 chatgpt claude
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(ValueFromRemainingArguments = $true)]
|
|
[string[]]$Provider
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$repo = Split-Path -Parent $PSScriptRoot
|
|
$launcher = Join-Path $repo 'ai-chat-exporter.cmd'
|
|
$envFile = Join-Path $repo '.env'
|
|
$logDir = Join-Path $repo 'cache\logs'
|
|
$runLog = Join-Path $logDir 'scheduled-sync.log'
|
|
|
|
# -File passes "chatgpt,claude" as one string; accept it as well as a list.
|
|
$Provider = @($Provider | ForEach-Object { $_ -split ',' } | Where-Object { $_ })
|
|
if ($Provider.Count -eq 0) { $Provider = @('all') }
|
|
foreach ($p in $Provider) {
|
|
# Each name is spliced into a cmd.exe command line below.
|
|
if ($p -notmatch '^[A-Za-z0-9_-]+$') { throw "Invalid provider name: '$p'" }
|
|
}
|
|
|
|
# Under Task Scheduler stdout is a file, not a console, so Python would encode
|
|
# it in the ANSI code page and die on the first character outside it.
|
|
$env:PYTHONIOENCODING = 'utf-8'
|
|
|
|
# NTFY_* as the app resolves them: the environment wins, then .env.
|
|
function Get-EnvValue([string]$Name) {
|
|
$value = [Environment]::GetEnvironmentVariable($Name)
|
|
if (-not $value -and (Test-Path -LiteralPath $envFile)) {
|
|
foreach ($line in Get-Content -LiteralPath $envFile) {
|
|
if ($line -match "^\s*$Name\s*=\s*(.*)$") { $value = $Matches[1] }
|
|
}
|
|
if ($value) { $value = ($value -replace '\s+#.*$', '').Trim().Trim('"', "'") }
|
|
}
|
|
return "$value"
|
|
}
|
|
|
|
function Send-FailurePush([string]$Body) {
|
|
$topic = Get-EnvValue 'NTFY_TOPIC'
|
|
$policy = (Get-EnvValue 'NTFY_NOTIFY').ToLower()
|
|
if (-not $topic -or $policy -eq 'off') { return }
|
|
$server = Get-EnvValue 'NTFY_SERVER'
|
|
if (-not $server) { $server = 'https://ntfy.sh' }
|
|
$token = Get-EnvValue 'NTFY_TOKEN'
|
|
|
|
# The same short name src/notify.py puts in its titles.
|
|
$machine = [System.Net.Dns]::GetHostName().Split('.')[0]
|
|
$headers = @{
|
|
Title = "AI archive FAILED - $machine"
|
|
Tags = 'rotating_light'
|
|
Priority = 'high'
|
|
}
|
|
if ($token) { $headers.Authorization = "Bearer $token" }
|
|
|
|
try {
|
|
# Windows PowerShell 5.1 on an older .NET may not offer TLS 1.2 by default.
|
|
[Net.ServicePointManager]::SecurityProtocol = `
|
|
[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
Invoke-WebRequest -UseBasicParsing -Method Post -TimeoutSec 15 `
|
|
-Uri "$($server.TrimEnd('/'))/$topic" -Headers $headers `
|
|
-ContentType 'text/plain; charset=utf-8' `
|
|
-Body ([Text.Encoding]::UTF8.GetBytes($Body)) | Out-Null
|
|
} catch {
|
|
Write-Warning "run-sync: could not send the failure notification ($($_.Exception.Message))"
|
|
}
|
|
}
|
|
|
|
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
|
|
if ((Test-Path -LiteralPath $runLog) -and (Get-Item -LiteralPath $runLog).Length -gt 1MB) {
|
|
Move-Item -LiteralPath $runLog -Destination "$runLog.1" -Force
|
|
}
|
|
|
|
$rc = 0
|
|
foreach ($p in $Provider) {
|
|
$out = [IO.Path]::GetTempFileName()
|
|
try {
|
|
# cmd.exe does the redirect, so stdout and stderr stay interleaved and
|
|
# PowerShell's native-command stderr handling never sees either. /s strips
|
|
# exactly the outer pair of quotes, leaving the quoted paths inside intact.
|
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
$psi.FileName = if ($env:ComSpec) { $env:ComSpec } else { 'cmd.exe' }
|
|
$psi.Arguments = "/d /s /c `"`"$launcher`" sync --provider $p --joplin-optional > `"$out`" 2>&1`""
|
|
$psi.WorkingDirectory = $repo
|
|
$psi.UseShellExecute = $false
|
|
$proc = [Diagnostics.Process]::Start($psi)
|
|
$proc.WaitForExit()
|
|
$status = $proc.ExitCode
|
|
|
|
$text = [IO.File]::ReadAllText($out, [Text.Encoding]::UTF8)
|
|
Write-Host $text
|
|
Add-Content -LiteralPath $runLog -Encoding UTF8 -Value (
|
|
"===== $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') sync --provider $p (exit $status) =====`r`n$text")
|
|
|
|
if ($status -ne 0) {
|
|
$rc = 1
|
|
if (-not $text.Contains('Sync completed with failures')) {
|
|
$crash = [regex]::Matches($text, '(?m)^[A-Za-z_][A-Za-z0-9_.]*(Error|Exception)\b') |
|
|
Select-Object -Last 1
|
|
if ($crash) {
|
|
$reason = "crashed ($($crash.Value))"
|
|
} else {
|
|
$reason = "exited $status before reporting a result"
|
|
}
|
|
Send-FailurePush "${p}: $reason`ncache\logs\scheduled-sync.log"
|
|
}
|
|
}
|
|
} catch {
|
|
# This script failing is a run that reports nothing, too.
|
|
$rc = 1
|
|
Write-Warning "run-sync: $p - $($_.Exception.Message)"
|
|
Send-FailurePush "${p}: run-sync.ps1 failed ($($_.Exception.GetType().Name))"
|
|
} finally {
|
|
Remove-Item -LiteralPath $out -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
exit $rc
|