Skip to content

Commit 195cf26

Browse files
committed
Implement #124 & add constants for exit codes
1 parent 5187cda commit 195cf26

File tree

6 files changed

+231
-21
lines changed

6 files changed

+231
-21
lines changed

VolumeControl/Helpers/Update/ReleaseInfo.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ public struct ReleaseInfo : IComparable<ReleaseInfo>, IEquatable<ReleaseInfo>, I
1818
/// Creates a new <see cref="ReleaseInfo"/> struct.
1919
/// </summary>
2020
/// <param name="responsePacket">The <see cref="GithubReleaseHttpResponse"/> packet received from the API.</param>
21-
public ReleaseInfo(GithubReleaseHttpResponse responsePacket) => _packet = responsePacket;
21+
public ReleaseInfo(GithubReleaseHttpResponse responsePacket) => packet = responsePacket;
2222
#endregion Constructor
2323

2424
#region Fields
25-
private readonly GithubReleaseHttpResponse _packet;
25+
internal readonly GithubReleaseHttpResponse packet;
2626
#endregion Fields
2727

2828
#region Properties
@@ -57,12 +57,12 @@ public static ReleaseInfo Latest
5757
/// <summary>
5858
/// The version number of the release represented by this struct.
5959
/// </summary>
60-
public SemVersion Version => _version ??= (_packet.tag_name.GetSemVer() ?? new(-1));
60+
public SemVersion Version => _version ??= (packet.tag_name.GetSemVer() ?? new(-1));
6161
private SemVersion? _version = null;
6262
/// <summary>
6363
/// The HTML URL of the release.
6464
/// </summary>
65-
public string URL => _packet.html_url;
65+
public string URL => packet.html_url;
6666
#endregion Properties
6767

6868
#region Methods

VolumeControl/Helpers/Update/UpdateChecker.cs

+171-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
using CodingSeb.Localization;
22
using Semver;
33
using System;
4+
using System.Collections.Generic;
45
using System.Diagnostics;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Net.Http;
9+
using System.Runtime.InteropServices;
510
using System.Windows;
611
using VolumeControl.Core;
712
using VolumeControl.Log;
@@ -34,6 +39,10 @@ public void CheckForUpdateNow()
3439
{
3540
ReleaseInfo latest = ReleaseInfo.Latest;
3641

42+
// FIX WHEN DONE
43+
this.ShowUpdatePrompt(latest);
44+
// FIX WHEN DONE
45+
3746
if (latest.CompareTo(this.CurrentVersion) > 0)
3847
{
3948
FLog.Debug($"Latest version ({latest.Version}) is newer than the current version.");
@@ -47,26 +56,144 @@ public void CheckForUpdateNow()
4756
}
4857
#endregion CheckForUpdateNow
4958

59+
#region MakeUpdatePromptMessage
60+
private string MakeUpdatePromptMessage(SemVersion latestVersion)
61+
{
62+
string messageCore = Loc.Tr("VolumeControl.Dialogs.UpdatePrompt.NewVersionAvailableMessage", defaultText:
63+
"A new version of Volume Control is available!\n" +
64+
"Current Version: ${CURRENT_VERSION}\n" +
65+
"Latest Version: ${LATEST_VERSION}")
66+
.Replace("${CURRENT_VERSION}", this.CurrentVersion.ToString())
67+
.Replace("${LATEST_VERSION}", latestVersion.ToString());
68+
69+
string options =
70+
#if RELEASE_FORINSTALLER
71+
Loc.Tr("VolumeControl.Dialogs.UpdatePrompt.NewVersionAvailable_AutoUpdate", defaultText:
72+
"Click 'Yes' to install the latest release.\n" +
73+
"Click 'No' if you don't want to update right now.\n" +
74+
"Click 'Cancel' to disable these prompts.");
75+
#else
76+
Loc.Tr("VolumeControl.Dialogs.UpdatePrompt.NewVersionAvailable_OpenBrowser", defaultText:
77+
"Click 'Yes' to go to the releases page.\n" +
78+
"Click 'No' if you don't want to update right now.\n" +
79+
"Click 'Cancel' to disable these prompts.");
80+
#endif
81+
82+
return messageCore + "\n\n" + options;
83+
}
84+
#endregion MakeUpdatePromptMessage
85+
86+
#region DownloadReleaseAsset
87+
private static string? DownloadReleaseAsset(GithubAssetHttpResponse targetAsset, string outputDirectoryPath)
88+
{
89+
if (!Directory.Exists(outputDirectoryPath))
90+
{
91+
FLog.Error($"[{nameof(Update)}] Download failed; output directory \"{outputDirectoryPath}\" doesn't exist!");
92+
return null;
93+
}
94+
95+
HttpResponseMessage? response;
96+
try
97+
{
98+
using var client = new HttpClient();
99+
using var req = new HttpRequestMessage()
100+
{
101+
RequestUri = new(targetAsset.url),
102+
Method = HttpMethod.Get
103+
};
104+
req.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/octet-stream"));
105+
req.Headers.Add("User-Agent", "Volume Control");
106+
response = client.Send(req);
107+
}
108+
catch (Exception ex)
109+
{
110+
FLog.Error($"[{nameof(Update)}] An exception occurred while downloading \"{targetAsset.name}\":", ex);
111+
return null;
112+
}
113+
if (!response.IsSuccessStatusCode)
114+
{
115+
FLog.Error($"[{nameof(Update)}] Download request failed with status code {response.StatusCode}!");
116+
return null;
117+
}
118+
var outputFilePath = Path.Combine(Path.GetFullPath(outputDirectoryPath), targetAsset.name);
119+
120+
using var stream = response.Content.ReadAsStream();
121+
try
122+
{
123+
using var fileStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
124+
stream.CopyTo(fileStream);
125+
fileStream.Flush();
126+
}
127+
catch (Exception ex)
128+
{
129+
FLog.Error($"[{nameof(Update)}] An exception occurred while writing to file \"{outputFilePath}\":", ex);
130+
return null;
131+
}
132+
133+
return outputFilePath;
134+
}
135+
#endregion DownloadReleaseAsset
136+
50137
#region ShowUpdatePrompt
51138
private void ShowUpdatePrompt(ReleaseInfo releaseInfo)
52139
{
53-
FLog.Info($"Showing update prompt for new version: {releaseInfo.Version}");
140+
FLog.Info($"[{nameof(Update)}] Showing update prompt for new version: {releaseInfo.Version}");
54141

55-
string msg = Loc.Tr("VolumeControl.Dialogs.UpdatePrompt.NewVersionAvailableFormatMsg", "A new version of Volume Control is available!\nDo you want to go to the releases page?\nCurrent Version: ${CURRENT_VERSION}\nLatest Version: ${LATEST_VERSION}\n\nClick 'Yes' to go to the releases page.\nClick 'No' if you don't want to update right now.\nClick 'Cancel' to disable these prompts.");
56-
msg = msg.Replace("${CURRENT_VERSION}", this.CurrentVersion.ToString());
57-
msg = msg.Replace("${LATEST_VERSION}", releaseInfo.Version.ToString());
142+
string msg = MakeUpdatePromptMessage(releaseInfo.Version);
58143

59144
switch (MessageBox.Show(msg, "Update Available", MessageBoxButton.YesNoCancel, MessageBoxImage.Question, MessageBoxResult.Yes))
60145
{
61146
case MessageBoxResult.Yes: // open browser
62-
FLog.Info($"User indicated they want to update to version {releaseInfo.Version}.");
63-
OpenBrowser(releaseInfo.URL);
64-
break;
147+
{
148+
FLog.Info($"[{nameof(Update)}] User indicated they want to update to version {releaseInfo.Version}.");
149+
#if RELEASE_FORINSTALLER
150+
const string installerAssetNameStartsWith = "VolumeControl-Installer";
151+
var installerAsset = releaseInfo.packet.assets.FirstOrDefault(a => a.name.StartsWith(installerAssetNameStartsWith));
152+
if (string.IsNullOrEmpty(installerAsset.url))
153+
{ // failed to find a release asset with the specified name
154+
FLog.Error(
155+
$"[{nameof(Update)}] Failed to download installer for \"{releaseInfo.Version}\"; couldn't find a release asset beginning with \"{installerAssetNameStartsWith}\"!",
156+
$" Found assets: \"{string.Join("\", \"", releaseInfo.packet.assets.Select(a => a.name))}\"");
157+
MessageBox.Show("", "Update Failed!", MessageBoxButton.OK, MessageBoxImage.Error);
158+
return;
159+
}
160+
// download the release asset
161+
if (DownloadReleaseAsset(installerAsset, KnownFolders.GetPath(KnownFolder.Downloads)) is string installerPath)
162+
{ // start the installer
163+
if (ShellHelper.OpenWithDefault(installerPath))
164+
{
165+
FLog.Info($"[{nameof(Update)}] Started \"{installerPath}\"; closing the application.");
166+
Application.Current.Shutdown(Program.EXITCODE_UPDATING);
167+
}
168+
else
169+
{
170+
MessageBox.Show(
171+
Loc.Tr("VolumeControl.Dialogs.UpdatePrompt.StartFailed.Message", "Failed to start the installer, but it was downloaded successfully.\nYour system permissions may not allow running applications from your downloads folder.\n\nYou will have to start the installer manually."),
172+
Loc.Tr("VolumeControl.Dialogs.UpdatePrompt.StartFailed.Caption", "Failed to Start Installer"),
173+
MessageBoxButton.OK,
174+
MessageBoxImage.Error);
175+
}
176+
}
177+
else
178+
{
179+
MessageBox.Show(
180+
Loc.Tr("VolumeControl.Dialogs.UpdatePrompt.DownloadFailed.Message", "An error occurred while downloading the installer, check the log for more information."),
181+
Loc.Tr("VolumeControl.Dialogs.UpdatePrompt.DownloadFailed.Caption", "Download Failed"),
182+
MessageBoxButton.OK,
183+
MessageBoxImage.Error);
184+
}
185+
#else
186+
// open the release URL
187+
OpenBrowser(releaseInfo.URL);
188+
#endif
189+
break;
190+
}
65191
case MessageBoxResult.No:
192+
FLog.Info($"[{nameof(Update)}] User indicated they do not want to update.");
66193
break;
67194
case MessageBoxResult.Cancel: // disable
68195
Settings.ShowUpdatePrompt = false;
69-
FLog.Info("Disabled automatic updates");
196+
FLog.Info($"[{nameof(Update)}] User indicated they want to disable update prompts, and they were disabled.");
70197
_ = MessageBox.Show(Loc.Tr("VolumeControl.Dialogs.UpdatePrompt.DontShowInFutureMsg", "Update prompts will not be shown in the future."));
71198
break;
72199
}
@@ -85,7 +212,7 @@ public static void OpenBrowser(string url)
85212
Verb = "open",
86213
UseShellExecute = true
87214
};
88-
var proc = Process.Start(psi);
215+
using var proc = Process.Start(psi);
89216
FLog.Debug($"Successfully opened default browser with process ID {proc?.Id}");
90217
}
91218
catch (Exception ex)
@@ -97,5 +224,40 @@ public static void OpenBrowser(string url)
97224
#endregion OpenBrowser
98225

99226
#endregion Methods
227+
228+
#region P/Invoke
229+
enum KnownFolder
230+
{
231+
Contacts,
232+
Downloads,
233+
Favorites,
234+
Links,
235+
SavedGames,
236+
SavedSearches
237+
}
238+
static class KnownFolders
239+
{
240+
private static readonly Dictionary<KnownFolder, Guid> _guids = new()
241+
{
242+
[KnownFolder.Contacts] = new("56784854-C6CB-462B-8169-88E350ACB882"),
243+
[KnownFolder.Downloads] = new("374DE290-123F-4565-9164-39C4925E467B"),
244+
[KnownFolder.Favorites] = new("1777F761-68AD-4D8A-87BD-30B759FA33DD"),
245+
[KnownFolder.Links] = new("BFB9D5E0-C6A9-404C-B2B2-AE6DB6AF4968"),
246+
[KnownFolder.SavedGames] = new("4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4"),
247+
[KnownFolder.SavedSearches] = new("7D1D3A04-DEBB-4115-95CF-2F29DA2920DA")
248+
};
249+
250+
public static string GetPath(KnownFolder knownFolder)
251+
{
252+
return SHGetKnownFolderPath(_guids[knownFolder], 0);
253+
}
254+
255+
[DllImport("shell32",
256+
CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)]
257+
private static extern string SHGetKnownFolderPath(
258+
[MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags,
259+
nint hToken = 0);
260+
}
261+
#endregion P/Invoke
100262
}
101263
}

VolumeControl/Localization/en.loc.json

+24-2
Original file line numberDiff line numberDiff line change
@@ -603,8 +603,30 @@
603603
"DontShowInFutureMsg": {
604604
"English (US/CA)": "Update prompts will not be shown in the future."
605605
},
606-
"NewVersionAvailableFormatMsg": {
607-
"English (US/CA)": "A new version of Volume Control is available!\nDo you want to go to the releases page?\nCurrent Version: ${CURRENT_VERSION}\nLatest Version: ${LATEST_VERSION}\n\nClick 'Yes' to go to the releases page.\nClick 'No' if you don't want to update right now.\nClick 'Cancel' to disable these prompts."
606+
"NewVersionAvailableMessage": {
607+
"English (US/CA)": "A new version of Volume Control is available!\nDo you want to go to the releases page?\nCurrent Version: ${CURRENT_VERSION}\nLatest Version: ${LATEST_VERSION}"
608+
},
609+
"NewVersionAvailableOptions_AutoUpdate": {
610+
"English (US/CA)": "Click 'Yes' to install the latest release.\nClick 'No' if you don't want to update right now.\nClick 'Cancel' to disable these prompts."
611+
},
612+
"NewVersionAvailableOptions_OpenBrowser": {
613+
"English (US/CA)": "Click 'Yes' to go to the releases page.\nClick 'No' if you don't want to update right now.\nClick 'Cancel' to disable these prompts."
614+
},
615+
"DownloadFailed": {
616+
"Caption": {
617+
"English (US/CA)": "Download Failed"
618+
},
619+
"Message": {
620+
"English (US/CA)": "An error occurred while downloading the installer, check the log for more information."
621+
}
622+
},
623+
"StartFailed": {
624+
"Caption": {
625+
"English (US/CA)": "Failed to Start Installer"
626+
},
627+
"Message": {
628+
"English (US/CA)": "Failed to start the installer, but it was downloaded successfully.\nYour system permissions may not allow running applications from your downloads folder.\n\nYou will have to start the installer manually."
629+
}
608630
}
609631
},
610632
"RemoveHotkey": {

VolumeControl/Mixer.xaml.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ private void Handle_TargetNameBoxDoubleClick(object sender, MouseButtonEventArgs
205205
}
206206
private void Handle_MinimizeClick(object sender, RoutedEventArgs e) => this.Hide();
207207
private void Handle_MaximizeClick(object sender, RoutedEventArgs e) => this.WindowState = WindowState.Maximized;
208-
private void Handle_CloseClick(object sender, RoutedEventArgs e) => Application.Current.Shutdown();
208+
private void Handle_CloseClick(object sender, RoutedEventArgs e) => Application.Current.Shutdown(Program.EXITCODE_SUCCESS);
209209
private void Handle_CheckForUpdatesClick(object sender, RoutedEventArgs e) => this.VCSettings.Updater.CheckForUpdateNow();
210210
private void ComboBox_RemoveSelection(object sender, SelectionChangedEventArgs e)
211211
{
@@ -336,7 +336,7 @@ private void MultiInstanceCheckbox_CheckStateChanged(object sender, RoutedEventA
336336
MessageBoxResult.OK)
337337
== MessageBoxResult.OK)
338338
{
339-
Application.Current.Shutdown(); //< this waits for the method to return; doing this first seems to prevent crashes
339+
Application.Current.Shutdown(Program.EXITCODE_RESTARTING); //< this waits for the method to return; doing this first seems to prevent crashes
340340
Process.Start(VCSettings.ExecutablePath, "--wait-for-mutex")?.Dispose();
341341
}
342342
}

VolumeControl/Program.cs

+29-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,31 @@ internal static class Program
2525
private const string appMutexIdentifier = "VolumeControlSingleInstance"; //< Don't change this without also changing the installer script (vcsetup.iss)
2626
private static Mutex appMutex = null!;
2727
private static Config Settings = null!;
28+
/// <summary>
29+
/// Occurs when the application failed to start.
30+
/// </summary>
31+
public const int EXITCODE_FAILED = -1;
32+
/// <summary>
33+
/// Occurs when the application succeeded.
34+
/// </summary>
35+
public const int EXITCODE_SUCCESS = 0;
36+
/// <summary>
37+
/// Occurs when the application closed due to a fatal error.
38+
/// </summary>
39+
public const int EXITCODE_ERROR = 1;
40+
/// <summary>
41+
/// Occurs when the application failed to acquire the mutex because another instance has it locked.
42+
/// </summary>
43+
public const int EXITCODE_MUTEX = 2;
44+
/// <summary>
45+
/// Occurs when the application is restarting itself.
46+
/// </summary>
47+
public const int EXITCODE_RESTARTING = 3;
48+
/// <summary>
49+
/// Occurs when the application launched the installer for a new version, and is shutting down to let it take over.
50+
/// Only used in RELEASE_FORINSTALLER configuration.
51+
/// </summary>
52+
public const int EXITCODE_UPDATING = 4;
2853
#endregion Fields
2954

3055
#region Methods
@@ -47,7 +72,7 @@ public static int Main(string[] args)
4772
#if DEBUG
4873
throw; //< rethrow in DEBUG configuration
4974
#else
50-
return 1; //< otherwise return error
75+
return EXITCODE_ERROR; //< otherwise return error
5176
#endif
5277
}
5378
}
@@ -96,7 +121,7 @@ private static int Main_Impl(string[] args)
96121
Console.Error.WriteLine($"Failed to acquire mutex \"{appMutexName}\" because another instance of Volume Control is using it!");
97122
LocalizationHelper.Initialize(false, false, null);
98123
MessageBox.Show(Loc.Tr($"VolumeControl.Dialogs.AnotherInstanceIsRunning.{(Settings.AllowMultipleDistinctInstances ? "MultiInstance" : "SingleInstance")}", "Another instance of Volume Control is already running!").Replace("${PATH}", Settings.Location));
99-
return 2;
124+
return EXITCODE_MUTEX;
100125
}
101126
}
102127
else Console.Out.WriteLine($"Acquired mutex \"{appMutexName}\".");
@@ -164,7 +189,7 @@ private static int Main_Impl(string[] args)
164189

165190
// create the application class
166191
var app = new App();
167-
int rc = -1;
192+
int rc = EXITCODE_FAILED;
168193
try
169194
{
170195
rc = app.Run();
@@ -174,7 +199,7 @@ private static int Main_Impl(string[] args)
174199
{
175200
// cleanup the notify icon if the program crashes (if it shuts down normally, it is correctly disposed of)
176201
app.TrayIcon.Dispose(); //< do this before literally ANYTHING else
177-
FLog.Fatal("App exited because of an unhandled exception:", ex);
202+
FLog.Fatal($"App exited with code {rc} because of an unhandled exception:", ex);
178203
FLog.Log.Flush();
179204
try
180205
{

vcsetup.iss

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ ArchitecturesInstallIn64BitMode=x64
5252
DefaultDirName={autopf}\VolumeControl
5353
DisableProgramGroupPage=yes
5454
; OutputDir=publish
55+
;DON'T CHANGE THIS ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ without also changing the name in UpdateChecker.cs
5556
OutputBaseFilename=VolumeControl-Installer_{#AppVersion}
5657
SetupIconFile=VolumeControl\Resources\icons\iconSilveredInstall.ico
5758
Compression=lzma

0 commit comments

Comments
 (0)