Add cockpit deployment package + single-instance guard

deploy/ builds a single zip for TeslaConsole: the self-contained tray app and
the signed ViGEmBus installer under a riojoy\ folder, with postinstall.bat at the
root. postinstall elevates and runs riojoy\install-rio.ps1, which installs
ViGEmBus silently (if absent) and registers RIOJoy to start at logon — no cert
trust, test signing, Secure Boot change, or reboot.

Also guard against two instances fighting over the COM port (machine-wide +
per-user autostart both firing): a per-session named mutex makes a second launch
exit immediately. Built zips (dist/) and the bundled installer (deploy/vendor/)
are git-ignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-06-29 09:29:01 -05:00
co-authored by Claude Opus 4.8
parent 2f434517b7
commit d17fd018eb
6 changed files with 275 additions and 0 deletions
+4
View File
@@ -3,6 +3,10 @@
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# RIOJoy deployment: built zips and bundled third-party installers (not source).
/dist/
/deploy/vendor/
# User-specific files
*.rsuser
*.suo
+54
View File
@@ -0,0 +1,54 @@
RIOJoy — cockpit deployment package
====================================
This package deploys RIOJoy to a single directory (e.g. C:\games\RIOJOY) and runs
all local installation steps from postinstall.bat. It uses the signed ViGEmBus
virtual-controller driver, so there is NO test signing, NO Secure Boot change, and
NO reboot.
Contents
--------
postinstall.bat Entry point (at the root). Elevates, then runs
riojoy\install-rio.ps1.
riojoy\ The payload folder:
app\ The RIOJoy tray application (self-contained .NET 8 — no runtime
install required on the target machine).
vendor\ The signed ViGEmBus installer (Nefarius, WHQL/attestation-signed).
install-rio.ps1 The actual install steps (idempotent).
VERSION.txt Build stamp (date + git short SHA).
What postinstall.bat does
-------------------------
1. Elevates to administrator (UAC) if not already elevated.
2. Installs ViGEmBus silently if its driver service is not already present.
3. Registers RIOJoy to start at logon (HKLM ...\Run, machine-wide).
4. Launches RIOJoy.
Deploying with TeslaConsole
---------------------------
1. Extract this zip so that postinstall.bat and the riojoy\ folder sit together
(e.g. under C:\games\, giving C:\games\postinstall.bat and C:\games\riojoy\).
2. Run postinstall.bat ELEVATED (TeslaConsole should run it as administrator).
It is idempotent — re-running it is safe (e.g. for updates). The app and
ViGEmBus install from the riojoy\ folder; install-rio.ps1 locates itself, so
the riojoy\ folder can live wherever it is extracted.
After install
-------------
* RIOJoy runs in the system tray. Right-click it to pick/edit profiles, set the
auto-switch executables, and toggle output to the PC.
* The RIO's joystick output appears to games as a standard "Xbox 360 Controller"
(verify in joy.cpl). Note the XInput layout limits joystick BUTTONS to 11
(A, B, X, Y, LB, RB, Back, Start, L3, R3, Guide); bind everything else to the
keyboard. The hat maps to the D-pad; axes map to the sticks and triggers.
* Per-machine config lives in %APPDATA%\RIOJoy\config.json for the running user.
Building this package (on a dev machine)
-----------------------------------------
Requires the .NET 8 SDK and the signed ViGEmBus installer.
powershell -ExecutionPolicy Bypass -File deploy\build-package.ps1 `
-VigemInstaller <path-to-ViGEmBus_x.y.z_x64_x86_arm64.exe>
(Or drop ViGEmBus_*.exe into deploy\vendor\ and omit -VigemInstaller.)
Output: dist\RIOJoy-<version>.zip
+103
View File
@@ -0,0 +1,103 @@
<#
.SYNOPSIS
Build the RIOJoy cockpit deployment zip: publish the tray app self-contained,
bundle the signed ViGEmBus installer + the post-install scripts, and zip it for
TeslaConsole. The zip extracts directly into a single directory (C:\games\RIOJOY).
.PARAMETER VigemInstaller
Path to the signed ViGEmBus installer to bundle. If omitted, looks in
deploy\vendor\ViGEmBus*.exe.
.PARAMETER OutDir
Where to write the zip (default: dist, relative to the repo root).
.PARAMETER Configuration
Build configuration (default: Release).
#>
param(
[string]$VigemInstaller,
[string]$OutDir = 'dist',
[string]$Configuration = 'Release'
)
$ErrorActionPreference = 'Stop'
$repo = Split-Path $PSScriptRoot -Parent # deploy\ -> repo root
$staging = Join-Path ([IO.Path]::GetTempPath()) "riojoy-pkg-$([Guid]::NewGuid().ToString('N'))"
Write-Host '== RIOJoy deployment package ==' -ForegroundColor Cyan
# --- version stamp --------------------------------------------------------
$sha = (& git -C $repo rev-parse --short HEAD 2>$null)
if (-not $sha) { $sha = 'nogit' }
$version = "{0}-{1}" -f (Get-Date -Format 'yyyyMMdd'), $sha
# --- locate the ViGEmBus installer ----------------------------------------
if (-not $VigemInstaller) {
$VigemInstaller = Get-ChildItem -Path (Join-Path $PSScriptRoot 'vendor') -Filter 'ViGEmBus*.exe' `
-ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullName
}
if (-not $VigemInstaller -or -not (Test-Path $VigemInstaller)) {
throw "ViGEmBus installer not found. Pass -VigemInstaller <path>, or drop ViGEmBus_*.exe into deploy\vendor\. Get it from https://github.com/nefarius/ViGEmBus/releases."
}
$sig = Get-AuthenticodeSignature $VigemInstaller
if ($sig.Status -ne 'Valid') { throw "ViGEmBus installer is not validly signed ($($sig.Status)): $VigemInstaller" }
Write-Host "ViGEmBus installer: $(Split-Path $VigemInstaller -Leaf) (signature $($sig.Status))"
try {
# The payload lives in a 'riojoy' folder; postinstall.bat sits beside it at the
# zip root (TeslaConsole runs it; it calls riojoy\install-rio.ps1).
$pkgDir = Join-Path $staging 'riojoy'
New-Item -ItemType Directory -Force -Path $pkgDir | Out-Null
# 1. Publish the tray app into riojoy\app (self-contained win-x64 — no .NET on target).
$appOut = Join-Path $pkgDir 'app'
Write-Host "Publishing RioJoy.Tray ($Configuration, self-contained win-x64)..."
& dotnet publish (Join-Path $repo 'src\RioJoy.Tray\RioJoy.Tray.csproj') `
-c $Configuration -r win-x64 --self-contained true `
-p:PublishSingleFile=false -p:DebugType=none `
-o $appOut | Out-Null
if ($LASTEXITCODE -ne 0) { throw 'dotnet publish failed.' }
# 2. Bundle the signed ViGEmBus installer into riojoy\vendor.
$vendorOut = Join-Path $pkgDir 'vendor'
New-Item -ItemType Directory -Force -Path $vendorOut | Out-Null
Copy-Item $VigemInstaller $vendorOut
# 3. Install script + readme + version stamp inside riojoy\; postinstall.bat at root.
Copy-Item (Join-Path $PSScriptRoot 'install-rio.ps1') $pkgDir
Copy-Item (Join-Path $PSScriptRoot 'README-DEPLOY.txt') $pkgDir
Set-Content -Path (Join-Path $pkgDir 'VERSION.txt') -Value "RIOJoy $version" -Encoding utf8
Copy-Item (Join-Path $PSScriptRoot 'postinstall.bat') $staging
# 4. Zip the package contents (so it extracts straight into C:\games\RIOJOY).
$outDirFull = if ([IO.Path]::IsPathRooted($OutDir)) { $OutDir } else { Join-Path $repo $OutDir }
New-Item -ItemType Directory -Force -Path $outDirFull | Out-Null
$zip = Join-Path $outDirFull "RIOJoy-$version.zip"
if (Test-Path $zip) { Remove-Item $zip -Force }
Write-Host "Zipping -> $zip"
# Build entries by hand with forward-slash names: both Compress-Archive and
# ZipFile.CreateFromDirectory write BACKSLASH separators under Windows PowerShell
# (.NET Framework), which some extractors mishandle. Entries are relative to the
# staging root, so the zip extracts straight into the target (C:\games\RIOJOY\...).
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$base = (Resolve-Path $staging).Path.TrimEnd('\') + '\'
$fs = [System.IO.File]::Open($zip, [System.IO.FileMode]::CreateNew)
try {
$archive = New-Object System.IO.Compression.ZipArchive($fs, [System.IO.Compression.ZipArchiveMode]::Create)
try {
foreach ($file in Get-ChildItem -Path $staging -Recurse -File) {
$entryName = $file.FullName.Substring($base.Length) -replace '\\', '/'
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
$archive, $file.FullName, $entryName,
[System.IO.Compression.CompressionLevel]::Optimal) | Out-Null
}
} finally { $archive.Dispose() }
} finally { $fs.Dispose() }
$size = '{0:N1} MB' -f ((Get-Item $zip).Length / 1MB)
Write-Host ''
Write-Host "Package built: $zip ($size)" -ForegroundColor Green
Write-Host 'Deploy: extract (postinstall.bat + riojoy\), then run postinstall.bat (elevated).'
}
finally {
if (Test-Path $staging) { Remove-Item $staging -Recurse -Force }
}
+75
View File
@@ -0,0 +1,75 @@
<#
.SYNOPSIS
RIOJoy cockpit post-install. Installs the signed ViGEmBus virtual-controller
driver (if absent) and sets RIOJoy to start at logon. Must run elevated
(postinstall.bat elevates for you). Idempotent — safe to re-run.
.DESCRIPTION
Run from inside the deployed package directory (e.g. C:\games\RIOJOY). Paths
are resolved relative to this script, so the layout is:
<root>\app\RioJoy.Tray.exe the published tray app (self-contained)
<root>\vendor\ViGEmBus_*.exe the signed ViGEmBus installer
<root>\install-rio.ps1 (this file)
ViGEmBus is WHQL/attestation-signed: it installs with NO test signing, NO
Secure Boot change, and NO reboot. The RIO appears to games as a standard
Xbox 360 controller.
.PARAMETER NoLaunch
Do not start the app after install (it still starts at the next logon).
#>
param([switch]$NoLaunch)
$ErrorActionPreference = 'Stop'
$root = $PSScriptRoot
$appExe = Join-Path $root 'app\RioJoy.Tray.exe'
$vendorDir = Join-Path $root 'vendor'
$runValueName = 'RIOJoy'
# --- guards ---------------------------------------------------------------
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'install-rio.ps1 must run elevated. Run postinstall.bat (it elevates).'
}
if (-not (Test-Path $appExe)) { throw "App not found: $appExe (is the package complete?)." }
Write-Host '== RIOJoy post-install ==' -ForegroundColor Cyan
# 1. Install ViGEmBus if its driver service is not already present.
$svc = Get-Service -Name ViGEmBus -ErrorAction SilentlyContinue
if ($svc) {
Write-Host "ViGEmBus already installed (service is $($svc.Status))."
} else {
$installer = Get-ChildItem -Path $vendorDir -Filter 'ViGEmBus*.exe' -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($installer) {
Write-Host "Installing ViGEmBus ($($installer.Name))..."
$p = Start-Process -FilePath $installer.FullName `
-ArgumentList '/install', '/quiet', '/norestart' -Wait -PassThru
if ($p.ExitCode -notin 0, 3010) {
throw "ViGEmBus install failed (exit code $($p.ExitCode))."
}
Write-Host 'ViGEmBus installed.'
} else {
Write-Warning "ViGEmBus installer not found in $vendorDir — joystick output will be"
Write-Warning "unavailable until ViGEmBus is installed (keyboard/mouse still work)."
}
}
# 2. Start RIOJoy at logon (machine-wide Run key -> runs in each user's session).
Write-Host 'Registering RIOJoy to start at logon...'
$runKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
New-ItemProperty -Path $runKey -Name $runValueName -Value "`"$appExe`"" -PropertyType String -Force | Out-Null
# --- summary --------------------------------------------------------------
Write-Host ''
Write-Host "RIOJoy installed to $root" -ForegroundColor Green
Write-Host 'RIOJoy starts automatically at logon.'
if (-not $NoLaunch) {
Write-Host 'Launching RIOJoy...'
# Launch via explorer so the tray app runs de-elevated in the user session.
Start-Process -FilePath (Join-Path $env:WINDIR 'explorer.exe') -ArgumentList $appExe
}
exit 0
+29
View File
@@ -0,0 +1,29 @@
@echo off
rem ===========================================================================
rem RIOJoy cockpit post-install entry point (run by TeslaConsole).
rem Sits beside the 'riojoy' payload folder; elevates, then runs
rem riojoy\install-rio.ps1.
rem ===========================================================================
setlocal
title RIOJoy post-install
rem --- elevate if we are not already administrator --------------------------
net session >nul 2>&1
if %errorlevel% neq 0 (
echo Requesting administrator privileges...
powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
exit /b 0
)
cd /d "%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0riojoy\install-rio.ps1"
set RC=%errorlevel%
echo.
if %RC% neq 0 (
echo postinstall FAILED with exit code %RC%.
) else (
echo postinstall completed.
)
exit /b %RC%
+10
View File
@@ -2,6 +2,12 @@ namespace RioJoy.Tray;
internal static class Program
{
// Per-session single-instance guard. Two instances would both grab the RIO COM
// port, so a second launch (e.g. machine-wide + per-user autostart both firing)
// must exit immediately. A named mutex is released by the OS if the owner dies,
// so a crash never leaves a stale lock.
private const string SingleInstanceMutex = "RIOJoy.Tray.SingleInstance";
/// <summary>
/// Entry point. RIOJoy runs as a background tray application with no main
/// window: an ApplicationContext owns the NotifyIcon and the runtime, so the
@@ -10,6 +16,10 @@ internal static class Program
[STAThread]
private static void Main()
{
using var instance = new Mutex(initiallyOwned: true, SingleInstanceMutex, out bool createdNew);
if (!createdNew)
return; // another RIOJoy is already running in this session
ApplicationConfiguration.Initialize();
Application.Run(new TrayApplicationContext());
}