Files
cmbot/scripts/update.ps1
T

196 lines
6.5 KiB
PowerShell
Raw Normal View History

<#
.SYNOPSIS
LAN auto-update launcher for CMBot (docs/10-lan-update.md, stage 3).
.DESCRIPTION
Run instead of launching CMBot.exe directly. On each start it:
1. reads the local version from current.txt;
2. reads update_source from data\config\app_config.json and the remote
manifest.json there;
3. if a newer version is advertised, copies it into staging, verifies it,
then atomically swaps it into versions\<ver> and flips current.txt;
4. launches versions\<current>\CMBot.exe with CMBOT_DATA_DIR pointed at the
shared data\ folder.
Degrades safely: an unreachable source, a malformed manifest, or a failed
copy never blocks startup — the existing local version is launched instead.
Never overwrites the running version (new version installs alongside).
.PARAMETER InstallRoot
Install directory holding current.txt, versions\, data\, staging\.
Defaults to the launcher's own folder.
.PARAMETER NoLaunch
Do everything except start the app. For testing the update flow.
#>
[CmdletBinding()]
param(
[string]$InstallRoot = $PSScriptRoot,
[switch]$NoLaunch
)
$ErrorActionPreference = "Stop"
$AppExe = "CMBot.exe"
$ManifestName = "manifest.json"
$CurrentTxt = Join-Path $InstallRoot "current.txt"
$VersionsDir = Join-Path $InstallRoot "versions"
$DataDir = Join-Path $InstallRoot "data"
$StagingDir = Join-Path $InstallRoot "staging"
$ConfigFile = Join-Path $DataDir "config\app_config.json"
$LogFile = Join-Path $InstallRoot "launcher.log"
function Write-Log {
param([string]$Message)
$line = "{0} {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message
Write-Host $line
try { Add-Content -LiteralPath $LogFile -Value $line -Encoding UTF8 } catch { }
}
function Get-LocalVersion {
if (Test-Path -LiteralPath $CurrentTxt) {
return (Get-Content -LiteralPath $CurrentTxt -Raw).TrimStart([char]0xFEFF).Trim()
}
return ""
}
function Get-UpdateSource {
if (-not (Test-Path -LiteralPath $ConfigFile)) { return "" }
try {
$cfg = Get-Content -LiteralPath $ConfigFile -Raw | ConvertFrom-Json
return [string]$cfg.update_source
} catch {
Write-Log "Cannot read update_source: $_"
return ""
}
}
function ConvertTo-VersionParts {
param([string]$Text)
$parts = @(0, 0, 0)
if ($Text) {
$chunks = $Text.Trim().Split(".")
for ($i = 0; $i -lt 3 -and $i -lt $chunks.Count; $i++) {
$digits = ($chunks[$i] -replace '^(\d*).*$', '$1')
if ($digits) { $parts[$i] = [int]$digits }
}
}
return ,$parts
}
function Test-IsNewer {
param([string]$Remote, [string]$Local)
$r = ConvertTo-VersionParts $Remote
$l = ConvertTo-VersionParts $Local
for ($i = 0; $i -lt 3; $i++) {
if ($r[$i] -gt $l[$i]) { return $true }
if ($r[$i] -lt $l[$i]) { return $false }
}
return $false
}
function Invoke-Update {
param([string]$Source, [string]$LocalVersion)
$manifestPath = Join-Path $Source $ManifestName
if (-not (Test-Path -LiteralPath $manifestPath)) {
Write-Log "No manifest at $manifestPath - skip update."
return
}
try {
$m = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
} catch {
Write-Log "Manifest unreadable - skip update: $_"
return
}
$remoteVer = [string]$m.version
if (-not $remoteVer) {
Write-Log "Manifest has no version - skip update."
return
}
if (-not (Test-IsNewer $remoteVer $LocalVersion)) {
Write-Log "Up to date (local '$LocalVersion', remote '$remoteVer')."
return
}
$srcFiles = [string]$m.source
if (-not $srcFiles) { $srcFiles = $Source }
if (-not (Test-Path -LiteralPath $srcFiles)) {
Write-Log "Version folder not found: $srcFiles - skip update."
return
}
$marker = if ($m.marker) { [string]$m.marker } else { $AppExe }
# 1. copy into a temp staging folder (never into versions\ directly)
$stageTmp = Join-Path $StagingDir ("{0}.tmp" -f $remoteVer)
if (Test-Path -LiteralPath $stageTmp) { Remove-Item -LiteralPath $stageTmp -Recurse -Force }
New-Item -ItemType Directory -Force -Path $stageTmp | Out-Null
Write-Log "Downloading v$remoteVer from $srcFiles ..."
& robocopy $srcFiles $stageTmp /E /NFL /NDL /NJH /NJS /NP /R:1 /W:1 | Out-Null
if ($LASTEXITCODE -ge 8) {
Remove-Item -LiteralPath $stageTmp -Recurse -Force -ErrorAction SilentlyContinue
throw "robocopy failed (exit $LASTEXITCODE)"
}
# 2. verify completeness via the marker file
if (-not (Test-Path -LiteralPath (Join-Path $stageTmp $marker))) {
Remove-Item -LiteralPath $stageTmp -Recurse -Force -ErrorAction SilentlyContinue
throw "marker '$marker' missing after copy - corrupt download"
}
# 3. atomic-ish swap: rename staging -> versions\<ver>, then flip pointer
$verDir = Join-Path $VersionsDir $remoteVer
if (Test-Path -LiteralPath $verDir) { Remove-Item -LiteralPath $verDir -Recurse -Force }
Move-Item -LiteralPath $stageTmp -Destination $verDir
$ptrTmp = "$CurrentTxt.tmp"
# ASCII keeps the pointer file BOM-free (version strings are ASCII).
Set-Content -LiteralPath $ptrTmp -Value $remoteVer -Encoding ascii -NoNewline
Move-Item -LiteralPath $ptrTmp -Destination $CurrentTxt -Force
Write-Log "Installed and switched to v$remoteVer."
}
# ── main ──────────────────────────────────────────────────────────────────────
New-Item -ItemType Directory -Force -Path $VersionsDir | Out-Null
New-Item -ItemType Directory -Force -Path $StagingDir | Out-Null
$local = Get-LocalVersion
$source = Get-UpdateSource
if ($source) {
try {
Invoke-Update -Source $source -LocalVersion $local
} catch {
Write-Log "Update skipped (degraded to local version): $_"
}
} else {
Write-Log "No update_source configured - skip update check."
}
$current = Get-LocalVersion
if (-not $current) {
Write-Log "ERROR: no installed version to launch (current.txt missing)."
exit 1
}
$exe = Join-Path (Join-Path $VersionsDir $current) $AppExe
if (-not (Test-Path -LiteralPath $exe)) {
Write-Log "ERROR: executable not found: $exe"
exit 1
}
if ($NoLaunch) {
Write-Log "NoLaunch: would start $exe (CMBOT_DATA_DIR=$DataDir)"
exit 0
}
Write-Log "Launching v$current ..."
$env:CMBOT_DATA_DIR = $DataDir
Start-Process -FilePath $exe