269 lines
11 KiB
PowerShell
269 lines
11 KiB
PowerShell
param(
|
|
[string]$ArchivePath = "runs/offline-evidence-intake/zyl-qqfile-20260709/archives/zyl.rar",
|
|
[string]$ExtractDir = "runs/offline-real-client-window/full",
|
|
[ValidateSet("Impp", "iSphere")]
|
|
[string]$ClientRoot = "Impp",
|
|
[string]$HelperExe = "runs/win-helper/ISphereWinHelper.exe",
|
|
[int]$WaitSeconds = 10,
|
|
[int]$X = 120,
|
|
[int]$Y = 120,
|
|
[int]$Width = 270,
|
|
[int]$Height = 570,
|
|
[switch]$SkipExtract,
|
|
[switch]$NoLaunch,
|
|
[switch]$KeepExisting,
|
|
[switch]$TopMost,
|
|
[switch]$NoScreenshot
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$repo = Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")
|
|
$InfoPromptTitle = -join ([char[]](0x4FE1, 0x606F, 0x63D0, 0x793A))
|
|
|
|
function Resolve-RepoPath([string]$PathValue) {
|
|
if ([System.IO.Path]::IsPathRooted($PathValue)) {
|
|
return $PathValue
|
|
}
|
|
return (Join-Path $repo $PathValue)
|
|
}
|
|
|
|
function Find-7Zip {
|
|
$candidates = @(
|
|
"C:\Program Files\7-Zip\7z.exe",
|
|
"C:\Program Files (x86)\7-Zip\7z.exe"
|
|
)
|
|
foreach ($candidate in $candidates) {
|
|
if (Test-Path -LiteralPath $candidate) {
|
|
return $candidate
|
|
}
|
|
}
|
|
$cmd = Get-Command "7z" -ErrorAction SilentlyContinue
|
|
if ($cmd) {
|
|
return $cmd.Source
|
|
}
|
|
$cmd = Get-Command "7zr" -ErrorAction SilentlyContinue
|
|
if ($cmd) {
|
|
return $cmd.Source
|
|
}
|
|
throw "7-Zip not found. Install 7-Zip or pass -SkipExtract after extracting the archive."
|
|
}
|
|
|
|
function Invoke-HelperJson([string]$Op, [hashtable]$OpArgs, [string]$RequestId) {
|
|
$request = @{
|
|
protocol = "isphere.helper.v1"
|
|
request_id = $RequestId
|
|
op = $Op
|
|
timeout_ms = 5000
|
|
args = $OpArgs
|
|
}
|
|
$json = $request | ConvertTo-Json -Depth 16 -Compress
|
|
$output = $json | & $script:HelperPath --json
|
|
try {
|
|
return $output | ConvertFrom-Json
|
|
}
|
|
catch {
|
|
throw "helper output was not JSON: $output"
|
|
}
|
|
}
|
|
|
|
function Stop-ExistingOfflineCopies([string]$RootPath) {
|
|
$rootFull = [System.IO.Path]::GetFullPath($RootPath).TrimEnd('\') + "\"
|
|
$targets = @("IMPlatformClient.exe", "IMPP.ISphere.exe", "IMPlatformClient.Web.exe")
|
|
$processes = Get-CimInstance Win32_Process |
|
|
Where-Object { $targets -contains $_.Name -and $_.ExecutablePath -and ([System.IO.Path]::GetFullPath($_.ExecutablePath).StartsWith($rootFull, [System.StringComparison]::OrdinalIgnoreCase)) }
|
|
foreach ($proc in $processes) {
|
|
Stop-Process -Id ([int]$proc.ProcessId) -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
|
|
function Add-NativeWindowApi {
|
|
Add-Type @"
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
public static class ISphereOfflineWindowApi {
|
|
[DllImport("user32.dll")] public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
|
|
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
[DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
|
}
|
|
"@ -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
function Move-ClientWindows([object[]]$Windows, [int]$MainX, [int]$MainY, [int]$MainWidth, [int]$MainHeight, [bool]$MakeTopMost) {
|
|
$topMostHandle = [IntPtr]::new(-1)
|
|
$mainWindow = $null
|
|
$promptWindow = $null
|
|
foreach ($window in $Windows) {
|
|
if ([string]$window.process_name -ne "IMPlatformClient") {
|
|
continue
|
|
}
|
|
$hwndValue = [Convert]::ToInt64(($window.hwnd -replace "^0x", ""), 16)
|
|
$hwnd = [IntPtr]::new($hwndValue)
|
|
[ISphereOfflineWindowApi]::ShowWindow($hwnd, 9) | Out-Null
|
|
if ([string]$window.title -eq $InfoPromptTitle) {
|
|
[ISphereOfflineWindowApi]::MoveWindow($hwnd, ($MainX + $MainWidth + 40), ($MainY + 40), 300, 190, $true) | Out-Null
|
|
[ISphereOfflineWindowApi]::SetForegroundWindow($hwnd) | Out-Null
|
|
$promptWindow = $window
|
|
}
|
|
else {
|
|
if ($MakeTopMost) {
|
|
[ISphereOfflineWindowApi]::SetWindowPos($hwnd, $topMostHandle, $MainX, $MainY, $MainWidth, $MainHeight, 0x0040) | Out-Null
|
|
}
|
|
else {
|
|
[ISphereOfflineWindowApi]::MoveWindow($hwnd, $MainX, $MainY, $MainWidth, $MainHeight, $true) | Out-Null
|
|
}
|
|
[ISphereOfflineWindowApi]::SetForegroundWindow($hwnd) | Out-Null
|
|
$mainWindow = $window
|
|
}
|
|
}
|
|
return @{
|
|
main = $mainWindow
|
|
prompt = $promptWindow
|
|
}
|
|
}
|
|
|
|
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
|
|
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
try {
|
|
$graphics.CopyFromScreen(($ScreenX - 30), ($ScreenY - 30), 0, 0, $bitmap.Size)
|
|
$bitmap.Save($OutputPath, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
}
|
|
finally {
|
|
$graphics.Dispose()
|
|
$bitmap.Dispose()
|
|
}
|
|
}
|
|
|
|
$archiveFull = Resolve-RepoPath $ArchivePath
|
|
$extractFull = Resolve-RepoPath $ExtractDir
|
|
$script:HelperPath = Resolve-RepoPath $HelperExe
|
|
$runOut = Join-Path $repo "runs\offline-real-client-window"
|
|
New-Item -ItemType Directory -Force -Path $runOut | Out-Null
|
|
|
|
if (-not (Test-Path -LiteralPath $script:HelperPath)) {
|
|
throw "WinHelper not found: $script:HelperPath. Run scripts\build-win-helper.ps1 first."
|
|
}
|
|
|
|
$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"
|
|
|
|
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)) {
|
|
throw "archive not found: $archiveFull"
|
|
}
|
|
New-Item -ItemType Directory -Force -Path $extractFull | Out-Null
|
|
$sevenZip = Find-7Zip
|
|
& $sevenZip x $archiveFull "-o$extractFull" "zyl\Impp\*" "zyl\iSphere\*" -y | Out-Host
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "7-Zip extraction failed with exit code $LASTEXITCODE"
|
|
}
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $clientExe)) {
|
|
throw "client exe not found after extraction: $clientExe"
|
|
}
|
|
|
|
Add-NativeWindowApi
|
|
|
|
if (-not $KeepExisting) {
|
|
Stop-ExistingOfflineCopies -RootPath $extractFull
|
|
Start-Sleep -Milliseconds 500
|
|
}
|
|
|
|
$process = $null
|
|
if (-not $NoLaunch) {
|
|
$process = Start-Process -FilePath $clientExe -WorkingDirectory $clientDir -PassThru
|
|
}
|
|
|
|
$deadline = (Get-Date).AddSeconds([Math]::Max(1, $WaitSeconds))
|
|
$scan = $null
|
|
$windows = @()
|
|
do {
|
|
Start-Sleep -Milliseconds 500
|
|
$scan = Invoke-HelperJson -Op "scan_windows" -OpArgs @{ include_all_visible = $false } -RequestId "offline-real-client-scan"
|
|
if ($scan.ok -and $scan.data.windows) {
|
|
$windows = @($scan.data.windows | Where-Object {
|
|
$_.process_name -eq "IMPlatformClient" -and
|
|
((-not $process) -or $_.pid -eq $process.Id)
|
|
})
|
|
}
|
|
} while ($windows.Count -eq 0 -and (Get-Date) -lt $deadline)
|
|
|
|
if ($windows.Count -eq 0) {
|
|
throw "no visible IMPlatformClient window found. Last scan: $($scan | ConvertTo-Json -Depth 12 -Compress)"
|
|
}
|
|
|
|
$moveResult = Move-ClientWindows -Windows $windows -MainX $X -MainY $Y -MainWidth $Width -MainHeight $Height -MakeTopMost ([bool]$TopMost)
|
|
Start-Sleep -Seconds 1
|
|
|
|
$scanAfterMove = Invoke-HelperJson -Op "scan_windows" -OpArgs @{ include_all_visible = $false } -RequestId "offline-real-client-scan-after-move"
|
|
$windowsAfterMove = @($scanAfterMove.data.windows | Where-Object {
|
|
$_.process_name -eq "IMPlatformClient" -and
|
|
((-not $process) -or $_.pid -eq $process.Id)
|
|
})
|
|
|
|
# The offline client can raise a late "信息提示" dialog after the login window is
|
|
# already visible. Move it to the right side so the login window remains usable
|
|
# for RPA probing, then refresh the final window list.
|
|
$latePromptWindows = @($windowsAfterMove | Where-Object { [string]$_.title -eq $InfoPromptTitle })
|
|
if ($latePromptWindows.Count -gt 0) {
|
|
foreach ($latePromptWindow in $latePromptWindows) {
|
|
$promptHwndValue = [Convert]::ToInt64(($latePromptWindow.hwnd -replace "^0x", ""), 16)
|
|
$promptHwnd = [IntPtr]::new($promptHwndValue)
|
|
[ISphereOfflineWindowApi]::ShowWindow($promptHwnd, 9) | Out-Null
|
|
[ISphereOfflineWindowApi]::MoveWindow($promptHwnd, ($X + $Width + 40), ($Y + 40), 300, 190, $true) | Out-Null
|
|
[ISphereOfflineWindowApi]::SetForegroundWindow($promptHwnd) | Out-Null
|
|
}
|
|
Start-Sleep -Milliseconds 500
|
|
$scanAfterMove = Invoke-HelperJson -Op "scan_windows" -OpArgs @{ include_all_visible = $false } -RequestId "offline-real-client-scan-after-prompt-move"
|
|
$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 }
|
|
$promptWindow = if ($promptWindowMatches.Count -gt 0) { $promptWindowMatches[0] } else { $null }
|
|
|
|
$dumpFile = $null
|
|
$dump = $null
|
|
if ($mainWindow) {
|
|
$dump = Invoke-HelperJson -Op "dump_uia" -OpArgs @{
|
|
hwnd = $mainWindow.hwnd
|
|
max_depth = 8
|
|
max_children = 200
|
|
include_text = $true
|
|
} -RequestId "offline-real-client-dump-uia"
|
|
$dumpFile = Join-Path $runOut ("uia-dump-open-offline-real-client-{0}.json" -f ((Get-Date).ToString("yyyyMMdd-HHmmss")))
|
|
$dump | ConvertTo-Json -Depth 40 | Set-Content -LiteralPath $dumpFile -Encoding UTF8
|
|
}
|
|
|
|
$screenshotFile = $null
|
|
if (-not $NoScreenshot) {
|
|
$screenshotFile = Join-Path $runOut ("open-offline-real-client-{0}.png" -f ((Get-Date).ToString("yyyyMMdd-HHmmss")))
|
|
$captureWidth = if ($promptWindow) { $Width + 420 } else { $Width + 80 }
|
|
Save-WindowScreenshot -OutputPath $screenshotFile -ScreenX $X -ScreenY $Y -CaptureWidth $captureWidth -CaptureHeight ($Height + 80)
|
|
}
|
|
|
|
[pscustomobject]@{
|
|
ok = $true
|
|
archive = $archiveFull
|
|
extracted_dir = $extractFull
|
|
client_root = $ClientRoot
|
|
client_exe = $clientExe
|
|
process_id = if ($process) { $process.Id } else { $null }
|
|
alive = if ($process) { $null -ne (Get-Process -Id $process.Id -ErrorAction SilentlyContinue) } else { $null }
|
|
main_window = $mainWindow
|
|
prompt_window = $promptWindow
|
|
uia_root = if ($dump -and $dump.ok) { $dump.data.root } else { $null }
|
|
uia_dump_file = $dumpFile
|
|
screenshot_file = $screenshotFile
|
|
windows = $windowsAfterMove
|
|
} | ConvertTo-Json -Depth 24
|