player: active foreground wait replaces fixed 600ms sleep (#14)
BringSutToForeground() now polls GetForegroundWindow() == SUT hwnd at 25ms intervals up to 2s, followed by a 100ms tail settle, instead of the brittle fixed 600ms sleep. First-attempt replay of box-v6.yaml is now reliable (previously dropped the opening "BOX" keystrokes on a cold start). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
5
PLAN.md
5
PLAN.md
@@ -10,9 +10,8 @@
|
|||||||
|
|
||||||
## P1 — 라이브 검증 (사용자 환경 필요)
|
## P1 — 라이브 검증 (사용자 환경 필요)
|
||||||
|
|
||||||
4. **이슈 #14 재생 안정화** — 첫 시도 BOX 타이핑 누락 문제. foreground settle 600ms→능동 대기 전환 또는 첫 type 이전 `Keyboard.Type` warm-up. `/contract player-foreground-stabilize`.
|
4. **recorder Gap I-1** — type 스텝 target을 `Automation.FocusedElement`로 직접 채워 null_target_steps=0 달성. Player fallback 의존도 줄이기.
|
||||||
5. **recorder Gap I-1** — type 스텝 target을 `Automation.FocusedElement`로 직접 채워 null_target_steps=0 달성. `/contract recorder-focused-element-target`.
|
5. **engine-bridge v3** — ReflectionEngineStateProvider 실매핑 (smoke test 이후)
|
||||||
6. **engine-bridge v3** — ReflectionEngineStateProvider 실매핑 (smoke test 이후)
|
|
||||||
|
|
||||||
## Follow-ups (non-blocking)
|
## Follow-ups (non-blocking)
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ _(없음)_
|
|||||||
- [ ] player: `wait_for` UIA 이벤트 매핑 강화 (현재 host passthrough).
|
- [ ] player: `wait_for` UIA 이벤트 매핑 강화 (현재 host passthrough).
|
||||||
- [ ] player: `UiaPlayerHost` uia_path resolver가 마지막 `@AutomationId`만 사용 — 전체 ancestor chain 지원 필요.
|
- [ ] player: `UiaPlayerHost` uia_path resolver가 마지막 `@AutomationId`만 사용 — 전체 ancestor chain 지원 필요.
|
||||||
- [ ] recorder: IME 조합 키 처리 (contract risks).
|
- [ ] recorder: IME 조합 키 처리 (contract risks).
|
||||||
- [ ] player: foreground settle이 경계선(600ms) — 1차 재생이 가끔 BOX 타이핑 누락. 능동 대기(focus-ready polling)로 전환 고려. (issue #14 후속)
|
- [x] ~~player: foreground settle 안정화~~ — 능동 대기(`GetForegroundWindow` polling 2s + 100ms settle)로 전환, 1차 재생 성공 확인
|
||||||
- [ ] recorder: null_target 이벤트 자체를 줄이기 — `Automation.FocusedElement` 직접 조회해 type 스텝 target 채우기 (issue #14 Gap I-1). 현재는 player fallback으로 우회.
|
- [ ] recorder: null_target 이벤트 자체를 줄이기 — `Automation.FocusedElement` 직접 조회해 type 스텝 target 채우기 (issue #14 Gap I-1). 현재는 player fallback으로 우회.
|
||||||
|
|
||||||
## Blocked
|
## Blocked
|
||||||
|
|||||||
@@ -212,21 +212,34 @@ public sealed class UiaPlayerHost : IPlayerHost, IDisposable
|
|||||||
/// was launched from a shell that still has focus when the first Type/Hotkey
|
/// was launched from a shell that still has focus when the first Type/Hotkey
|
||||||
/// step fires.
|
/// step fires.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr GetForegroundWindow();
|
||||||
|
|
||||||
public void BringSutToForeground()
|
public void BringSutToForeground()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var w = _app?.GetMainWindow(_automation, TimeSpan.FromSeconds(5));
|
var w = _app?.GetMainWindow(_automation, TimeSpan.FromSeconds(5));
|
||||||
if (w is null) return;
|
if (w is null) return;
|
||||||
|
var targetHwnd = w.Properties.NativeWindowHandle.ValueOrDefault;
|
||||||
try { w.SetForeground(); } catch { /* best-effort */ }
|
try { w.SetForeground(); } catch { /* best-effort */ }
|
||||||
try { w.Focus(); } catch { /* best-effort */ }
|
try { w.Focus(); } catch { /* best-effort */ }
|
||||||
// Small settle so the OS-level focus switch takes effect before
|
|
||||||
// the first SendInput. 150ms is enough in practice on Win10.
|
// Issue #14 follow-up: active wait instead of fixed 600ms sleep.
|
||||||
// Increased from 150→600ms because FlaUI Keyboard.Type drops the
|
// Poll until the OS reports the SUT as the foreground window, up
|
||||||
// first couple of characters if SendInput fires before the OS
|
// to 2s. Previously a 600ms fixed sleep was threshold-sensitive
|
||||||
// finishes the focus transition (observed: "BOX" lost on first
|
// and caused the first "BOX" keystroke to get dropped on a cold
|
||||||
// step, "10" succeeded later once the app had settled).
|
// first run.
|
||||||
System.Threading.Thread.Sleep(600);
|
var deadline = DateTime.UtcNow.AddSeconds(2);
|
||||||
|
while (DateTime.UtcNow < deadline)
|
||||||
|
{
|
||||||
|
if (targetHwnd != IntPtr.Zero && GetForegroundWindow() == targetHwnd)
|
||||||
|
break;
|
||||||
|
System.Threading.Thread.Sleep(25);
|
||||||
|
}
|
||||||
|
// Tiny additional settle for the OS keyboard-focus IPC to finish
|
||||||
|
// after the foreground transition is observed.
|
||||||
|
System.Threading.Thread.Sleep(100);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user