# DevFlow 自宿主换装脚本 v3 # 流程: 等旧进程退出并探测句柄释放, 静默安装, 按构建产物哈希校验且失败重试一次, 交互用户可见重启, 结束自清理 $ErrorActionPreference = 'Stop' $installer = 'E:\wk-lab\devflow\target\release\bundle\nsis\devflow_0.1.0_x64-setup.exe' $target = 'C:\Users\23780\AppData\Local\devflow\devflow.exe' $builtExe = 'E:\wk-lab\devflow\target\release\devflow.exe' $log = 'E:\wk-lab\devflow\swap-install.log' $lockFile = 'E:\wk-lab\devflow\swap-install.lock' # 构建产物哈希是唯一强基准; 产物缺失时退化为与安装前快照比对的弱校验 $hashWant = $null if (Test-Path $builtExe) { $hashWant = (Get-FileHash $builtExe -Algorithm SHA256).Hash } # 0. SYSTEM 上下文转投用户会话 $who = whoami if ($who -like '*SYSTEM*') { $loggedUser = (Get-CimInstance Win32_ComputerSystem).UserName if ($loggedUser) { $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$PSCommandPath`"" $principal = New-ScheduledTaskPrincipal -UserId $loggedUser -LogonType Interactive Register-ScheduledTask -TaskName 'DevFlowSwapUser' -Action $action -Principal $principal -Force | Out-Null Start-ScheduledTask -TaskName 'DevFlowSwapUser' # 锁由重入脚本创建删除, 是其存活的可靠信号; 按 task State 等有 Ready 竞态且超时会腰斩换装 $deadline = (Get-Date).AddSeconds(30) while (-not (Test-Path $lockFile) -and (Get-Date) -lt $deadline) { Start-Sleep -Seconds 1 } $deadline = (Get-Date).AddSeconds(300) while ((Test-Path $lockFile) -and (Get-Date) -lt $deadline) { Start-Sleep -Seconds 2 } Unregister-ScheduledTask -TaskName 'DevFlowSwapUser' -Confirm:$false -ErrorAction SilentlyContinue } else { "[FAIL] SYSTEM context and no logged-on user found" | Out-File $log -Encoding utf8 } Unregister-ScheduledTask -TaskName 'DevFlowSwapInstall' -Confirm:$false -ErrorAction SilentlyContinue exit 0 } if (Test-Path $lockFile) { $lockAge = (Get-Date) - (Get-Item $lockFile).LastWriteTime if ($lockAge.TotalMinutes -lt 5) { exit 0 } } New-Item $lockFile -Force | Out-Null # 独占打开目标 exe 成功即旧句柄已释放; Windows 文件锁延迟释放会让 NSIS 静默伪成功 function Wait-HandleReleased { if (-not (Test-Path $target)) { return $true } $deadline = (Get-Date).AddSeconds(30) while ((Get-Date) -lt $deadline) { try { $fs = [IO.File]::Open($target, 'Open', 'ReadWrite', 'None') $fs.Close() "[$(Get-Date -Format 'HH:mm:ss')] handle released" | Add-Content $log return $true } catch { Start-Sleep -Milliseconds 500 } } "[$(Get-Date -Format 'HH:mm:ss')] handle wait timeout, continue" | Add-Content $log return $false } # 静默安装并校验新 exe, 返回换装是否成功 function Invoke-SwapInstall { param([string]$Attempt) $p = Start-Process $installer -ArgumentList '/S' -Wait -PassThru "[$(Get-Date -Format 'HH:mm:ss')] installer exit=$($p.ExitCode) (attempt $Attempt)" | Add-Content $log if ($p.ExitCode -ne 0) { return $false } if (-not (Test-Path $target)) { "[$(Get-Date -Format 'HH:mm:ss')] FAIL: target missing after install" | Add-Content $log return $false } $afterHash = (Get-FileHash $target -Algorithm SHA256).Hash $afterSize = (Get-Item $target).Length "[$(Get-Date -Format 'HH:mm:ss')] installed size=$afterSize hash=$($afterHash.Substring(0, 12))" | Add-Content $log if ($hashWant) { if ($afterHash -ne $hashWant) { return $false } } else { "[$(Get-Date -Format 'HH:mm:ss')] weak check: built exe absent, compare with pre-install hash" | Add-Content $log if ($afterHash -eq $before.hash) { return $false } } return $true } try { "[$(Get-Date -Format 'HH:mm:ss')] swap start (user=$(whoami))" | Out-File $log -Encoding utf8 if ($hashWant) { "[$(Get-Date -Format 'HH:mm:ss')] verify baseline: built exe hash=$($hashWant.Substring(0, 12))" | Add-Content $log } else { "[$(Get-Date -Format 'HH:mm:ss')] verify baseline: weak check (built exe absent)" | Add-Content $log } # 1. 等旧进程退出(最多 30s), 超时强杀 $deadline = (Get-Date).AddSeconds(30) while ((Get-Date) -lt $deadline) { if (-not (Get-Process devflow -ErrorAction SilentlyContinue)) { break } Start-Sleep -Milliseconds 500 } Get-Process devflow -ErrorAction SilentlyContinue | Stop-Process -Force Start-Sleep -Seconds 2 "[$(Get-Date -Format 'HH:mm:ss')] old process stopped" | Add-Content $log # 2. 探测句柄释放 Wait-HandleReleased | Out-Null # 3. 安装前快照, 供弱校验与前后比对 $before = @{ hash = 'absent'; size = 0 } if (Test-Path $target) { $before.hash = (Get-FileHash $target -Algorithm SHA256).Hash $before.size = (Get-Item $target).Length } $beforeHashShort = $before.hash if ($beforeHashShort.Length -gt 12) { $beforeHashShort = $beforeHashShort.Substring(0, 12) } "[$(Get-Date -Format 'HH:mm:ss')] before size=$($before.size) hash=$beforeHashShort" | Add-Content $log # 4. 静默安装 + 校验, 校验失败重试一次 if (Invoke-SwapInstall 'first') { "[$(Get-Date -Format 'HH:mm:ss')] verify pass (first attempt)" | Add-Content $log } else { "[$(Get-Date -Format 'HH:mm:ss')] verify fail, retry once" | Add-Content $log Wait-HandleReleased | Out-Null if (Invoke-SwapInstall 'retry') { "[$(Get-Date -Format 'HH:mm:ss')] verify pass (retry)" | Add-Content $log } else { "[$(Get-Date -Format 'HH:mm:ss')] FAIL: verify failed twice" | Add-Content $log exit 1 } } # 5. 重启并验证; GUI 必须起在交互用户会话才可见 Start-Process $target Start-Sleep -Seconds 5 $proc = Get-Process devflow -ErrorAction SilentlyContinue | Select-Object -First 1 if ($proc) { "[$(Get-Date -Format 'HH:mm:ss')] OK: restarted pid=$($proc.Id)" | Add-Content $log exit 0 } # SYSTEM 会话直启的 GUI 不可见, 转投交互登录用户名下的一次性计划任务 $explorer = Get-CimInstance Win32_Process -Filter "Name='explorer.exe'" | Select-Object -First 1 if ($explorer) { $o = Invoke-CimMethod -InputObject $explorer -MethodName GetOwner $user = "$($o.Domain)\$($o.User)" try { $action = New-ScheduledTaskAction -Execute $target $principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive Register-ScheduledTask -TaskName 'DevFlowStartOnce' -Action $action -Principal $principal -Force -ErrorAction Stop | Out-Null Start-ScheduledTask -TaskName 'DevFlowStartOnce' Start-Sleep -Seconds 5 } catch { "[$(Get-Date -Format 'HH:mm:ss')] start task error: $($_.Exception.Message)" | Add-Content $log } finally { Unregister-ScheduledTask -TaskName 'DevFlowStartOnce' -Confirm:$false -ErrorAction SilentlyContinue } $proc = Get-Process devflow -ErrorAction SilentlyContinue | Select-Object -First 1 if ($proc) { "[$(Get-Date -Format 'HH:mm:ss')] OK: restarted as $user pid=$($proc.Id)" | Add-Content $log exit 0 } } # 仍无进程, 落标记等人工拉起 Set-Content -Path 'E:\wk-lab\devflow\swap-need-start.flag' -Value (Get-Date -Format o) "[$(Get-Date -Format 'HH:mm:ss')] FAIL: need manual start" | Add-Content $log exit 1 } finally { Remove-Item $lockFile -ErrorAction SilentlyContinue Unregister-ScheduledTask -TaskName 'DevFlowSwapInstall' -Confirm:$false -ErrorAction SilentlyContinue }