Repository navigation
Expand file tree
/
Copy pathProgram.cs
More file actions
379 lines (336 loc) · 14 KB
/
Copy pathProgram.cs
File metadata and controls
379 lines (336 loc) · 14 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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
using WinGetScout;
if (args.Length > 0 && args [0] == "--check-updates")
{
WorkflowStore store = new ();
bool notifyOnChange = false;
try { notifyOnChange = store.Schedule ().NotifyOnChange; }
catch (Exception ex) { Console.Error.WriteLine ($"Notification settings unavailable: {ex.Message}"); }
IBackend checkBackend = SelectBackend (args);
if (checkBackend is MockBackend)
{
UpdateCheckSnapshot unavailable = new (DateTimeOffset.UtcNow, "Failed",
"A real WinGet backend is unavailable.", "Unavailable", [], 0);
try { store.SaveCheck (unavailable); }
catch (Exception ex)
{
Console.Error.WriteLine ($"Update check could not be saved: {ex.Message}");
Environment.ExitCode = 1;
return;
}
if (notifyOnChange)
{
string? notificationError = await UpdateNotification.TryShowAsync (unavailable, CancellationToken.None);
if (notificationError is not null) Console.Error.WriteLine ($"Notification unavailable: {notificationError}");
}
Console.Error.WriteLine ($"Update check failed: {unavailable.Error}");
Environment.ExitCode = 1;
return;
}
UpdateCheckSnapshot check;
try { check = await UpdateChecks.CheckAsync (checkBackend, store, CancellationToken.None); }
catch (Exception ex)
{
Console.Error.WriteLine ($"Update check could not be saved: {ex.Message}");
Environment.ExitCode = 1;
return;
}
if (notifyOnChange)
{
string? notificationError = await UpdateNotification.TryShowAsync (check, CancellationToken.None);
if (notificationError is not null) Console.Error.WriteLine ($"Notification unavailable: {notificationError}");
}
Console.WriteLine (check.Status == "Succeeded"
? $"{check.Actionable} available, {check.Pinned} pinned, {check.NewOrChanged} new or changed."
: $"Update check failed: {check.Error}");
Environment.ExitCode = check.Status == "Succeeded" ? 0 : 1;
return;
}
if (args.Length > 0 && args [0] is "--dump")
{
// Diagnostic mode: invoke winget the way the backend would and print the raw output
// verbatim, plus a hex dump of the bytes immediately around the dash-line separator.
// Use this on Windows to verify the encoding and figure out why ParseTable is empty:
//
// wingetscout.exe --dump search vscode
// wingetscout.exe --dump list
// wingetscout.exe --dump upgrade
string [] cmd = args.Length > 1
? [.. args.Skip (1), "--accept-source-agreements"]
: ["list", "--accept-source-agreements"];
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine ($"Invoking: winget {string.Join (' ', cmd)}");
Console.WriteLine ($"Console.OutputEncoding: {Console.OutputEncoding.WebName}");
Console.WriteLine ();
ProcessRunner.RunResult dumpResult = await CliBackend.RunDetailedWithCodeAsync (
cmd,
"winget",
TimeSpan.FromMinutes (2),
CancellationToken.None);
int code = dumpResult.Code;
string output = dumpResult.Output;
Console.WriteLine ($"--- exit code: {code}");
Console.WriteLine ($"--- output length: {output.Length} chars");
Console.WriteLine ($"--- output complete: {dumpResult.OutputComplete}");
if (!dumpResult.OutputComplete)
{
Console.WriteLine (
$"--- incomplete reasons: stdout-truncated={dumpResult.StdoutTruncated}, "
+ $"stderr-truncated={dumpResult.StderrTruncated}, cleanup-incomplete={dumpResult.CleanupIncomplete}");
}
Console.WriteLine ("--- output:");
Console.WriteLine (output);
if (!dumpResult.OutputComplete)
{
Console.WriteLine ("--- parser skipped: incomplete bounded output cannot be parsed safely");
return;
}
Console.WriteLine ("--- parser trace:");
if (cmd [0] == "show")
{
// Extract the id from --id <X> for the parser's name fallback.
int idIdx = Array.IndexOf (cmd, "--id");
string id = idIdx >= 0 && idIdx + 1 < cmd.Length ? cmd [idIdx + 1] : string.Empty;
PackageDetail? detail = CliBackend.ParseShowTraced (id, output, Console.Out);
Console.WriteLine ();
if (detail is null)
{
Console.WriteLine ("--- parsed detail: null");
}
else
{
Console.WriteLine ("--- parsed detail:");
Console.WriteLine ($" Name={detail.Name}");
Console.WriteLine ($" Id={detail.Id}");
Console.WriteLine ($" Version={detail.Version}");
Console.WriteLine ($" Publisher={detail.Publisher}");
Console.WriteLine ($" Homepage={detail.Homepage}");
Console.WriteLine ($" License={detail.License}");
Console.WriteLine ($" ReleaseNotesUrl={detail.ReleaseNotesUrl}");
Console.WriteLine ($" Description={detail.Description}");
}
}
else
{
IReadOnlyList<Package> rows = CliBackend.ParseTableTraced (output, hasAvailable: cmd [0] == "upgrade", Console.Out);
Console.WriteLine ();
Console.WriteLine ($"--- parsed rows: {rows.Count}");
foreach (Package row in rows.Take (5))
{
Console.WriteLine ($" Name='{row.Name}' Id='{row.Id}' Version='{row.Version}' Available='{row.AvailableVersion}' Source='{row.Source}'");
}
}
return;
}
#if WINGET_COM
// Apartment + COM-activation probe. This is the decisive "is the COM server actually reachable?"
// diagnostic — run it as the FIRST COM activity after a clean reboot to separate a genuine
// AOT-activation bug from a wedged WinGet OOP server (both surface as 0x80073D54
// APPMODEL_ERROR_NO_PACKAGE). Prints the apartment state + catalog count for the main and a
// threadpool thread; a healthy in-proc activation reports "activation OK; catalogs = 3".
// wingetscout.exe --comdiag
if (args.Length > 0 && args [0] is "--comdiag")
{
Console.WriteLine ($"main thread apartment = {Thread.CurrentThread.GetApartmentState ()}");
try
{
Microsoft.Management.Deployment.PackageManager pm = new ();
Console.WriteLine ($"main-thread activation OK; catalogs = {pm.GetPackageCatalogs ().Count}");
}
catch (Exception ex)
{
Console.WriteLine ($"main-thread activation FAILED: 0x{(uint) ex.HResult:X8} {ex.Message}");
}
await Task.Run (() =>
{
Console.WriteLine ($"threadpool apartment = {Thread.CurrentThread.GetApartmentState ()}");
try
{
Microsoft.Management.Deployment.PackageManager pm = new ();
Console.WriteLine ($"threadpool activation OK; catalogs = {pm.GetPackageCatalogs ().Count}");
}
catch (Exception ex)
{
Console.WriteLine ($"threadpool activation FAILED: 0x{(uint) ex.HResult:X8} {ex.Message}");
}
});
return;
}
#endif
// Backend selection (precedence: --mock > --cli > --com > default):
// --mock / -m the in-memory mock (cross-platform dev/parity)
// --cli the winget.exe CLI parser
// --com the WinGet COM API backend (Windows builds only)
// (default) COM on Windows builds, CLI elsewhere
// These are preferences, not hard guarantees: a requested backend that can't run degrades
// (with a stderr note) — --com on a non-Windows build → CLI, and any CLI path with no winget
// on PATH → mock. Scripts that need a guaranteed backend should check that note.
bool smokeMode = args.Any (a => a == "--smoke");
IBackend backend = SelectBackend (args);
// Theme selection: --theme=<amber|sage|moss|rose>. Defaults to Sage. An unrecognized id
// degrades gracefully (stderr note, keep the default) rather than crashing. TryApply already
// registers schemes on success, so Register() below only needs to run for the default case.
string? themeArg = args.FirstOrDefault (a => a.StartsWith ("--theme=", StringComparison.OrdinalIgnoreCase))
?[8..];
if (themeArg is null || !Theme.TryApply (themeArg))
{
if (themeArg is not null)
{
string validIds = string.Join (", ", Theme.Palettes.Select (p => p.Id));
Console.Error.WriteLine ($"Unknown theme '{themeArg}' - valid values are: {validIds}. Using the default (sage).");
}
Theme.Register ();
}
IApplication? app = null;
App? window = null;
int exitCode = 0;
try
{
// Assign before Init so a partially-initialized terminal still reaches the finally below.
// Init is what puts the terminal into raw mode and the alternate screen; if it throws
// midway, only Dispose can put it back.
app = Application.Create ();
app.Init ();
window = new (backend, smokeMode ? TimeSpan.FromSeconds (1) : null);
app.Run (window);
}
catch (Exception ex)
{
Console.Error.WriteLine ($"Application failed: {ex.Message}");
exitCode = 1;
}
finally
{
if (window is not null)
{
try
{
// Application.Run installs a UI synchronization context that is no longer pumped once
// Run returns. Block on the context-free shutdown task on this original thread instead
// of awaiting a continuation that could be posted to that stopped context.
bool drained = window.ShutdownAsync (TimeSpan.FromSeconds (5))
.ConfigureAwait (false)
.GetAwaiter ()
.GetResult ();
if (!drained)
{
Console.Error.WriteLine ("Shutdown timed out while waiting for background work.");
exitCode = 1;
}
if (window.BackgroundFailures.Count > 0 || window.DroppedBackgroundFailureCount > 0)
{
Console.Error.WriteLine (
$"Background work failed {window.BackgroundFailures.Count + window.DroppedBackgroundFailureCount} time(s)." +
(window.DroppedBackgroundFailureCount > 0
? $" {window.DroppedBackgroundFailureCount} additional failure(s) were not retained."
: string.Empty));
foreach (Exception failure in window.BackgroundFailures)
{
Console.Error.WriteLine ($"- {failure.GetType ().Name}: {failure.Message}");
}
exitCode = 1;
}
}
catch (Exception ex)
{
Console.Error.WriteLine ($"Shutdown failed: {ex.Message}");
exitCode = 1;
}
// A failed window disposal must not skip the application's own Terminal.Gui teardown,
// which is what restores the terminal (raw mode, alternate screen, cursor).
try
{
window.Dispose ();
}
catch (Exception ex)
{
Console.Error.WriteLine ($"Window disposal failed: {ex.Message}");
exitCode = 1;
}
}
try
{
app?.Dispose ();
}
catch (Exception ex)
{
Console.Error.WriteLine ($"Application disposal failed: {ex.Message}");
exitCode = 1;
}
}
Environment.ExitCode = exitCode;
return;
static IBackend SelectBackend (string [] args)
{
bool wantMock = args.Any (a => a is "--mock" or "-m" or "--smoke");
bool wantCli = args.Any (a => a is "--cli");
bool wantCom = args.Any (a => a is "--com");
if (wantMock)
{
return new MockBackend ();
}
#if WINGET_COM
// Precedence: an explicit --cli always wins over --com (and over the Windows COM default).
// So COM is chosen only when --cli was NOT passed and either --com was, or we're the
// default on Windows.
if (!wantCli && (wantCom || OperatingSystem.IsWindows ()))
{
try
{
return new ComBackend ();
}
catch (Exception ex)
{
// COM server not registered / activation failed — degrade gracefully rather than crash.
// The stderr note is immediately painted over by the TUI redraw (invisible in practice),
// so also stash the reason for the in-app Help dialog ('?') to surface.
StartupDiagnostics.ComFallbackReason = $"0x{(uint) ex.HResult:X8} — {ex.Message}";
Console.Error.WriteLine ($"COM backend unavailable ({ex.Message}); falling back to the CLI backend.");
}
}
#else
if (wantCom)
{
Console.Error.WriteLine ("--com is only available in the Windows build (net10.0-windows…); using the CLI backend instead.");
}
#endif
// CLI path (explicit --cli, or the non-Windows / COM-unavailable default).
if (!IsWingetAvailable ())
{
if (!wantCli)
{
Console.Error.WriteLine ("winget not found on PATH — falling back to mock backend. Run with `winget` available to drive the real CLI.");
}
else
{
Console.Error.WriteLine ("winget not found on PATH — mock backend used despite --cli.");
}
return new MockBackend ();
}
return new CliBackend ();
}
static bool IsWingetAvailable ()
{
try
{
(int code, _) = CliBackend.ProbeWingetAsync (TimeSpan.FromMilliseconds (1500), CancellationToken.None)
.GetAwaiter ()
.GetResult ();
return code == 0;
}
catch
{
return false;
}
}
namespace WinGetScout
{
/// <summary>
/// Startup-time diagnostics carried into the running app. Currently just the reason a requested
/// COM backend fell back to CLI — the stderr note is invisible (overdrawn by the TUI), so the
/// Help dialog surfaces this so a Windows user can see *why* the badge reads "CLI".
/// </summary>
internal static class StartupDiagnostics
{
public static string? ComFallbackReason;
}
}