diff --git a/CHANGELOG.md b/CHANGELOG.md index b1bdf4f..0366594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable user-facing changes are documented here. This project follows [Seman ## [Unreleased] +## [1.3.1] - 2026-07-13 + +### Fixed + +- Quarantine the complete WDDM dedicated-memory snapshot when Windows returns an impossible per-process value, preventing smaller stale values from being presented as trustworthy VRAM usage. +- Detach the installer helper from the terminal and stop relaunching a competing foreground monitor after the invoking shell regains keyboard input. +- Remove the persistent `Click headers to sort` hint from the interactive footer. + +### Security and quality + +- Return `null`/blank dedicated-memory values in JSON and CSV whenever the affected WDDM snapshot is quarantined. +- Exercise snapshot-wide WDDM quarantine and verify that live update handoff never relaunches an input-competing monitor. + ## [1.3.0] - 2026-07-13 ### Added diff --git a/README.md b/README.md index d421b57..53f99c2 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ It is a real full-screen terminal application, not a wrapper around `nvidia-smi` - Pause/resume and adjustable 250-5000 ms refresh interval - CSV logging - One-shot human-readable and JSON output for scripts -- Automatic, checksum-verified updates for installed interactive launches +- Automatic, checksum-verified background updates for installed interactive launches - Statically linked MSVC runtime; no NVML SDK or third-party runtime required ## Runtime requirements and compatibility @@ -63,7 +63,7 @@ vgpu --json Existing terminal processes keep their old environment. Uninstalling VGPU-Mon from Windows Settings removes the PATH entry only when the installer originally added it. -Starting with VGPU-Mon 1.2.0, an installed interactive launch checks the latest stable release's small `version.txt` manifest. If a newer version exists, VGPU-Mon downloads that exact versioned installer, verifies its SHA-256 with Windows cryptography, installs it without elevation, and reopens with the same interactive options. Network or GitHub failures do not prevent the monitor from opening. Redirected output and script commands (`--json`, `--once`, `--version`, and `--demo`) never perform an automatic update check. +Starting with VGPU-Mon 1.2.0, an installed interactive launch checks the latest stable release's small `version.txt` manifest. If a newer version exists, VGPU-Mon downloads that exact versioned installer, verifies its SHA-256 with Windows cryptography, and installs it without elevation. The command then returns cleanly; run `vgpu` again after a few seconds to start the updated monitor. The updater deliberately does not relaunch inside the old terminal because the shell would already own that terminal's input. Network or GitHub failures do not prevent the monitor from opening. Redirected output and script commands (`--json`, `--once`, `--version`, and `--demo`) never perform an automatic update check. Use `vgpu --update` to check immediately. Use `--no-update` for one launch or set `VGPU_MON_NO_UPDATE=1` to disable automatic checks. Portable ZIP copies do not update silently; `vgpu --update` converts a portable copy into the normal per-user installation. @@ -211,9 +211,9 @@ Options: - **DXGI 1.4** enumerates hardware adapters and reports local video-memory usage and the current OS memory budget. - **NVML** supplies NVIDIA board telemetry. VGPU-Mon uses the versioned memory API to separate driver/firmware-reserved VRAM from application allocation. `nvml.dll` is discovered and loaded at runtime; the project does not ship NVIDIA libraries. -VRAM figures from NVML, DXGI, and WDDM can differ because they describe different layers: physical board usage, the operating-system budget, and per-process commitments. A WDDM process commitment can exceed physical VRAM because it is not a resident-byte measurement. VGPU-Mon marks process columns with `*`, labels JSON/CSV fields as commitments, and keeps the physical total and OS budget separate. +VRAM figures from NVML, DXGI, and WDDM can differ because they describe different accounting layers. Per-process WDDM values include memory shared across processes, so adding every row can exceed the physical total even though a valid individual dedicated-memory reading cannot. VGPU-Mon marks process columns with `*`, labels JSON/CSV fields as commitments, and keeps the physical total and OS budget separate. -Windows also has a documented GPU Process Memory counter issue that can produce implausibly large per-process values. VGPU-Mon appends `!`, raises `wddm_process_memory_warning` in JSON, and preserves the raw value for diagnosis instead of silently clamping it. Physical board usage at the top remains sourced from NVML/DXGI. +Windows also has a documented GPU Process Memory counter issue that accumulates stale allocations and can produce implausibly large per-process values. If one process's dedicated value exceeds the GPU's physical total, VGPU-Mon quarantines that snapshot's entire dedicated column: every row displays `N/A !`, CSV fields are blank, JSON values are `null`, and `wddm_process_memory_warning` is raised. The complete snapshot is quarantined because smaller values from the same affected counter source can look plausible while still being stale. Physical board usage at the top remains sourced from NVML/DXGI. Terminating a process is not the same as resetting a GPU. VGPU-Mon deliberately does not expose driver resets or unsafe attempts to cancel individual command queues. diff --git a/docs/architecture.md b/docs/architecture.md index 8331aba..919e63b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ # Architecture -VGPU-Mon is a native Windows application. It has no service, kernel component, injected DLL, persistent background updater, or vendor SDK dependency. An installed interactive launch performs a bounded HTTPS update check before provider initialization; a verified update uses a short-lived PowerShell handoff only after the monitor exits. +VGPU-Mon is a native Windows application. It has no service, kernel component, injected DLL, persistent background updater, or vendor SDK dependency. An installed interactive launch performs a bounded HTTPS update check before provider initialization; a verified update uses a short-lived, console-detached PowerShell handoff only after the monitor exits. ## Data flow @@ -10,7 +10,7 @@ VGPU-Mon is a native Windows application. It has no service, kernel component, i 4. The sampler normalizes those sources into one `GpuTelemetry` snapshot plus bounded process rows and chart histories. 5. The renderer builds one bounded frame in memory and repaints the visible terminal region in a single write. -The updater reads `version.txt` from the latest stable GitHub Release, validates its strict version, installer, and hash fields, and compares semantic versions. It downloads an exact tag-specific installer to the user temporary directory and hashes it with Windows CNG before starting a transient installer helper. Offline, malformed, oversized, downgraded, or hash-mismatched responses fail closed for the update and fail open for monitoring. Non-interactive output paths do not access the network. +The updater reads `version.txt` from the latest stable GitHub Release, validates its strict version, installer, and hash fields, and compares semantic versions. It downloads an exact tag-specific installer to the user temporary directory and hashes it with Windows CNG before starting a transient installer helper. The helper has no inherited console handles and never relaunches a second foreground process after the invoking shell regains input ownership. Offline, malformed, oversized, downgraded, or hash-mismatched responses fail closed for the update and fail open for monitoring. Non-interactive output paths do not access the network. `--demo` replaces steps 1–3 with deterministic values while keeping the real sampling, layout, JSON, input, and rendering paths. It exists for testing and UI previews; it is never presented as hardware telemetry. @@ -28,7 +28,7 @@ The interactive UI uses Windows console input records and VT output. Every updat ## Accounting caveats -NVML physical allocation, DXGI OS budget/usage, and WDDM per-process commitments describe different layers and do not necessarily sum. The UI labels commitments explicitly and preserves implausible WDDM values with a warning instead of disguising a Windows counter anomaly. +NVML physical allocation, DXGI OS budget/usage, and WDDM per-process commitments describe different layers and do not necessarily sum. Microsoft documents that GPU Process Memory can accumulate stale allocations. If one dedicated row exceeds the physical GPU total, the entire dedicated snapshot is reported as unavailable: once the counter source is demonstrably corrupt, smaller rows cannot be distinguished safely from stale values. The displayed per-process GPU percentage is the busiest engine for that process, matching the accounting style used by Windows Task Manager. It is not additive across dissimilar engines. diff --git a/src/main.c b/src/main.c index 983a978..a8e9b20 100644 --- a/src/main.c +++ b/src/main.c @@ -554,9 +554,11 @@ static void log_sample(App *app) { } sanitize_csv_field(process->name, process_name, sizeof(process_name)); sanitize_csv_field(process->engine, engine, sizeof(engine)); - fprintf(app->log_file, "%lu,\"%s\",%.2f,%llu,%llu,\"%s\"\n", - (unsigned long)process->pid, process_name, process->gpu_percent, - (unsigned long long)process->dedicated_bytes, + fprintf(app->log_file, "%lu,\"%s\",%.2f,", + (unsigned long)process->pid, process_name, process->gpu_percent); + if (!process->dedicated_memory_invalid) + fprintf(app->log_file, "%llu", (unsigned long long)process->dedicated_bytes); + fprintf(app->log_file, ",%llu,\"%s\"\n", (unsigned long long)process->shared_bytes, engine); } if (fflush(app->log_file) != 0 || ferror(app->log_file)) { @@ -681,15 +683,13 @@ static bool sample_app(App *app) { } else { app->process_count = 0; } - app->wddm_process_memory_suspect = false; - if (app->telemetry.memory_total) { - for (size_t i = 0; i < app->process_count; ++i) { - if (app->processes[i].dedicated_bytes > app->telemetry.memory_total) { - app->wddm_process_memory_suspect = true; - break; - } - } - } + /* Microsoft documents that one bad GPU Process Memory value means the + affected counter source is accumulating stale allocations. There is no + trustworthy way to identify the other poisoned rows, so quarantine the + dedicated column for the whole snapshot instead of allowing a smaller + but equally stale value (for example DWM) to look real. */ + app->wddm_process_memory_suspect = quarantine_invalid_dedicated_gpu_memory( + app->processes, app->process_count, app->telemetry.memory_total); update_visible_processes(app); add_history_sample(app); log_sample(app); @@ -835,8 +835,9 @@ static void render_help(TextBuffer *buffer) { " Home/End Oldest retained/live Hover Exact point value\n\n" "%sAccounting%s\n" " Process GPU %% is the busiest Windows GPU engine for that PID, matching Task Manager's\n" - " model. Dedicated/shared values come from WDDM counters. A trailing ! marks a value\n" - " larger than physical VRAM, a known Windows counter anomaly rather than residency.\n" + " model. Dedicated/shared values come from WDDM counters. If Windows returns one\n" + " impossible dedicated value, the full dedicated snapshot is shown as N/A because\n" + " other rows can contain the same documented stale-allocation counter error.\n" " Board telemetry comes from NVML; DXGI supplies a vendor-neutral memory fallback.\n", ANSI_BOLD, ANSI_RESET, ANSI_BOLD, ANSI_RESET, ANSI_BOLD, ANSI_RESET); } @@ -955,10 +956,11 @@ static void render_process_table(App *app, TextBuffer *buffer, int columns, int char pid[32], gpu[32], dedicated[32], shared[32]; snprintf(pid, sizeof(pid), "%lu", (unsigned long)process->pid); snprintf(gpu, sizeof(gpu), "%.1f%%", process->gpu_percent); - format_bytes(process->dedicated_bytes, dedicated, sizeof(dedicated)); + if (process->dedicated_memory_invalid) + snprintf(dedicated, sizeof(dedicated), "N/A !"); + else + format_bytes(process->dedicated_bytes, dedicated, sizeof(dedicated)); format_bytes(process->shared_bytes, shared, sizeof(shared)); - if (app->telemetry.memory_total && process->dedicated_bytes > app->telemetry.memory_total) - strncat_s(dedicated, sizeof(dedicated), " !", _TRUNCATE); if (row == app->selection) text_buffer_append(buffer, "%s", ANSI_REVERSE); for (int i = 0; i < layout.indent; ++i) text_buffer_append(buffer, " "); @@ -1416,7 +1418,7 @@ static void render_app(App *app, TextBuffer *buffer) { char footer[512], clipped[512]; const char *health = !app->pdh_ready && !app->demo_mode ? "! WDDM counters unavailable | " : app->wddm_process_memory_suspect ? "! WDDM process-memory anomaly | " : ""; - snprintf(footer, sizeof(footer), "%sClick headers to sort | %s %s | %zu processes | filter: %s | c chart | h help | q quit", + snprintf(footer, sizeof(footer), "%s%s %s | %zu processes | filter: %s | c chart | h help | q quit", health, sort_name(app->sort_mode), app->sort_descending ? "high-low" : "low-high", app->visible_count, app->filter[0] ? app->filter : "none"); @@ -1780,8 +1782,10 @@ static void print_once_json(const App *app) { const GpuProcess *process = app->visible[i]; if (i) putchar(','); printf("{\"pid\":%lu,\"name\":", (unsigned long)process->pid); write_json_string(process->name); - printf(",\"gpu_percent\":%.2f,\"dedicated_commit_bytes\":%llu,\"shared_commit_bytes\":%llu,\"engine\":", - process->gpu_percent, (unsigned long long)process->dedicated_bytes, + printf(",\"gpu_percent\":%.2f,\"dedicated_commit_bytes\":", process->gpu_percent); + if (process->dedicated_memory_invalid) fputs("null", stdout); + else printf("%llu", (unsigned long long)process->dedicated_bytes); + printf(",\"shared_commit_bytes\":%llu,\"engine\":", (unsigned long long)process->shared_bytes); write_json_string(process->engine); putchar('}'); @@ -1805,9 +1809,10 @@ static void print_once_table(const App *app) { const GpuProcess *process = app->visible[i]; char name[64], dedicated[32], shared[32]; truncate_text(process->name, name, sizeof(name), 32); + if (process->dedicated_memory_invalid) snprintf(dedicated, sizeof(dedicated), "N/A !"); + else format_bytes(process->dedicated_bytes, dedicated, sizeof(dedicated)); printf("%-8lu %-32s %7.1f%% %12s %12s %-14s\n", (unsigned long)process->pid, name, - process->gpu_percent, - format_bytes(process->dedicated_bytes, dedicated, sizeof(dedicated)), + process->gpu_percent, dedicated, format_bytes(process->shared_bytes, shared, sizeof(shared)), process->engine); } } @@ -1830,8 +1835,8 @@ static bool parse_chart_metric(const char *name, ChartMetric *metric) { } static void print_help(void) { - printf( - "VGPU-Mon %s - live Windows GPU process monitor\n\n" + fputs( + "VGPU-Mon " VGPU_VERSION " - live Windows GPU process monitor\n\n" "Usage: vgpu [options]\n\n" " --once Print one snapshot and exit\n" " --json Print one snapshot as JSON\n" @@ -1854,7 +1859,7 @@ static void print_help(void) { "Run without options in Windows Terminal for the interactive UI.\n" "Headers are mouse-clickable; click again to reverse the sort.\n" "Charts support wheel zoom, Shift+wheel or arrow-key panning, and exact hover values.\n", - VGPU_VERSION); + stdout); } static bool initialize_app(App *app) { diff --git a/src/updater.c b/src/updater.c index 16bab99..80175cf 100644 --- a/src/updater.c +++ b/src/updater.c @@ -3,7 +3,6 @@ #include #include -#include #include #include #include @@ -382,14 +381,6 @@ static bool executable_is_installed(const wchar_t *executable) { return _wcsicmp(current, directory) == 0; } -static bool get_default_installed_executable(wchar_t *path, size_t capacity) { - wchar_t local_app_data[MAX_PATH]; - if (SHGetFolderPathW(NULL, CSIDL_LOCAL_APPDATA | CSIDL_FLAG_CREATE, NULL, - SHGFP_TYPE_CURRENT, local_app_data) != S_OK) return false; - return _snwprintf_s(path, capacity, _TRUNCATE, - L"%ls\\Programs\\VGPU-Mon\\vgpu.exe", local_app_data) > 0; -} - static bool script_append(ScriptBuffer *buffer, const char *text) { size_t length = strlen(text); if (!buffer || !buffer->data || length > buffer->capacity - buffer->length - 1U) @@ -400,30 +391,7 @@ static bool script_append(ScriptBuffer *buffer, const char *text) { return true; } -static bool script_append_quoted_wide(ScriptBuffer *buffer, const wchar_t *text) { - if (!script_append(buffer, "'")) return false; - int required = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, text, -1, - NULL, 0, NULL, NULL); - if (required <= 0) return false; - char *utf8 = (char *)HeapAlloc(GetProcessHeap(), 0, (size_t)required); - if (!utf8) return false; - bool success = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, text, -1, - utf8, required, NULL, NULL) > 0; - if (success) { - for (char *cursor = utf8; *cursor; ++cursor) { - if (*cursor == '\'') success = script_append(buffer, "''"); - else { - char value[2] = {*cursor, '\0'}; - success = script_append(buffer, value); - } - if (!success) break; - } - } - HeapFree(GetProcessHeap(), 0, utf8); - return success && script_append(buffer, "'"); -} - -static bool write_helper_script(const wchar_t *path, int argc, wchar_t **argv) { +static bool write_helper_script(const wchar_t *path) { ScriptBuffer script; memset(&script, 0, sizeof(script)); script.capacity = UPDATE_HELPER_CAPACITY; @@ -431,41 +399,22 @@ static bool write_helper_script(const wchar_t *path, int argc, wchar_t **argv) { if (!script.data) return false; bool success = script_append(&script, "[CmdletBinding()]\r\n" - "param([Parameter(Mandatory=$true)][string]$InstallerPath," - "[Parameter(Mandatory=$true)][string]$RelaunchPath)\r\n" + "param([Parameter(Mandatory=$true)][string]$InstallerPath)\r\n" "$ErrorActionPreference = 'Stop'\r\n" - "$relaunchArguments = @('--no-update'"); - if (success && updater_is_forced(argc, argv)) { - success = script_append(&script, ",'--version'"); - } - for (int index = 1; success && index < argc; ++index) { - if (wide_argument_equals(argv[index], L"--update") || - wide_argument_equals(argv[index], L"--no-update")) continue; - success = script_append(&script, ",") && - script_append_quoted_wide(&script, argv[index]); - } - success = success && script_append(&script, - ")\r\n" - "$updateError = $null\r\n" + "$exitCode = 1\r\n" "try {\r\n" " Start-Sleep -Milliseconds 750\r\n" " $process = Start-Process -FilePath $InstallerPath -ArgumentList " "@('/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART','/SP-') " "-Wait -PassThru -WindowStyle Hidden\r\n" - " if ($process.ExitCode -ne 0) { throw \"Setup exited with code " - "$($process.ExitCode).\" }\r\n" + " $exitCode = $process.ExitCode\r\n" "}\r\n" - "catch { $updateError = $_.Exception.Message }\r\n" + "catch { $exitCode = 1 }\r\n" "finally {\r\n" " Remove-Item -LiteralPath $InstallerPath -Force -ErrorAction SilentlyContinue\r\n" " Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue\r\n" "}\r\n" - "if ($updateError) { Write-Warning \"VGPU-Mon update failed: $updateError\" }\r\n" - "if (Test-Path -LiteralPath $RelaunchPath) {\r\n" - " & $RelaunchPath @relaunchArguments\r\n" - " exit $LASTEXITCODE\r\n" - "}\r\n" - "exit 1\r\n"); + "exit $exitCode\r\n"); HANDLE file = INVALID_HANDLE_VALUE; if (success) { @@ -488,8 +437,7 @@ static bool write_helper_script(const wchar_t *path, int argc, wchar_t **argv) { return success; } -static bool launch_helper(const wchar_t *helper, const wchar_t *installer, - const wchar_t *relaunch) { +static bool launch_helper(const wchar_t *helper, const wchar_t *installer) { wchar_t system_directory[MAX_PATH], powershell[MAX_PATH]; const size_t command_capacity = 32768U; wchar_t *command = (wchar_t *)HeapAlloc( @@ -502,8 +450,8 @@ static bool launch_helper(const wchar_t *helper, const wchar_t *installer, system_directory) <= 0 || _snwprintf_s(command, command_capacity, _TRUNCATE, L"\"%ls\" -NoLogo -NoProfile -ExecutionPolicy Bypass " - L"-File \"%ls\" -InstallerPath \"%ls\" -RelaunchPath \"%ls\"", - powershell, helper, installer, relaunch) <= 0) { + L"-File \"%ls\" -InstallerPath \"%ls\"", + powershell, helper, installer) <= 0) { HeapFree(GetProcessHeap(), 0, command); return false; } @@ -513,8 +461,12 @@ static bool launch_helper(const wchar_t *helper, const wchar_t *installer, memset(&startup, 0, sizeof(startup)); memset(&process, 0, sizeof(process)); startup.cb = sizeof(startup); - if (!CreateProcessW(powershell, command, NULL, NULL, TRUE, - CREATE_UNICODE_ENVIRONMENT, NULL, NULL, &startup, &process)) { + /* The invoking shell starts reading from the console again as soon as this + process exits. The installer helper must therefore be fully detached and + must never relaunch another foreground console reader in the same tab. */ + if (!CreateProcessW(powershell, command, NULL, NULL, FALSE, + CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, + NULL, NULL, &startup, &process)) { HeapFree(GetProcessHeap(), 0, command); return false; } @@ -527,14 +479,13 @@ static bool launch_helper(const wchar_t *helper, const wchar_t *installer, VgpuUpdateResult updater_check_and_start(int argc, wchar_t **argv, const char *current_version, bool force) { - wchar_t current_executable[MAX_PATH], relaunch_path[MAX_PATH]; + (void)argc; + (void)argv; + wchar_t current_executable[MAX_PATH]; if (!get_current_executable(current_executable, _countof(current_executable))) return VGPU_UPDATE_ERROR; bool installed = executable_is_installed(current_executable); if (!installed && !force) return VGPU_UPDATE_SKIPPED; - if (installed) wcscpy_s(relaunch_path, _countof(relaunch_path), current_executable); - else if (!get_default_installed_executable(relaunch_path, _countof(relaunch_path))) - return VGPU_UPDATE_ERROR; char manifest_text[UPDATE_MAX_MANIFEST_BYTES + 1U]; size_t manifest_size = 0; @@ -576,13 +527,14 @@ VgpuUpdateResult updater_check_and_start(int argc, wchar_t **argv, !sha256_file(installer_path, actual_hash) || memcmp(actual_hash, manifest.sha256, sizeof(actual_hash)) != 0) goto cleanup; installer_ready = true; - if (!write_helper_script(helper_path, argc, argv)) goto cleanup; + if (!write_helper_script(helper_path)) goto cleanup; helper_ready = true; - printf("Updating VGPU-Mon %s to %s; the monitor will reopen automatically...\n", + printf("Updating VGPU-Mon %s to %s in the background. " + "Run vgpu again in a few seconds.\n", current_version, manifest.version); fflush(stdout); - if (!launch_helper(helper_path, installer_path, relaunch_path)) goto cleanup; + if (!launch_helper(helper_path, installer_path)) goto cleanup; SecureZeroMemory(actual_hash, sizeof(actual_hash)); return VGPU_UPDATE_STARTED; diff --git a/src/util.c b/src/util.c index de2817e..3d0f17c 100644 --- a/src/util.c +++ b/src/util.c @@ -88,6 +88,29 @@ bool contains_case_insensitive(const char *haystack, const char *needle) { return false; } +bool dedicated_gpu_memory_plausible(uint64_t bytes, uint64_t physical_total) { + return physical_total == 0 || bytes <= physical_total; +} + +bool quarantine_invalid_dedicated_gpu_memory(GpuProcess *processes, size_t count, + uint64_t physical_total) { + if (!processes || physical_total == 0) return false; + bool invalid = false; + for (size_t i = 0; i < count; ++i) { + if (!dedicated_gpu_memory_plausible(processes[i].dedicated_bytes, + physical_total)) { + invalid = true; + break; + } + } + if (!invalid) return false; + for (size_t i = 0; i < count; ++i) { + processes[i].dedicated_memory_invalid = true; + processes[i].dedicated_bytes = 0; + } + return true; +} + void iso_timestamp(char *buffer, size_t buffer_size) { SYSTEMTIME st; GetLocalTime(&st); diff --git a/src/version.h b/src/version.h index 395d02c..7e57ec0 100644 --- a/src/version.h +++ b/src/version.h @@ -1,7 +1,7 @@ #ifndef VGPU_VERSION_H #define VGPU_VERSION_H -#define VGPU_VERSION "1.3.0" -#define VGPU_VERSION_NUM 1,3,0,0 +#define VGPU_VERSION "1.3.1" +#define VGPU_VERSION_NUM 1,3,1,0 #endif diff --git a/src/vgpu.h b/src/vgpu.h index 1e6bfe9..1855a61 100644 --- a/src/vgpu.h +++ b/src/vgpu.h @@ -54,6 +54,7 @@ typedef struct { double gpu_percent; uint64_t dedicated_bytes; uint64_t shared_bytes; + bool dedicated_memory_invalid; char engine[48]; } GpuProcess; @@ -90,6 +91,9 @@ const char *format_rate(double mib_s, char *buffer, size_t buffer_size); void wide_to_utf8(const wchar_t *source, char *target, size_t target_size); void sanitize_csv_field(const char *source, char *target, size_t target_size); bool contains_case_insensitive(const char *haystack, const char *needle); +bool dedicated_gpu_memory_plausible(uint64_t bytes, uint64_t physical_total); +bool quarantine_invalid_dedicated_gpu_memory(GpuProcess *processes, size_t count, + uint64_t physical_total); void iso_timestamp(char *buffer, size_t buffer_size); bool query_process_details(DWORD pid, ProcessDetails *details); bool terminate_process_safely(DWORD pid, char *message, size_t message_size); diff --git a/tests/test_conpty.c b/tests/test_conpty.c index 4506116..abaf92f 100644 --- a/tests/test_conpty.c +++ b/tests/test_conpty.c @@ -298,19 +298,20 @@ int main(int argc, char **argv) { bool clicked_sort = strstr(capture.data, "GPU ^") != NULL; bool zoomed_chart = strstr(capture.data, "Span 2m00s") != NULL; bool rendered_hover = strstr(capture.data, "Point ") != NULL; + bool no_sort_hint = strstr(capture.data, "Click headers to sort") == NULL; bool no_debug_leaks = strstr(capture.data, "Detected memory leaks!") == NULL; bool passed = SUCCEEDED(resize_small) && SUCCEEDED(resize_large) && wait == WAIT_OBJECT_0 && exit_code == 0 && clears >= 1 && line_erases >= 20 && max_frame_lines <= 29 && max_frame_columns <= 120 && entered_alternate && left_alternate && rendered_chart && clicked_sort && - zoomed_chart && rendered_hover && no_debug_leaks && !capture.overflow; + zoomed_chart && rendered_hover && no_sort_hint && no_debug_leaks && !capture.overflow; if (!passed) { fprintf(stderr, - "ConPTY test failed: exit=%lu wait=%lu clears=%zu line-erases=%zu max-frame-newlines=%zu max-frame-columns=%zu enter=%d leave=%d chart=%d mouse-sort=%d zoom=%d hover=%d no-leaks=%d overflow=%d resize=0x%08lx/0x%08lx bytes=%zu\n", + "ConPTY test failed: exit=%lu wait=%lu clears=%zu line-erases=%zu max-frame-newlines=%zu max-frame-columns=%zu enter=%d leave=%d chart=%d mouse-sort=%d zoom=%d hover=%d no-sort-hint=%d no-leaks=%d overflow=%d resize=0x%08lx/0x%08lx bytes=%zu\n", (unsigned long)exit_code, (unsigned long)wait, clears, line_erases, max_frame_lines, max_frame_columns, entered_alternate, left_alternate, rendered_chart, clicked_sort, - zoomed_chart, rendered_hover, no_debug_leaks, capture.overflow, + zoomed_chart, rendered_hover, no_sort_hint, no_debug_leaks, capture.overflow, (unsigned long)resize_small, (unsigned long)resize_large, capture.length); fputs("--- captured pseudoconsole output ---\n", stderr); fwrite(capture.data, 1, capture.length, stderr); diff --git a/tests/test_core.c b/tests/test_core.c index 008b577..84ef9d6 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -24,6 +24,29 @@ static void test_format_bytes(void) { EXPECT(strcmp(format_bytes(0, buffer, sizeof(buffer)), "0 B") == 0); EXPECT(strcmp(format_bytes(1024, buffer, sizeof(buffer)), "1.00 KiB") == 0); EXPECT(strcmp(format_bytes(1024ULL * 1024ULL * 1024ULL, buffer, sizeof(buffer)), "1.00 GiB") == 0); + EXPECT(dedicated_gpu_memory_plausible(16ULL * 1024ULL * 1024ULL * 1024ULL, + 16ULL * 1024ULL * 1024ULL * 1024ULL)); + EXPECT(!dedicated_gpu_memory_plausible(75ULL * 1024ULL * 1024ULL * 1024ULL, + 16ULL * 1024ULL * 1024ULL * 1024ULL)); + EXPECT(dedicated_gpu_memory_plausible(UINT64_MAX, 0)); + + GpuProcess valid[2] = {{0}}; + valid[0].dedicated_bytes = 9ULL * 1024ULL * 1024ULL * 1024ULL; + valid[1].dedicated_bytes = 512ULL * 1024ULL * 1024ULL; + EXPECT(!quarantine_invalid_dedicated_gpu_memory( + valid, _countof(valid), 16ULL * 1024ULL * 1024ULL * 1024ULL)); + EXPECT(!valid[0].dedicated_memory_invalid && valid[0].dedicated_bytes != 0); + + GpuProcess corrupt[3] = {{0}}; + corrupt[0].dedicated_bytes = 9ULL * 1024ULL * 1024ULL * 1024ULL; + corrupt[1].dedicated_bytes = 75ULL * 1024ULL * 1024ULL * 1024ULL; + corrupt[2].dedicated_bytes = 512ULL * 1024ULL * 1024ULL; + EXPECT(quarantine_invalid_dedicated_gpu_memory( + corrupt, _countof(corrupt), 16ULL * 1024ULL * 1024ULL * 1024ULL)); + for (size_t i = 0; i < _countof(corrupt); ++i) { + EXPECT(corrupt[i].dedicated_memory_invalid); + EXPECT(corrupt[i].dedicated_bytes == 0); + } } static void test_parse_gpu_instance(void) { diff --git a/tests/test_live_update.ps1 b/tests/test_live_update.ps1 index efbd6f8..1453a4e 100644 --- a/tests/test_live_update.ps1 +++ b/tests/test_live_update.ps1 @@ -103,6 +103,12 @@ try { throw "The updater did not install VGPU-Mon $ExpectedVersion before the timeout." } + $unexpectedRelaunch = @(Get-Process -Name 'vgpu' -ErrorAction SilentlyContinue | + Where-Object { $_.Path -and $_.Path -ieq $installedExe }) + if ($unexpectedRelaunch.Count -ne 0) { + throw 'The updater relaunched a foreground monitor after returning terminal ownership.' + } + $remainingTemporaryFiles = @(Wait-UpdaterTemporaryCleanup -Seconds 30) if ($remainingTemporaryFiles.Count -ne 0) { throw "The updater handoff did not finish: $($remainingTemporaryFiles.FullName -join ', ')" @@ -146,4 +152,4 @@ if ($newTemporaryFiles.Count -ne 0) { throw "Updater temporary files were not cleaned: $($newTemporaryFiles.FullName -join ', ')" } -Write-Host "Live automatic update, relaunch, PATH, cleanup, and uninstall tests passed for $ExpectedVersion." +Write-Host "Live automatic update, clean terminal return, PATH, cleanup, and uninstall tests passed for $ExpectedVersion."