-
Notifications
You must be signed in to change notification settings - Fork 1.7k
VS Code WinGet log viewer tool #6149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| --- | ||
| applyTo: "tools/WinGetLogViewer/**" | ||
| --- | ||
| # WinGet Log Viewer — Copilot Instructions | ||
|
|
||
| This is a VS Code extension that renders WinGet diagnostic log files in a rich WebView custom editor. | ||
|
|
||
| ## Log Format | ||
|
|
||
| ``` | ||
| YYYY-MM-DD HH:MM:SS.mmm <L> [CHAN ] message | ||
| YYYY-MM-DD HH:MM:SS.mmm <L> [CHAN ] [SUBCHAN] message ← subchannel variant | ||
| ``` | ||
|
|
||
| - `<L>` is optional; values: `V`=Verbose, `I`=Info, `W`=Warning, `E`=Error, `C`=Critical | ||
| - Channel is padded to 8 chars with spaces inside the brackets | ||
| - Subchannel: when a sub-component routes logs through a parent, its original `[CHAN]` tag appears at the start of `message` and is treated as a subchannel | ||
| - Older files without `<L>` are also supported | ||
| - Main parse regex (used in both `src/logParser.ts` and `media/viewer.js`): | ||
| ``` | ||
| /^(\d{2,4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s+(?:<([VIWEC])>\s+)?\[([A-Z]{2,8})\s*\]\s?(.*)$/ | ||
| ``` | ||
|
|
||
| ## Architecture | ||
|
|
||
| ``` | ||
| src/ | ||
| extension.ts — activate/deactivate; registers provider and openFile command | ||
| logParser.ts — parses raw text → LogEntry[]; compiled to out/logParser.js | ||
| logViewerProvider.ts — CustomTextEditorProvider; manages WebView lifecycle, follow mode | ||
| media/ | ||
| viewer.html — WebView HTML template (nonces injected at runtime for CSP) | ||
| viewer.css — All styles; --row-height CSS var must match ROW_HEIGHT in viewer.js | ||
| viewer.js — All client-side logic (not compiled — served directly) | ||
| syntaxes/ | ||
| winget-log.tmLanguage.json — TextMate grammar for standard editor syntax highlighting | ||
| ``` | ||
|
|
||
| ## Virtual Scroll Architecture | ||
|
|
||
| - All rows are **uniform height** (`ROW_HEIGHT = 20` px in `viewer.js`, `--row-height: 20px` in `viewer.css`) — these **must stay in sync** | ||
| - `displayRows[]` is a flat array built from filtered log entries; continuation lines are promoted to full rows with parent metadata replicated | ||
| - `logSpacerTop` and `logSpacerBot` are `<div>` elements whose `.style.height` creates the illusion of the full list | ||
| - `overflow-anchor: none` on `#log-container` is **critical** — without it, when `logSpacerTop` first grows from 0, CSS scroll anchoring cascades and jumps the view to the end | ||
| - `scheduledRender` must be declared on its **own line** (not embedded in a comment) or it will be silently undefined | ||
|
|
||
| ## WebView CSP Rules | ||
|
|
||
| - No inline `style="..."` attributes in HTML — **always use CSS classes or `element.style.property = value`** via CSSOM | ||
| - Script/style nonces are replaced at runtime in `logViewerProvider.ts` | ||
| - After any edit to `media/viewer.js`, validate syntax: `node --check media/viewer.js` | ||
|
|
||
| ## Follow Mode | ||
|
|
||
| - Uses **Node.js `fs.watchFile({ interval: 500, persistent: false })`**, not VS Code's `createFileSystemWatcher` | ||
| - Reason: `ReadDirectoryChangesW` (used by VS Code's watcher) misses events when another process holds the file open — WinGet keeps log files open while writing | ||
| - `fs.unwatchFile()` is called in `panel.onDidDispose()` to clean up | ||
| - Both `resolveCustomEditor` and `openFile` code paths set up the watcher | ||
|
|
||
| ## Ready Handshake | ||
|
|
||
| The WebView sends `{ type: 'ready' }` when its JS has loaded. `logViewerProvider.ts` waits for this message before calling `sendLog()`. This prevents `postMessage` being dropped before the listener is registered. | ||
|
|
||
| ## Channel Colors | ||
|
|
||
| | Channel | Color (CSS class) | Emoji proxy in README | | ||
| |---------|-------------------|-----------------------| | ||
| | `FAIL` | red | 🔴 | | ||
| | `CLI` | blue | 🔵 | | ||
| | `SQL` | purple | 🟣 | | ||
| | `REPO` | light-blue/cyan | 🩵 | | ||
| | `YAML` | yellow | 🟡 | | ||
| | `CORE` | white/light-gray | ⬜ | | ||
| | `TEST` | green | 🟢 | | ||
| | `CONF` | gray | 🩶 | | ||
| | `WORK` | orange | 🟠 | | ||
|
Comment on lines
+68
to
+76
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It bothers me that not all the emoji are the same shame... |
||
|
|
||
| ## Build & Package | ||
|
|
||
| ```bash | ||
| # Compile TypeScript (required after any .ts change) | ||
| npx tsc | ||
|
|
||
| # Build + package VSIX | ||
| npm run package # → winget-log-viewer-<version>.vsix | ||
|
|
||
| # Install locally | ||
| code --install-extension winget-log-viewer-0.1.0.vsix | ||
| ``` | ||
|
|
||
| ## .vscodeignore Notes | ||
|
|
||
| - `out/` must **NOT** be in `.vscodeignore` — the compiled JS lives there and is the extension entrypoint | ||
| - `src/`, `tsconfig.json`, `out/**/*.map` are excluded (source only, not needed at runtime) | ||
|
|
||
| ## Related C++ Files | ||
|
|
||
| The C++ logging infrastructure (in the parent repo) writes the `<L>` level marker: | ||
| - `src/AppInstallerCommonCore/FileLogger.cpp` — `ToLogLine()` writes `<L> ` between timestamp and `[CHAN]` | ||
| - `src/AppInstallerSharedLib/AppInstallerLogging.h/.cpp` — `GetLevelChar(Level)` returns V/I/W/E/C | ||
| - `src/AppInstallerCLICore/Commands/DebugCommand.h/.cpp` — `LogViewerTestCommand` exercises all viewer features (`winget debug log-viewer [--follow]`) | ||
|
|
||
| ## Known Gotchas | ||
|
|
||
| - **Scroll jump at 8–9 wheel clicks**: caused by `overflow-anchor` — fixed with `overflow-anchor: none` on `#log-container` | ||
| - **Follow mode not updating**: VS Code's file watcher uses `ReadDirectoryChangesW` and misses events when WinGet holds the file open — use `fs.watchFile()` polling instead | ||
| - **`postMessage` dropped on open**: webview JS may not be listening yet — always use the ready-handshake | ||
| - **Orphaned JS fragments**: regex replacements can leave stale code that causes silent runtime errors — always validate with `node --check media/viewer.js` | ||
| - **CRLF logs**: the parser handles both `\r\n` and `\n` line endings; trailing empty lines are discarded | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,6 +62,7 @@ namespace AppInstaller::CLI | |
| std::make_unique<DumpErrorResourceCommand>(FullName()), | ||
| std::make_unique<ShowSixelCommand>(FullName()), | ||
| std::make_unique<ProgressCommand>(FullName()), | ||
| std::make_unique<LogViewerTestCommand>(FullName()), | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -365,6 +366,134 @@ namespace AppInstaller::CLI | |
| context.Reporter.Info() << context.Args.GetArg(WINGET_DEBUG_PROGRESS_POST) << std::endl; | ||
| } | ||
| } | ||
|
|
||
| // ── LogViewerTestCommand ───────────────────────────────────────────────────── | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have these type of comments elsewhere? |
||
|
|
||
| #define WINGET_DEBUG_LOG_VIEWER_FOLLOW Args::Type::Force | ||
|
|
||
| std::vector<Argument> LogViewerTestCommand::GetArguments() const | ||
| { | ||
| return { | ||
| Argument{ "follow", 'f', WINGET_DEBUG_LOG_VIEWER_FOLLOW, Resource::String::SourceListUpdatedNever, ArgumentType::Flag }, | ||
| }; | ||
| } | ||
|
|
||
| Resource::LocString LogViewerTestCommand::ShortDescription() const | ||
| { | ||
| return Utility::LocIndString("Emit test logs for the log viewer extension"sv); | ||
| } | ||
|
|
||
| Resource::LocString LogViewerTestCommand::LongDescription() const | ||
| { | ||
| return Utility::LocIndString( | ||
| "Emits log entries exercising every channel, level, subchannel, continuation line, " | ||
| "and long-line feature of the WinGet Log Viewer VS Code extension. " | ||
| "Use --follow to stream additional log lines every 3 seconds (up to 100 iterations)."sv); | ||
| } | ||
|
|
||
| void LogViewerTestCommand::ExecuteInternal(Execution::Context& context) const | ||
| { | ||
| // Ensure all channels and the most verbose level are active so every test entry lands in the file. | ||
| auto& logger = AppInstaller::Logging::Log(); | ||
| logger.EnableChannel(AppInstaller::Logging::Channel::All); | ||
| logger.SetLevel(AppInstaller::Logging::Level::Verbose); | ||
|
|
||
| // ── All five levels on CLI ──────────────────────────────────────────── | ||
| AICLI_LOG(CLI, Verbose, << "Log viewer test: Verbose level message"); | ||
| AICLI_LOG(CLI, Info, << "Log viewer test: Info level message"); | ||
| AICLI_LOG(CLI, Warning, << "Log viewer test: Warning level message"); | ||
| AICLI_LOG(CLI, Error, << "Log viewer test: Error level message"); | ||
| AICLI_LOG(CLI, Crit, << "Log viewer test: Critical level message"); | ||
|
|
||
| // ── One Info entry on every channel ────────────────────────────────── | ||
| AICLI_LOG(Fail, Info, << "Log viewer test: Failure channel"); | ||
| AICLI_LOG(SQL, Info, << "Log viewer test: SQL channel"); | ||
| AICLI_LOG(Repo, Info, << "Log viewer test: Repository channel"); | ||
| AICLI_LOG(YAML, Info, << "Log viewer test: YAML channel"); | ||
| AICLI_LOG(Core, Info, << "Log viewer test: Core channel"); | ||
| AICLI_LOG(Test, Info, << "Log viewer test: Test channel"); | ||
| AICLI_LOG(Config, Info, << "Log viewer test: Configuration channel"); | ||
| AICLI_LOG(Workflow, Info, << "Log viewer test: Workflow channel"); | ||
|
|
||
| // ── Subchannel simulation (sub-component logs routed through CLI) ───── | ||
| AICLI_LOG(CLI, Info, << "[SQL ] Subchannel test: database query initiated for package lookup"); | ||
| AICLI_LOG(CLI, Info, << "[REPO] Subchannel test: fetching package metadata from remote source"); | ||
|
|
||
| // ── Continuation lines (newlines in the message become continuation rows) ── | ||
| AICLI_LOG(Core, Warning, << "Package installation encountered multiple issues:\n" | ||
| " - Dependency 'vcredist' version 14.0.30704 not found in any configured source\n" | ||
| " - Insufficient disk space on C:\\ (requires 512 MB, available 203 MB)\n" | ||
| " - Installation directory is read-only: C:\\Program Files\\TestPackage\\1.0.0"); | ||
|
|
||
| // ── Long line (should require horizontal scroll or wrap in the viewer) ─ | ||
| AICLI_LOG(Workflow, Info, << "Resolving full package dependency graph: The following packages are required " | ||
| "as dependencies and will be installed in sequence if not already present on the system: " | ||
| "Microsoft.VCRedist.2015+.x64 (>= 14.0.30704), Microsoft.DotNet.Runtime.7 (>= 7.0.14), " | ||
| "Microsoft.WebView2.Runtime (>= 113.0.1774.35), Microsoft.WindowsAppRuntime.1.4 (>= 1.4.231219000). " | ||
| "Total estimated download size: 847 MB across 4 installers."); | ||
|
Comment on lines
+419
to
+433
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see why you would use test strings that look like real logs instead of just quoting the classics like the bee movie script
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 |
||
|
|
||
| context.Reporter.Info() << "Log viewer test burst complete. Open the WinGet log file to review all viewer features." << std::endl; | ||
|
|
||
| if (!context.Args.Contains(WINGET_DEBUG_LOG_VIEWER_FOLLOW)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // ── Follow mode: stream log lines every 3 seconds ──────────────────── | ||
| context.Reporter.Info() << "Follow mode active (up to 100 iterations). Press Ctrl-C to stop." << std::endl; | ||
|
|
||
| struct FollowEntry { AppInstaller::Logging::Channel Channel; AppInstaller::Logging::Level Level; std::string_view Message; }; | ||
| static constexpr FollowEntry s_entries[] = | ||
| { | ||
| { AppInstaller::Logging::Channel::CLI, AppInstaller::Logging::Level::Info, "Follow: searching for available package updates" }, | ||
| { AppInstaller::Logging::Channel::Repo, AppInstaller::Logging::Level::Info, "Follow: refreshing source index from remote endpoint" }, | ||
| { AppInstaller::Logging::Channel::SQL, AppInstaller::Logging::Level::Verbose, "Follow: executing SELECT query on packages table" }, | ||
| { AppInstaller::Logging::Channel::Core, AppInstaller::Logging::Level::Info, "Follow: applying version comparison for upgrade eligibility" }, | ||
| { AppInstaller::Logging::Channel::YAML, AppInstaller::Logging::Level::Verbose, "Follow: parsing manifest for Microsoft.TestPackage 2.1.0" }, | ||
| { AppInstaller::Logging::Channel::Workflow, AppInstaller::Logging::Level::Info, "Follow: evaluating installer selection policy for current architecture" }, | ||
| { AppInstaller::Logging::Channel::Config, AppInstaller::Logging::Level::Info, "Follow: reading configuration resource state from DSC provider" }, | ||
| { AppInstaller::Logging::Channel::CLI, AppInstaller::Logging::Level::Warning, "Follow: package is pinned to a specific version, skipping upgrade" }, | ||
| { AppInstaller::Logging::Channel::Core, AppInstaller::Logging::Level::Info, "Follow: SHA-256 hash verification passed for downloaded installer" }, | ||
| { AppInstaller::Logging::Channel::CLI, AppInstaller::Logging::Level::Info, "[SQL ] Follow: subchannel activity routed through CLI during follow" }, | ||
| }; | ||
|
|
||
| auto progress = context.Reporter.BeginAsyncProgress(true); | ||
|
|
||
| for (int i = 1; i <= 100; ++i) | ||
| { | ||
| // Wait 3 seconds, checking for cancellation every 100 ms. | ||
| for (int t = 0; t < 30; ++t) | ||
| { | ||
| if (progress->Callback().IsCancelledBy(CancelReason::Any)) { return; } | ||
| std::this_thread::sleep_for(100ms); | ||
| } | ||
|
|
||
| // Emit the cycling entry for this iteration. | ||
| const auto& e = s_entries[static_cast<size_t>(i) % ARRAYSIZE(s_entries)]; | ||
| const std::string msg = std::string(e.Message) + " [" + std::to_string(i) + "/100]"; | ||
| logger.Write(e.Channel, e.Level, msg); | ||
|
|
||
| // Every 5 iterations: multi-line status summary (continuation lines). | ||
| if (i % 5 == 0) | ||
| { | ||
| AICLI_LOG(Core, Info, << "Follow iteration " << i << " status summary:\n" | ||
| " Packages checked: " << (i * 7) << "\n" | ||
| " Updates available: " << (i % 3) << "\n" | ||
| " Sources refreshed: 2"); | ||
| } | ||
|
|
||
| // Every 20 iterations: simulated error with a stack trace (continuation lines + HRESULT). | ||
| if (i % 20 == 0) | ||
| { | ||
| AICLI_LOG(Fail, Error, << "Simulated transient error at follow iteration " << i << " [HRESULT 0x80070005]:\n" | ||
| " at AppInstaller::Repository::SourceList::OpenSource(std::string_view)\n" | ||
| " at AppInstaller::CLI::Workflow::OpenSourcesForSearch(Context&)\n" | ||
| " at AppInstaller::CLI::LogViewerTestCommand::ExecuteInternal(Context&)"); | ||
| } | ||
|
Comment on lines
+476
to
+492
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You had the perfect opportunity to do FizzBuzz in your real job and you didn't take it... |
||
| } | ||
|
|
||
| context.Reporter.Info() << "Follow mode complete (100 iterations)." << std::endl; | ||
| } | ||
| } | ||
|
|
||
| #endif | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,6 +85,20 @@ namespace AppInstaller::CLI | |
| protected: | ||
| void ExecuteInternal(Execution::Context& context) const override; | ||
| }; | ||
| // Tests the log viewer extension by emitting logs that exercise all channels, levels, subchannels, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: add blank line before |
||
| // continuation lines, long lines, and optionally a streaming follow mode. | ||
| struct LogViewerTestCommand final : public Command | ||
| { | ||
| LogViewerTestCommand(std::string_view parent) : Command("log-viewer", {}, parent) {} | ||
|
|
||
| std::vector<Argument> GetArguments() const override; | ||
|
|
||
| Resource::LocString ShortDescription() const override; | ||
| Resource::LocString LongDescription() const override; | ||
|
|
||
| protected: | ||
| void ExecuteInternal(Execution::Context& context) const override; | ||
| }; | ||
| } | ||
|
|
||
| #endif | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this file in particular?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I told it to leave instructions for itself 🤷