Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ COMGLB
commandline
compressapi
concurrencysal
Consolas
constexpr
contactsupport
contentfiles
Expand Down Expand Up @@ -204,6 +205,7 @@ GHS
github
gitlab
gity
Gotchas
goku
GRPICONDIR
GRPICONDIRENTRY
Expand Down Expand Up @@ -236,6 +238,7 @@ hybridcrt
Hyperlink
IARP
IAttachment
ical
ICONDIR
ICONDIRENTRY
ICONIMAGE
Expand Down Expand Up @@ -340,6 +343,7 @@ minidump
MINORVERSION
missingdependency
mkgmtime
mmm
MMmmbbbb
MODULEENTRY
mof
Expand Down Expand Up @@ -519,6 +523,7 @@ servercertificate
setmetadatabymanifestid
SETTINGCHANGE
SETTINGMAPPING
sev
sfs
sfsclient
SHCONTF
Expand Down Expand Up @@ -553,6 +558,7 @@ storeapps
storeorigin
STRRET
stylecop
SUBCHAN
subdir
subkey
Sudarshan
Expand Down Expand Up @@ -617,9 +623,12 @@ uswgp
uwp
VALUENAMECASE
vclib
vcredist
versioned
VERSIONINFO
VIWEC
vns
vscodeignore
vsconfig
vstest
waitable
Expand Down Expand Up @@ -670,13 +679,15 @@ xcopy
Xes
XFile
XManifest
Xms
XMUGIWARAMODULE
XName
XPLATSTR
XRESOURCEZORO
xsi
yamato
yao
Zabcdefghijklmnopqrstuvwxyz
Zanollo
ZIPHASH
zoro
109 changes: 109 additions & 0 deletions .github/instructions/winget-log-viewer.instructions.md
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`
Copy link
Copy Markdown
Member

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?

Copy link
Copy Markdown
Member Author

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 🤷


## 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,6 @@ src/PowerShell/scripts/Module

# Interop nuget
src/WinGetUtilInterop/scripts/Nuget*

# VS Code extension packages
*.vsix
129 changes: 129 additions & 0 deletions src/AppInstallerCLICore/Commands/DebugCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
});
}

Expand Down Expand Up @@ -365,6 +366,134 @@ namespace AppInstaller::CLI
context.Reporter.Info() << context.Args.GetArg(WINGET_DEBUG_PROGRESS_POST) << std::endl;
}
}

// ── LogViewerTestCommand ─────────────────────────────────────────────────────
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
14 changes: 14 additions & 0 deletions src/AppInstallerCLICore/Commands/DebugCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
8 changes: 4 additions & 4 deletions src/AppInstallerCommonCore/FileLogger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ namespace AppInstaller::Logging
static constexpr std::string_view s_fileLoggerDefaultFileExt = ".log"sv;

// Send to a string first to create a single block to write to a file.
std::string ToLogLine(Channel channel, std::string_view message)
std::string ToLogLine(Channel channel, Level level, std::string_view message)
{
std::stringstream strstr;
strstr << std::chrono::system_clock::now() << " [" << std::setw(GetMaxChannelNameLength()) << std::left << std::setfill(' ') << GetChannelName(channel) << "] " << message;
strstr << std::chrono::system_clock::now() << " <" << GetLevelChar(level) << "> [" << std::setw(GetMaxChannelNameLength()) << std::left << std::setfill(' ') << GetChannelName(channel) << "] " << message;
return std::move(strstr).str();
}

Expand Down Expand Up @@ -94,7 +94,7 @@ namespace AppInstaller::Logging

void FileLogger::Write(Channel channel, Level level, std::string_view message) noexcept try
{
std::string log = ToLogLine(channel, message);
std::string log = ToLogLine(channel, level, message);
WriteDirect(channel, level, log);
}
catch (...) {}
Expand Down Expand Up @@ -229,6 +229,6 @@ namespace AppInstaller::Logging
{
m_stream.seekp(m_headersEnd);
// Yes, we may go over the size limit slightly due to this and the unaccounted for newlines
m_stream << ToLogLine(Channel::Core, "--- log file has wrapped ---") << std::endl;
m_stream << ToLogLine(Channel::Core, Level::Info, "--- log file has wrapped ---") << std::endl;
}
}
Loading
Loading