Skip to content

Commit 2f8f3a1

Browse files
committed
feat: add auto updater
1 parent 22c9bcd commit 2f8f3a1

29 files changed

+2594
-47
lines changed

.idea/.idea.Coder.Desktop/.idea/projectSettingsUpdater.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

App/App.csproj

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
<DefineConstants>DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION</DefineConstants>
2020

2121
<AssemblyName>Coder Desktop</AssemblyName>
22+
<AssemblyTitle>Coder Desktop</AssemblyTitle>
23+
<Company>Coder Technologies Inc.</Company>
24+
<Product>Coder Desktop</Product>
25+
<Copyright>© Coder Technologies Inc.</Copyright>
2226
<ApplicationIcon>coder.ico</ApplicationIcon>
2327
</PropertyGroup>
2428

@@ -30,10 +34,12 @@
3034
</PropertyGroup>
3135

3236
<ItemGroup>
33-
<Content Include="coder.ico" />
37+
<None Remove="Views\Pages\UpdaterDownloadProgressMainPage.xaml" />
3438
</ItemGroup>
3539

3640
<ItemGroup>
41+
<Content Include="coder.ico" />
42+
<EmbeddedResource Include="Assets\changelog.css" />
3743
<Manifest Include="$(ApplicationManifest)" />
3844
</ItemGroup>
3945

@@ -67,16 +73,27 @@
6773
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
6874
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.4" />
6975
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.250108002" />
76+
<PackageReference Include="NetSparkleUpdater.SparkleUpdater" Version="3.0.2" />
7077
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
7178
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
7279
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
7380
<PackageReference Include="WinUIEx" Version="2.5.1" />
7481
</ItemGroup>
7582

83+
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
84+
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
85+
</ItemGroup>
86+
7687
<ItemGroup>
7788
<ProjectReference Include="..\CoderSdk\CoderSdk.csproj" />
7889
<ProjectReference Include="..\MutagenSdk\MutagenSdk.csproj" />
7990
<ProjectReference Include="..\Vpn.Proto\Vpn.Proto.csproj" />
8091
<ProjectReference Include="..\Vpn\Vpn.csproj" />
8192
</ItemGroup>
93+
94+
<ItemGroup>
95+
<Page Update="Views\Pages\UpdaterDownloadProgressMainPage.xaml">
96+
<Generator>MSBuild:Compile</Generator>
97+
</Page>
98+
</ItemGroup>
8299
</Project>

App/App.xaml.cs

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,38 @@
2121
using Microsoft.Win32;
2222
using Microsoft.Windows.AppLifecycle;
2323
using Microsoft.Windows.AppNotifications;
24+
using NetSparkleUpdater.Interfaces;
2425
using Serilog;
2526
using LaunchActivatedEventArgs = Microsoft.UI.Xaml.LaunchActivatedEventArgs;
2627

2728
namespace Coder.Desktop.App;
2829

2930
public partial class App : Application
3031
{
31-
private readonly IServiceProvider _services;
32-
33-
private bool _handleWindowClosed = true;
3432
private const string MutagenControllerConfigSection = "MutagenController";
33+
private const string UpdaterConfigSection = "Updater";
3534

3635
#if !DEBUG
3736
private const string ConfigSubKey = @"SOFTWARE\Coder Desktop\App";
38-
private const string logFilename = "app.log";
37+
private const string LogFilename = "app.log";
38+
private const string DefaultLogLevel = "Information";
3939
#else
4040
private const string ConfigSubKey = @"SOFTWARE\Coder Desktop\DebugApp";
41-
private const string logFilename = "debug-app.log";
41+
private const string LogFilename = "debug-app.log";
42+
private const string DefaultLogLevel = "Debug";
4243
#endif
4344

45+
// HACK: This is exposed for dispatcher queue access. The notifier uses
46+
// this to ensure action callbacks run in the UI thread (as
47+
// activation events aren't in the main thread).
48+
public TrayWindow? TrayWindow;
49+
50+
private readonly IServiceProvider _services;
4451
private readonly ILogger<App> _logger;
4552
private readonly IUriHandler _uriHandler;
53+
private readonly IUserNotifier _userNotifier;
54+
55+
private bool _handleWindowClosed = true;
4656

4757
public App()
4858
{
@@ -55,7 +65,17 @@ public App()
5565
configBuilder.Add(
5666
new RegistryConfigurationSource(Registry.LocalMachine, ConfigSubKey));
5767
configBuilder.Add(
58-
new RegistryConfigurationSource(Registry.CurrentUser, ConfigSubKey));
68+
new RegistryConfigurationSource(
69+
Registry.CurrentUser,
70+
ConfigSubKey,
71+
// Block "Updater:" configuration from HKCU, so that updater
72+
// settings can only be set at the HKLM level.
73+
//
74+
// HACK: This isn't super robust, but the security risk is
75+
// minor anyway. Malicious apps running as the user could
76+
// likely override this setting by altering the memory of
77+
// this app.
78+
UpdaterConfigSection + ":"));
5979

6080
var services = builder.Services;
6181

@@ -81,6 +101,12 @@ public App()
81101
services.AddSingleton<IRdpConnector, RdpConnector>();
82102
services.AddSingleton<IUriHandler, UriHandler>();
83103

104+
services.AddOptions<UpdaterConfig>()
105+
.Bind(builder.Configuration.GetSection(UpdaterConfigSection));
106+
services.AddSingleton<IUpdaterUpdateAvailableViewModelFactory, UpdaterUpdateAvailableViewModelFactory>();
107+
services.AddSingleton<IUIFactory, CoderSparkleUIFactory>();
108+
services.AddSingleton<IUpdateController, SparkleUpdateController>();
109+
84110
// SignInWindow views and view models
85111
services.AddTransient<SignInViewModel>();
86112
services.AddTransient<SignInWindow>();
@@ -107,8 +133,9 @@ public App()
107133
services.AddTransient<TrayWindow>();
108134

109135
_services = services.BuildServiceProvider();
110-
_logger = (ILogger<App>)_services.GetService(typeof(ILogger<App>))!;
111-
_uriHandler = (IUriHandler)_services.GetService(typeof(IUriHandler))!;
136+
_logger = _services.GetRequiredService<ILogger<App>>();
137+
_uriHandler = _services.GetRequiredService<IUriHandler>();
138+
_userNotifier = _services.GetRequiredService<IUserNotifier>();
112139

113140
InitializeComponent();
114141
}
@@ -129,6 +156,18 @@ public async Task ExitApplication()
129156
protected override void OnLaunched(LaunchActivatedEventArgs args)
130157
{
131158
_logger.LogInformation("new instance launched");
159+
160+
// Prevent the TrayWindow from closing, just hide it.
161+
if (TrayWindow != null)
162+
throw new InvalidOperationException("OnLaunched was called multiple times? TrayWindow is already set");
163+
TrayWindow = _services.GetRequiredService<TrayWindow>();
164+
TrayWindow.Closed += (_, closedArgs) =>
165+
{
166+
if (!_handleWindowClosed) return;
167+
closedArgs.Handled = true;
168+
TrayWindow.AppWindow.Hide();
169+
};
170+
132171
// Start connecting to the manager in the background.
133172
var rpcController = _services.GetRequiredService<IRpcController>();
134173
if (rpcController.GetState().RpcLifecycle == RpcLifecycle.Disconnected)
@@ -179,15 +218,6 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
179218

180219
syncSessionCts.Dispose();
181220
}, CancellationToken.None);
182-
183-
// Prevent the TrayWindow from closing, just hide it.
184-
var trayWindow = _services.GetRequiredService<TrayWindow>();
185-
trayWindow.Closed += (_, closedArgs) =>
186-
{
187-
if (!_handleWindowClosed) return;
188-
closedArgs.Handled = true;
189-
trayWindow.AppWindow.Hide();
190-
};
191221
}
192222

193223
public void OnActivated(object? sender, AppActivationArguments args)
@@ -229,27 +259,36 @@ public void OnActivated(object? sender, AppActivationArguments args)
229259

230260
public void HandleNotification(AppNotificationManager? sender, AppNotificationActivatedEventArgs args)
231261
{
232-
// right now, we don't do anything other than log
233-
_logger.LogInformation("handled notification activation");
262+
_logger.LogInformation("handled notification activation: {Argument}", args.Argument);
263+
_userNotifier.HandleActivation(args);
234264
}
235265

236266
private static void AddDefaultConfig(IConfigurationBuilder builder)
237267
{
238268
var logPath = Path.Combine(
239269
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
240270
"CoderDesktop",
241-
logFilename);
271+
LogFilename);
242272
builder.AddInMemoryCollection(new Dictionary<string, string?>
243273
{
244274
[MutagenControllerConfigSection + ":MutagenExecutablePath"] = @"C:\mutagen.exe",
275+
245276
["Serilog:Using:0"] = "Serilog.Sinks.File",
246-
["Serilog:MinimumLevel"] = "Information",
277+
["Serilog:MinimumLevel"] = DefaultLogLevel,
247278
["Serilog:Enrich:0"] = "FromLogContext",
248279
["Serilog:WriteTo:0:Name"] = "File",
249280
["Serilog:WriteTo:0:Args:path"] = logPath,
250281
["Serilog:WriteTo:0:Args:outputTemplate"] =
251282
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}",
252283
["Serilog:WriteTo:0:Args:rollingInterval"] = "Day",
284+
285+
#if DEBUG
286+
["Serilog:Using:1"] = "Serilog.Sinks.Debug",
287+
["Serilog:Enrich:1"] = "FromLogContext",
288+
["Serilog:WriteTo:1:Name"] = "Debug",
289+
["Serilog:WriteTo:1:Args:outputTemplate"] =
290+
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}",
291+
#endif
253292
});
254293
}
255294
}

0 commit comments

Comments
 (0)