diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24948fc..46b81d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,9 @@ 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: @@ -64,5 +67,6 @@ jobs: path: | dist/*.zip dist/*-setup.exe + dist/install.ps1 if-no-files-found: error retention-days: 14 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b41b07c..47d4095 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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)" @@ -64,6 +65,7 @@ jobs: path: | dist/*.zip dist/*-setup.exe + dist/install.ps1 dist/SHA256SUMS.txt if-no-files-found: error @@ -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" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c4a6b9..c6c309b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/README.md b/README.md index feb7a11..705e0c7 100644 --- a/README.md +++ b/README.md @@ -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--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 @@ -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 diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..6b407f1 --- /dev/null +++ b/install.ps1 @@ -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 + } + } +} diff --git a/src/version.h b/src/version.h index cf061fa..2290b4a 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.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 diff --git a/tests/test_installer.ps1 b/tests/test_installer.ps1 index 8983ba1..7010437 100644 --- a/tests/test_installer.ps1 +++ b/tests/test_installer.ps1 @@ -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.'