Remove auto-start: launcher owns the app lifecycle

The TeslaConsole launcher starts and stops RIOJoy, so the app no longer
registers any auto-start entry:
- install-rio.ps1: drop the HKLM ...\Run registration and the post-install
  launch (and the -NoLaunch param).
- Tray: remove the "Start with Windows" menu item and delete AutoStartManager
  (HKCU ...\Run writer).
- AppConfig: drop the now-inert AutoStart field (+ test).
- Docs (PLAN.md, README-DEPLOY.txt) updated to reflect launcher-managed start/stop.

uninstall-rio.ps1 still clears any leftover Run entry from older auto-starting
builds. Solution builds; 241 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-06-30 14:36:32 -05:00
co-authored by Claude Opus 4.8
parent 0a4b3e3c25
commit b275892451
8 changed files with 31 additions and 75 deletions
+9 -5
View File
@@ -25,8 +25,9 @@ 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.
RIOJoy is NOT registered to auto-start and is NOT launched by the installer —
the TeslaConsole launcher starts and stops the app.
Deploying with TeslaConsole
---------------------------
@@ -40,7 +41,8 @@ Deploying with TeslaConsole
Uninstalling with TeslaConsole
------------------------------
Before deleting the RIOJoy folder, run RIOJoy\pre-uninstall.bat ELEVATED. It
stops the tray app (releasing file locks), removes the logon entry, uninstalls
stops the tray app (releasing file locks), removes any leftover logon entry
(from older builds that auto-started), uninstalls
ViGEmBus, and clears per-user config (%APPDATA%\RIOJoy) for every profile. It is
idempotent and non-fatal — safe even if RIOJoy was never fully installed. The
console can then delete the folder cleanly. Use -KeepDriver to leave ViGEmBus in
@@ -48,8 +50,10 @@ Uninstalling with TeslaConsole
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.
* RIOJoy is started and stopped by the TeslaConsole launcher (it is not
registered to auto-start at logon).
* When running, RIOJoy lives 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
+10 -20
View File
@@ -1,14 +1,15 @@
<#
.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.
driver (if absent). Must run elevated (postinstall.bat elevates for you).
Idempotent — safe to re-run. RIOJoy is NOT registered to auto-start: the
TeslaConsole launcher owns the app's start/stop lifecycle.
.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>\app\RioJoy.Tray.exe the published tray app (net48)
<root>\vendor\ViGEmBus_*.exe the signed ViGEmBus installer
<root>\install-rio.ps1 (this file)
@@ -16,17 +17,13 @@
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'
$root = $PSScriptRoot
$appExe = Join-Path $root 'app\RioJoy.Tray.exe'
$vendorDir = Join-Path $root 'vendor'
# --- guards ---------------------------------------------------------------
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
@@ -58,18 +55,11 @@ if ($svc) {
}
}
# 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
# RIOJoy itself is deliberately NOT registered to auto-start and NOT launched
# here: the TeslaConsole launcher starts and stops the app.
# --- 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
}
Write-Host 'RIOJoy is not auto-started; the TeslaConsole launcher starts and stops it.'
exit 0
+6 -5
View File
@@ -51,7 +51,8 @@ cockpit's outputs on their behalf.
language features that net48 lacks are supplied by the PolySharp source
generator + a few NuGet shims; see `src/RioJoy.Core/Compat/`.) Driver is C
(WDK), separate toolchain.
- **Form:** background **tray app** (NotifyIcon), auto-start with Windows.
- **Form:** background **tray app** (NotifyIcon); start/stop managed by the
TeslaConsole launcher (no logon auto-start).
- **Targets:** Windows 10/11 **x64 only**. The legacy x86 / WinXP targets are
dropped.
@@ -198,10 +199,10 @@ Core logic in `src/RioJoy.Core/Profiles` + `RioRuntime`; UI/OS in `src/RioJoy.Tr
commands → calibration resets + serial requests + lamp re-init. End-to-end tested
over the fake transport.
- Tray: `NotifyIcon` menu mirroring the legacy console menu (axis resets,
version/status, diagnostic toggles, quit) + profile selection (auto vs. manual)
+ "start with Windows"; `RioCoordinator` owns the serial acquire/release tied to
the watcher (native-game COM-port yield); OS adapters
(`ForegroundProcessProvider`, `AutoStartManager`).
version/status, diagnostic toggles, quit) + profile selection (auto vs. manual);
`RioCoordinator` owns the serial acquire/release tied to the watcher (native-game
COM-port yield); OS adapters (`ForegroundProcessProvider`). The app's start/stop
lifecycle is owned by the TeslaConsole launcher (no logon auto-start).
- Joystick output now uses the real `HidFeederJoystickSink` when the driver is
present (verified end-to-end); `NullJoystickSink` remains only as the
no-driver fallback.
-3
View File
@@ -29,9 +29,6 @@ public sealed class AppConfig
/// </summary>
public string? NeutralProfileName { get; set; }
/// <summary>Start RIOJoy with Windows.</summary>
public bool AutoStart { get; set; }
/// <summary>
/// Path to the cockpit overlay template (<c>regions.json</c>) used to generate
/// per-profile wallpapers (Phase 7). Null disables wallpaper generation. The
-29
View File
@@ -1,29 +0,0 @@
using Microsoft.Win32;
namespace RioJoy.Tray.Os;
/// <summary>
/// Manages the Windows "run at login" registration via the per-user
/// <c>Run</c> registry key. Per-user (HKCU) needs no elevation and matches the
/// tray app running in the interactive session.
/// </summary>
public static class AutoStartManager
{
private const string RunKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
private const string ValueName = "RIOJoy";
public static bool IsEnabled()
{
using RegistryKey? key = Registry.CurrentUser.OpenSubKey(RunKey);
return key?.GetValue(ValueName) is not null;
}
public static void SetEnabled(bool enabled)
{
using RegistryKey key = Registry.CurrentUser.CreateSubKey(RunKey);
if (enabled)
key.SetValue(ValueName, $"\"{Application.ExecutablePath}\"");
else
key.DeleteValue(ValueName, throwOnMissingValue: false);
}
}
+2 -2
View File
@@ -3,8 +3,8 @@ 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,
// port, so a second launch (e.g. the launcher starting it while an instance is
// already running) 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";
+4 -9
View File
@@ -9,7 +9,8 @@ namespace RioJoy.Tray;
/// <summary>
/// Owns the tray icon, menu, and the RIOJoy runtime. The menu mirrors the legacy
/// console menu (axis resets, version/status, diagnostic toggles, quit) and adds
/// profile selection (auto vs. manual) and a "start with Windows" toggle. The
/// profile selection (auto vs. manual). The app's start/stop lifecycle is owned by
/// the TeslaConsole launcher, so there is no "start with Windows" toggle. The
/// auto-switch watcher is polled on a UI timer so menu/status updates stay on the
/// UI thread.
/// </summary>
@@ -91,14 +92,8 @@ internal sealed class TrayApplicationContext : ApplicationContext
menu.Items.Add(new ToolStripSeparator());
var autoStart = new ToolStripMenuItem("Start with Windows")
{
CheckOnClick = true,
Checked = AutoStartManager.IsEnabled(),
};
autoStart.Click += (_, _) => AutoStartManager.SetEnabled(autoStart.Checked);
menu.Items.Add(autoStart);
// No "start with Windows" toggle: the TeslaConsole launcher owns the app's
// start/stop lifecycle, so RIOJoy deliberately registers no auto-start entry.
menu.Items.Add("Exit", null, (_, _) => Quit());
return menu;
@@ -15,7 +15,6 @@ public class ConfigStoreTests
DefaultPlasmaComPort = "COM4",
NativeGameExecutables = { "firestorm.exe", "redplanet.exe" },
NeutralProfileName = "Desktop",
AutoStart = true,
Profiles =
{
new RioProfile
@@ -35,7 +34,6 @@ public class ConfigStoreTests
Assert.Equal("COM3", back.DefaultRioComPort);
Assert.Equal(new[] { "firestorm.exe", "redplanet.exe" }, back.NativeGameExecutables);
Assert.True(back.AutoStart);
RioProfile p = Assert.Single(back.Profiles);
Assert.Equal("Doom", p.Name);