LauncherUI (src/Recordingtest.LauncherUI/): - 시나리오 폴더 브라우저 + 목록 선택 - SUT(EG-BIM Modeler) 프로세스 자동 탐지 + 상태 표시 - 3초 카운트다운 후 런처 최소화 → 재생 시작 - 재생 중 Console.WriteLine → WPF 로그 박스 실시간 출력 - 중단(Ctrl+C 대체) 버튼 UiaPlayerHost: - _sutHwnd 캐싱 (BringSutToForeground 이후) - EnsureSutForegroundQuick(): Click/Type/Hotkey 직전 포커스 재확인 (GetForegroundWindow != sutHwnd 시 SetForegroundWindow + 300ms 대기) - 매 입력 스텝마다 SUT가 포커스를 잃으면 자동 복구 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
220 lines
7.8 KiB
C#
220 lines
7.8 KiB
C#
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Windows;
|
|
using System.Windows.Media;
|
|
using Microsoft.Win32;
|
|
using Recordingtest.Player;
|
|
using Recordingtest.Player.Model;
|
|
|
|
namespace Recordingtest.LauncherUI;
|
|
|
|
public partial class MainWindow : Window
|
|
{
|
|
private CancellationTokenSource? _cts;
|
|
private bool _refreshingPath;
|
|
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
ScenariosPathBox.Text = FindScenariosDir();
|
|
RefreshScenarioList();
|
|
RefreshSutStatus();
|
|
}
|
|
|
|
// ── Scenario folder ─────────────────────────────────────────────────────
|
|
|
|
private static string FindScenariosDir()
|
|
{
|
|
var candidates = new[]
|
|
{
|
|
Path.Combine(AppContext.BaseDirectory, "scenarios"),
|
|
Path.Combine(Directory.GetCurrentDirectory(), "scenarios"),
|
|
// walk up 3 levels from exe (dev layout: bin/Debug/net8.0-windows)
|
|
Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "scenarios")),
|
|
Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "scenarios")),
|
|
};
|
|
foreach (var c in candidates)
|
|
if (Directory.Exists(c)) return c;
|
|
return Path.Combine(Directory.GetCurrentDirectory(), "scenarios");
|
|
}
|
|
|
|
private void RefreshScenarioList()
|
|
{
|
|
var dir = ScenariosPathBox.Text?.Trim() ?? "";
|
|
ScenarioListBox.Items.Clear();
|
|
if (!Directory.Exists(dir)) return;
|
|
foreach (var f in Directory.GetFiles(dir, "*.yaml").OrderBy(x => x))
|
|
ScenarioListBox.Items.Add(Path.GetFileNameWithoutExtension(f));
|
|
if (ScenarioListBox.Items.Count > 0)
|
|
ScenarioListBox.SelectedIndex = ScenarioListBox.Items.Count - 1; // select latest
|
|
}
|
|
|
|
private void ScenariosPath_TextChanged(object sender,
|
|
System.Windows.Controls.TextChangedEventArgs e)
|
|
{
|
|
if (_refreshingPath) return;
|
|
RefreshScenarioList();
|
|
}
|
|
|
|
private void BrowseScenarios_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var dlg = new OpenFolderDialog { Title = "시나리오 폴더 선택" };
|
|
if (dlg.ShowDialog() == true)
|
|
{
|
|
_refreshingPath = true;
|
|
ScenariosPathBox.Text = dlg.FolderName;
|
|
_refreshingPath = false;
|
|
RefreshScenarioList();
|
|
}
|
|
}
|
|
|
|
private void RefreshScenarios_Click(object sender, RoutedEventArgs e) =>
|
|
RefreshScenarioList();
|
|
|
|
// ── SUT status ───────────────────────────────────────────────────────────
|
|
|
|
private void RefreshSutStatus()
|
|
{
|
|
var procs = Process.GetProcessesByName("EG-BIM Modeler");
|
|
if (procs.Length > 0)
|
|
{
|
|
SutStatusText.Text = $"✓ EG-BIM Modeler 실행 중 (PID {procs[0].Id})";
|
|
SutStatusText.Foreground = new SolidColorBrush(Color.FromRgb(0x2E, 0x7D, 0x32));
|
|
}
|
|
else
|
|
{
|
|
SutStatusText.Text = "✗ SUT 없음 — EG-BIM Modeler를 먼저 실행하세요";
|
|
SutStatusText.Foreground = new SolidColorBrush(Color.FromRgb(0xC6, 0x28, 0x28));
|
|
}
|
|
}
|
|
|
|
private void RefreshSut_Click(object sender, RoutedEventArgs e) => RefreshSutStatus();
|
|
|
|
// ── Run / Stop ───────────────────────────────────────────────────────────
|
|
|
|
private async void Run_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (ScenarioListBox.SelectedItem is not string scenarioName)
|
|
{
|
|
AppendLog("[warn] 시나리오를 선택하세요.");
|
|
return;
|
|
}
|
|
|
|
var procs = Process.GetProcessesByName("EG-BIM Modeler");
|
|
if (procs.Length == 0)
|
|
{
|
|
AppendLog("[error] EG-BIM Modeler가 실행 중이 아닙니다.");
|
|
return;
|
|
}
|
|
|
|
RunButton.IsEnabled = false;
|
|
StopButton.IsEnabled = true;
|
|
LogBox.Clear();
|
|
AppendLog($"[launcher] 시나리오: {scenarioName}");
|
|
|
|
// 3-second countdown so user can move cursor away
|
|
CountdownText.Visibility = Visibility.Visible;
|
|
for (int i = 3; i >= 1; i--)
|
|
{
|
|
CountdownText.Text = $"▶ {i}초 후 시작...";
|
|
await Task.Delay(1000);
|
|
}
|
|
CountdownText.Visibility = Visibility.Collapsed;
|
|
|
|
// Minimize launcher so it can't steal focus during playback
|
|
var prevState = WindowState;
|
|
WindowState = WindowState.Minimized;
|
|
|
|
_cts = new CancellationTokenSource();
|
|
var scenariosDir = ScenariosPathBox.Text?.Trim() ?? "";
|
|
|
|
try
|
|
{
|
|
await Task.Run(() => RunScenario(scenarioName, scenariosDir, _cts.Token));
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
AppendLog("[launcher] 중단됨.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppendLog($"[launcher] 오류: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
WindowState = prevState;
|
|
RunButton.IsEnabled = true;
|
|
StopButton.IsEnabled = false;
|
|
}
|
|
}
|
|
|
|
private void Stop_Click(object sender, RoutedEventArgs e) => _cts?.Cancel();
|
|
|
|
// ── Playback ─────────────────────────────────────────────────────────────
|
|
|
|
private void RunScenario(string scenarioName, string scenariosDir,
|
|
CancellationToken ct)
|
|
{
|
|
var yamlPath = Path.Combine(scenariosDir, scenarioName + ".yaml");
|
|
if (!File.Exists(yamlPath))
|
|
{
|
|
AppendLog($"[error] 파일 없음: {yamlPath}");
|
|
return;
|
|
}
|
|
|
|
Scenario scenario;
|
|
try { scenario = ScenarioLoader.LoadFromFile(yamlPath); }
|
|
catch (Exception ex) { AppendLog($"[error] yaml 파싱 실패: {ex.Message}"); return; }
|
|
|
|
var app = UiaPlayerHost.AttachByExeName("EG-BIM Modeler.exe");
|
|
if (app is null)
|
|
{
|
|
AppendLog("[error] EG-BIM Modeler 프로세스에 연결할 수 없습니다.");
|
|
return;
|
|
}
|
|
|
|
var artifactDir = Path.Combine("artifacts", "launcher-out", scenarioName);
|
|
Directory.CreateDirectory(artifactDir);
|
|
|
|
using var host = new UiaPlayerHost(app, artifactDir);
|
|
|
|
// Redirect Console.WriteLine → WPF log box
|
|
var prevOut = Console.Out;
|
|
Console.SetOut(new DispatcherWriter(AppendLog));
|
|
try
|
|
{
|
|
host.BringSutToForeground();
|
|
ct.ThrowIfCancellationRequested();
|
|
|
|
var engine = new PlayerEngine(new PlayerEngineOptions { PreserveTiming = true });
|
|
engine.Run(scenario, host);
|
|
|
|
AppendLog($"[launcher] ✓ {scenarioName} 완료.");
|
|
}
|
|
finally
|
|
{
|
|
Console.SetOut(prevOut);
|
|
}
|
|
}
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
|
|
private void AppendLog(string msg) =>
|
|
Dispatcher.Invoke(() =>
|
|
{
|
|
LogBox.AppendText(msg + "\n");
|
|
LogScroll.ScrollToBottom();
|
|
});
|
|
|
|
/// <summary>Redirects Console.WriteLine output to the WPF log box.</summary>
|
|
private sealed class DispatcherWriter : TextWriter
|
|
{
|
|
private readonly Action<string> _log;
|
|
public override Encoding Encoding => Encoding.UTF8;
|
|
public DispatcherWriter(Action<string> log) => _log = log;
|
|
public override void WriteLine(string? value) => _log(value ?? "");
|
|
public override void Write(string? value) { /* absorb partial writes */ }
|
|
}
|
|
}
|