tools: add offline rpa window watchdog

This commit is contained in:
zhaoyilun
2026-07-11 20:04:34 +08:00
parent e8b4110cd2
commit c2a1ca560f
3 changed files with 209 additions and 6 deletions

View File

@@ -9,6 +9,8 @@ param(
[int]$Y = 120,
[int]$Width = 270,
[int]$Height = 570,
[int]$WatchSeconds = 0,
[int]$PollIntervalMs = 500,
[ValidateSet("Visible", "Offscreen", "Minimized")]
[string]$WindowMode = "Visible",
[switch]$SkipExtract,
@@ -107,6 +109,19 @@ public static class ISphereOfflineWindowApi {
"@ -ErrorAction SilentlyContinue
}
function Start-OfflineClient {
if ($script:UserSilentMode -or $script:WindowModeValue -ne "Visible" -or $script:NoActivateMode) {
return Start-Process -FilePath $script:ClientExePath -WorkingDirectory $script:ClientDirPath -WindowStyle Minimized -PassThru
}
return Start-Process -FilePath $script:ClientExePath -WorkingDirectory $script:ClientDirPath -PassThru
}
function Relaunch-OfflineClient {
Stop-ExistingOfflineCopies -RootPath $script:ExtractRootPath
Start-Sleep -Milliseconds 500
return Start-OfflineClient
}
function Move-ClientWindows([object[]]$Windows, [int]$MainX, [int]$MainY, [int]$MainWidth, [int]$MainHeight, [bool]$MakeTopMost, [bool]$Activate, [string]$Mode) {
$hwndTop = [IntPtr]::Zero
$topMostHandle = [IntPtr]::new(-1)
@@ -157,6 +172,32 @@ function Move-ClientWindows([object[]]$Windows, [int]$MainX, [int]$MainY, [int]$
}
}
function Get-ClientWindowsForProcess([object]$Process) {
$scanResult = Invoke-HelperJson -Op "scan_windows" -OpArgs @{ include_all_visible = $false } -RequestId "offline-real-client-watch-scan"
if (-not $scanResult.ok -or -not $scanResult.data.windows) {
return @()
}
return @($scanResult.data.windows | Where-Object {
$_.process_name -eq "IMPlatformClient" -and
((-not $Process) -or $_.pid -eq $Process.Id)
})
}
function Test-WindowNear([object]$Window, [int]$ExpectedX, [int]$ExpectedY, [int]$ExpectedWidth, [int]$ExpectedHeight) {
if (-not $Window -or -not $Window.bounds) {
return $false
}
$tolerance = 3
return ([Math]::Abs([int]$Window.bounds.x - $ExpectedX) -le $tolerance) -and
([Math]::Abs([int]$Window.bounds.y - $ExpectedY) -le $tolerance) -and
([Math]::Abs([int]$Window.bounds.width - $ExpectedWidth) -le $tolerance) -and
([Math]::Abs([int]$Window.bounds.height - $ExpectedHeight) -le $tolerance)
}
function Repair-ClientWindowPlacement([object[]]$Windows) {
Move-ClientWindows -Windows $Windows -MainX $script:EffectiveX -MainY $script:EffectiveY -MainWidth $script:DesiredWidth -MainHeight $script:DesiredHeight -MakeTopMost ([bool]$script:TopMostMode) -Activate (-not [bool]$script:NoActivateMode) -Mode $script:WindowModeValue | Out-Null
}
function Save-WindowScreenshot([string]$OutputPath, [int]$ScreenX, [int]$ScreenY, [int]$CaptureWidth, [int]$CaptureHeight) {
Add-Type -AssemblyName System.Drawing
$bitmap = New-Object System.Drawing.Bitmap $CaptureWidth, $CaptureHeight
@@ -174,6 +215,7 @@ function Save-WindowScreenshot([string]$OutputPath, [int]$ScreenX, [int]$ScreenY
$archiveFull = Resolve-RepoPath $ArchivePath
$extractFull = Resolve-RepoPath $ExtractDir
$script:HelperPath = Resolve-RepoPath $HelperExe
$script:ExtractRootPath = $extractFull
$runOut = Join-Path $repo "runs\offline-real-client-window"
New-Item -ItemType Directory -Force -Path $runOut | Out-Null
@@ -185,6 +227,16 @@ $clientDir = Join-Path $extractFull ("zyl\" + $ClientRoot)
$clientExe = Join-Path $clientDir "IMPlatformClient.exe"
$clientConfig = Join-Path $clientDir "IMPlatformClient.exe.config"
$requiredDependency = Join-Path $clientDir "Utilities.Lib.Base.dll"
$script:ClientDirPath = $clientDir
$script:ClientExePath = $clientExe
$script:UserSilentMode = [bool]$UserSilent
$script:NoActivateMode = [bool]$NoActivate
$script:WindowModeValue = $WindowMode
$script:EffectiveX = $effectiveX
$script:EffectiveY = $effectiveY
$script:DesiredWidth = $Width
$script:DesiredHeight = $Height
$script:TopMostMode = [bool]$TopMost
if (-not $SkipExtract -and (-not (Test-Path -LiteralPath $clientExe) -or -not (Test-Path -LiteralPath $clientConfig) -or -not (Test-Path -LiteralPath $requiredDependency))) {
if (-not (Test-Path -LiteralPath $archiveFull)) {
@@ -211,12 +263,7 @@ if (-not $KeepExisting) {
$process = $null
if (-not $NoLaunch) {
if ($UserSilent -or $WindowMode -ne "Visible" -or $NoActivate) {
$process = Start-Process -FilePath $clientExe -WorkingDirectory $clientDir -WindowStyle Minimized -PassThru
}
else {
$process = Start-Process -FilePath $clientExe -WorkingDirectory $clientDir -PassThru
}
$process = Start-OfflineClient
}
$deadline = (Get-Date).AddSeconds([Math]::Max(1, $WaitSeconds))
@@ -276,6 +323,65 @@ if ($latePromptWindows.Count -gt 0) {
})
}
$watchIterationCount = 0
$repairCount = 0
$relaunchCount = 0
$recoveryActions = @()
if ($WatchSeconds -gt 0) {
$safePollIntervalMs = [Math]::Max(100, $PollIntervalMs)
$watchDeadline = (Get-Date).AddSeconds($WatchSeconds)
while ((Get-Date) -lt $watchDeadline) {
Start-Sleep -Milliseconds $safePollIntervalMs
$watchIterationCount++
$watchedWindows = Get-ClientWindowsForProcess -Process $process
$watchedMain = @($watchedWindows | Where-Object { [string]$_.title -ne $InfoPromptTitle }) | Select-Object -First 1
$watchedPrompt = @($watchedWindows | Where-Object { [string]$_.title -eq $InfoPromptTitle }) | Select-Object -First 1
if (-not $watchedMain) {
if (-not $NoLaunch) {
$process = Relaunch-OfflineClient
$relaunchCount++
$recoveryActions += "relaunch_missing_main_window"
$relaunchDeadline = (Get-Date).AddSeconds([Math]::Max(1, $WaitSeconds))
do {
Start-Sleep -Milliseconds $safePollIntervalMs
$watchedWindows = Get-ClientWindowsForProcess -Process $process
$watchedMain = @($watchedWindows | Where-Object { [string]$_.title -ne $InfoPromptTitle }) | Select-Object -First 1
} while (-not $watchedMain -and (Get-Date) -lt $relaunchDeadline)
if ($watchedWindows.Count -gt 0) {
Repair-ClientWindowPlacement -Windows $watchedWindows
$repairCount++
$recoveryActions += "repair_after_relaunch"
}
}
continue
}
$expectedPromptX = if ($WindowMode -eq "Offscreen") { $effectiveX } else { $effectiveX + $Width + 40 }
$expectedPromptY = if ($WindowMode -eq "Offscreen") { $effectiveY + $Height + 40 } else { $effectiveY + 40 }
$needsRepair = -not (Test-WindowNear -Window $watchedMain -ExpectedX $effectiveX -ExpectedY $effectiveY -ExpectedWidth $Width -ExpectedHeight $Height)
if ($watchedPrompt) {
$needsRepair = $needsRepair -or -not (Test-WindowNear -Window $watchedPrompt -ExpectedX $expectedPromptX -ExpectedY $expectedPromptY -ExpectedWidth 300 -ExpectedHeight 190)
}
if ($needsRepair) {
Repair-ClientWindowPlacement -Windows $watchedWindows
$repairCount++
$recoveryActions += "repair_window_placement"
}
}
$scanAfterMove = Invoke-HelperJson -Op "scan_windows" -OpArgs @{ include_all_visible = $false } -RequestId "offline-real-client-final-scan-after-watch"
$windowsAfterMove = @($scanAfterMove.data.windows | Where-Object {
$_.process_name -eq "IMPlatformClient" -and
((-not $process) -or $_.pid -eq $process.Id)
})
}
$mainWindowMatches = @($windowsAfterMove | Where-Object { [string]$_.title -ne $InfoPromptTitle })
$promptWindowMatches = @($windowsAfterMove | Where-Object { [string]$_.title -eq $InfoPromptTitle })
$mainWindow = if ($mainWindowMatches.Count -gt 0) { $mainWindowMatches[0] } else { $null }
@@ -306,6 +412,12 @@ if (-not $NoScreenshot -and $WindowMode -eq "Visible") {
user_silent = [bool]$UserSilent
window_mode = $WindowMode
no_activate = [bool]$NoActivate
watch_seconds = $WatchSeconds
poll_interval_ms = $PollIntervalMs
watch_iteration_count = $watchIterationCount
relaunch_count = $relaunchCount
repair_count = $repairCount
recovery_actions = $recoveryActions
effective_bounds = @{
x = $effectiveX
y = $effectiveY