Skip to content

Commit dc85cd8

Browse files
authored
Register autostart for every user on per-machine installs (#5691)
1 parent 8edcfa8 commit dc85cd8

4 files changed

Lines changed: 223 additions & 1 deletion

File tree

‎UniGetUI.iss‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
#define MyAppExeName "UniGetUI.exe"
99
#define MyAppPublisherURL "https://devolutions.net/unigetui/"
1010
#define MyAppCopyright "Copyright 2021-2026 " + MyAppPublisher
11+
#define AutostartComponentKey "Software\Microsoft\Active Setup\Installed Components\Devolutions.UniGetUI.Autostart"
12+
#define AutostartRemovalKey "Software\Microsoft\Active Setup\Installed Components\Devolutions.UniGetUI.Autostart.Removal"
13+
#define UserRunKey "Software\Microsoft\Windows\CurrentVersion\Run"
1114
#ifndef InstallerCompression
1215
#define InstallerCompression "lzma"
1316
#endif
@@ -86,6 +89,8 @@ AppModifyPath="{app}\UniGetUI.Installer.exe" /silent /NoDeployInstaller
8689
8790
var
8891
PreserveAutostartDisabled: Boolean;
92+
AutostartComponentVersionValue: String;
93+
AutostartRegisteredExecutable: String;
8994
// Set once the marker is written (which happens only after {app} is initialized).
9095
MarkerWritten: Boolean;
9196
@@ -101,6 +106,81 @@ begin
101106
Result := (Length(Data) >= 1) and ((Ord(Data[1]) and 1) = 1);
102107
end;
103108
109+
function NewActiveSetupVersion: String;
110+
begin
111+
Result := GetDateTimeString('yyyy,mmdd,hhnn,ss', #0, #0);
112+
end;
113+
114+
function AutostartStubPath: String;
115+
begin
116+
Result := '"' + ExpandConstant('{app}\{#MyAppExeName}') + '" --register-autostart';
117+
end;
118+
119+
function ResolveAutostartComponentVersion: String;
120+
var
121+
ExistingVersion, ExistingStubPath: String;
122+
begin
123+
if RegQueryStringValue(HKLM64, '{#AutostartComponentKey}', 'Version', ExistingVersion)
124+
and RegQueryStringValue(HKLM64, '{#AutostartComponentKey}', 'StubPath', ExistingStubPath)
125+
and (ExistingVersion <> '')
126+
and (CompareText(ExistingStubPath, AutostartStubPath) = 0) then
127+
Result := ExistingVersion
128+
else
129+
Result := NewActiveSetupVersion;
130+
end;
131+
132+
function AutostartComponentVersion(Param: String): String;
133+
begin
134+
if AutostartComponentVersionValue = '' then
135+
AutostartComponentVersionValue := ResolveAutostartComponentVersion;
136+
Result := AutostartComponentVersionValue;
137+
end;
138+
139+
function AutostartStubPathValue(Param: String): String;
140+
begin
141+
Result := AutostartStubPath;
142+
end;
143+
144+
function RegisteredAutostartExecutable: String;
145+
var
146+
StubPath, Rest: String;
147+
ClosingQuote: Integer;
148+
begin
149+
Result := '';
150+
if not RegQueryStringValue(HKLM64, '{#AutostartComponentKey}', 'StubPath', StubPath) then
151+
Exit;
152+
if (Length(StubPath) < 2) or (StubPath[1] <> '"') then
153+
Exit;
154+
Rest := Copy(StubPath, 2, Length(StubPath) - 1);
155+
ClosingQuote := Pos('"', Rest);
156+
if ClosingQuote > 1 then
157+
Result := Copy(Rest, 1, ClosingQuote - 1);
158+
end;
159+
160+
procedure RegisterAutostartRemovalForAllUsers(const ExecutablePath: String);
161+
var
162+
Reg, Find, Cmd, Command: String;
163+
begin
164+
Reg := '"' + ExpandConstant('{win}\System32\reg.exe') + '"';
165+
Find := '"' + ExpandConstant('{win}\System32\find.exe') + '"';
166+
Cmd := '"' + ExpandConstant('{win}\System32\cmd.exe') + '"';
167+
Command := Cmd + ' /d /c "' + Reg + ' query "HKCU\{#UserRunKey}" /v WingetUI | ' + Find + ' /i "'
168+
+ ExecutablePath + '" >nul && ' + Reg + ' delete "HKCU\{#UserRunKey}" /v WingetUI /f"';
169+
RegWriteStringValue(HKLM64, '{#AutostartRemovalKey}', '', '{#MyAppName}');
170+
RegWriteStringValue(HKLM64, '{#AutostartRemovalKey}', 'Version', NewActiveSetupVersion);
171+
RegWriteStringValue(HKLM64, '{#AutostartRemovalKey}', 'StubPath', Command);
172+
RegWriteDWordValue(HKLM64, '{#AutostartRemovalKey}', 'IsInstalled', 1);
173+
end;
174+
175+
procedure UnregisterAutostartForAllUsers;
176+
var
177+
ExecutablePath: String;
178+
begin
179+
ExecutablePath := RegisteredAutostartExecutable;
180+
if ExecutablePath <> '' then
181+
RegisterAutostartRemovalForAllUsers(ExecutablePath);
182+
end;
183+
104184
procedure InitializeWizard;
105185
begin
106186
WizardForm.Bevel.Visible := False;
@@ -182,6 +262,7 @@ function PrepareToInstall(var NeedsRestart: Boolean): String;
182262
begin
183263
// Capture before [Registry] rewrites the Run key, so updates keep the user's choice.
184264
PreserveAutostartDisabled := IsAutostartDisabledByUser;
265+
AutostartComponentVersionValue := ResolveAutostartComponentVersion;
185266
KillRunningApps;
186267
WriteUpdateMarker;
187268
Result := '';
@@ -203,7 +284,13 @@ end;
203284
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
204285
begin
205286
if CurUninstallStep = usUninstall then
287+
begin
288+
if IsAdminInstallMode then
289+
AutostartRegisteredExecutable := RegisteredAutostartExecutable;
206290
UpdateUnigetPath(False);
291+
end;
292+
if (CurUninstallStep = usPostUninstall) and (AutostartRegisteredExecutable <> '') then
293+
RegisterAutostartRemovalForAllUsers(AutostartRegisteredExecutable);
207294
end;
208295
209296
function GetCustomSetupExitCode: Integer;
@@ -261,6 +348,16 @@ begin
261348
Result := CmdLineParamExists('/NoRunOnStartup') or IsMSStoreInstall or PreserveAutostartDisabled;
262349
end;
263350
351+
function ShouldRegisterAutostartForAllUsers: Boolean;
352+
begin
353+
Result := IsAdminInstallMode and not (CmdLineParamExists('/NoRunOnStartup') or IsMSStoreInstall);
354+
end;
355+
356+
function ShouldUnregisterAutostartForAllUsers: Boolean;
357+
begin
358+
Result := IsAdminInstallMode and (CmdLineParamExists('/NoRunOnStartup') or IsMSStoreInstall);
359+
end;
360+
264361
var CustomExitCode: integer;
265362
266363
procedure ExitProcess(exitCode:integer);
@@ -336,6 +433,12 @@ Name: "regularinstall\addtopath"; Description: "{cm:RegAddToPath}";
336433
[Registry]
337434
Root: HKCU; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "WingetUI"; ValueData: """{app}\UniGetUI.exe"" --daemon"; Flags: uninsdeletevalue noerror; Tasks: regularinstall;
338435
Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run"; ValueType: binary; ValueName: "WingetUI"; ValueData: "03"; Flags: uninsdeletevalue; Tasks: regularinstall; Check: ShouldSuppressRunOnStartup;
436+
Root: HKLM64; Subkey: "{#AutostartComponentKey}"; ValueType: none; Flags: deletekey; Tasks: regularinstall; Check: ShouldUnregisterAutostartForAllUsers; BeforeInstall: UnregisterAutostartForAllUsers;
437+
Root: HKLM64; Subkey: "{#AutostartRemovalKey}"; ValueType: none; Flags: deletekey; Tasks: regularinstall; Check: ShouldRegisterAutostartForAllUsers;
438+
Root: HKLM64; Subkey: "{#AutostartComponentKey}"; ValueType: string; ValueData: "{#MyAppName}"; Flags: uninsdeletekey; Tasks: regularinstall; Check: ShouldRegisterAutostartForAllUsers;
439+
Root: HKLM64; Subkey: "{#AutostartComponentKey}"; ValueType: string; ValueName: "Version"; ValueData: "{code:AutostartComponentVersion}"; Tasks: regularinstall; Check: ShouldRegisterAutostartForAllUsers;
440+
Root: HKLM64; Subkey: "{#AutostartComponentKey}"; ValueType: dword; ValueName: "IsInstalled"; ValueData: 1; Tasks: regularinstall; Check: ShouldRegisterAutostartForAllUsers;
441+
Root: HKLM64; Subkey: "{#AutostartComponentKey}"; ValueType: string; ValueName: "StubPath"; ValueData: "{code:AutostartStubPathValue}"; Tasks: regularinstall; Check: ShouldRegisterAutostartForAllUsers;
339442

340443
// Register the unigetui:// deep link
341444
Root: HKA; Subkey: "Software\Classes\unigetui"; ValueType: "string"; ValueData: "URL:UniGetUI Protocol"; Flags: uninsdeletekey; Tasks: regularinstall;

‎docs/CLI.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ These parameters are accepted by the app executables in addition to the automati
390390
| `--uninstall-unigetui` | Unregisters UniGetUI from the notification panel and quits. | Historical; only valid for specific old versions. |
391391
| `--uninstall-wingetui` | Unregisters the legacy WingetUI install from the notification panel and quits. | Historical; used by the WingetUI uninstaller. |
392392
| `--migrate-wingetui-to-unigetui` | Migrates legacy WingetUI data and shortcuts, then quits. | Migration helper. |
393+
| `--register-autostart` | Registers UniGetUI to start minimized at login for the current user, then quits. | Windows. Run once per user at logon by per-machine installs; the on/off toggle in Startup Apps is kept. |
393394
| `--help` / `-h` | Prints CLI help. | For the direct verb-based CLI. |
394395
| `--import-settings <file>` | Imports settings from a JSON file. | Existing settings are replaced. |
395396
| `--export-settings <file>` | Exports settings to a JSON file. | Creates or overwrites the file. |
@@ -464,7 +465,7 @@ The installer is Inno Setup based. It supports the standard [Inno Setup command-
464465
| Parameter | Meaning |
465466
| --- | --- |
466467
| `/NoAutoStart` | Do not launch UniGetUI after installation. |
467-
| `/NoRunOnStartup` | Do not register UniGetUI to start minimized at login. |
468+
| `/NoRunOnStartup` | Do not register UniGetUI to start minimized at login. On a per-machine install this applies to every user: an earlier all-users registration is removed, and each user's entry from it is deleted at their next logon. |
468469
| `/NoVCRedist` | Skip installation of the MSVC x64 runtime. |
469470
| `/NoEdgeWebView` | Skip installation of the Microsoft Edge WebView runtime. |
470471
| `/NoChocolatey` | Deprecated no-op kept for compatibility. |

‎src/Shared/SharedPreUiCommandDispatcher.cs‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Microsoft.Win32;
12
using UniGetUI.Core.Logging;
23
using UniGetUI.Core.SettingsEngine;
34
using UniGetUI.Core.SettingsEngine.SecureSettings;
@@ -43,6 +44,9 @@ internal static class SharedPreUiCommandDispatcher
4344
internal const string MigrateWingetUIToUniGetUIArgument = "--migrate-wingetui-to-unigetui";
4445
internal const string UninstallWingetUIArgument = "--uninstall-wingetui";
4546
internal const string UninstallUniGetUIArgument = "--uninstall-unigetui";
47+
internal const string RegisterAutostartArgument = "--register-autostart";
48+
internal const string AutostartRunSubKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
49+
internal const string AutostartValueName = "WingetUI";
4650

4751
private const string EnableSecureSettingForUserArgument = SecureSettings.Args.ENABLE_FOR_USER;
4852
private const string DisableSecureSettingForUserArgument = SecureSettings.Args.DISABLE_FOR_USER;
@@ -131,6 +135,11 @@ public static string[] IgnoreArgumentsInjectedIntoProtocolLaunch(string[] args)
131135
return MigrateWingetUIToUniGetUI();
132136
}
133137

138+
if (args.Contains(RegisterAutostartArgument))
139+
{
140+
return RegisterAutostart(exitCodes);
141+
}
142+
134143
if (args.Contains(UninstallWingetUIArgument) || args.Contains(UninstallUniGetUIArgument))
135144
{
136145
return exitCodes.Success;
@@ -349,6 +358,40 @@ SharedPreUiCommandExitCodes exitCodes
349358
}
350359
}
351360

361+
public static int RegisterAutostart(SharedPreUiCommandExitCodes exitCodes)
362+
{
363+
if (!OperatingSystem.IsWindows() || Environment.ProcessPath is not { Length: > 0 } executablePath)
364+
{
365+
return exitCodes.Failed;
366+
}
367+
368+
return RegisterAutostart(Registry.CurrentUser, AutostartRunSubKey, executablePath, exitCodes);
369+
}
370+
371+
internal static int RegisterAutostart(
372+
RegistryKey root,
373+
string runSubKey,
374+
string executablePath,
375+
SharedPreUiCommandExitCodes exitCodes
376+
)
377+
{
378+
if (!OperatingSystem.IsWindows())
379+
{
380+
return exitCodes.Failed;
381+
}
382+
383+
try
384+
{
385+
using RegistryKey runKey = root.CreateSubKey(runSubKey);
386+
runKey.SetValue(AutostartValueName, $"\"{executablePath}\" --daemon", RegistryValueKind.String);
387+
return exitCodes.Success;
388+
}
389+
catch (Exception ex)
390+
{
391+
return ex.HResult;
392+
}
393+
}
394+
352395
public static int MigrateWingetUIToUniGetUI()
353396
{
354397
try

‎src/UniGetUI.Tests/CLIHandlerTests.cs‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,81 @@ public void DeleteShortcuts_ReportsAnInvalidParameterWhenNoPathIsGiven()
353353
);
354354
}
355355

356+
[Fact]
357+
public void RegisterAutostart_WritesTheQuotedDaemonCommandForTheGivenExecutable()
358+
{
359+
if (!OperatingSystem.IsWindows())
360+
{
361+
return;
362+
}
363+
364+
string testRoot = $@"Software\UniGetUI.Tests\{Guid.NewGuid():N}";
365+
string runSubKey = $@"{testRoot}\Run";
366+
try
367+
{
368+
Assert.Equal(
369+
SharedPreUiCommandDispatcher.WindowsCliExitCodes.Success,
370+
SharedPreUiCommandDispatcher.RegisterAutostart(
371+
Microsoft.Win32.Registry.CurrentUser,
372+
runSubKey,
373+
@"C:\Program Files\UniGetUI\UniGetUI.exe",
374+
SharedPreUiCommandDispatcher.WindowsCliExitCodes
375+
)
376+
);
377+
378+
using Microsoft.Win32.RegistryKey? runKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(runSubKey);
379+
Assert.NotNull(runKey);
380+
Assert.Equal(
381+
Microsoft.Win32.RegistryValueKind.String,
382+
runKey.GetValueKind(SharedPreUiCommandDispatcher.AutostartValueName)
383+
);
384+
Assert.Equal(
385+
"\"C:\\Program Files\\UniGetUI\\UniGetUI.exe\" --daemon",
386+
runKey.GetValue(SharedPreUiCommandDispatcher.AutostartValueName)
387+
);
388+
}
389+
finally
390+
{
391+
Microsoft.Win32.Registry.CurrentUser.DeleteSubKeyTree(testRoot, throwOnMissingSubKey: false);
392+
}
393+
}
394+
395+
[Fact]
396+
public void RegisterAutostart_OverwritesAStaleEntry()
397+
{
398+
if (!OperatingSystem.IsWindows())
399+
{
400+
return;
401+
}
402+
403+
string testRoot = $@"Software\UniGetUI.Tests\{Guid.NewGuid():N}";
404+
string runSubKey = $@"{testRoot}\Run";
405+
try
406+
{
407+
using (Microsoft.Win32.RegistryKey stale = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(runSubKey))
408+
{
409+
stale.SetValue(SharedPreUiCommandDispatcher.AutostartValueName, @"C:\Old\WingetUI.exe");
410+
}
411+
412+
SharedPreUiCommandDispatcher.RegisterAutostart(
413+
Microsoft.Win32.Registry.CurrentUser,
414+
runSubKey,
415+
@"D:\Apps\UniGetUI\UniGetUI.exe",
416+
SharedPreUiCommandDispatcher.WindowsCliExitCodes
417+
);
418+
419+
using Microsoft.Win32.RegistryKey? runKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(runSubKey);
420+
Assert.Equal(
421+
"\"D:\\Apps\\UniGetUI\\UniGetUI.exe\" --daemon",
422+
runKey?.GetValue(SharedPreUiCommandDispatcher.AutostartValueName)
423+
);
424+
}
425+
finally
426+
{
427+
Microsoft.Win32.Registry.CurrentUser.DeleteSubKeyTree(testRoot, throwOnMissingSubKey: false);
428+
}
429+
}
430+
356431
[Theory]
357432
[InlineData("--help")]
358433
[InlineData("-h")]

0 commit comments

Comments
 (0)