diff --git a/App/App.csproj b/App/App.csproj
index d4f2bed..8b7e810 100644
--- a/App/App.csproj
+++ b/App/App.csproj
@@ -62,11 +62,14 @@
+
+
+
diff --git a/App/App.xaml.cs b/App/App.xaml.cs
index 9895fc8..e1c5cb4 100644
--- a/App/App.xaml.cs
+++ b/App/App.xaml.cs
@@ -6,8 +6,12 @@
using Coder.Desktop.App.ViewModels;
using Coder.Desktop.App.Views;
using Coder.Desktop.App.Views.Pages;
+using Coder.Desktop.Vpn;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
using Microsoft.UI.Xaml;
+using Microsoft.Win32;
namespace Coder.Desktop.App;
@@ -17,12 +21,28 @@ public partial class App : Application
private bool _handleWindowClosed = true;
+#if !DEBUG
+ private const string MutagenControllerConfigSection = "AppMutagenController";
+#else
+ private const string MutagenControllerConfigSection = "DebugAppMutagenController";
+#endif
+
public App()
{
- var services = new ServiceCollection();
+ var builder = Host.CreateApplicationBuilder();
+
+ (builder.Configuration as IConfigurationBuilder).Add(
+ new RegistryConfigurationSource(Registry.LocalMachine, @"SOFTWARE\Coder Desktop"));
+
+ var services = builder.Services;
+
services.AddSingleton();
services.AddSingleton();
+ services.AddOptions()
+ .Bind(builder.Configuration.GetSection(MutagenControllerConfigSection));
+ services.AddSingleton();
+
// SignInWindow views and view models
services.AddTransient();
services.AddTransient();
diff --git a/App/Services/MutagenController.cs b/App/Services/MutagenController.cs
new file mode 100644
index 0000000..7f48426
--- /dev/null
+++ b/App/Services/MutagenController.cs
@@ -0,0 +1,438 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Coder.Desktop.MutagenSdk;
+using Coder.Desktop.MutagenSdk.Proto.Selection;
+using Coder.Desktop.MutagenSdk.Proto.Service.Daemon;
+using Coder.Desktop.MutagenSdk.Proto.Service.Synchronization;
+using Coder.Desktop.Vpn.Utilities;
+using Microsoft.Extensions.Options;
+using TerminateRequest = Coder.Desktop.MutagenSdk.Proto.Service.Daemon.TerminateRequest;
+
+namespace Coder.Desktop.App.Services;
+
+//
+// A file synchronization session to a Coder workspace agent.
+//
+//
+// This implementation is a placeholder while implementing the daemon lifecycle. It's implementation
+// will be backed by the MutagenSDK eventually.
+//
+public class SyncSession
+{
+ public string name { get; init; } = "";
+ public string localPath { get; init; } = "";
+ public string workspace { get; init; } = "";
+ public string agent { get; init; } = "";
+ public string remotePath { get; init; } = "";
+}
+
+public interface ISyncSessionController
+{
+ Task> ListSyncSessions(CancellationToken ct);
+ Task CreateSyncSession(SyncSession session, CancellationToken ct);
+
+ Task TerminateSyncSession(SyncSession session, CancellationToken ct);
+
+ //
+ // Initializes the controller; running the daemon if there are any saved sessions. Must be called and
+ // complete before other methods are allowed.
+ //
+ Task Initialize(CancellationToken ct);
+}
+
+// These values are the config option names used in the registry. Any option
+// here can be configured with `(Debug)?AppMutagenController:OptionName` in the registry.
+//
+// They should not be changed without backwards compatibility considerations.
+// If changed here, they should also be changed in the installer.
+public class MutagenControllerConfig
+{
+ [Required] public string MutagenExecutablePath { get; set; } = @"c:\mutagen.exe";
+}
+
+//
+// A file synchronization controller based on the Mutagen Daemon.
+//
+public sealed class MutagenController : ISyncSessionController, IAsyncDisposable
+{
+ // Lock to protect all non-readonly class members.
+ private readonly RaiiSemaphoreSlim _lock = new(1, 1);
+
+ // daemonProcess is non-null while the daemon is running, starting, or
+ // in the process of stopping.
+ private Process? _daemonProcess;
+
+ private LogWriter? _logWriter;
+
+ // holds an in-progress task starting or stopping the daemon. If task is null,
+ // then we are not starting or stopping, and the _daemonProcess will be null if
+ // the daemon is currently stopped. If the task is not null, the daemon is
+ // starting or stopping. If stopping, the result is null.
+ private Task? _inProgressTransition;
+
+ // holds a client connected to the running mutagen daemon, if the daemon is running.
+ private MutagenClient? _mutagenClient;
+
+ // holds a local count of SyncSessions, primarily so we can tell when to shut down
+ // the daemon because it is unneeded.
+ private int _sessionCount = -1;
+
+ // set to true if we are disposing the controller. Prevents the daemon from being
+ // restarted.
+ private bool _disposing;
+
+ private readonly string _mutagenExecutablePath;
+
+
+ private readonly string _mutagenDataDirectory = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "CoderDesktop",
+ "mutagen");
+
+ public MutagenController(IOptions config)
+ {
+ _mutagenExecutablePath = config.Value.MutagenExecutablePath;
+ }
+
+ public MutagenController(string executablePath, string dataDirectory)
+ {
+ _mutagenExecutablePath = executablePath;
+ _mutagenDataDirectory = dataDirectory;
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ Task? transition = null;
+ using (_ = await _lock.LockAsync(CancellationToken.None))
+ {
+ _disposing = true;
+ if (_inProgressTransition == null && _daemonProcess == null && _mutagenClient == null) return;
+ transition = _inProgressTransition;
+ }
+
+ if (transition != null) await transition;
+ await StopDaemon(new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token);
+ GC.SuppressFinalize(this);
+ }
+
+
+ public async Task CreateSyncSession(SyncSession session, CancellationToken ct)
+ {
+ // reads of _sessionCount are atomic, so don't bother locking for this quick check.
+ if (_sessionCount == -1) throw new InvalidOperationException("Controller must be Initialized first");
+ var client = await EnsureDaemon(ct);
+ // TODO: implement
+ using (_ = await _lock.LockAsync(ct))
+ {
+ _sessionCount += 1;
+ }
+
+ return session;
+ }
+
+
+ public async Task> ListSyncSessions(CancellationToken ct)
+ {
+ // reads of _sessionCount are atomic, so don't bother locking for this quick check.
+ switch (_sessionCount)
+ {
+ case < 0:
+ throw new InvalidOperationException("Controller must be Initialized first");
+ case 0:
+ // If we already know there are no sessions, don't start up the daemon
+ // again.
+ return new List();
+ }
+
+ var client = await EnsureDaemon(ct);
+ // TODO: implement
+ return new List();
+ }
+
+ public async Task Initialize(CancellationToken ct)
+ {
+ using (_ = await _lock.LockAsync(ct))
+ {
+ if (_sessionCount != -1) throw new InvalidOperationException("Initialized more than once");
+ _sessionCount = -2; // in progress
+ }
+
+ var client = await EnsureDaemon(ct);
+ var sessions = await client.Synchronization.ListAsync(new ListRequest
+ {
+ Selection = new Selection
+ {
+ All = true,
+ },
+ }, cancellationToken: ct);
+
+ using (_ = await _lock.LockAsync(ct))
+ {
+ _sessionCount = sessions == null ? 0 : sessions.SessionStates.Count;
+ // check first that no other transition is happening
+ if (_sessionCount != 0 || _inProgressTransition != null)
+ return;
+
+ // don't pass the CancellationToken; we're not going to wait for
+ // this Task anyway.
+ var transition = StopDaemon(new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token);
+ _inProgressTransition = transition;
+ _ = transition.ContinueWith(RemoveTransition, CancellationToken.None);
+ // here we don't need to wait for the transition to complete
+ // before returning from Initialize(), since other operations
+ // will wait for the _inProgressTransition to complete before
+ // doing anything.
+ }
+ }
+
+ public async Task TerminateSyncSession(SyncSession session, CancellationToken ct)
+ {
+ if (_sessionCount < 0) throw new InvalidOperationException("Controller must be Initialized first");
+ var client = await EnsureDaemon(ct);
+ // TODO: implement
+
+ // here we don't use the Cancellation Token, since we want to decrement and possibly
+ // stop the daemon even if we were cancelled, since we already successfully terminated
+ // the session.
+ using (_ = await _lock.LockAsync(CancellationToken.None))
+ {
+ _sessionCount -= 1;
+ if (_sessionCount == 0)
+ // check first that no other transition is happening
+ if (_inProgressTransition == null)
+ {
+ var transition = StopDaemon(CancellationToken.None);
+ _inProgressTransition = transition;
+ _ = transition.ContinueWith(RemoveTransition, CancellationToken.None);
+ // here we don't need to wait for the transition to complete
+ // before returning, since other operations
+ // will wait for the _inProgressTransition to complete before
+ // doing anything.
+ }
+ }
+ }
+
+
+ private async Task EnsureDaemon(CancellationToken ct)
+ {
+ while (true)
+ {
+ ct.ThrowIfCancellationRequested();
+ Task transition;
+ using (_ = await _lock.LockAsync(ct))
+ {
+ if (_disposing) throw new ObjectDisposedException(ToString(), "async disposal underway");
+ if (_mutagenClient != null && _inProgressTransition == null) return _mutagenClient;
+ if (_inProgressTransition != null)
+ {
+ transition = _inProgressTransition;
+ }
+ else
+ {
+ // no transition in progress, this implies the _mutagenClient
+ // must be null, and we are stopped.
+ _inProgressTransition = StartDaemon(ct);
+ transition = _inProgressTransition;
+ _ = transition.ContinueWith(RemoveTransition, ct);
+ }
+ }
+
+ // wait for the transition without holding the lock.
+ var result = await transition;
+ if (result != null) return result;
+ }
+ }
+
+ //
+ // Remove the completed transition from _inProgressTransition
+ //
+ private void RemoveTransition(Task transition)
+ {
+ using var _ = _lock.Lock();
+ if (_inProgressTransition == transition) _inProgressTransition = null;
+ }
+
+ private async Task StartDaemon(CancellationToken ct)
+ {
+ // stop any orphaned daemon
+ try
+ {
+ var client = new MutagenClient(_mutagenDataDirectory);
+ await client.Daemon.TerminateAsync(new TerminateRequest(), cancellationToken: ct);
+ }
+ catch (FileNotFoundException)
+ {
+ // Mainline; no daemon running.
+ }
+
+ // If we get some failure while creating the log file or starting the process, we'll retry
+ // it up to 5 times x 100ms. Those issues should resolve themselves quickly if they are
+ // going to at all.
+ const int maxAttempts = 5;
+ ListResponse? sessions = null;
+ for (var attempts = 1; attempts <= maxAttempts; attempts++)
+ {
+ ct.ThrowIfCancellationRequested();
+ try
+ {
+ using (_ = await _lock.LockAsync(ct))
+ {
+ StartDaemonProcessLocked();
+ }
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ if (attempts == maxAttempts)
+ throw;
+ // back off a little and try again.
+ await Task.Delay(100, ct);
+ continue;
+ }
+
+ break;
+ }
+
+ return await WaitForDaemon(ct);
+ }
+
+ private async Task WaitForDaemon(CancellationToken ct)
+ {
+ while (true)
+ {
+ ct.ThrowIfCancellationRequested();
+ try
+ {
+ MutagenClient? client;
+ using (_ = await _lock.LockAsync(ct))
+ {
+ client = _mutagenClient ?? new MutagenClient(_mutagenDataDirectory);
+ }
+
+ _ = await client.Daemon.VersionAsync(new VersionRequest(), cancellationToken: ct);
+
+ using (_ = await _lock.LockAsync(ct))
+ {
+ if (_mutagenClient != null)
+ // Some concurrent process already wrote a client; unexpected
+ // since we should be ensuring only one transition is happening
+ // at a time. Start over with the new client.
+ continue;
+ _mutagenClient = client;
+ return _mutagenClient;
+ }
+ }
+ catch (Exception e) when
+ (e is not OperationCanceledException) // TODO: Are there other permanent errors we can detect?
+ {
+ // just wait a little longer for the daemon to come up
+ await Task.Delay(100, ct);
+ }
+ }
+ }
+
+ private void StartDaemonProcessLocked()
+ {
+ if (_daemonProcess != null)
+ throw new InvalidOperationException("startDaemonLock called when daemonProcess already present");
+
+ // create the log file first, so ensure we have permissions
+ var logPath = Path.Combine(_mutagenDataDirectory, "daemon.log");
+ var logStream = new StreamWriter(logPath, true);
+
+ _daemonProcess = new Process();
+ _daemonProcess.StartInfo.FileName = _mutagenExecutablePath;
+ _daemonProcess.StartInfo.Arguments = "daemon run";
+ _daemonProcess.StartInfo.Environment.Add("MUTAGEN_DATA_DIRECTORY", _mutagenDataDirectory);
+ // shell needs to be disabled since we set the environment
+ // https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.environment?view=net-8.0
+ _daemonProcess.StartInfo.UseShellExecute = false;
+ _daemonProcess.StartInfo.RedirectStandardError = true;
+ _daemonProcess.Start();
+
+ var writer = new LogWriter(_daemonProcess.StandardError, logStream);
+ Task.Run(() => { _ = writer.Run(); });
+ _logWriter = writer;
+ }
+
+ private async Task StopDaemon(CancellationToken ct)
+ {
+ Process? process;
+ MutagenClient? client;
+ LogWriter? writer;
+ using (_ = await _lock.LockAsync(ct))
+ {
+ process = _daemonProcess;
+ client = _mutagenClient;
+ writer = _logWriter;
+ _daemonProcess = null;
+ _mutagenClient = null;
+ _logWriter = null;
+ }
+
+ try
+ {
+ if (client == null)
+ {
+ if (process == null) return null;
+ process.Kill(true);
+ }
+ else
+ {
+ try
+ {
+ await client.Daemon.TerminateAsync(new TerminateRequest(), cancellationToken: ct);
+ }
+ catch
+ {
+ if (process == null) return null;
+ process.Kill(true);
+ }
+ }
+
+ if (process == null) return null;
+ var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
+ cts.CancelAfter(TimeSpan.FromSeconds(5));
+ await process.WaitForExitAsync(cts.Token);
+ }
+ finally
+ {
+ client?.Dispose();
+ process?.Dispose();
+ writer?.Dispose();
+ }
+
+ return null;
+ }
+}
+
+public class LogWriter(StreamReader reader, StreamWriter writer) : IDisposable
+{
+ public void Dispose()
+ {
+ reader.Dispose();
+ writer.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ public async Task Run()
+ {
+ try
+ {
+ string? line;
+ while ((line = await reader.ReadLineAsync()) != null) await writer.WriteLineAsync(line);
+ }
+ catch
+ {
+ // TODO: Log?
+ }
+ finally
+ {
+ Dispose();
+ }
+ }
+}
diff --git a/App/packages.lock.json b/App/packages.lock.json
index 264df38..8988638 100644
--- a/App/packages.lock.json
+++ b/App/packages.lock.json
@@ -35,6 +35,46 @@
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1"
}
},
+ "Microsoft.Extensions.Hosting": {
+ "type": "Direct",
+ "requested": "[9.0.1, )",
+ "resolved": "9.0.1",
+ "contentHash": "3wZNcVvC8RW44HDqqmIq+BqF5pgmTQdbNdR9NyYw33JSMnJuclwoJ2PEkrJ/KvD1U/hmqHVL3l5If+Hn3D1fWA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "9.0.1",
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Configuration.Binder": "9.0.1",
+ "Microsoft.Extensions.Configuration.CommandLine": "9.0.1",
+ "Microsoft.Extensions.Configuration.EnvironmentVariables": "9.0.1",
+ "Microsoft.Extensions.Configuration.FileExtensions": "9.0.1",
+ "Microsoft.Extensions.Configuration.Json": "9.0.1",
+ "Microsoft.Extensions.Configuration.UserSecrets": "9.0.1",
+ "Microsoft.Extensions.DependencyInjection": "9.0.1",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Diagnostics": "9.0.1",
+ "Microsoft.Extensions.FileProviders.Abstractions": "9.0.1",
+ "Microsoft.Extensions.FileProviders.Physical": "9.0.1",
+ "Microsoft.Extensions.Hosting.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Logging": "9.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Logging.Configuration": "9.0.1",
+ "Microsoft.Extensions.Logging.Console": "9.0.1",
+ "Microsoft.Extensions.Logging.Debug": "9.0.1",
+ "Microsoft.Extensions.Logging.EventLog": "9.0.1",
+ "Microsoft.Extensions.Logging.EventSource": "9.0.1",
+ "Microsoft.Extensions.Options": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Direct",
+ "requested": "[9.0.1, )",
+ "resolved": "9.0.1",
+ "contentHash": "nggoNKnWcsBIAaOWHA+53XZWrslC7aGeok+aR+epDPRy7HI7GwMnGZE8yEsL2Onw7kMOHVHwKcsDls1INkNUJQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Primitives": "9.0.1"
+ }
+ },
"Microsoft.WindowsAppSDK": {
"type": "Direct",
"requested": "[1.6.250108002, )",
@@ -50,6 +90,28 @@
"resolved": "3.29.3",
"contentHash": "t7nZFFUFwigCwZ+nIXHDLweXvwIpsOXi+P7J7smPT/QjI3EKxnCzTQOhBqyEh6XEzc/pNH+bCFOOSjatrPt6Tw=="
},
+ "Grpc.Core.Api": {
+ "type": "Transitive",
+ "resolved": "2.67.0",
+ "contentHash": "cL1/2f8kc8lsAGNdfCU25deedXVehhLA6GXKLLN4hAWx16XN7BmjYn3gFU+FBpir5yJynvDTHEypr3Tl0j7x/Q=="
+ },
+ "Grpc.Net.Client": {
+ "type": "Transitive",
+ "resolved": "2.67.0",
+ "contentHash": "ofTjJQfegWkVlk5R4k/LlwpcucpsBzntygd4iAeuKd/eLMkmBWoXN+xcjYJ5IibAahRpIJU461jABZvT6E9dwA==",
+ "dependencies": {
+ "Grpc.Net.Common": "2.67.0",
+ "Microsoft.Extensions.Logging.Abstractions": "6.0.0"
+ }
+ },
+ "Grpc.Net.Common": {
+ "type": "Transitive",
+ "resolved": "2.67.0",
+ "contentHash": "gazn1cD2Eol0/W5ZJRV4PYbNrxJ9oMs8pGYux5S9E4MymClvl7aqYSmpqgmWAUWvziRqK9K+yt3cjCMfQ3x/5A==",
+ "dependencies": {
+ "Grpc.Core.Api": "2.67.0"
+ }
+ },
"H.GeneratedIcons.System.Drawing": {
"type": "Transitive",
"resolved": "2.2.0",
@@ -66,15 +128,242 @@
"H.GeneratedIcons.System.Drawing": "2.2.0"
}
},
+ "Microsoft.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "VuthqFS+ju6vT8W4wevdhEFiRi1trvQtkzWLonApfF5USVzzDcTBoY3F24WvN/tffLSrycArVfX1bThm/9xY2A==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Primitives": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "+4hfFIY1UjBCXFTTOd+ojlDPq6mep3h5Vq5SYE3Pjucr7dNXmq4S/6P/LoVnZFz2e/5gWp/om4svUFgznfULcA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "w7kAyu1Mm7eParRV6WvGNNwA8flPTub16fwH49h7b/yqJZFTgYxnOVCuiah3G2bgseJMEq4DLjjsyQRvsdzRgA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Configuration.CommandLine": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "5WC1OsXfljC1KHEyL0yefpAyt1UZjrZ0/xyOqFowc5VntbE79JpCYOTSYFlxEuXm3Oq5xsgU2YXeZLTgAAX+DA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "9.0.1",
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Configuration.EnvironmentVariables": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "5HShUdF8KFAUSzoEu0DOFbX09FlcFtHxEalowyjM7Kji0EjdF0DLjHajb2IBvoqsExAYox+Z2GfbfGF7dH7lKQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "9.0.1",
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Configuration.FileExtensions": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "QBOI8YVAyKqeshYOyxSe6co22oag431vxMu5xQe1EjXMkYE4xK4J71xLCW3/bWKmr9Aoy1VqGUARSLFnotk4Bg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "9.0.1",
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+ "Microsoft.Extensions.FileProviders.Abstractions": "9.0.1",
+ "Microsoft.Extensions.FileProviders.Physical": "9.0.1",
+ "Microsoft.Extensions.Primitives": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Json": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "z+g+lgPET1JRDjsOkFe51rkkNcnJgvOK5UIpeTfF1iAi0GkBJz5/yUuTa8a9V8HUh4gj4xFT5WGoMoXoSDKfGg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "9.0.1",
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Configuration.FileExtensions": "9.0.1",
+ "Microsoft.Extensions.FileProviders.Abstractions": "9.0.1",
+ "System.Text.Json": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Configuration.UserSecrets": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "esGPOgLZ1tZddEomexhrU+LJ5YIsuJdkh0tU7r4WVpNZ15dLuMPqPW4Xe4txf3T2PDUX2ILe3nYQEDjZjfSEJg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Configuration.Json": "9.0.1",
+ "Microsoft.Extensions.FileProviders.Abstractions": "9.0.1",
+ "Microsoft.Extensions.FileProviders.Physical": "9.0.1"
+ }
+ },
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "9.0.1",
"contentHash": "Tr74eP0oQ3AyC24ch17N8PuEkrPbD0JqIfENCYqmgKYNOmL8wQKzLJu3ObxTUDrjnn4rHoR1qKa37/eQyHmCDA=="
},
+ "Microsoft.Extensions.Diagnostics": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "4ZmP6turxMFsNwK/MCko2fuIITaYYN/eXyyIRq1FjLDKnptdbn6xMb7u0zfSMzCGpzkx4RxH/g1jKN2IchG7uA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "9.0.1",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Diagnostics.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "pfAPuVtHvG6dvZtAa0OQbXdDqq6epnr8z0/IIUjdmV0tMeI8Aj9KxDXvdDvqr+qNHTkmA7pZpChNxwNZt4GXVg==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Options": "9.0.1",
+ "System.Diagnostics.DiagnosticSource": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.FileProviders.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "DguZYt1DWL05+8QKWL3b6bW7A2pC5kYFMY5iXM6W2M23jhvcNa8v6AU8PvVJBcysxHwr9/jax0agnwoBumsSwg==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.FileProviders.Physical": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "TKDMNRS66UTMEVT38/tU9hA63UTMvzI3DyNm5mx8+JCf3BaOtxgrvWLCI1y3J52PzT5yNl/T2KN5Z0KbApLZcg==",
+ "dependencies": {
+ "Microsoft.Extensions.FileProviders.Abstractions": "9.0.1",
+ "Microsoft.Extensions.FileSystemGlobbing": "9.0.1",
+ "Microsoft.Extensions.Primitives": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.FileSystemGlobbing": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "Mxcp9NXuQMvAnudRZcgIb5SqlWrlullQzntBLTwuv0MPIJ5LqiGwbRqiyxgdk+vtCoUkplb0oXy5kAw1t469Ug=="
+ },
+ "Microsoft.Extensions.Hosting.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "CwSMhLNe8HLkfbFzdz0CHWJhtWH3TtfZSicLBd/itFD+NqQtfGHmvqXHQbaFFl3mQB5PBb2gxwzWQyW2pIj7PA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.1",
+ "Microsoft.Extensions.FileProviders.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "E/k5r7S44DOW+08xQPnNbO8DKAQHhkspDboTThNJ6Z3/QBb4LC6gStNWzVmy3IvW7sUD+iJKf4fj0xEkqE7vnQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection": "9.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Options": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "w2gUqXN/jNIuvqYwX3lbXagsizVNXYyt6LlF57+tMve4JYCEgCMMAjRce6uKcDASJgpMbErRT1PfHy2OhbkqEA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+ "System.Diagnostics.DiagnosticSource": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Logging.Configuration": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "MeZePlyu3/74Wk4FHYSzXijADJUhWa7gxtaphLxhS8zEPWdJuBCrPo0sezdCSZaKCL+cZLSLobrb7xt2zHOxZQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "9.0.1",
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Configuration.Binder": "9.0.1",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Logging": "9.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Options": "9.0.1",
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Logging.Console": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "YUzguHYlWfp4upfYlpVe3dnY59P25wc+/YLJ9/NQcblT3EvAB1CObQulClll7NtnFbbx4Js0a0UfyS8SbRsWXQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Logging": "9.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Logging.Configuration": "9.0.1",
+ "Microsoft.Extensions.Options": "9.0.1",
+ "System.Text.Json": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Logging.Debug": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "pzdyibIV8k4sym0Sszcp2MJCuXrpOGs9qfOvY+hCRu8k4HbdVoeKOLnacxHK6vEPITX5o5FjjsZW2zScLXTjYA==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Logging": "9.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Logging.EventLog": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "+a4RlbwFWjsMujNNhf1Jy9Nm5CpMT+nxXxfgrkRSloPo0OAWhPSPsrFo6VWpvgIPPS41qmfAVWr3DqAmOoVZgQ==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Logging": "9.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Options": "9.0.1",
+ "System.Diagnostics.EventLog": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Logging.EventSource": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "d47ZRZUOg1dGOX+yisWScQ7w4+92OlR9beS2UXaiadUCA3RFoZzobzVgrzBX7Oo/qefx9LxdRcaeFpWKb3BNBw==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Logging": "9.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Options": "9.0.1",
+ "Microsoft.Extensions.Primitives": "9.0.1",
+ "System.Text.Json": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Options.ConfigurationExtensions": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "8RRKWtuU4fR+8MQLR/8CqZwZ9yc2xCpllw/WPRY7kskIqEq0hMcEI4AfUJO72yGiK2QJkrsDcUvgB5Yc+3+lyg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Configuration.Binder": "9.0.1",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Options": "9.0.1",
+ "Microsoft.Extensions.Primitives": "9.0.1"
+ }
+ },
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
- "resolved": "5.0.1",
- "contentHash": "5WPSmL4YeP7eW+Vc8XZ4DwjYWBAiSwDV9Hm63JJWcz1Ie3Xjv4KuJXzgCstj48LkLfVCYa7mLcx7y+q6yqVvtw=="
+ "resolved": "9.0.1",
+ "contentHash": "bHtTesA4lrSGD1ZUaMIx6frU3wyy0vYtTa/hM6gGQu5QNrydObv8T5COiGUWsisflAfmsaFOe9Xvw5NSO99z0g=="
},
"Microsoft.Web.WebView2": {
"type": "Transitive",
@@ -104,6 +393,16 @@
"resolved": "9.0.0",
"contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w=="
},
+ "System.Diagnostics.DiagnosticSource": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "yOcDWx4P/s1I83+7gQlgQLmhny2eNcU0cfo1NBWi+en4EAI38Jau+/neT85gUW6w1s7+FUJc2qNOmmwGLIREqA=="
+ },
+ "System.Diagnostics.EventLog": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "iVnDpgYJsRaRFnk77kcLA3+913WfWDtnAKrQl9tQ5ahqKANTaJKmQdsuPWWiAPWE9pk1Kj4Pg9JGXWfFYYyakQ=="
+ },
"System.Drawing.Common": {
"type": "Transitive",
"resolved": "9.0.0",
@@ -125,13 +424,35 @@
"System.Collections.Immutable": "9.0.0"
}
},
+ "System.Text.Encodings.Web": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "XkspqduP2t1e1x2vBUAD/xZ5ZDvmywuUwsmB93MvyQLospJfqtX0GsR/kU0vUL2h4kmvf777z3txV2W4NrQ9Qg=="
+ },
+ "System.Text.Json": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "eqWHDZqYPv1PvuvoIIx5pF74plL3iEOZOl/0kQP+Y0TEbtgNnM2W6k8h8EPYs+LTJZsXuWa92n5W5sHTWvE3VA==",
+ "dependencies": {
+ "System.IO.Pipelines": "9.0.1",
+ "System.Text.Encodings.Web": "9.0.1"
+ }
+ },
"Coder.Desktop.CoderSdk": {
"type": "Project"
},
+ "Coder.Desktop.MutagenSdk": {
+ "type": "Project",
+ "dependencies": {
+ "Google.Protobuf": "[3.29.3, )",
+ "Grpc.Net.Client": "[2.67.0, )"
+ }
+ },
"Coder.Desktop.Vpn": {
"type": "Project",
"dependencies": {
"Coder.Desktop.Vpn.Proto": "[1.0.0, )",
+ "Microsoft.Extensions.Configuration": "[9.0.1, )",
"Semver": "[3.0.0, )",
"System.IO.Pipelines": "[9.0.1, )"
}
@@ -163,6 +484,16 @@
"type": "Transitive",
"resolved": "9.0.0",
"contentHash": "z8FfGIaoeALdD+KF44A2uP8PZIQQtDGiXsOLuN8nohbKhkyKt7zGaZb+fKiCxTuBqG22Q7myIAioSWaIcOOrOw=="
+ },
+ "System.Diagnostics.EventLog": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "iVnDpgYJsRaRFnk77kcLA3+913WfWDtnAKrQl9tQ5ahqKANTaJKmQdsuPWWiAPWE9pk1Kj4Pg9JGXWfFYYyakQ=="
+ },
+ "System.Text.Encodings.Web": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "XkspqduP2t1e1x2vBUAD/xZ5ZDvmywuUwsmB93MvyQLospJfqtX0GsR/kU0vUL2h4kmvf777z3txV2W4NrQ9Qg=="
}
},
"net8.0-windows10.0.19041/win-x64": {
@@ -185,6 +516,16 @@
"type": "Transitive",
"resolved": "9.0.0",
"contentHash": "z8FfGIaoeALdD+KF44A2uP8PZIQQtDGiXsOLuN8nohbKhkyKt7zGaZb+fKiCxTuBqG22Q7myIAioSWaIcOOrOw=="
+ },
+ "System.Diagnostics.EventLog": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "iVnDpgYJsRaRFnk77kcLA3+913WfWDtnAKrQl9tQ5ahqKANTaJKmQdsuPWWiAPWE9pk1Kj4Pg9JGXWfFYYyakQ=="
+ },
+ "System.Text.Encodings.Web": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "XkspqduP2t1e1x2vBUAD/xZ5ZDvmywuUwsmB93MvyQLospJfqtX0GsR/kU0vUL2h4kmvf777z3txV2W4NrQ9Qg=="
}
},
"net8.0-windows10.0.19041/win-x86": {
@@ -207,6 +548,16 @@
"type": "Transitive",
"resolved": "9.0.0",
"contentHash": "z8FfGIaoeALdD+KF44A2uP8PZIQQtDGiXsOLuN8nohbKhkyKt7zGaZb+fKiCxTuBqG22Q7myIAioSWaIcOOrOw=="
+ },
+ "System.Diagnostics.EventLog": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "iVnDpgYJsRaRFnk77kcLA3+913WfWDtnAKrQl9tQ5ahqKANTaJKmQdsuPWWiAPWE9pk1Kj4Pg9JGXWfFYYyakQ=="
+ },
+ "System.Text.Encodings.Web": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "XkspqduP2t1e1x2vBUAD/xZ5ZDvmywuUwsmB93MvyQLospJfqtX0GsR/kU0vUL2h4kmvf777z3txV2W4NrQ9Qg=="
}
}
}
diff --git a/Installer/Program.cs b/Installer/Program.cs
index 0bec102..7945f5b 100644
--- a/Installer/Program.cs
+++ b/Installer/Program.cs
@@ -250,17 +250,22 @@ private static int BuildMsiPackage(MsiOptions opts)
programFiles64Folder.AddDir(installDir);
project.AddDir(programFiles64Folder);
- // Add registry values that are consumed by the manager. Note that these
- // should not be changed. See Vpn.Service/Program.cs and
- // Vpn.Service/ManagerConfig.cs for more details.
+
project.AddRegValues(
+ // Add registry values that are consumed by the manager. Note that these
+ // should not be changed. See Vpn.Service/Program.cs and
+ // Vpn.Service/ManagerConfig.cs for more details.
new RegValue(RegistryHive, RegistryKey, "Manager:ServiceRpcPipeName", "Coder.Desktop.Vpn"),
new RegValue(RegistryHive, RegistryKey, "Manager:TunnelBinaryPath",
$"[INSTALLFOLDER]{opts.VpnDir}\\coder-vpn.exe"),
new RegValue(RegistryHive, RegistryKey, "Manager:LogFileLocation",
@"[INSTALLFOLDER]coder-desktop-service.log"),
new RegValue(RegistryHive, RegistryKey, "Manager:TunnelBinarySignatureSigner", "Coder Technologies Inc."),
- new RegValue(RegistryHive, RegistryKey, "Manager:TunnelBinaryAllowVersionMismatch", "false"));
+ new RegValue(RegistryHive, RegistryKey, "Manager:TunnelBinaryAllowVersionMismatch", "false"),
+ // Add registry values that are consumed by the App MutagenController. See App/Services/MutagenController.cs
+ new RegValue(RegistryHive, RegistryKey, "AppMutagenController:MutagenExecutablePath",
+ @"[INSTALLFOLDER]mutagen.exe")
+ );
// Note: most of this control panel info will not be visible as this
// package is usually hidden in favor of the bootstrapper showing
diff --git a/Tests.App/Services/MutagenControllerTest.cs b/Tests.App/Services/MutagenControllerTest.cs
new file mode 100644
index 0000000..40d6a48
--- /dev/null
+++ b/Tests.App/Services/MutagenControllerTest.cs
@@ -0,0 +1,138 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Coder.Desktop.App.Services;
+
+namespace Coder.Desktop.Tests.App.Services;
+
+[TestFixture]
+public class MutagenControllerTest
+{
+ [OneTimeSetUp]
+ public async Task DownloadMutagen()
+ {
+ var ct = new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token;
+ var scriptDirectory = Path.GetFullPath(Path.Combine(TestContext.CurrentContext.TestDirectory,
+ "..", "..", "..", "..", "scripts"));
+ var process = new Process();
+ process.StartInfo.FileName = "powershell.exe";
+ process.StartInfo.UseShellExecute = false;
+ process.StartInfo.Arguments = $"-ExecutionPolicy Bypass -File Get-Mutagen.ps1 -arch {_arch}";
+ process.StartInfo.RedirectStandardError = true;
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.WorkingDirectory = scriptDirectory;
+ process.Start();
+ var output = await process.StandardOutput.ReadToEndAsync(ct);
+ TestContext.Out.Write(output);
+ var error = await process.StandardError.ReadToEndAsync(ct);
+ TestContext.Error.Write(error);
+ Assert.That(process.ExitCode, Is.EqualTo(0));
+ _mutagenBinaryPath = Path.Combine(scriptDirectory, "files", $"mutagen-windows-{_arch}.exe");
+ Assert.That(File.Exists(_mutagenBinaryPath));
+ }
+
+ [SetUp]
+ public void CreateTempDir()
+ {
+ _tempDirectory = Directory.CreateTempSubdirectory(GetType().Name);
+ TestContext.Out.WriteLine($"temp directory: {_tempDirectory}");
+ }
+
+ private readonly string _arch = RuntimeInformation.ProcessArchitecture switch
+ {
+ Architecture.X64 => "x64",
+ Architecture.Arm64 => "arm64",
+ // We only support amd64 and arm64 on Windows currently.
+ _ => throw new PlatformNotSupportedException(
+ $"Unsupported architecture '{RuntimeInformation.ProcessArchitecture}'. Coder only supports x64 and arm64."),
+ };
+
+ private string _mutagenBinaryPath;
+ private DirectoryInfo _tempDirectory;
+
+ [Test(Description = "Shut down daemon when no sessions")]
+ [CancelAfter(30_000)]
+ public async Task ShutdownNoSessions(CancellationToken ct)
+ {
+ // NUnit runs each test in a temporary directory
+ var dataDirectory = _tempDirectory.FullName;
+ await using var controller = new MutagenController(_mutagenBinaryPath, dataDirectory);
+ await controller.Initialize(ct);
+
+ // log file tells us the daemon was started.
+ var logPath = Path.Combine(dataDirectory, "daemon.log");
+ Assert.That(File.Exists(logPath));
+
+ var lockPath = Path.Combine(dataDirectory, "daemon", "daemon.lock");
+ // If we can lock the daemon.lock file, it means the daemon has stopped.
+ while (true)
+ {
+ ct.ThrowIfCancellationRequested();
+ try
+ {
+ await using var lockFile = new FileStream(lockPath, FileMode.Open, FileAccess.Write, FileShare.None);
+ }
+ catch (IOException e)
+ {
+ TestContext.Out.WriteLine($"Didn't get lock (will retry): {e.Message}");
+ await Task.Delay(100, ct);
+ }
+
+ break;
+ }
+ }
+
+ [Test(Description = "Daemon is restarted when we create a session")]
+ [CancelAfter(30_000)]
+ public async Task CreateRestartsDaemon(CancellationToken ct)
+ {
+ // NUnit runs each test in a temporary directory
+ var dataDirectory = _tempDirectory.FullName;
+ await using (var controller = new MutagenController(_mutagenBinaryPath, dataDirectory))
+ {
+ await controller.Initialize(ct);
+ await controller.CreateSyncSession(new SyncSession(), ct);
+ }
+
+ var logPath = Path.Combine(dataDirectory, "daemon.log");
+ Assert.That(File.Exists(logPath));
+ var logLines = File.ReadAllLines(logPath);
+
+ // Here we're going to use the log to verify the daemon was started 2 times.
+ // slightly brittle, but unlikely this log line will change.
+ Assert.That(logLines.Count(s => s.Contains("[sync] Session manager initialized")), Is.EqualTo(2));
+ }
+
+ [Test(Description = "Controller kills orphaned daemon")]
+ [CancelAfter(30_000)]
+ public async Task Orphaned(CancellationToken ct)
+ {
+ // NUnit runs each test in a temporary directory
+ var dataDirectory = _tempDirectory.FullName;
+ MutagenController? controller1 = null;
+ MutagenController? controller2 = null;
+ try
+ {
+ controller1 = new MutagenController(_mutagenBinaryPath, dataDirectory);
+ await controller1.Initialize(ct);
+ await controller1.CreateSyncSession(new SyncSession(), ct);
+
+ controller2 = new MutagenController(_mutagenBinaryPath, dataDirectory);
+ await controller2.Initialize(ct);
+ }
+ finally
+ {
+ if (controller1 != null) await controller1.DisposeAsync();
+ if (controller2 != null) await controller2.DisposeAsync();
+ }
+
+ var logPath = Path.Combine(dataDirectory, "daemon.log");
+ Assert.That(File.Exists(logPath));
+ var logLines = File.ReadAllLines(logPath);
+
+ // Here we're going to use the log to verify the daemon was started 3 times.
+ // slightly brittle, but unlikely this log line will change.
+ Assert.That(logLines.Count(s => s.Contains("[sync] Session manager initialized")), Is.EqualTo(3));
+ }
+
+ // TODO: Add more tests once we actually implement creating sessions on the daemon
+}
diff --git a/Tests.Vpn.Service/packages.lock.json b/Tests.Vpn.Service/packages.lock.json
index 45e0457..7ba4c03 100644
--- a/Tests.Vpn.Service/packages.lock.json
+++ b/Tests.Vpn.Service/packages.lock.json
@@ -474,6 +474,7 @@
"type": "Project",
"dependencies": {
"Coder.Desktop.Vpn.Proto": "[1.0.0, )",
+ "Microsoft.Extensions.Configuration": "[9.0.1, )",
"Semver": "[3.0.0, )",
"System.IO.Pipelines": "[9.0.1, )"
}
diff --git a/Tests.Vpn/Tests.Vpn.csproj b/Tests.Vpn/Tests.Vpn.csproj
index b1ff6c6..2b9e30f 100644
--- a/Tests.Vpn/Tests.Vpn.csproj
+++ b/Tests.Vpn/Tests.Vpn.csproj
@@ -3,7 +3,7 @@
Coder.Desktop.Tests.Vpn
Coder.Desktop.Tests.Vpn
- net8.0
+ net8.0-windows
enable
enable
true
diff --git a/Tests.Vpn/packages.lock.json b/Tests.Vpn/packages.lock.json
index 10f6f62..725c743 100644
--- a/Tests.Vpn/packages.lock.json
+++ b/Tests.Vpn/packages.lock.json
@@ -1,7 +1,7 @@
{
"version": 1,
"dependencies": {
- "net8.0": {
+ "net8.0-windows7.0": {
"coverlet.collector": {
"type": "Direct",
"requested": "[6.0.4, )",
@@ -46,10 +46,27 @@
"resolved": "17.12.0",
"contentHash": "4svMznBd5JM21JIG2xZKGNanAHNXplxf/kQDFfLHXQ3OnpJkayRK/TjacFjA+EYmoyuNXHo/sOETEfcYtAzIrA=="
},
+ "Microsoft.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "VuthqFS+ju6vT8W4wevdhEFiRi1trvQtkzWLonApfF5USVzzDcTBoY3F24WvN/tffLSrycArVfX1bThm/9xY2A==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Primitives": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "+4hfFIY1UjBCXFTTOd+ojlDPq6mep3h5Vq5SYE3Pjucr7dNXmq4S/6P/LoVnZFz2e/5gWp/om4svUFgznfULcA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "9.0.1"
+ }
+ },
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
- "resolved": "5.0.1",
- "contentHash": "5WPSmL4YeP7eW+Vc8XZ4DwjYWBAiSwDV9Hm63JJWcz1Ie3Xjv4KuJXzgCstj48LkLfVCYa7mLcx7y+q6yqVvtw=="
+ "resolved": "9.0.1",
+ "contentHash": "bHtTesA4lrSGD1ZUaMIx6frU3wyy0vYtTa/hM6gGQu5QNrydObv8T5COiGUWsisflAfmsaFOe9Xvw5NSO99z0g=="
},
"Microsoft.TestPlatform.ObjectModel": {
"type": "Transitive",
@@ -95,6 +112,7 @@
"type": "Project",
"dependencies": {
"Coder.Desktop.Vpn.Proto": "[1.0.0, )",
+ "Microsoft.Extensions.Configuration": "[9.0.1, )",
"Semver": "[3.0.0, )",
"System.IO.Pipelines": "[9.0.1, )"
}
diff --git a/Vpn.DebugClient/Vpn.DebugClient.csproj b/Vpn.DebugClient/Vpn.DebugClient.csproj
index bc81b6b..0eda43d 100644
--- a/Vpn.DebugClient/Vpn.DebugClient.csproj
+++ b/Vpn.DebugClient/Vpn.DebugClient.csproj
@@ -4,7 +4,7 @@
Coder.Desktop.Vpn.DebugClient
Coder.Desktop.Vpn.DebugClient
Exe
- net8.0
+ net8.0-windows
enable
enable
true
diff --git a/Vpn.DebugClient/packages.lock.json b/Vpn.DebugClient/packages.lock.json
index 403a41b..473422b 100644
--- a/Vpn.DebugClient/packages.lock.json
+++ b/Vpn.DebugClient/packages.lock.json
@@ -1,16 +1,33 @@
{
"version": 1,
"dependencies": {
- "net8.0": {
+ "net8.0-windows7.0": {
"Google.Protobuf": {
"type": "Transitive",
"resolved": "3.29.3",
"contentHash": "t7nZFFUFwigCwZ+nIXHDLweXvwIpsOXi+P7J7smPT/QjI3EKxnCzTQOhBqyEh6XEzc/pNH+bCFOOSjatrPt6Tw=="
},
+ "Microsoft.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "VuthqFS+ju6vT8W4wevdhEFiRi1trvQtkzWLonApfF5USVzzDcTBoY3F24WvN/tffLSrycArVfX1bThm/9xY2A==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Primitives": "9.0.1"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "+4hfFIY1UjBCXFTTOd+ojlDPq6mep3h5Vq5SYE3Pjucr7dNXmq4S/6P/LoVnZFz2e/5gWp/om4svUFgznfULcA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "9.0.1"
+ }
+ },
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
- "resolved": "5.0.1",
- "contentHash": "5WPSmL4YeP7eW+Vc8XZ4DwjYWBAiSwDV9Hm63JJWcz1Ie3Xjv4KuJXzgCstj48LkLfVCYa7mLcx7y+q6yqVvtw=="
+ "resolved": "9.0.1",
+ "contentHash": "bHtTesA4lrSGD1ZUaMIx6frU3wyy0vYtTa/hM6gGQu5QNrydObv8T5COiGUWsisflAfmsaFOe9Xvw5NSO99z0g=="
},
"Semver": {
"type": "Transitive",
@@ -29,6 +46,7 @@
"type": "Project",
"dependencies": {
"Coder.Desktop.Vpn.Proto": "[1.0.0, )",
+ "Microsoft.Extensions.Configuration": "[9.0.1, )",
"Semver": "[3.0.0, )",
"System.IO.Pipelines": "[9.0.1, )"
}
diff --git a/Vpn.Service/packages.lock.json b/Vpn.Service/packages.lock.json
index ace2cdb..fb4185a 100644
--- a/Vpn.Service/packages.lock.json
+++ b/Vpn.Service/packages.lock.json
@@ -416,6 +416,7 @@
"type": "Project",
"dependencies": {
"Coder.Desktop.Vpn.Proto": "[1.0.0, )",
+ "Microsoft.Extensions.Configuration": "[9.0.1, )",
"Semver": "[3.0.0, )",
"System.IO.Pipelines": "[9.0.1, )"
}
diff --git a/Vpn.Service/RegistryConfigurationSource.cs b/Vpn/RegistryConfigurationSource.cs
similarity index 96%
rename from Vpn.Service/RegistryConfigurationSource.cs
rename to Vpn/RegistryConfigurationSource.cs
index 8e2dd0d..2e67b87 100644
--- a/Vpn.Service/RegistryConfigurationSource.cs
+++ b/Vpn/RegistryConfigurationSource.cs
@@ -1,7 +1,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Win32;
-namespace Coder.Desktop.Vpn.Service;
+namespace Coder.Desktop.Vpn;
public class RegistryConfigurationSource : IConfigurationSource
{
diff --git a/Vpn/Vpn.csproj b/Vpn/Vpn.csproj
index c08b669..76a72eb 100644
--- a/Vpn/Vpn.csproj
+++ b/Vpn/Vpn.csproj
@@ -3,7 +3,7 @@
Coder.Desktop.Vpn
Coder.Desktop.Vpn
- net8.0
+ net8.0-windows
enable
enable
true
@@ -14,6 +14,7 @@
+
diff --git a/Vpn/packages.lock.json b/Vpn/packages.lock.json
index 5eca812..8876fe4 100644
--- a/Vpn/packages.lock.json
+++ b/Vpn/packages.lock.json
@@ -1,7 +1,17 @@
{
"version": 1,
"dependencies": {
- "net8.0": {
+ "net8.0-windows7.0": {
+ "Microsoft.Extensions.Configuration": {
+ "type": "Direct",
+ "requested": "[9.0.1, )",
+ "resolved": "9.0.1",
+ "contentHash": "VuthqFS+ju6vT8W4wevdhEFiRi1trvQtkzWLonApfF5USVzzDcTBoY3F24WvN/tffLSrycArVfX1bThm/9xY2A==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+ "Microsoft.Extensions.Primitives": "9.0.1"
+ }
+ },
"Semver": {
"type": "Direct",
"requested": "[3.0.0, )",
@@ -22,10 +32,18 @@
"resolved": "3.29.3",
"contentHash": "t7nZFFUFwigCwZ+nIXHDLweXvwIpsOXi+P7J7smPT/QjI3EKxnCzTQOhBqyEh6XEzc/pNH+bCFOOSjatrPt6Tw=="
},
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "9.0.1",
+ "contentHash": "+4hfFIY1UjBCXFTTOd+ojlDPq6mep3h5Vq5SYE3Pjucr7dNXmq4S/6P/LoVnZFz2e/5gWp/om4svUFgznfULcA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "9.0.1"
+ }
+ },
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
- "resolved": "5.0.1",
- "contentHash": "5WPSmL4YeP7eW+Vc8XZ4DwjYWBAiSwDV9Hm63JJWcz1Ie3Xjv4KuJXzgCstj48LkLfVCYa7mLcx7y+q6yqVvtw=="
+ "resolved": "9.0.1",
+ "contentHash": "bHtTesA4lrSGD1ZUaMIx6frU3wyy0vYtTa/hM6gGQu5QNrydObv8T5COiGUWsisflAfmsaFOe9Xvw5NSO99z0g=="
},
"Coder.Desktop.Vpn.Proto": {
"type": "Project",
diff --git a/scripts/Get-Mutagen.ps1 b/scripts/Get-Mutagen.ps1
new file mode 100644
index 0000000..c540809
--- /dev/null
+++ b/scripts/Get-Mutagen.ps1
@@ -0,0 +1,44 @@
+# Usage: Get-Mutagen.ps1 -arch
+param (
+ [ValidateSet("x64", "arm64")]
+ [Parameter(Mandatory = $true)]
+ [string] $arch
+)
+
+function Download-File([string] $url, [string] $outputPath, [string] $etagFile) {
+ Write-Host "Downloading '$url' to '$outputPath'"
+ # We use `curl.exe` here because `Invoke-WebRequest` is notoriously slow.
+ & curl.exe `
+ --progress-bar `
+ --show-error `
+ --fail `
+ --location `
+ --etag-compare $etagFile `
+ --etag-save $etagFile `
+ --output $outputPath `
+ $url
+ if ($LASTEXITCODE -ne 0) { throw "Failed to download $url" }
+ if (!(Test-Path $outputPath) -or (Get-Item $outputPath).Length -eq 0) {
+ throw "Failed to download '$url', output file '$outputPath' is missing or empty"
+ }
+}
+
+$goArch = switch ($arch) {
+ "x64" { "amd64" }
+ "arm64" { "arm64" }
+ default { throw "Unsupported architecture: $arch" }
+}
+
+# Download the mutagen binary from our bucket for this platform if we don't have
+# it yet (or it's different).
+$mutagenVersion = "v0.18.1"
+$mutagenPath = Join-Path $PSScriptRoot "files\mutagen-windows-$($arch).exe"
+$mutagenUrl = "https://storage.googleapis.com/coder-desktop/mutagen/$($mutagenVersion)/mutagen-windows-$($goArch).exe"
+$mutagenEtagFile = $mutagenPath + ".etag"
+Download-File $mutagenUrl $mutagenPath $mutagenEtagFile
+
+# Download mutagen agents tarball.
+$mutagenAgentsPath = Join-Path $PSScriptRoot "files\mutagen-agents.tar.gz"
+$mutagenAgentsUrl = "https://storage.googleapis.com/coder-desktop/mutagen/$($mutagenVersion)/mutagen-agents.tar.gz"
+$mutagenAgentsEtagFile = $mutagenAgentsPath + ".etag"
+Download-File $mutagenAgentsUrl $mutagenAgentsPath $mutagenAgentsEtagFile
diff --git a/scripts/Publish.ps1 b/scripts/Publish.ps1
index fa3a571..5f7a25e 100644
--- a/scripts/Publish.ps1
+++ b/scripts/Publish.ps1
@@ -83,30 +83,6 @@ function Add-CoderSignature([string] $path) {
}
}
-function Download-File([string] $url, [string] $outputPath, [string] $etagFile) {
- Write-Host "Downloading '$url' to '$outputPath'"
- # We use `curl.exe` here because `Invoke-WebRequest` is notoriously slow.
- & curl.exe `
- --progress-bar `
- --show-error `
- --fail `
- --location `
- --etag-compare $etagFile `
- --etag-save $etagFile `
- --output $outputPath `
- $url
- if ($LASTEXITCODE -ne 0) { throw "Failed to download $url" }
- if (!(Test-Path $outputPath) -or (Get-Item $outputPath).Length -eq 0) {
- throw "Failed to download '$url', output file '$outputPath' is missing or empty"
- }
-}
-
-$goArch = switch ($arch) {
- "x64" { "amd64" }
- "arm64" { "arm64" }
- default { throw "Unsupported architecture: $arch" }
-}
-
# CD to the root of the repo
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
Push-Location $repoRoot
@@ -169,21 +145,15 @@ if ($null -eq $wintunDllSrc) {
$wintunDllDest = Join-Path $vpnFilesPath "wintun.dll"
Copy-Item $wintunDllSrc $wintunDllDest
-# Download the mutagen binary from our bucket for this platform if we don't have
-# it yet (or it's different).
-$mutagenVersion = "v0.18.1"
-$mutagenSrcPath = Join-Path $repoRoot "scripts\files\mutagen-windows-$($goArch).exe"
-$mutagenSrcUrl = "https://storage.googleapis.com/coder-desktop/mutagen/$($mutagenVersion)/mutagen-windows-$($goArch).exe"
-$mutagenEtagFile = $mutagenSrcPath + ".etag"
-Download-File $mutagenSrcUrl $mutagenSrcPath $mutagenEtagFile
+$scriptRoot = Join-Path $repoRoot "scripts"
+$getMutagen = Join-Path $scriptRoot "Get-Mutagen.ps1"
+& $getMutagen -arch $arch
+
+$mutagenSrcPath = Join-Path $scriptRoot "files\mutagen-windows-$($arch).exe"
$mutagenDestPath = Join-Path $vpnFilesPath "mutagen.exe"
Copy-Item $mutagenSrcPath $mutagenDestPath
-# Download mutagen agents tarball.
-$mutagenAgentsSrcPath = Join-Path $repoRoot "scripts\files\mutagen-agents.tar.gz"
-$mutagenAgentsSrcUrl = "https://storage.googleapis.com/coder-desktop/mutagen/$($mutagenVersion)/mutagen-agents.tar.gz"
-$mutagenAgentsEtagFile = $mutagenAgentsSrcPath + ".etag"
-Download-File $mutagenAgentsSrcUrl $mutagenAgentsSrcPath $mutagenAgentsEtagFile
+$mutagenAgentsSrcPath = Join-Path $scriptRoot "files\mutagen-agents.tar.gz"
$mutagenAgentsDestPath = Join-Path $vpnFilesPath "mutagen-agents.tar.gz"
Copy-Item $mutagenAgentsSrcPath $mutagenAgentsDestPath