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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,16 @@ 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: Upload release candidates
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: VGPU-Mon-${{ github.sha }}
path: |
dist/*.zip
dist/*-setup.exe
dist/install.ps1
if-no-files-found: error
retention-days: 14
6 changes: 4 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ jobs:

- name: Write SHA-256 checksums
run: |
$assets = Get-ChildItem .\dist\VGPU-Mon-*.zip, .\dist\VGPU-Mon-*-setup.exe -File | Sort-Object Name
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)"
Expand All @@ -64,6 +65,7 @@ jobs:
path: |
dist/*.zip
dist/*-setup.exe
dist/install.ps1
dist/SHA256SUMS.txt
if-no-files-found: error

Expand All @@ -74,5 +76,5 @@ 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\SHA256SUMS.txt `
gh release create $env:GITHUB_REF_NAME $zip $setup .\dist\install.ps1 .\dist\SHA256SUMS.txt `
--verify-tag --generate-notes --title "VGPU-Mon $env:GITHUB_REF_NAME"
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable user-facing changes are documented here. This project follows [Seman

## [Unreleased]

## [1.1.3] - 2026-07-13

### Added

- Add a one-line `curl.exe` installation command for PowerShell and Windows Terminal.
- Resolve the latest stable GitHub Release automatically and install `vgpu` for the current user.

### Security and quality

- Verify the downloaded setup executable against the release's `SHA256SUMS.txt` before execution.
- Exercise the bootstrap install, PATH command, and exact uninstall restoration in CI.

## [1.1.2] - 2026-07-13

### Security and quality
Expand Down Expand Up @@ -35,6 +47,7 @@ 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.2...HEAD
[Unreleased]: https://github.com/xptea/VGPU-Mon/compare/v1.1.3...HEAD
[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
[1.1.1]: https://github.com/xptea/VGPU-Mon/releases/tag/v1.1.1
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@ Some protected processes cannot be inspected or terminated without elevation. VG

## Install

Download the installer or portable ZIP from [GitHub Releases](https://github.com/xptea/VGPU-Mon/releases). Run `VGPU-Mon-<version>-setup.exe` for a per-user installation. The installer adds the application directory to your user PATH, so a new terminal tab can run:
Open PowerShell or Windows Terminal and paste this one command:

```powershell
curl.exe -fsSL https://raw.githubusercontent.com/xptea/VGPU-Mon/main/install.ps1 | powershell.exe -NoProfile -ExecutionPolicy Bypass -Command -
```

The bootstrap script finds the latest stable [GitHub Release](https://github.com/xptea/VGPU-Mon/releases), downloads its per-user installer and `SHA256SUMS.txt`, verifies the installer SHA-256, and only then runs it silently. It does not require administrator access. Open a new terminal tab after installation, then run:

```powershell
vgpu
Expand All @@ -54,9 +60,9 @@ vgpu --chart 3d
vgpu --json
```

No administrator prompt is required. Existing terminal processes keep their old environment, so open a new tab after installation. Uninstalling VGPU-Mon removes the PATH entry only when the installer originally added it.
Existing terminal processes keep their old environment. Uninstalling VGPU-Mon from Windows Settings removes the PATH entry only when the installer originally added it.

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.
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
183 changes: 183 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
[CmdletBinding()]
param(
[ValidatePattern('^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$')]
[string]$Repository = 'xptea/VGPU-Mon',

[ValidatePattern('^v?[0-9]+\.[0-9]+\.[0-9]+$')]
[string]$Version,

[Parameter(DontShow)]
[string]$InstallerPath,

[Parameter(DontShow)]
[string]$ChecksumPath
)

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'

function Get-ExpectedChecksum {
param(
[Parameter(Mandatory)]
[string]$ManifestPath,

[Parameter(Mandatory)]
[string]$FileName
)

$escapedName = [regex]::Escape($FileName)
$pattern = "^([0-9A-Fa-f]{64})[ `t]+\*?$escapedName$"
$hashes = @(
foreach ($line in Get-Content -LiteralPath $ManifestPath) {
$match = [regex]::Match($line, $pattern)
if ($match.Success) {
$match.Groups[1].Value.ToLowerInvariant()
}
}
)
if ($hashes.Count -ne 1) {
throw "Expected exactly one SHA-256 entry for $FileName in SHA256SUMS.txt; found $($hashes.Count)."
}
return $hashes[0]
}

function Assert-FileChecksum {
param(
[Parameter(Mandatory)]
[string]$FilePath,

[Parameter(Mandatory)]
[string]$ManifestPath
)

$fileName = Split-Path -Leaf $FilePath
$expected = Get-ExpectedChecksum -ManifestPath $ManifestPath -FileName $fileName
$actual = (Get-FileHash -LiteralPath $FilePath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -cne $expected) {
throw "SHA-256 verification failed for $fileName. Expected $expected but received $actual. The installer was not run."
}
Write-Host "Verified SHA-256: $actual"
}

function Get-ReleaseAsset {
param(
[Parameter(Mandatory)]
[object[]]$Assets,

[Parameter(Mandatory)]
[string]$NamePattern,

[Parameter(Mandatory)]
[string]$Description
)

$matches = @($Assets | Where-Object { [string]$_.name -match $NamePattern })
if ($matches.Count -ne 1) {
throw "The GitHub release must contain exactly one $Description; found $($matches.Count)."
}
return $matches[0]
}

if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
throw 'VGPU-Mon supports 64-bit Windows only.'
}
if (-not [Environment]::Is64BitOperatingSystem) {
throw 'VGPU-Mon requires 64-bit Windows.'
}
if ([string]::IsNullOrWhiteSpace($InstallerPath) -xor [string]::IsNullOrWhiteSpace($ChecksumPath)) {
throw 'InstallerPath and ChecksumPath must be supplied together.'
}

$temporaryDirectory = $null
try {
if (-not [string]::IsNullOrWhiteSpace($InstallerPath)) {
$installer = (Resolve-Path -LiteralPath $InstallerPath).Path
$checksumManifest = (Resolve-Path -LiteralPath $ChecksumPath).Path
}
else {
# Windows PowerShell 5.1 on older Windows 10 installations may not
# negotiate TLS 1.2 by default. GitHub requires it.
[Net.ServicePointManager]::SecurityProtocol =
[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12

$headers = @{
Accept = 'application/vnd.github+json'
'User-Agent' = 'VGPU-Mon-Installer'
'X-GitHub-Api-Version' = '2022-11-28'
}
if ([string]::IsNullOrWhiteSpace($Version)) {
$releaseUri = "https://api.github.com/repos/$Repository/releases/latest"
}
else {
$tag = if ($Version.StartsWith('v')) { $Version } else { "v$Version" }
$encodedTag = [Uri]::EscapeDataString($tag)
$releaseUri = "https://api.github.com/repos/$Repository/releases/tags/$encodedTag"
}

Write-Host "Resolving VGPU-Mon release from $Repository..."
try {
$release = Invoke-RestMethod -Uri $releaseUri -Headers $headers -UseBasicParsing
}
catch {
throw "Could not resolve the requested GitHub release: $($_.Exception.Message)"
}

$assets = @($release.assets)
$installerAsset = Get-ReleaseAsset -Assets $assets `
-NamePattern '^VGPU-Mon-[0-9]+\.[0-9]+\.[0-9]+-setup\.exe$' `
-Description 'Windows setup executable'
$checksumAsset = Get-ReleaseAsset -Assets $assets `
-NamePattern '^SHA256SUMS\.txt$' `
-Description 'SHA256SUMS.txt asset'

foreach ($asset in @($installerAsset, $checksumAsset)) {
$assetUri = [Uri][string]$asset.browser_download_url
if ($assetUri.Scheme -cne 'https' -or $assetUri.Host -cne 'github.com') {
throw "Release asset $($asset.name) has an unexpected download URL."
}
}

$temporaryBase = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
$temporaryDirectory = [IO.Path]::GetFullPath(
(Join-Path $temporaryBase "vgpu-mon-install-$([Guid]::NewGuid().ToString('N'))")
)
if (-not $temporaryDirectory.StartsWith($temporaryBase, [StringComparison]::OrdinalIgnoreCase)) {
throw 'Refusing to create an installer directory outside the system temporary directory.'
}
[void](New-Item -ItemType Directory -Path $temporaryDirectory)

$installer = Join-Path $temporaryDirectory ([string]$installerAsset.name)
$checksumManifest = Join-Path $temporaryDirectory 'SHA256SUMS.txt'
Write-Host "Downloading $($installerAsset.name)..."
Invoke-WebRequest -Uri ([string]$installerAsset.browser_download_url) `
-Headers $headers -UseBasicParsing -OutFile $installer
Invoke-WebRequest -Uri ([string]$checksumAsset.browser_download_url) `
-Headers $headers -UseBasicParsing -OutFile $checksumManifest
}

Assert-FileChecksum -FilePath $installer -ManifestPath $checksumManifest

Write-Host 'Installing VGPU-Mon for the current user...'
$process = Start-Process -FilePath $installer -ArgumentList @(
'/VERYSILENT',
'/SUPPRESSMSGBOXES',
'/NORESTART',
'/SP-'
) -Wait -PassThru -WindowStyle Hidden
if ($process.ExitCode -ne 0) {
throw "VGPU-Mon setup exited with code $($process.ExitCode)."
}

Write-Host ''
Write-Host 'VGPU-Mon installed successfully.' -ForegroundColor Green
Write-Host 'Open a new terminal, then run: vgpu'
}
finally {
if ($null -ne $temporaryDirectory -and (Test-Path -LiteralPath $temporaryDirectory)) {
$temporaryBase = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
$resolvedTemporaryDirectory = [IO.Path]::GetFullPath($temporaryDirectory)
if ($resolvedTemporaryDirectory.StartsWith($temporaryBase, [StringComparison]::OrdinalIgnoreCase)) {
Remove-Item -LiteralPath $resolvedTemporaryDirectory -Recurse -Force
}
}
}
4 changes: 2 additions & 2 deletions src/version.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#ifndef VGPU_VERSION_H
#define VGPU_VERSION_H

#define VGPU_VERSION "1.1.2"
#define VGPU_VERSION_NUM 1,1,2,0
#define VGPU_VERSION "1.1.3"
#define VGPU_VERSION_NUM 1,1,3,0

#endif
76 changes: 75 additions & 1 deletion tests/test_installer.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,78 @@ if (Test-Path -LiteralPath 'HKCU:\Software\VGPU-Mon') {
throw 'Uninstall left VGPU-Mon installer state behind.'
}

Write-Host 'Installer install, PATH command, and uninstall restoration tests passed.'
$checksumPath = Join-Path ([IO.Path]::GetTempPath()) "vgpu-mon-checksums-$([Guid]::NewGuid().ToString('N')).txt"
$bootstrapInstalled = $false
try {
$installerHash = (Get-FileHash -LiteralPath $installer -Algorithm SHA256).Hash.ToLowerInvariant()
"$('0' * 64) $(Split-Path -Leaf $installer)" |
Set-Content -LiteralPath $checksumPath -Encoding ascii
$rejectedBadChecksum = $false
try {
& (Join-Path $root 'install.ps1') -InstallerPath $installer -ChecksumPath $checksumPath
}
catch {
if ($_.Exception.Message -like 'SHA-256 verification failed*') {
$rejectedBadChecksum = $true
}
else {
throw
}
}
if (-not $rejectedBadChecksum) {
throw 'Bootstrap installer accepted an invalid SHA-256 checksum.'
}
if (Test-Path -LiteralPath $installDir) {
throw 'Bootstrap installer ran setup after checksum verification failed.'
}

"$installerHash $(Split-Path -Leaf $installer)" |
Set-Content -LiteralPath $checksumPath -Encoding ascii

& (Join-Path $root 'install.ps1') -InstallerPath $installer -ChecksumPath $checksumPath
$bootstrapInstalled = $true
if (-not (Test-Path -LiteralPath $installedExe)) {
throw 'Bootstrap installer did not create vgpu.exe.'
}
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$entries = @($userPath -split ';' | Where-Object {
$_.Trim().Trim('"').TrimEnd('\') -ieq $installDir
})
if ($entries.Count -ne 1) {
throw "Bootstrap installer produced $($entries.Count) VGPU-Mon PATH entries."
}
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$oldProcessPath = $env:Path
try {
$env:Path = "$userPath;$machinePath"
$version = & vgpu --version
if ($LASTEXITCODE -ne 0 -or $version -ne "VGPU-Mon $expectedVersion") {
throw "Bootstrap-installed vgpu smoke test failed: $version"
}
}
finally {
$env:Path = $oldProcessPath
}

Invoke-HiddenProcess $uninstaller @('/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART')
$bootstrapInstalled = $false
}
finally {
if ($bootstrapInstalled -and (Test-Path -LiteralPath $uninstaller)) {
Invoke-HiddenProcess $uninstaller @('/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART')
}
Remove-Item -LiteralPath $checksumPath -Force -ErrorAction SilentlyContinue
}

$afterBootstrap = Get-UserPathState
if ($afterBootstrap.Value -cne $before.Value -or $afterBootstrap.Kind -cne $before.Kind) {
throw 'Bootstrap install/uninstall changed the original user PATH.'
}
if (Test-Path -LiteralPath $installDir) {
throw 'Bootstrap uninstall left the application directory behind.'
}
if (Test-Path -LiteralPath 'HKCU:\Software\VGPU-Mon') {
throw 'Bootstrap uninstall left VGPU-Mon installer state behind.'
}

Write-Host 'Installer, curl bootstrap, PATH command, upgrade, and uninstall tests passed.'
Loading