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(); }); /// Redirects Console.WriteLine output to the WPF log box. private sealed class DispatcherWriter : TextWriter { private readonly Action _log; public override Encoding Encoding => Encoding.UTF8; public DispatcherWriter(Action log) => _log = log; public override void WriteLine(string? value) => _log(value ?? ""); public override void Write(string? value) { /* absorb partial writes */ } } }