Skip to content

Commit 6804c17

Browse files
Run update installer silently (#314)
Co-authored-by: Owen McGirr <o.a.mcgirr@gmail.com>
1 parent b44a69a commit 6804c17

8 files changed

Lines changed: 90 additions & 16 deletions

File tree

installer/SwitchifyPc.DotNet.nsi

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ Section "Switchify PC" SecMain
6666
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Switchify PC" "UninstallString" "$INSTDIR\Uninstall.exe"
6767
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Switchify PC" "NoModify" 1
6868
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Switchify PC" "NoRepair" 1
69+
70+
IfSilent 0 done
71+
Exec '"$INSTDIR\${APP_EXE}"'
72+
done:
6973
SectionEnd
7074

7175
Section "Uninstall"
@@ -118,6 +122,20 @@ Function CloseRunningAppForInstall
118122
Return
119123
${EndIf}
120124

125+
IfSilent silent_force_close force_prompt
126+
127+
silent_force_close:
128+
DetailPrint "Closing Switchify PC..."
129+
nsExec::ExecToStack 'cmd /c taskkill /IM "${APP_EXE}" /F /T >NUL 2>NUL'
130+
Pop $0
131+
Pop $1
132+
Call WaitForAppExit
133+
${If} $0 == 0
134+
Return
135+
${EndIf}
136+
SetErrorLevel 1
137+
Abort
138+
121139
force_prompt:
122140
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "Switchify PC needs to close before installation can continue.$\r$\n$\r$\nClick OK to close Switchify PC and continue installing, or Cancel to stop the installer." IDOK force_close IDCANCEL cancel
123141

@@ -176,6 +194,20 @@ Function un.CloseRunningAppForInstall
176194
Return
177195
${EndIf}
178196

197+
IfSilent silent_force_close force_prompt
198+
199+
silent_force_close:
200+
DetailPrint "Closing Switchify PC..."
201+
nsExec::ExecToStack 'cmd /c taskkill /IM "${APP_EXE}" /F /T >NUL 2>NUL'
202+
Pop $0
203+
Pop $1
204+
Call un.WaitForAppExit
205+
${If} $0 == 0
206+
Return
207+
${EndIf}
208+
SetErrorLevel 1
209+
Abort
210+
179211
force_prompt:
180212
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "Switchify PC needs to close before uninstalling can continue.$\r$\n$\r$\nClick OK to close Switchify PC and continue uninstalling, or Cancel to stop the uninstaller." IDOK force_close IDCANCEL cancel
181213

scripts/Verify-DotnetPackage.ps1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ function Assert-InstallerScript {
4949
Assert-Includes -Content $content -Expected 'find /I "${APP_EXE}"' -Label 'installer process detection'
5050
Assert-Includes -Content $content -Expected '--quit-for-install' -Label 'installer graceful app quit signal'
5151
Assert-Includes -Content $content -Expected 'taskkill /IM "${APP_EXE}" /F /T' -Label 'installer force-close fallback'
52+
Assert-Includes -Content $content -Expected 'IfSilent' -Label 'installer silent-mode handling'
53+
Assert-Includes -Content $content -Expected 'Exec ''"$INSTDIR\${APP_EXE}"''' -Label 'installer silent relaunch'
54+
Assert-Includes -Content $content -Expected 'silent_force_close:' -Label 'installer silent force-close path'
55+
Assert-Includes -Content $content -Expected 'SetErrorLevel 1' -Label 'installer silent failure exit code'
5256
Assert-Includes -Content $content -Expected 'Software\Microsoft\Windows\CurrentVersion\Uninstall\Switchify PC' -Label 'installer uninstall registry key'
5357
}
5458

src/SwitchifyPc.App/SettingsWindow.xaml.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,6 @@ await RunActionAsync(
145145
private async void InstallUpdate_Click(object sender, RoutedEventArgs e)
146146
{
147147
if (controller is null) return;
148-
MessageBoxResult confirmation = WpfMessageBox.Show(
149-
"Open the downloaded Switchify PC installer?\n\nThe installer will open in Windows and may close Switchify PC while the update is installed. If you rely on Switchify to control this computer, make sure you have another way to complete the installer before continuing.",
150-
"Install update?",
151-
MessageBoxButton.OKCancel,
152-
MessageBoxImage.Warning,
153-
MessageBoxResult.Cancel);
154-
if (confirmation != MessageBoxResult.OK) return;
155148

156149
await RunActionAsync(async () =>
157150
{

src/SwitchifyPc.Core/Updates/UpdateModels.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,5 @@ public sealed record UpdateInstallerLaunchResult(bool Ok, UpdateInstallFailureRe
9696
public static UpdateInstallerLaunchResult Success() => new(true);
9797
public static UpdateInstallerLaunchResult Failure(UpdateInstallFailureReason reason) => new(false, reason);
9898
}
99+
100+
public sealed record UpdateInstallerLaunchOptions(bool Silent);

src/SwitchifyPc.Core/Updates/UpdateService.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ Task<UpdateDownloadOutcome> DownloadUpdateAsync(
1212

1313
public interface IUpdateInstallerLauncher
1414
{
15-
Task<UpdateInstallerLaunchResult> LaunchAsync(string? installerPath, CancellationToken cancellationToken = default);
15+
Task<UpdateInstallerLaunchResult> LaunchAsync(
16+
string? installerPath,
17+
UpdateInstallerLaunchOptions options,
18+
CancellationToken cancellationToken = default);
1619
}
1720

1821
public interface IUpdatePollScheduler
@@ -228,7 +231,9 @@ public async Task<UpdateInstallResult> InstallDownloadedUpdateAsync(Cancellation
228231
installerPath = downloadedInstallerPath;
229232
}
230233

231-
UpdateInstallerLaunchResult result = await installerLauncher.LaunchAsync(installerPath, cancellationToken).ConfigureAwait(false);
234+
UpdateInstallerLaunchResult result = await installerLauncher
235+
.LaunchAsync(installerPath, new UpdateInstallerLaunchOptions(Silent: true), cancellationToken)
236+
.ConfigureAwait(false);
232237
return result.Ok
233238
? UpdateInstallResult.Success()
234239
: UpdateInstallResult.Failure(result.Reason ?? UpdateInstallFailureReason.InstallerLaunchFailed);

src/SwitchifyPc.Tests/UpdateServiceTests.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public async Task InstallOpensDownloadedInstallerAndKeepsServiceState()
103103
Assert.True(result.Ok);
104104
Assert.Equal(1, launcher.LaunchCalls);
105105
Assert.Equal(@"C:\cache\Switchify-PC-Setup-0.2.0-x64.exe", launcher.LastInstallerPath);
106+
Assert.Equal(new UpdateInstallerLaunchOptions(Silent: true), launcher.LastOptions);
106107
Assert.Equal(UpdateDownloadStatus.Downloaded, service.GetState().Download.Status);
107108
}
108109

@@ -291,12 +292,17 @@ private sealed class FakeInstallerLauncher : IUpdateInstallerLauncher
291292
{
292293
public int LaunchCalls { get; private set; }
293294
public string? LastInstallerPath { get; private set; }
295+
public UpdateInstallerLaunchOptions? LastOptions { get; private set; }
294296
public UpdateInstallerLaunchResult Result { get; init; } = UpdateInstallerLaunchResult.Success();
295297

296-
public Task<UpdateInstallerLaunchResult> LaunchAsync(string? installerPath, CancellationToken cancellationToken = default)
298+
public Task<UpdateInstallerLaunchResult> LaunchAsync(
299+
string? installerPath,
300+
UpdateInstallerLaunchOptions options,
301+
CancellationToken cancellationToken = default)
297302
{
298303
LaunchCalls++;
299304
LastInstallerPath = installerPath;
305+
LastOptions = options;
300306
return Task.FromResult(Result);
301307
}
302308
}

src/SwitchifyPc.Tests/WindowsUpdateInstallerLauncherTests.cs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ public async Task ReturnsInstallerUnavailableWhenInstallerPathIsMissing()
1212
FakeProcessShell shell = new();
1313
WindowsUpdateInstallerLauncher launcher = new(shell, _ => false);
1414

15-
UpdateInstallerLaunchResult result = await launcher.LaunchAsync(@"C:\cache\missing.exe");
15+
UpdateInstallerLaunchResult result = await launcher.LaunchAsync(
16+
@"C:\cache\missing.exe",
17+
new UpdateInstallerLaunchOptions(Silent: false));
1618

1719
Assert.False(result.Ok);
1820
Assert.Equal(UpdateInstallFailureReason.InstallerUnavailable, result.Reason);
@@ -25,7 +27,9 @@ public async Task ReturnsInstallerUnavailableWhenInstallerPathIsNull()
2527
FakeProcessShell shell = new();
2628
WindowsUpdateInstallerLauncher launcher = new(shell, _ => true);
2729

28-
UpdateInstallerLaunchResult result = await launcher.LaunchAsync(null);
30+
UpdateInstallerLaunchResult result = await launcher.LaunchAsync(
31+
null,
32+
new UpdateInstallerLaunchOptions(Silent: false));
2933

3034
Assert.False(result.Ok);
3135
Assert.Equal(UpdateInstallFailureReason.InstallerUnavailable, result.Reason);
@@ -38,23 +42,45 @@ public async Task OpensInstallerThroughWindowsShell()
3842
FakeProcessShell shell = new();
3943
WindowsUpdateInstallerLauncher launcher = new(shell, _ => true);
4044

41-
UpdateInstallerLaunchResult result = await launcher.LaunchAsync(@"C:\cache\Switchify-PC-Setup-0.2.0-x64.exe");
45+
UpdateInstallerLaunchResult result = await launcher.LaunchAsync(
46+
@"C:\cache\Switchify-PC-Setup-0.2.0-x64.exe",
47+
new UpdateInstallerLaunchOptions(Silent: false));
4248

4349
Assert.True(result.Ok);
4450
Assert.NotNull(shell.LastStartInfo);
4551
Assert.Equal(@"C:\cache\Switchify-PC-Setup-0.2.0-x64.exe", shell.LastStartInfo.FileName);
52+
Assert.Equal(string.Empty, shell.LastStartInfo.Arguments);
4653
Assert.True(shell.LastStartInfo.UseShellExecute);
4754
Assert.Equal(ProcessWindowStyle.Normal, shell.LastStartInfo.WindowStyle);
4855
Assert.Equal(@"C:\cache", shell.LastStartInfo.WorkingDirectory);
4956
}
5057

58+
[Fact]
59+
public async Task LaunchesSilentInstallerWithNsisSilentArgument()
60+
{
61+
FakeProcessShell shell = new();
62+
WindowsUpdateInstallerLauncher launcher = new(shell, _ => true);
63+
64+
UpdateInstallerLaunchResult result = await launcher.LaunchAsync(
65+
@"C:\cache\Switchify-PC-Setup-0.2.0-x64.exe",
66+
new UpdateInstallerLaunchOptions(Silent: true));
67+
68+
Assert.True(result.Ok);
69+
Assert.NotNull(shell.LastStartInfo);
70+
Assert.Equal("/S", shell.LastStartInfo.Arguments);
71+
Assert.True(shell.LastStartInfo.UseShellExecute);
72+
Assert.Equal(ProcessWindowStyle.Normal, shell.LastStartInfo.WindowStyle);
73+
}
74+
5175
[Fact]
5276
public async Task ReturnsLaunchFailedWhenShellReturnsFalse()
5377
{
5478
FakeProcessShell shell = new() { StartResult = false };
5579
WindowsUpdateInstallerLauncher launcher = new(shell, _ => true);
5680

57-
UpdateInstallerLaunchResult result = await launcher.LaunchAsync(@"C:\cache\setup.exe");
81+
UpdateInstallerLaunchResult result = await launcher.LaunchAsync(
82+
@"C:\cache\setup.exe",
83+
new UpdateInstallerLaunchOptions(Silent: false));
5884

5985
Assert.False(result.Ok);
6086
Assert.Equal(UpdateInstallFailureReason.InstallerLaunchFailed, result.Reason);
@@ -66,7 +92,9 @@ public async Task ReturnsLaunchFailedWhenShellThrows()
6692
FakeProcessShell shell = new() { ThrowOnStart = true };
6793
WindowsUpdateInstallerLauncher launcher = new(shell, _ => true);
6894

69-
UpdateInstallerLaunchResult result = await launcher.LaunchAsync(@"C:\cache\setup.exe");
95+
UpdateInstallerLaunchResult result = await launcher.LaunchAsync(
96+
@"C:\cache\setup.exe",
97+
new UpdateInstallerLaunchOptions(Silent: false));
7098

7199
Assert.False(result.Ok);
72100
Assert.Equal(UpdateInstallFailureReason.InstallerLaunchFailed, result.Reason);

src/SwitchifyPc.Windows/Updates/WindowsUpdateInstallerLauncher.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ public WindowsUpdateInstallerLauncher(IProcessShell? processShell = null, Func<s
2929
this.fileExists = fileExists ?? File.Exists;
3030
}
3131

32-
public Task<UpdateInstallerLaunchResult> LaunchAsync(string? installerPath, CancellationToken cancellationToken = default)
32+
public Task<UpdateInstallerLaunchResult> LaunchAsync(
33+
string? installerPath,
34+
UpdateInstallerLaunchOptions options,
35+
CancellationToken cancellationToken = default)
3336
{
3437
if (string.IsNullOrWhiteSpace(installerPath) || !fileExists(installerPath))
3538
{
@@ -42,6 +45,7 @@ public Task<UpdateInstallerLaunchResult> LaunchAsync(string? installerPath, Canc
4245
ProcessStartInfo startInfo = new()
4346
{
4447
FileName = installerPath,
48+
Arguments = options.Silent ? "/S" : string.Empty,
4549
UseShellExecute = true,
4650
WindowStyle = ProcessWindowStyle.Normal,
4751
WorkingDirectory = Path.GetDirectoryName(installerPath) ?? string.Empty

0 commit comments

Comments
 (0)