Files
comun/utile/Teste/watchdog_vfp.ps1
Marius Mutu 13b4f65723 sync SVN r18004/r18007: #6 editare factura emisa - pagina de articole
ofacturare_editare.prg (nou): helpere comune - garda eFactura, cursoarele
notei si ale rulajelor, randul de vanzare corespunzator notei si liniile de
articole citite din view-ul VVANZARI_ARTICOLE.

omodificari.vc2 (frm_modific2024): pagina noua de articole ale facturii,
deocamdata doar afisare. Apare numai cand ofacturare_editare.prg e
inregistrat si documentul are rand in VANZARI; in ROACONT si ROAGEST, unde
fisierul nu e incarcat, pagina lipseste si registrul jurnal ramane neatins.
Randul de vanzare al notei se cauta pe toate tripletele distincte (nract,
serie_act, dataact) din nota, pentru ca primul rand poate fi o incasare.

ofacturare_comun.vc2 (frm_facturi): do_editare_factura si butonul aferent,
dupa modelul lui do_sterge, cu garzile de luna inchisa, luna curenta,
document sters, referinte si eFactura.

docs: comentariile se scriu strict necesar, si in cod si in scripturile de
migrare - fara referinte la planuri, stories, decizii sau erori, istoricul
doar in antetul fisierului. Plus regula zero (predarea contextului),
capcanele de la testarea headless si completari pe fluxul text -> binar.

utile/Teste: suita de regresie pentru editarea facturii si harness-ul
watchdog (nu sunt in SVN, unde utile/Teste e ignorat). .gitignore ignora si
capturile PNG si watchdog_out/, ramase din rulari.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SN8snvkk94KuhWwoXUUey3
2026-08-08 16:47:24 +03:00

391 lines
18 KiB
PowerShell

# watchdog_vfp.ps1 - lanseaza un test VFP headless si detecteaza dialoguri native blocante
# (mesaje Windows, "View Parameter", erori modale) care nu apar in logul scris de script si
# nu sunt prinse de ON ERROR / mock-uri - cazul clasic de proces "agatat" cu CPU~0.
#
# Ce face: sterge .fxp-ul vechi, lanseaza `vfp9.exe -A -T "<script>"`, apoi polleaza la interval
# scurt ferestrele TOP-LEVEL ale procesului (EnumWindows + GetWindowThreadProcessId). Fereastra
# principala VFP se identifica prin Process.MainWindowHandle (.NET) - NU dupa numele clasei:
# VFP inregistreaza clase diferite prefixate "vfp9..." atat pentru shell-ul principal cat si
# pentru dialogurile proprii (ex. "View Parameter"), deci clasificarea pe clasa e nesigura.
# Orice fereastra NOUA, diferita de MainWindowHandle, e tratata ca dialog blocant.
#
# Pentru fiecare dialog detectat: captura PNG (PrintWindow, fallback CopyFromScreen daca iese
# neagra) + dump text (titlu, clasa, si textul/clasa fiecarui control copil via WM_GETTEXT - se
# citesc controale native Windows chiar daca apartin unui proces strain pe alta arhitectura).
#
# INTERZIS: watchdog-ul NU are voie sa foloseasca input real de tastatura/mouse (keybd_event,
# SetForegroundWindow, SendInput, mouse_event) - masina e PARTAJATA cu utilizatorul, iar un
# ESCAPE/click injectat la nivel de sistem ajunge in orice fereastra are focus in acel moment,
# nu neaparat in dialogul tintit (verificat: a ajuns in IDE-ul VFP al utilizatorului aflat in
# lucru in paralel, intrerupandu-l). -AutoDismiss foloseste STRICT mesaje Windows tintite pe
# handle-ul dialogului: buton copil cu text Cancel/Anulare/Renunta -> BM_CLICK; altfel
# WM_COMMAND IDCANCEL -> ESCAPE ca mesaj (WM_KEYDOWN/WM_KEYUP, nu input real) -> WM_CLOSE, in
# ordine, primul care inchide fereastra castiga. Dialogurile proprii VFP owner-drawn (ex.
# "View Parameter" - butoanele sunt desenate, nu HWND-uri reale) pot sa NU raspunda la niciunul -
# atunci dismiss-ul ESUEAZA CINSTIT (ramane deschis pana la timeout), nu se cade pe input real.
# Captura (PNG+dump text) e intotdeauna de incredere, indiferent de rezultatul dismiss-ului.
# Continua polling-ul dupa fiecare dismiss reusit, ca sa prinda si eventuale dialoguri urmatoare
# (pana la -MaxDialogs). Fara -AutoDismiss: captureaza primul dialog gasit, apoi opreste
# polling-ul (nu-l atinge).
#
# Procesul e omorat mereu la iesire (finally) - nu ramane niciodata viu.
#
# Utilizare:
# powershell -ExecutionPolicy Bypass -File watchdog_vfp.ps1 -Script "D:\...\test_x.prg" -AutoDismiss
# powershell -ExecutionPolicy Bypass -File watchdog_vfp.ps1 -Script "D:\...\test_x.prg" -TimeoutSec 60 -OutDir "D:\...\out"
#
# Parametri:
# -Script cale completa la .prg de rulat (obligatoriu)
# -Vfp cale la vfp9.exe (implicit instalarea standard x86)
# -TimeoutSec timeout total de polling (implicit 90)
# -OutDir folder pentru PNG-uri + log text (implicit "<folderul scriptului>\watchdog_out")
# -AutoDismiss inchide automat dialogurile gasite (Cancel/WM_CLOSE) si continua polling-ul
# -MaxDialogs cate dialoguri succesive sa trateze cu -AutoDismiss (implicit 10)
#
# Iesire: rezumat pe stdout (numar dialoguri, textul fiecaruia, caile PNG, exit code-ul
# procesului) + acelasi continut scris in "<Script>_watchdog.log" din -OutDir.
param(
[Parameter(Mandatory=$true)][string]$Script,
[string]$Vfp = 'C:\Program Files (x86)\Microsoft Visual FoxPro 9\vfp9.exe',
[int]$TimeoutSec = 90,
[string]$OutDir,
[switch]$AutoDismiss,
[int]$MaxDialogs = 10
)
$ErrorActionPreference = 'Stop'
if (-not (Test-Path $Script)) { throw "Nu gasesc scriptul: $Script" }
$Script = (Resolve-Path $Script).Path
$ScriptDir = [System.IO.Path]::GetDirectoryName($Script)
$ScriptBase = [System.IO.Path]::GetFileNameWithoutExtension($Script)
if (-not $OutDir) { $OutDir = Join-Path $ScriptDir 'watchdog_out' }
if (-not (Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null }
$LogPath = Join-Path $OutDir ($ScriptBase + '_watchdog.log')
Add-Type -AssemblyName System.Drawing
Add-Type @"
using System;
using System.Text;
using System.Runtime.InteropServices;
public class WdWin32 {
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("user32.dll")]
public static extern bool EnumChildWindows(IntPtr hWndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")]
public static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool IsWindow(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
[DllImport("user32.dll")]
public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, EntryPoint = "SendMessage")]
public static extern IntPtr SendMessageText(IntPtr hWnd, uint Msg, int wParam, StringBuilder lParam);
[StructLayout(LayoutKind.Sequential)]
public struct RECT { public int Left, Top, Right, Bottom; }
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
public static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, uint nFlags);
}
"@
$WM_GETTEXT = 0x000D
$WM_CLOSE = 0x0010
$BM_CLICK = 0x00F5
function Get-TopWindowsForPid([int]$ProcId) {
$result = New-Object System.Collections.Generic.List[IntPtr]
$cb = {
param($hWnd, $lParam)
$owner = 0
[void][WdWin32]::GetWindowThreadProcessId($hWnd, [ref]$owner)
if ($owner -eq $ProcId -and [WdWin32]::IsWindowVisible($hWnd)) { $result.Add($hWnd) }
return $true
}
[WdWin32]::EnumWindows($cb, [IntPtr]::Zero) | Out-Null
return $result
}
function Get-ChildWindowsOf([IntPtr]$hWndParent) {
$result = New-Object System.Collections.Generic.List[IntPtr]
$cb = {
param($hWnd, $lParam)
$result.Add($hWnd)
return $true
}
[WdWin32]::EnumChildWindows($hWndParent, $cb, [IntPtr]::Zero) | Out-Null
return $result
}
# WM_GETTEXT (nu GetWindowText) - citeste text de la controale ale unui proces strain, indiferent
# de arhitectura (32/64 bit); GetWindowText poate esua tacut pe controale custom VFP.
function Get-WmText([IntPtr]$hWnd, [int]$MaxLen = 1024) {
$sb = New-Object System.Text.StringBuilder $MaxLen
[WdWin32]::SendMessageText($hWnd, $WM_GETTEXT, $MaxLen, $sb) | Out-Null
return $sb.ToString()
}
function Get-WinClassName([IntPtr]$hWnd) {
$sb = New-Object System.Text.StringBuilder 256
[WdWin32]::GetClassName($hWnd, $sb, $sb.Capacity) | Out-Null
return $sb.ToString()
}
function Test-BitmapAllBlack([System.Drawing.Bitmap]$bmp) {
$w = $bmp.Width; $h = $bmp.Height
if ($w -le 0 -or $h -le 0) { return $true }
for ($i = 0; $i -lt 9; $i++) {
$x = [Math]::Min($w - 1, [int]($w * (($i % 3) + 0.5) / 3))
$y = [Math]::Min($h - 1, [int]($h * ([Math]::Floor($i / 3) + 0.5) / 3))
$px = $bmp.GetPixel($x, $y)
if ($px.R -ne 0 -or $px.G -ne 0 -or $px.B -ne 0) { return $false }
}
return $true
}
# PrintWindow direct pe handle; fallback CopyFromScreen daca iese neagra (fereastra themed/GDI+
# care nu se randeaza corect prin PrintWindow). Foloseste doar pentru dialoguri surprinse
# on-screen - nu se muta off-screen aici (spre deosebire de vfp_ui_harness.ps1), fiindca scopul
# e sa surprindem exact dialogul care blocheaza, o singura data.
function Save-DialogScreenshot([IntPtr]$hWnd, [string]$Path) {
$rect = New-Object WdWin32+RECT
[WdWin32]::GetWindowRect($hWnd, [ref]$rect) | Out-Null
$w = $rect.Right - $rect.Left
$h = $rect.Bottom - $rect.Top
if ($w -le 0 -or $h -le 0) { return $false }
$bmp = New-Object System.Drawing.Bitmap $w, $h
$g = [System.Drawing.Graphics]::FromImage($bmp)
$hdc = $g.GetHdc()
$ok = [WdWin32]::PrintWindow($hWnd, $hdc, 2)
$g.ReleaseHdc($hdc)
$g.Dispose()
if ((-not $ok) -or (Test-BitmapAllBlack $bmp)) {
$bmp.Dispose()
$bmp = New-Object System.Drawing.Bitmap $w, $h
$g2 = [System.Drawing.Graphics]::FromImage($bmp)
try { $g2.CopyFromScreen($rect.Left, $rect.Top, 0, 0, (New-Object System.Drawing.Size $w, $h)) } catch {}
$g2.Dispose()
}
$bmp.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
return $true
}
function Get-DialogDump([IntPtr]$hWnd, [System.Collections.Generic.List[IntPtr]]$children) {
$title = Get-WmText $hWnd
$class = Get-WinClassName $hWnd
$lines = New-Object System.Collections.Generic.List[string]
$lines.Add("dialog hwnd=$hWnd class='$class' title='$title'")
foreach ($c in $children) {
$ct = (Get-WmText $c 512).Trim()
$cc = Get-WinClassName $c
if ($ct -or $cc) { $lines.Add(" child hwnd=$c class='$cc' text='$ct'") }
}
# unary comma obligatoriu: fara ea, un List[string] cu UN SINGUR element (dialog fara copii,
# ex. "View Parameter") e "descompus" de PowerShell la scalar string simplu la return, iar
# indexarea ulterioara $dumpLines[0] citeste primul CARACTER, nu primul rand - prins la testul
# pe test_page3_articole.prg (rezumatul arata titlul "d" in loc de linia intreaga)
return , $lines.ToArray()
}
function Find-DismissButton([System.Collections.Generic.List[IntPtr]]$children) {
foreach ($c in $children) {
if ((Get-WinClassName $c) -ne 'Button') { continue }
$txt = ((Get-WmText $c 256) -replace '&', '').Trim()
if ($txt -ieq 'Cancel' -or $txt -ieq 'Anulare' -or $txt -ieq 'Renunta' -or $txt -ieq 'Renunță') { return $c }
}
return [IntPtr]::Zero
}
$WM_COMMAND = 0x0111
$WM_KEYDOWN = 0x0100
$WM_KEYUP = 0x0101
$VK_ESCAPE = 0x1B
$IDCANCEL = 2
# INTERZIS: input real de tastatura/mouse (keybd_event, SetForegroundWindow, SendInput,
# mouse_event). Masina e PARTAJATA cu utilizatorul - un ESCAPE injectat la nivel de sistem
# ajunge in orice fereastra are focus in acel moment, INDIFERENT carui proces ii apartine
# (verificat: a ajuns in IDE-ul VFP al utilizatorului, aflat in lucru in paralel, intrerupandu-l
# cu o eroare + "Execution was canceled by the user"). Foloseste STRICT mesaje Windows tintite pe
# HANDLE-ul dialogului (PostMessage/SendMessage: WM_COMMAND IDCANCEL, WM_KEYDOWN/WM_KEYUP
# VK_ESCAPE, WM_CLOSE, BM_CLICK) - astea nu ating focusul/inputul global, raman izolate la
# fereastra tintita. Dialogurile native VFP proprii (ex. "View Parameter") NU au controale copil
# reale (owner-drawn) si pot sa NU raspunda la niciunul dintre mesajele de mai jos - in acel caz
# dismiss-ul ESUEAZA CINSTIT (dialogul ramane deschis, procesul e omorat la timeout de finally),
# nu se cade pe input real. Captura (PNG+dump text) ramane oricum de incredere, indiferent de
# rezultatul dismiss-ului.
function Dismiss-Dialog([IntPtr]$hWnd, [System.Collections.Generic.List[IntPtr]]$children) {
$btn = Find-DismissButton $children
if ($btn -ne [IntPtr]::Zero) {
[WdWin32]::SendMessage($btn, $BM_CLICK, [IntPtr]::Zero, [IntPtr]::Zero) | Out-Null
Start-Sleep -Milliseconds 300
if (-not [WdWin32]::IsWindow($hWnd)) { return 'click Cancel/Anulare' }
return 'NEREUSIT: BM_CLICK pe Cancel/Anulare nu a inchis dialogul'
}
[WdWin32]::SendMessage($hWnd, $WM_COMMAND, [IntPtr]$IDCANCEL, [IntPtr]::Zero) | Out-Null
Start-Sleep -Milliseconds 300
if (-not [WdWin32]::IsWindow($hWnd)) { return 'WM_COMMAND IDCANCEL' }
[WdWin32]::SendMessage($hWnd, $WM_KEYDOWN, [IntPtr]$VK_ESCAPE, [IntPtr]::Zero) | Out-Null
[WdWin32]::SendMessage($hWnd, $WM_KEYUP, [IntPtr]$VK_ESCAPE, [IntPtr]::Zero) | Out-Null
Start-Sleep -Milliseconds 300
if (-not [WdWin32]::IsWindow($hWnd)) { return 'tasta ESCAPE trimisa cu SendMessage (mesaj, nu input real)' }
[WdWin32]::SendMessage($hWnd, $WM_CLOSE, [IntPtr]::Zero, [IntPtr]::Zero) | Out-Null
Start-Sleep -Milliseconds 300
if (-not [WdWin32]::IsWindow($hWnd)) { return 'WM_CLOSE' }
$motiv = if ($children.Count -eq 0) { 'fara controale copil (owner-drawn) si niciun mesaj tintit nu a functionat' } else { 'niciun mesaj tintit (WM_COMMAND/ESCAPE mesaj/WM_CLOSE) nu a functionat' }
return "NEREUSIT: dialogul a ramas deschis ($motiv) - fara input real, per regula masinii partajate"
}
# omoara doar vfp9 lansate pe ACEST script (linia de comanda contine calea lui), nu instante straine
function Stop-VfpForScript([string]$ScriptPath) {
Get-CimInstance Win32_Process -Filter "Name='vfp9.exe'" -ErrorAction SilentlyContinue | ForEach-Object {
if ($_.CommandLine -and $_.CommandLine.ToLower().Contains($ScriptPath.ToLower())) {
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
}
}
}
# --- pregatire ---
$Fxp = [System.IO.Path]::ChangeExtension($Script, 'fxp')
if (Test-Path $Fxp) { Remove-Item $Fxp -Force }
Stop-VfpForScript $Script
Start-Sleep -Milliseconds 300
$log = New-Object System.Collections.Generic.List[string]
function Log([string]$s) { Write-Host $s; $log.Add($s) }
Log "watchdog_vfp: lansez $Vfp -A -T `"$Script`""
$proc = Start-Process -FilePath $Vfp -ArgumentList @('-A', '-T', "`"$Script`"") -PassThru
$dialogs = New-Object System.Collections.Generic.List[object]
$exitCode = $null
try {
# Fereastra principala = Process.MainWindowHandle (.NET), latch-uita o singura data. NU se
# poate distinge dupa numele clasei: VFP inregistreaza clase diferite prefixate "vfp9..." atat
# pentru shell-ul principal ("vfp99400000") CAT SI pentru dialoguri native proprii precum
# "View Parameter" ("vfp994000002") - verificat empiric, clasificarea pe clasa a lasat
# "View Parameter" nedetectat. MainWindowHandle e acelasi mecanism folosit deja cu succes in
# vfp_ui_harness.ps1 (Wait-MainWindowHandle) pentru shell-ul VFP.
$mainHwnd = [IntPtr]::Zero
$mwDeadline = (Get-Date).AddSeconds(20)
while ($mainHwnd -eq [IntPtr]::Zero -and (Get-Date) -lt $mwDeadline) {
$proc.Refresh()
if ($proc.HasExited) { break }
if ($proc.MainWindowHandle -ne [IntPtr]::Zero) { $mainHwnd = $proc.MainWindowHandle }
else { Start-Sleep -Milliseconds 200 }
}
if ($mainHwnd -ne [IntPtr]::Zero) {
Log ("fereastra principala VFP: hwnd={0} class='{1}' title='{2}'" -f $mainHwnd, (Get-WinClassName $mainHwnd), (Get-WmText $mainHwnd))
} else {
Log "ATENTIE: MainWindowHandle nu a aparut in 20s - orice fereastra gasita va fi tratata ca dialog."
}
$knownWindows = New-Object System.Collections.Generic.HashSet[IntPtr]
if ($mainHwnd -ne [IntPtr]::Zero) { [void]$knownWindows.Add($mainHwnd) }
$deadline = (Get-Date).AddSeconds($TimeoutSec)
$stop = $false
while (-not $stop -and (Get-Date) -lt $deadline) {
$proc.Refresh()
if ($proc.HasExited) { Log "proces terminat singur (exit code $($proc.ExitCode))"; break }
# daca MainWindowHandle inca nu era prins, incearca din nou (fereastra principala poate
# aparea mai tarziu decat asteptarea initiala de 20s in cazuri lente)
if ($mainHwnd -eq [IntPtr]::Zero -and $proc.MainWindowHandle -ne [IntPtr]::Zero) {
$mainHwnd = $proc.MainWindowHandle
[void]$knownWindows.Add($mainHwnd)
Log ("fereastra principala VFP (intarziata): hwnd={0} class='{1}' title='{2}'" -f $mainHwnd, (Get-WinClassName $mainHwnd), (Get-WmText $mainHwnd))
}
$windows = Get-TopWindowsForPid $proc.Id
foreach ($w in $windows) {
if ($knownWindows.Contains($w)) { continue }
[void]$knownWindows.Add($w)
if ($w -eq $mainHwnd) { continue }
$children = Get-ChildWindowsOf $w
$dumpLines = Get-DialogDump $w $children
$idx = $dialogs.Count
$png = Join-Path $OutDir ("{0}_dialog{1}.png" -f $ScriptBase, $idx)
Save-DialogScreenshot $w $png | Out-Null
Log "=== DIALOG $idx detectat ==="
foreach ($l in $dumpLines) { Log $l }
Log " screenshot: $png"
$dismissMethod = $null
if ($AutoDismiss) {
$dismissMethod = Dismiss-Dialog $w $children
Log " dismiss: $dismissMethod"
}
$dialogs.Add([pscustomobject]@{
Index = $idx
Title = ($dumpLines[0])
Png = $png
Dismiss = $dismissMethod
DumpText = ($dumpLines -join "`r`n")
})
if ($dialogs.Count -ge $MaxDialogs) { Log "MaxDialogs ($MaxDialogs) atins."; $stop = $true; break }
if (-not $AutoDismiss) { Log "AutoDismiss oprit - opresc polling-ul dupa primul dialog."; $stop = $true; break }
}
if (-not $stop) { Start-Sleep -Milliseconds 700 }
}
if (-not $proc.HasExited -and (Get-Date) -ge $deadline) {
Log "TIMEOUT (${TimeoutSec}s) - procesul e inca viu, il opresc."
}
$proc.Refresh()
if ($proc.HasExited) { $exitCode = $proc.ExitCode }
}
finally {
if ($proc -and -not $proc.HasExited) {
try { $proc.Kill() } catch {}
}
Stop-VfpForScript $Script
}
# --- rezumat ---
Log ""
Log "=== REZUMAT ==="
Log ("dialoguri detectate: {0}" -f $dialogs.Count)
foreach ($d in $dialogs) {
Log (" [{0}] {1}" -f $d.Index, $d.Title)
Log (" png: {0}" -f $d.Png)
if ($d.Dismiss) { Log (" dismiss: {0}" -f $d.Dismiss) }
}
Log ("exit code proces: {0}" -f ($(if ($null -ne $exitCode) { $exitCode } else { '(omorat de watchdog, nu s-a terminat singur)' })))
Log ("log complet: {0}" -f $LogPath)
[System.IO.File]::WriteAllLines($LogPath, $log)