-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
261 lines (223 loc) · 9.99 KB
/
Copy pathProgram.cs
File metadata and controls
261 lines (223 loc) · 9.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Vantuz.Core;
using Vantuz.Host;
namespace VantuzLauncher;
class Program
{
/// F_doc: {WorkspacePath returns incorrect result or throws unexpectedly} E_doc: Unit test or static analysis verifies WorkspacePath behavior
public static string WorkspacePath { get; private set; } = string.Empty;
// Win32 MessageBox for WinExe error surfacing - console is invisible
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int MessageBoxW(nint hWnd, string text, string caption, uint type);
private const uint MB_OK = 0x0;
private const uint MB_ICONERROR = 0x10;
[STAThread]
static async Task Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
var totalSw = Stopwatch.StartNew();
Console.Title = "Vantuz Launcher - Initializing...";
WorkspacePath = DetermineWorkspace();
AppDomain.CurrentDomain.UnhandledException += (s, e) =>
{
var ex = (Exception)e.ExceptionObject;
string msg = $"CRITICAL: {ex.Message}\n{ex.StackTrace}";
Console.Error.WriteLine(msg);
MessageBoxW(0, msg, Properties.Resources.ErrorCriticalTitle, MB_OK | MB_ICONERROR);
Environment.Exit(2);
};
if (TryParseHeadlessArgs(args, out var headlessOptions))
{
await RunHeadlessAsync(headlessOptions);
return;
}
await RunGuiModeAsync(totalSw);
}
static async Task RunGuiModeAsync(Stopwatch totalSw)
{
var splashSw = Stopwatch.StartNew();
Win32SplashScreen.Show();
splashSw.Stop();
totalSw.Stop();
// F_doc: {Splash latency > 100ms} E_doc: {Stopwatch measurement logged; CI can enforce threshold}
Console.WriteLine($"[STARTUP] Splash latency: {splashSw.ElapsedMilliseconds}ms");
Console.WriteLine($"[STARTUP] Total to-splash: {totalSw.ElapsedMilliseconds}ms");
if (totalSw.ElapsedMilliseconds > 3000)
{
Console.WriteLine("[STARTUP] WARNING: Total startup latency exceeds 3000ms. Consider ReadyToRun or self-contained build.");
}
try
{
string testFile = Path.Combine(WorkspacePath, ".access_test");
File.WriteAllText(testFile, "");
File.Delete(testFile);
}
catch (UnauthorizedAccessException)
{
Win32SplashScreen.Close();
string msg = string.Format(Properties.Resources.ErrorAccessDenied, WorkspacePath);
Console.Error.WriteLine(msg);
MessageBoxW(0, msg, Properties.Resources.ErrorAccessDeniedTitle, MB_OK | MB_ICONERROR);
Environment.Exit(2);
}
string bootJsonPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "boot.gui.json");
string pluginsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "plugins");
string crashLogPath = Path.Combine(WorkspacePath, "crash.log");
if (!File.Exists(bootJsonPath))
{
string msg = $"boot.gui.json not found at {bootJsonPath}";
Console.Error.WriteLine(msg);
MessageBoxW(0, msg, Properties.Resources.ErrorMissingConfigTitle, MB_OK | MB_ICONERROR);
Environment.Exit(2);
}
var reporter = new ConsoleReporter();
var engine = new VantuzEngine(pluginsDir, reporter, crashLogPath);
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (s, e) => { e.Cancel = true; cts.Cancel(); };
var result = await engine.RunAsync(bootJsonPath, cts.Token);
if (!result.Success)
{
Win32SplashScreen.Close();
string error = result.ErrorMessage ?? "Unknown error";
string detailedMsg = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Pipeline failed: {error}";
Console.Error.WriteLine(detailedMsg);
// Persist to crash log for diagnostics first
try
{
File.AppendAllText(crashLogPath, detailedMsg + "\n");
}
catch (Exception ex)
{
// F_doc: {Crash log write fails silently} E_doc: {User still sees MessageBox with actionable info; log absence is non-fatal}
Console.Error.WriteLine($"Failed to write crash log: {ex.Message}");
}
// Surface to user — WinExe hides console, so use Win32 MessageBox
// Keep message user-friendly and actionable
string userFriendly = string.Format(Properties.Resources.ErrorLaunchFailed, error, crashLogPath);
MessageBoxW(0, userFriendly, Properties.Resources.ErrorLaunchFailedTitle, MB_OK | MB_ICONERROR);
// Give the dialog a moment to render before the process exits
await Task.Delay(100);
Environment.Exit(1);
}
Win32SplashScreen.Close();
if (result.Payload != null &&
result.Payload.TryGetValue("UpdateReady", out var updateReadyObj) &&
updateReadyObj is bool updateReady && updateReady)
{
string hostExe = result.Payload.TryGetValue("hostExecutable", out var hostExeObj) && hostExeObj is string he ? he : "VantuzLauncher.exe";
string? updateScript = result.Payload.TryGetValue("UpdateScript", out var scriptObj) && scriptObj is string s ? s : null;
if (!string.IsNullOrEmpty(updateScript))
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = updateScript,
Arguments = $"\"{hostExe}\"",
UseShellExecute = true,
WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
});
Environment.Exit(0);
}
}
Console.WriteLine("Pipeline completed successfully.");
// Let the GUI lifetime control process exit (ShutdownMode.OnMainWindowClose).
}
static async Task RunHeadlessAsync(HeadlessRunner.HeadlessOptions options)
{
options = options with { WorkspacePath = WorkspacePath };
var result = await HeadlessRunner.RunAsync(options);
string outputPath = Path.Combine(WorkspacePath, "test-result.json");
HeadlessRunner.SaveResult(result, outputPath);
Console.WriteLine($"\n=== TEST RESULT ===");
Console.WriteLine($"Status: {result.Status}");
Console.WriteLine($"Duration: {result.Duration.TotalSeconds:F2}s");
if (!string.IsNullOrEmpty(result.ErrorMessage))
Console.WriteLine($"Error: {result.ErrorMessage}");
Console.WriteLine($"Output: {outputPath}");
Environment.Exit(result.Success ? 0 : 1);
}
static string DetermineWorkspace()
{
string baseDir = AppDomain.CurrentDomain.BaseDirectory.TrimEnd('\\', '/');
if (File.Exists(Path.Combine(baseDir, ".portable"))) return baseDir;
string productId = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name?.ToLowerInvariant() ?? "vantuzlauncher";
string appData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "." + productId);
Directory.CreateDirectory(appData);
return appData;
}
static bool TryParseHeadlessArgs(string[] args, out HeadlessRunner.HeadlessOptions options)
{
options = new HeadlessRunner.HeadlessOptions();
var dict = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
foreach (var arg in args)
{
if (arg.StartsWith("--"))
{
var parts = arg.Substring(2).Split('=', 2);
if (parts.Length == 2)
dict[parts[0]] = parts[1];
else if (parts.Length == 1)
dict[parts[0]] = "true";
}
else if (arg.StartsWith("-"))
{
var key = arg.Substring(1);
dict[key] = "true";
}
}
if (!dict.ContainsKey("headless"))
return false;
options = new HeadlessRunner.HeadlessOptions
{
Username = dict.GetValueOrDefault("username", "test")!,
Password = dict.GetValueOrDefault("password", "test")!,
RamMb = int.TryParse(dict.GetValueOrDefault("ram", "4096"), out var ram) ? ram : 4096,
TestMode = dict.ContainsKey("test-mode") || dict.ContainsKey("test"),
BootPath = dict.GetValueOrDefault("boot-path", null) ?? dict.GetValueOrDefault("boot", null),
};
return true;
}
}
class ConsoleReporter : IStatusReporter
{
private readonly string? _logPath;
public ConsoleReporter()
{
try
{
_logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "launcher_trace.log");
}
catch { /* Silent fail - logging to console is sufficient */ }
}
/// F_doc: {ReportProgress returns incorrect result or throws unexpectedly} E_doc: Unit test or static analysis verifies ReportProgress behavior
public void ReportProgress(string taskName, double percentage)
{
string line = $"[{DateTime.UtcNow:HH:mm:ss}] {taskName}: {percentage:F1}%";
Console.WriteLine(line);
AppendToLog(line);
ReportHub.ReportProgress(taskName, percentage);
}
/// F_doc: {ReportState returns incorrect result or throws unexpectedly} E_doc: Unit test or static analysis verifies ReportState behavior
public void ReportState(string message)
{
string line = $"[{DateTime.UtcNow:HH:mm:ss}] {message}";
Console.WriteLine(line);
AppendToLog(line);
ReportHub.ReportState(message);
}
private void AppendToLog(string line)
{
if (_logPath == null) return;
try
{
File.AppendAllText(_logPath, line + Environment.NewLine);
}
catch { /* Silent fail */ }
}
}