Skip to content

Commit 11c1f28

Browse files
committed
Merge branch 'main' into dean/remote-dir-picker
2 parents 9ae1dad + b803aa1 commit 11c1f28

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1033
-749
lines changed

App/App.csproj

+6-3
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,13 @@
6262
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
6363
</PackageReference>
6464
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.2.0" />
65-
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
66-
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
67-
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.1" />
65+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
66+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
67+
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.4" />
6868
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.250108002" />
69+
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
70+
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
71+
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
6972
<PackageReference Include="WinUIEx" Version="2.5.1" />
7073
</ItemGroup>
7174

App/App.xaml.cs

+97-17
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Diagnostics;
4+
using System.IO;
35
using System.Threading;
46
using System.Threading.Tasks;
7+
using Windows.ApplicationModel.Activation;
58
using Coder.Desktop.App.Models;
69
using Coder.Desktop.App.Services;
710
using Coder.Desktop.App.ViewModels;
@@ -12,8 +15,12 @@
1215
using Microsoft.Extensions.Configuration;
1316
using Microsoft.Extensions.DependencyInjection;
1417
using Microsoft.Extensions.Hosting;
18+
using Microsoft.Extensions.Logging;
1519
using Microsoft.UI.Xaml;
1620
using Microsoft.Win32;
21+
using Microsoft.Windows.AppLifecycle;
22+
using Serilog;
23+
using LaunchActivatedEventArgs = Microsoft.UI.Xaml.LaunchActivatedEventArgs;
1724

1825
namespace Coder.Desktop.App;
1926

@@ -22,22 +29,39 @@ public partial class App : Application
2229
private readonly IServiceProvider _services;
2330

2431
private bool _handleWindowClosed = true;
32+
private const string MutagenControllerConfigSection = "MutagenController";
2533

2634
#if !DEBUG
27-
private const string MutagenControllerConfigSection = "AppMutagenController";
35+
private const string ConfigSubKey = @"SOFTWARE\Coder Desktop\App";
36+
private const string logFilename = "app.log";
2837
#else
29-
private const string MutagenControllerConfigSection = "DebugAppMutagenController";
38+
private const string ConfigSubKey = @"SOFTWARE\Coder Desktop\DebugApp";
39+
private const string logFilename = "debug-app.log";
3040
#endif
3141

42+
private readonly ILogger<App> _logger;
43+
3244
public App()
3345
{
3446
var builder = Host.CreateApplicationBuilder();
47+
var configBuilder = builder.Configuration as IConfigurationBuilder;
3548

36-
(builder.Configuration as IConfigurationBuilder).Add(
37-
new RegistryConfigurationSource(Registry.LocalMachine, @"SOFTWARE\Coder Desktop"));
49+
// Add config in increasing order of precedence: first builtin defaults, then HKLM, finally HKCU
50+
// so that the user's settings in the registry take precedence.
51+
AddDefaultConfig(configBuilder);
52+
configBuilder.Add(
53+
new RegistryConfigurationSource(Registry.LocalMachine, ConfigSubKey));
54+
configBuilder.Add(
55+
new RegistryConfigurationSource(Registry.CurrentUser, ConfigSubKey));
3856

3957
var services = builder.Services;
4058

59+
// Logging
60+
builder.Services.AddSerilog((_, loggerConfig) =>
61+
{
62+
loggerConfig.ReadFrom.Configuration(builder.Configuration);
63+
});
64+
4165
services.AddSingleton<IAgentApiClientFactory, AgentApiClientFactory>();
4266

4367
services.AddSingleton<ICredentialManager, CredentialManager>();
@@ -71,12 +95,14 @@ public App()
7195
services.AddTransient<TrayWindow>();
7296

7397
_services = services.BuildServiceProvider();
98+
_logger = (ILogger<App>)_services.GetService(typeof(ILogger<App>))!;
7499

75100
InitializeComponent();
76101
}
77102

78103
public async Task ExitApplication()
79104
{
105+
_logger.LogDebug("exiting app");
80106
_handleWindowClosed = false;
81107
Exit();
82108
var syncController = _services.GetRequiredService<ISyncSessionController>();
@@ -89,36 +115,39 @@ public async Task ExitApplication()
89115

90116
protected override void OnLaunched(LaunchActivatedEventArgs args)
91117
{
118+
_logger.LogInformation("new instance launched");
92119
// Start connecting to the manager in the background.
93120
var rpcController = _services.GetRequiredService<IRpcController>();
94121
if (rpcController.GetState().RpcLifecycle == RpcLifecycle.Disconnected)
95122
// Passing in a CT with no cancellation is desired here, because
96123
// the named pipe open will block until the pipe comes up.
97-
// TODO: log
98-
_ = rpcController.Reconnect(CancellationToken.None).ContinueWith(t =>
124+
_logger.LogDebug("reconnecting with VPN service");
125+
_ = rpcController.Reconnect(CancellationToken.None).ContinueWith(t =>
126+
{
127+
if (t.Exception != null)
99128
{
129+
_logger.LogError(t.Exception, "failed to connect to VPN service");
100130
#if DEBUG
101-
if (t.Exception != null)
102-
{
103-
Debug.WriteLine(t.Exception);
104-
Debugger.Break();
105-
}
131+
Debug.WriteLine(t.Exception);
132+
Debugger.Break();
106133
#endif
107-
});
134+
}
135+
});
108136

109137
// Load the credentials in the background.
110138
var credentialManagerCts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
111139
var credentialManager = _services.GetRequiredService<ICredentialManager>();
112140
_ = credentialManager.LoadCredentials(credentialManagerCts.Token).ContinueWith(t =>
113141
{
114-
// TODO: log
115-
#if DEBUG
116142
if (t.Exception != null)
117143
{
144+
_logger.LogError(t.Exception, "failed to load credentials");
145+
#if DEBUG
118146
Debug.WriteLine(t.Exception);
119147
Debugger.Break();
120-
}
121148
#endif
149+
}
150+
122151
credentialManagerCts.Dispose();
123152
}, CancellationToken.None);
124153

@@ -127,10 +156,14 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
127156
var syncSessionController = _services.GetRequiredService<ISyncSessionController>();
128157
_ = syncSessionController.RefreshState(syncSessionCts.Token).ContinueWith(t =>
129158
{
130-
// TODO: log
159+
if (t.IsCanceled || t.Exception != null)
160+
{
161+
_logger.LogError(t.Exception, "failed to refresh sync state (canceled = {canceled})", t.IsCanceled);
131162
#if DEBUG
132-
if (t.IsCanceled || t.Exception != null) Debugger.Break();
163+
Debugger.Break();
133164
#endif
165+
}
166+
134167
syncSessionCts.Dispose();
135168
}, CancellationToken.None);
136169

@@ -143,4 +176,51 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
143176
trayWindow.AppWindow.Hide();
144177
};
145178
}
179+
180+
public void OnActivated(object? sender, AppActivationArguments args)
181+
{
182+
switch (args.Kind)
183+
{
184+
case ExtendedActivationKind.Protocol:
185+
var protoArgs = args.Data as IProtocolActivatedEventArgs;
186+
if (protoArgs == null)
187+
{
188+
_logger.LogWarning("URI activation with null data");
189+
return;
190+
}
191+
192+
HandleURIActivation(protoArgs.Uri);
193+
break;
194+
195+
default:
196+
_logger.LogWarning("activation for {kind}, which is unhandled", args.Kind);
197+
break;
198+
}
199+
}
200+
201+
public void HandleURIActivation(Uri uri)
202+
{
203+
// don't log the query string as that's where we include some sensitive information like passwords
204+
_logger.LogInformation("handling URI activation for {path}", uri.AbsolutePath);
205+
}
206+
207+
private static void AddDefaultConfig(IConfigurationBuilder builder)
208+
{
209+
var logPath = Path.Combine(
210+
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
211+
"CoderDesktop",
212+
logFilename);
213+
builder.AddInMemoryCollection(new Dictionary<string, string?>
214+
{
215+
[MutagenControllerConfigSection + ":MutagenExecutablePath"] = @"C:\mutagen.exe",
216+
["Serilog:Using:0"] = "Serilog.Sinks.File",
217+
["Serilog:MinimumLevel"] = "Information",
218+
["Serilog:Enrich:0"] = "FromLogContext",
219+
["Serilog:WriteTo:0:Name"] = "File",
220+
["Serilog:WriteTo:0:Args:path"] = logPath,
221+
["Serilog:WriteTo:0:Args:outputTemplate"] =
222+
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}",
223+
["Serilog:WriteTo:0:Args:rollingInterval"] = "Day",
224+
});
225+
}
146226
}

App/Package.appxmanifest

-52
This file was deleted.

App/Program.cs

+23-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Reflection;
23
using System.Runtime.InteropServices;
34
using System.Threading;
45
using Microsoft.UI.Dispatching;
@@ -26,7 +27,23 @@ private static void Main(string[] args)
2627
try
2728
{
2829
ComWrappersSupport.InitializeComWrappers();
29-
if (!CheckSingleInstance()) return;
30+
var mainInstance = GetMainInstance();
31+
if (!mainInstance.IsCurrent)
32+
{
33+
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
34+
mainInstance.RedirectActivationToAsync(activationArgs).AsTask().Wait();
35+
return;
36+
}
37+
38+
// Register for URI handling (known as "protocol activation")
39+
#if DEBUG
40+
const string scheme = "coder-debug";
41+
#else
42+
const string scheme = "coder";
43+
#endif
44+
var thisBin = Assembly.GetExecutingAssembly().Location;
45+
ActivationRegistrationManager.RegisterForProtocolActivation(scheme, thisBin + ",1", "Coder Desktop", "");
46+
3047
Application.Start(p =>
3148
{
3249
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
@@ -38,6 +55,9 @@ private static void Main(string[] args)
3855
e.Handled = true;
3956
ShowExceptionAndCrash(e.Exception);
4057
};
58+
59+
// redirections via RedirectActivationToAsync above get routed to the App
60+
mainInstance.Activated += app.OnActivated;
4161
});
4262
}
4363
catch (Exception e)
@@ -46,20 +66,17 @@ private static void Main(string[] args)
4666
}
4767
}
4868

49-
[STAThread]
50-
private static bool CheckSingleInstance()
69+
private static AppInstance GetMainInstance()
5170
{
5271
#if !DEBUG
5372
const string appInstanceName = "Coder.Desktop.App";
5473
#else
5574
const string appInstanceName = "Coder.Desktop.App.Debug";
5675
#endif
5776

58-
var instance = AppInstance.FindOrRegisterForKey(appInstanceName);
59-
return instance.IsCurrent;
77+
return AppInstance.FindOrRegisterForKey(appInstanceName);
6078
}
6179

62-
[STAThread]
6380
private static void ShowExceptionAndCrash(Exception e)
6481
{
6582
const string title = "Coder Desktop Fatal Error";

App/Services/MutagenController.cs

+13-2
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ public interface ISyncSessionController : IAsyncDisposable
8686
/// </summary>
8787
Task<SyncSessionControllerStateModel> RefreshState(CancellationToken ct = default);
8888

89-
Task<SyncSessionModel> CreateSyncSession(CreateSyncSessionRequest req, CancellationToken ct = default);
89+
Task<SyncSessionModel> CreateSyncSession(CreateSyncSessionRequest req, Action<string> progressCallback,
90+
CancellationToken ct = default);
91+
9092
Task<SyncSessionModel> PauseSyncSession(string identifier, CancellationToken ct = default);
9193
Task<SyncSessionModel> ResumeSyncSession(string identifier, CancellationToken ct = default);
9294
Task TerminateSyncSession(string identifier, CancellationToken ct = default);
@@ -201,12 +203,16 @@ public async Task<SyncSessionControllerStateModel> RefreshState(CancellationToke
201203
return state;
202204
}
203205

204-
public async Task<SyncSessionModel> CreateSyncSession(CreateSyncSessionRequest req, CancellationToken ct = default)
206+
public async Task<SyncSessionModel> CreateSyncSession(CreateSyncSessionRequest req,
207+
Action<string>? progressCallback = null, CancellationToken ct = default)
205208
{
206209
using var _ = await _lock.LockAsync(ct);
207210
var client = await EnsureDaemon(ct);
208211

209212
await using var prompter = await Prompter.Create(client, true, ct);
213+
if (progressCallback != null)
214+
prompter.OnProgress += (_, progress) => progressCallback(progress);
215+
210216
var createRes = await client.Synchronization.CreateAsync(new CreateRequest
211217
{
212218
Prompter = prompter.Identifier,
@@ -607,6 +613,8 @@ private async Task StopDaemon(CancellationToken ct)
607613

608614
private class Prompter : IAsyncDisposable
609615
{
616+
public event EventHandler<string>? OnProgress;
617+
610618
private readonly AsyncDuplexStreamingCall<HostRequest, HostResponse> _dup;
611619
private readonly CancellationTokenSource _cts;
612620
private readonly Task _handleRequestsTask;
@@ -688,6 +696,9 @@ private async Task HandleRequests(CancellationToken ct)
688696
if (response.Message == null)
689697
throw new InvalidOperationException("Prompting.Host response stream returned a null message");
690698

699+
if (!response.IsPrompt)
700+
OnProgress?.Invoke(this, response.Message);
701+
691702
// Currently we only reply to SSH fingerprint messages with
692703
// "yes" and send an empty reply for everything else.
693704
var reply = "";

0 commit comments

Comments
 (0)