Skip to content
Merged
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
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ jobs:
.\tests\test_installer.ps1
-InstallerPath (Get-ChildItem .\dist\VGPU-Mon-*-setup.exe -File).FullName

- name: Stage bootstrap installer
run: Copy-Item .\install.ps1 .\dist\install.ps1
- name: Stage release metadata and checksums
run: .\tools\stage-release-metadata.ps1

- name: Upload release candidates
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand All @@ -68,5 +68,7 @@ jobs:
dist/*.zip
dist/*-setup.exe
dist/install.ps1
dist/version.txt
dist/SHA256SUMS.txt
if-no-files-found: error
retention-days: 14
21 changes: 12 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,7 @@ jobs:
.\tests\test_installer.ps1 -InstallerPath (Get-ChildItem .\dist\VGPU-Mon-*-setup.exe -File).FullName

- name: Write SHA-256 checksums
run: |
Copy-Item .\install.ps1 .\dist\install.ps1
$assets = Get-ChildItem .\dist\VGPU-Mon-*.zip, .\dist\VGPU-Mon-*-setup.exe, .\dist\install.ps1 -File | Sort-Object Name
$lines = foreach ($asset in $assets) {
$hash = (Get-FileHash $asset.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
"$hash $($asset.Name)"
}
$lines | Set-Content .\dist\SHA256SUMS.txt -Encoding ascii
run: .\tools\stage-release-metadata.ps1

- name: Upload verified release assets
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand All @@ -66,6 +59,7 @@ jobs:
dist/*.zip
dist/*-setup.exe
dist/install.ps1
dist/version.txt
dist/SHA256SUMS.txt
if-no-files-found: error

Expand All @@ -76,5 +70,14 @@ jobs:
run: |
$zip = (Get-ChildItem .\dist\VGPU-Mon-*.zip -File).FullName
$setup = (Get-ChildItem .\dist\VGPU-Mon-*-setup.exe -File).FullName
gh release create $env:GITHUB_REF_NAME $zip $setup .\dist\install.ps1 .\dist\SHA256SUMS.txt `
gh release create $env:GITHUB_REF_NAME $zip $setup .\dist\install.ps1 `
.\dist\version.txt .\dist\SHA256SUMS.txt `
--verify-tag --generate-notes --title "VGPU-Mon $env:GITHUB_REF_NAME"

- name: Test live automatic update lifecycle
if: github.event_name == 'push'
run: |
$version = $env:GITHUB_REF_NAME.TrimStart('v')
.\tests\test_live_update.ps1 `
-Executable .\build\vgpu-update-smoke.exe `
-ExpectedVersion $version
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable user-facing changes are documented here. This project follows [Seman

## [Unreleased]

## [1.2.0] - 2026-07-13

### Added

- Check a small version manifest on each installed interactive launch and automatically install newer stable releases.
- Verify the exact versioned setup executable with Windows SHA-256 APIs before starting an update.
- Relaunch the monitor with its original interactive options after an update handoff.
- Add `--update`, `--no-update`, and the `VGPU_MON_NO_UPDATE` environment opt-out.

### Security and quality

- Keep JSON, one-shot, version, help, and demo commands network-free and deterministic.
- Reject malformed manifests, path-like installer names, HTTPS downgrades, oversized downloads, and checksum mismatches.
- Publish `version.txt` and checksum it alongside every release asset.

## [1.1.4] - 2026-07-13

### Fixed
Expand Down Expand Up @@ -55,7 +70,8 @@ All notable user-facing changes are documented here. This project follows [Seman

- Added `/W4 /WX /sdl`, control-flow protection, ASLR/DEP/CET-compatible linker flags, reproducible Release builds, MSVC AddressSanitizer tests, CRT leak checks, static analysis, CodeQL, and pinned CI dependencies.

[Unreleased]: https://github.com/xptea/VGPU-Mon/compare/v1.1.4...HEAD
[Unreleased]: https://github.com/xptea/VGPU-Mon/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/xptea/VGPU-Mon/compare/v1.1.4...v1.2.0
[1.1.4]: https://github.com/xptea/VGPU-Mon/compare/v1.1.3...v1.1.4
[1.1.3]: https://github.com/xptea/VGPU-Mon/compare/v1.1.2...v1.1.3
[1.1.2]: https://github.com/xptea/VGPU-Mon/compare/v1.1.1...v1.1.2
Expand Down
15 changes: 12 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,32 @@ add_executable(vgpu-mon
src/nvml_dyn.c
src/dxgi_gpu.c
src/pdh_gpu.c
src/updater.c
src/vgpu.rc)
vgpu_configure_target(vgpu-mon)
target_link_libraries(vgpu-mon PRIVATE
pdh dxgi dxguid psapi advapi32 shell32 ole32)
pdh dxgi dxguid psapi advapi32 shell32 ole32 winhttp bcrypt)

include(CTest)
if(BUILD_TESTING)
add_executable(vgpu-tests
tests/test_core.c
src/util.c
src/pdh_gpu.c)
src/pdh_gpu.c
src/updater.c)
vgpu_configure_target(vgpu-tests)
target_link_libraries(vgpu-tests PRIVATE pdh psapi advapi32)
target_link_libraries(vgpu-tests PRIVATE pdh psapi advapi32 shell32 winhttp bcrypt)

add_executable(vgpu-conpty-tests tests/test_conpty.c)
vgpu_configure_target(vgpu-conpty-tests)

add_executable(vgpu-update-smoke
tests/update_smoke.c
src/updater.c)
vgpu_configure_target(vgpu-update-smoke)
target_link_libraries(vgpu-update-smoke PRIVATE
advapi32 shell32 winhttp bcrypt)

add_test(NAME core COMMAND vgpu-tests)
add_test(NAME conpty COMMAND vgpu-conpty-tests $<TARGET_FILE:vgpu-mon>)
endif()
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +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
- Statically linked MSVC runtime; no NVML SDK or third-party runtime required

## Runtime requirements and compatibility
Expand Down Expand Up @@ -62,6 +63,10 @@ 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.

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.

Prefer to inspect scripts before running them? [Read `install.ps1`](install.ps1), or download the versioned installer/portable ZIP manually from Releases. Release assets include `SHA256SUMS.txt`. Windows SmartScreen may warn about community-built releases until the project has a trusted code-signing certificate; always verify the download came from this repository.

## Build and run
Expand Down Expand Up @@ -185,6 +190,8 @@ Options:
--interval MS Sampling interval from 250 to 5000
--log PATH Start CSV logging when interactive mode opens
--demo Use deterministic sample data for previews and tests
--update Check for and install an update now
--no-update Skip the automatic update check for this run
--help Show help
--version Show the version
```
Expand Down
8 changes: 6 additions & 2 deletions analyze.cmd
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,22 @@ if exist build\obj\analyze rmdir /s /q build\obj\analyze
mkdir build\obj\analyze\app
mkdir build\obj\analyze\core
mkdir build\obj\analyze\conpty
mkdir build\obj\analyze\update

set "ANALYZE_FLAGS=/nologo /c /std:c11 /utf-8 /W4 /WX /sdl /analyze /analyze:external- /D_DEBUG"

cl %ANALYZE_FLAGS% /Fo:build\obj\analyze\app\ ^
src\main.c src\util.c src\nvml_dyn.c src\dxgi_gpu.c src\pdh_gpu.c
src\main.c src\util.c src\nvml_dyn.c src\dxgi_gpu.c src\pdh_gpu.c src\updater.c
if errorlevel 1 exit /b 1

cl %ANALYZE_FLAGS% /Fo:build\obj\analyze\core\ ^
tests\test_core.c src\util.c src\pdh_gpu.c
tests\test_core.c src\util.c src\pdh_gpu.c src\updater.c
if errorlevel 1 exit /b 1

cl %ANALYZE_FLAGS% /Fo:build\obj\analyze\conpty\ tests\test_conpty.c
if errorlevel 1 exit /b 1

cl %ANALYZE_FLAGS% /Fo:build\obj\analyze\update\ tests\update_smoke.c
if errorlevel 1 exit /b 1

echo MSVC static analysis passed.
13 changes: 9 additions & 4 deletions build.cmd
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,20 @@ if /i "%CONFIG%"=="Sanitize" (

if not exist build\obj\app mkdir build\obj\app
if not exist build\obj\tests mkdir build\obj\tests
if not exist build\obj\tests\update mkdir build\obj\tests\update

rc /nologo /fo build\obj\app\vgpu.res src\vgpu.rc
if errorlevel 1 exit /b %errorlevel%

cl %CFLAGS% /Fo:build\obj\app\ /Fd:build\obj\app\compiler.pdb /Fe:build\%OUTPUT_NAME%.exe ^
src\main.c src\util.c src\nvml_dyn.c src\dxgi_gpu.c src\pdh_gpu.c build\obj\app\vgpu.res ^
/link %LFLAGS% pdh.lib dxgi.lib dxguid.lib psapi.lib advapi32.lib shell32.lib ole32.lib
src\main.c src\util.c src\nvml_dyn.c src\dxgi_gpu.c src\pdh_gpu.c src\updater.c build\obj\app\vgpu.res ^
/link %LFLAGS% pdh.lib dxgi.lib dxguid.lib psapi.lib advapi32.lib shell32.lib ole32.lib winhttp.lib bcrypt.lib
if errorlevel 1 exit /b %errorlevel%

if /i "%RUN_TESTS%"=="test" (
cl %CFLAGS% /Fo:build\obj\tests\ /Fd:build\obj\tests\compiler.pdb /Fe:build\vgpu-tests.exe ^
tests\test_core.c src\util.c src\pdh_gpu.c ^
/link %LFLAGS% pdh.lib psapi.lib advapi32.lib
tests\test_core.c src\util.c src\pdh_gpu.c src\updater.c ^
/link %LFLAGS% pdh.lib psapi.lib advapi32.lib shell32.lib winhttp.lib bcrypt.lib
if errorlevel 1 exit /b 1
build\vgpu-tests.exe
if errorlevel 1 exit /b 1
Expand All @@ -65,6 +66,10 @@ if /i "%RUN_TESTS%"=="test" (
if errorlevel 1 exit /b 1
build\vgpu-conpty-tests.exe build\%OUTPUT_NAME%.exe
if errorlevel 1 exit /b 1
cl %CFLAGS% /Fo:build\obj\tests\update\ /Fd:build\obj\tests\update\compiler.pdb /Fe:build\vgpu-update-smoke.exe ^
tests\update_smoke.c src\updater.c ^
/link %LFLAGS% shell32.lib advapi32.lib winhttp.lib bcrypt.lib
if errorlevel 1 exit /b 1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests\test_cli.ps1 -Executable build\%OUTPUT_NAME%.exe
if errorlevel 1 exit /b 1
)
Expand Down
8 changes: 7 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Architecture

VGPU-Mon is a single native Windows process. It has no service, kernel component, injected DLL, background updater, or vendor SDK dependency.
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.

## Data flow

Expand All @@ -10,12 +10,16 @@ VGPU-Mon is a single native Windows process. It has no service, kernel component
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.

`--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.

## Ownership and cleanup

Each provider owns its query/library/COM handles and exposes an idempotent close function. Application cleanup calls all close functions even after partial initialization. Dynamic arrays, command-line conversion buffers, terminal buffers, process handles, tokens, pseudo-console resources, and test capture buffers have explicit paired cleanup.

Updater HTTP, file, registry, hashing, and child-process handles are closed on every path. Partial downloads and helper scripts are deleted after failure or handoff. The updater runs before GPU providers open, so no telemetry handles are inherited by the installer helper.

Debug builds use the MSVC debug heap to report leaks. Sanitizer builds compile the application and native tests with MSVC AddressSanitizer. CI also runs `/analyze` and CodeQL. These checks reduce risk but do not prove the absence of every defect.

## Terminal model
Expand All @@ -32,4 +36,6 @@ The displayed per-process GPU percentage is the busiest engine for that process,

VGPU-Mon runs with the caller’s privileges. Process termination is opt-in, confirmed, and rejects self/system targets. Windows remains the authority for process access. The app does not elevate itself or bypass protected processes.

Automatic updates trust HTTPS certificate validation and the assets attached to this repository's stable GitHub Release. The downloaded setup executable must match the release manifest SHA-256 before execution. Updates remain per-user and do not request elevation.

Process names, GPU UUIDs, CSV files, and JSON snapshots can disclose local system activity. Users should review telemetry before sharing it.
27 changes: 27 additions & 0 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "nvml_dyn.h"
#include "dxgi_gpu.h"
#include "pdh_gpu.h"
#include "updater.h"

#include <stdio.h>
#include <stdlib.h>
Expand Down Expand Up @@ -1409,6 +1410,8 @@ static void print_help(void) {
" --interval MS Sampling interval, 250-5000 (default 1000)\n"
" --log PATH Start interactive CSV logging immediately\n"
" --demo Use deterministic sample data (UI preview/testing)\n"
" --update Check for and install an update now\n"
" --no-update Skip the automatic update check for this run\n"
" --help Show this help\n"
" --version Show the version\n\n"
"Run without options in Windows Terminal for the interactive UI.\n"
Expand Down Expand Up @@ -1512,6 +1515,7 @@ static int app_main(int argc, char **argv) {
}
else if (strcmp(argument, "--log") == 0 && argument_index < argc) initial_log = argv[argument_index++];
else if (strcmp(argument, "--demo") == 0) app->demo_mode = true;
else if (strcmp(argument, "--no-update") == 0) { /* handled before app initialization */ }
else if (strcmp(argument, "--help") == 0 || strcmp(argument, "-h") == 0) { print_help(); free(app); return 0; }
else if (strcmp(argument, "--version") == 0) { printf("VGPU-Mon %s\n", VGPU_VERSION); free(app); return 0; }
else { fprintf(stderr, "Unknown or incomplete option: %s\n", argument); print_help(); free(app); return 2; }
Expand Down Expand Up @@ -1635,6 +1639,29 @@ int wmain(int argc, wchar_t **wide_argv) {
_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR);
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF);
#endif
bool force_update = updater_is_forced(argc, wide_argv);
DWORD input_mode = 0, output_mode = 0;
bool interactive_console =
GetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), &input_mode) != FALSE &&
GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &output_mode) != FALSE;
if (force_update || (interactive_console && updater_should_check(argc, wide_argv))) {
VgpuUpdateResult update = updater_check_and_start(
argc, wide_argv, VGPU_VERSION, force_update);
if (update == VGPU_UPDATE_STARTED) return 0;
if (force_update) {
if (update == VGPU_UPDATE_CURRENT) {
printf("VGPU-Mon %s is already up to date.\n", VGPU_VERSION);
return 0;
}
if (update == VGPU_UPDATE_SKIPPED) {
fputs("VGPU-Mon: the update check was skipped.\n", stderr);
} else {
fputs("VGPU-Mon: the update check failed; the current version was not changed.\n",
stderr);
}
return 1;
}
}
char **argv = (char **)calloc((size_t)argc, sizeof(*argv));
if (!argv) {
fputs("VGPU-Mon: out of memory while reading arguments.\n", stderr);
Expand Down
Loading
Loading