├── .idea └── .idea.Coder.Desktop │ └── .idea │ ├── .name │ ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml │ ├── vcs.xml │ ├── indexLayout.xml │ └── projectSettingsUpdater.xml ├── Tests.Vpn.Service ├── testdata │ ├── .gitignore │ ├── hello.exe │ ├── coder-ev.crt │ ├── google-llc-ev.crt │ ├── hello-self-signed.exe │ ├── mozilla-corporation.crt │ ├── hello-invalid-version.exe │ ├── hello-versioned-signed.exe │ ├── self-signed.crt │ ├── gen-certs.sh │ ├── winres.json │ ├── README.md │ ├── self-signed-ev.crt │ └── Build-Assets.ps1 ├── TelemetryEnricherTest.cs └── Tests.Vpn.Service.csproj ├── .gitattributes ├── App ├── coder.ico ├── Assets │ ├── coder_icon_32_dark.ico │ └── coder_icon_32_light.ico ├── Properties │ ├── launchSettings.json │ └── PublishProfiles │ │ ├── win-x64.pubxml │ │ ├── win-x86.pubxml │ │ └── win-arm64.pubxml ├── Services │ ├── DispatcherQueueManager.cs │ ├── RdpConnector.cs │ └── StartupManager.cs ├── Utils │ ├── TitleBarIcon.cs │ ├── ForegroundWindow.cs │ └── DisplayScale.cs ├── Controls │ ├── HorizontalRule.xaml.cs │ ├── ExpandChevron.xaml.cs │ ├── HorizontalRule.xaml │ ├── ExpandChevron.xaml │ ├── TrayIcon.xaml.cs │ ├── ExpandContent.xaml.cs │ ├── SizedFrame.cs │ └── ExpandContent.xaml ├── Views │ ├── Pages │ │ ├── TrayWindowLoadingPage.xaml.cs │ │ ├── SettingsMainPage.xaml.cs │ │ ├── TrayWindowDisconnectedPage.xaml.cs │ │ ├── TrayWindowLoginRequiredPage.xaml.cs │ │ ├── UpdaterDownloadProgressMainPage.xaml.cs │ │ ├── UpdaterUpdateAvailableMainPage.xaml.cs │ │ ├── DirectoryPickerMainPage.xaml.cs │ │ ├── FileSyncListMainPage.xaml.cs │ │ ├── SignInUrlPage.xaml.cs │ │ ├── TrayWindowLoadingPage.xaml │ │ ├── SignInTokenPage.xaml.cs │ │ ├── TrayWindowMainPage.xaml.cs │ │ ├── TrayWindowLoginRequiredPage.xaml │ │ ├── UpdaterDownloadProgressMainPage.xaml │ │ ├── TrayWindowDisconnectedPage.xaml │ │ ├── SignInUrlPage.xaml │ │ └── SettingsMainPage.xaml │ ├── SettingsWindow.xaml.cs │ ├── SignInWindow.xaml │ ├── FileSyncListWindow.xaml.cs │ ├── SettingsWindow.xaml │ ├── FileSyncListWindow.xaml │ ├── DirectoryPickerWindow.xaml │ ├── UpdaterDownloadProgressWindow.xaml │ ├── UpdaterUpdateAvailableWindow.xaml │ ├── UpdaterCheckingForUpdatesWindow.xaml.cs │ ├── MessageWindow.xaml.cs │ ├── TrayWindow.xaml │ ├── UpdaterCheckingForUpdatesWindow.xaml │ ├── MessageWindow.xaml │ ├── SignInWindow.xaml.cs │ ├── UpdaterDownloadProgressWindow.xaml.cs │ └── UpdaterUpdateAvailableWindow.xaml.cs ├── Converters │ ├── BoolToVisibilityConverter.cs │ ├── InverseBoolToVisibilityConverter.cs │ ├── InverseBoolConverter.cs │ ├── BoolToObjectConverter.cs │ ├── VpnLifecycleToBoolConverter.cs │ └── FriendlyByteConverter.cs ├── App.xaml ├── app.manifest ├── ViewModels │ ├── TrayWindowLoginRequiredViewModel.cs │ ├── TrayWindowDisconnectedViewModel.cs │ ├── SyncSessionViewModel.cs │ └── SettingsViewModel.cs └── Models │ ├── CredentialModel.cs │ ├── SyncSessionControllerStateModel.cs │ └── Settings.cs ├── scripts ├── files │ ├── .gitignore │ ├── logo.png │ ├── WixUIBannerBmp.bmp │ ├── WixUIDialogBmp.bmp │ ├── wintun-0.14.1-x64.dll │ └── wintun-0.14.1-arm64.dll ├── Create-AppCastSigningKey.ps1 ├── Release.ps1 └── Get-Mutagen.ps1 ├── CoderSdk ├── packages.lock.json ├── CoderSdk.csproj ├── Coder │ ├── Deployment.cs │ ├── Users.cs │ └── WorkspaceAgents.cs ├── Agent │ ├── AgentApiClient.cs │ └── ListDirectory.cs └── JsonHttpClient.cs ├── Vpn.Service ├── coder.ico ├── Rebuild-Service.ps1 ├── Stop-Service.ps1 ├── Restart-Service.ps1 ├── ManagerRpcService.cs ├── Delete-Service.ps1 ├── ManagerConfig.cs ├── ManagerService.cs ├── Create-Service.ps1 ├── TelemetryEnricher.cs └── Vpn.Service.csproj ├── Vpn.Proto ├── packages.lock.json ├── Vpn.Proto.csproj └── RpcHeader.cs ├── Vpn.DebugClient ├── Vpn.DebugClient.csproj └── packages.lock.json ├── Vpn ├── Vpn.csproj ├── Utilities │ ├── RaiiSemaphoreSlim.cs │ ├── ServerVersionUtilities.cs │ └── TaskUtilities.cs ├── RegistryConfigurationSource.cs └── packages.lock.json ├── Installer └── Installer.csproj ├── MutagenSdk ├── MutagenSdk.csproj ├── NamedPipesConnectionFactory.cs ├── packages.lock.json └── Proto │ ├── synchronization │ ├── core │ │ ├── problem.proto │ │ ├── change.proto │ │ ├── ignore │ │ │ ├── syntax.proto │ │ │ └── ignore_vcs_mode.proto │ │ ├── permissions_mode.proto │ │ ├── conflict.proto │ │ └── symbolic_link_mode.proto │ ├── version.proto │ ├── scan_mode.proto │ ├── hashing │ │ └── algorithm.proto │ ├── compression │ │ └── algorithm.proto │ ├── stage_mode.proto │ ├── watch_mode.proto │ └── rsync │ │ └── receive.proto │ ├── service │ └── daemon │ │ └── daemon.proto │ ├── selection │ └── selection.proto │ └── filesystem │ └── behavior │ └── probe_mode.proto ├── Tests.App ├── Services │ ├── RdpConnectorTest.cs │ └── SettingsManagerTest.cs ├── Converters │ └── FriendlyByteConverterTest.cs └── Tests.App.csproj ├── Tests.Vpn.Proto ├── RpcMessageTest.cs ├── Tests.Vpn.Proto.csproj └── RpcHeaderTest.cs ├── Tests.CoderSdk └── Tests.CoderSdk.csproj ├── README.md ├── Tests.Vpn ├── Tests.Vpn.csproj └── Utilities │ └── ServerVersionUtilitiesTest.cs └── .github └── workflows └── ci.yaml /.idea/.idea.Coder.Desktop/.idea/.name: -------------------------------------------------------------------------------- 1 | Coder.Desktop -------------------------------------------------------------------------------- /Tests.Vpn.Service/testdata/.gitignore: -------------------------------------------------------------------------------- 1 | *.go 2 | *.pfx 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | MutagenSdk/Proto/**/*.proto linguist-generated=true 2 | -------------------------------------------------------------------------------- /App/coder.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/App/coder.ico -------------------------------------------------------------------------------- /scripts/files/.gitignore: -------------------------------------------------------------------------------- 1 | mutagen-*.tar.gz 2 | mutagen-*.exe 3 | *.etag 4 | windows-app-sdk-*.exe -------------------------------------------------------------------------------- /CoderSdk/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net8.0": {} 5 | } 6 | } -------------------------------------------------------------------------------- /Vpn.Service/coder.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/Vpn.Service/coder.ico -------------------------------------------------------------------------------- /scripts/files/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/scripts/files/logo.png -------------------------------------------------------------------------------- /scripts/files/WixUIBannerBmp.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/scripts/files/WixUIBannerBmp.bmp -------------------------------------------------------------------------------- /scripts/files/WixUIDialogBmp.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/scripts/files/WixUIDialogBmp.bmp -------------------------------------------------------------------------------- /App/Assets/coder_icon_32_dark.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/App/Assets/coder_icon_32_dark.ico -------------------------------------------------------------------------------- /App/Assets/coder_icon_32_light.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/App/Assets/coder_icon_32_light.ico -------------------------------------------------------------------------------- /scripts/files/wintun-0.14.1-x64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/scripts/files/wintun-0.14.1-x64.dll -------------------------------------------------------------------------------- /Tests.Vpn.Service/testdata/hello.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/Tests.Vpn.Service/testdata/hello.exe -------------------------------------------------------------------------------- /scripts/files/wintun-0.14.1-arm64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/scripts/files/wintun-0.14.1-arm64.dll -------------------------------------------------------------------------------- /Tests.Vpn.Service/testdata/coder-ev.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/Tests.Vpn.Service/testdata/coder-ev.crt -------------------------------------------------------------------------------- /App/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "App (Unpackaged)": { 4 | "commandName": "Project" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Tests.Vpn.Service/testdata/google-llc-ev.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/Tests.Vpn.Service/testdata/google-llc-ev.crt -------------------------------------------------------------------------------- /Vpn.Service/Rebuild-Service.ps1: -------------------------------------------------------------------------------- 1 | & $PSScriptRoot/Stop-Service.ps1 2 | dotnet build -c Debug ./Vpn.Service.csproj 3 | & $PSScriptRoot/Restart-Service.ps1 4 | -------------------------------------------------------------------------------- /Tests.Vpn.Service/testdata/hello-self-signed.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/Tests.Vpn.Service/testdata/hello-self-signed.exe -------------------------------------------------------------------------------- /Tests.Vpn.Service/testdata/mozilla-corporation.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/Tests.Vpn.Service/testdata/mozilla-corporation.crt -------------------------------------------------------------------------------- /Tests.Vpn.Service/testdata/hello-invalid-version.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/Tests.Vpn.Service/testdata/hello-invalid-version.exe -------------------------------------------------------------------------------- /Tests.Vpn.Service/testdata/hello-versioned-signed.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coder/coder-desktop-windows/HEAD/Tests.Vpn.Service/testdata/hello-versioned-signed.exe -------------------------------------------------------------------------------- /.idea/.idea.Coder.Desktop/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/.idea.Coder.Desktop/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /App/Services/DispatcherQueueManager.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Dispatching; 2 | 3 | namespace Coder.Desktop.App.Services; 4 | 5 | public interface IDispatcherQueueManager 6 | { 7 | public void RunInUiThread(DispatcherQueueHandler action); 8 | } 9 | -------------------------------------------------------------------------------- /.idea/.idea.Coder.Desktop/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/.idea.Coder.Desktop/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /App/Utils/TitleBarIcon.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml; 2 | 3 | namespace Coder.Desktop.App.Utils; 4 | 5 | public static class TitleBarIcon 6 | { 7 | public static void SetTitlebarIcon(Window window) 8 | { 9 | window.AppWindow.SetIcon("coder.ico"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Controls/HorizontalRule.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml.Controls; 2 | 3 | namespace Coder.Desktop.App.Controls; 4 | 5 | public sealed partial class HorizontalRule : UserControl 6 | { 7 | public HorizontalRule() 8 | { 9 | InitializeComponent(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Views/Pages/TrayWindowLoadingPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml.Controls; 2 | 3 | namespace Coder.Desktop.App.Views.Pages; 4 | 5 | public sealed partial class TrayWindowLoadingPage : Page 6 | { 7 | public TrayWindowLoadingPage() 8 | { 9 | InitializeComponent(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Vpn.Service/Stop-Service.ps1: -------------------------------------------------------------------------------- 1 | $name = "Coder Desktop (Debug)" 2 | 3 | try { 4 | Stop-Service -Name $name -Force 5 | Write-Host "Service '$name' stopped successfully" 6 | } catch { 7 | Write-Host $_ -ForegroundColor Red 8 | Write-Host "Press Return to exit..." 9 | Read-Host 10 | } 11 | -------------------------------------------------------------------------------- /App/Converters/BoolToVisibilityConverter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml; 2 | 3 | namespace Coder.Desktop.App.Converters; 4 | 5 | public partial class BoolToVisibilityConverter : BoolToObjectConverter 6 | { 7 | public BoolToVisibilityConverter() 8 | { 9 | TrueValue = Visibility.Visible; 10 | FalseValue = Visibility.Collapsed; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.idea/.idea.Coder.Desktop/.idea/projectSettingsUpdater.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /App/Converters/InverseBoolToVisibilityConverter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml; 2 | 3 | namespace Coder.Desktop.App.Converters; 4 | 5 | public partial class InverseBoolToVisibilityConverter : BoolToObjectConverter 6 | { 7 | public InverseBoolToVisibilityConverter() 8 | { 9 | TrueValue = Visibility.Collapsed; 10 | FalseValue = Visibility.Visible; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /App/Views/Pages/SettingsMainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.App.ViewModels; 2 | using Microsoft.UI.Xaml.Controls; 3 | 4 | namespace Coder.Desktop.App.Views.Pages; 5 | 6 | public sealed partial class SettingsMainPage : Page 7 | { 8 | public SettingsViewModel ViewModel; 9 | 10 | public SettingsMainPage(SettingsViewModel viewModel) 11 | { 12 | ViewModel = viewModel; 13 | InitializeComponent(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Vpn.Service/Restart-Service.ps1: -------------------------------------------------------------------------------- 1 | $name = "Coder Desktop (Debug)" 2 | 3 | try { 4 | Restart-Service -Name $name -Force 5 | if ((Get-Service -Name $name -ErrorAction Stop).Status -ne "Running") { 6 | throw "Service '$name' is not running" 7 | } 8 | Write-Host "Service '$name' restarted successfully" 9 | } catch { 10 | Write-Host $_ -ForegroundColor Red 11 | Write-Host "Press Return to exit..." 12 | Read-Host 13 | } 14 | -------------------------------------------------------------------------------- /CoderSdk/CoderSdk.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Coder.Desktop.CoderSdk 4 | Coder.Desktop.CoderSdk 5 | net8.0 6 | enable 7 | enable 8 | true 9 | 10 | 11 | -------------------------------------------------------------------------------- /App/Views/Pages/TrayWindowDisconnectedPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.App.ViewModels; 2 | using Microsoft.UI.Xaml.Controls; 3 | 4 | namespace Coder.Desktop.App.Views.Pages; 5 | 6 | public sealed partial class TrayWindowDisconnectedPage : Page 7 | { 8 | public TrayWindowDisconnectedViewModel ViewModel { get; } 9 | 10 | public TrayWindowDisconnectedPage(TrayWindowDisconnectedViewModel viewModel) 11 | { 12 | InitializeComponent(); 13 | ViewModel = viewModel; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /App/Views/Pages/TrayWindowLoginRequiredPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.App.ViewModels; 2 | using Microsoft.UI.Xaml.Controls; 3 | 4 | namespace Coder.Desktop.App.Views.Pages; 5 | 6 | public sealed partial class TrayWindowLoginRequiredPage : Page 7 | { 8 | public TrayWindowLoginRequiredViewModel ViewModel { get; } 9 | 10 | public TrayWindowLoginRequiredPage(TrayWindowLoginRequiredViewModel viewModel) 11 | { 12 | InitializeComponent(); 13 | ViewModel = viewModel; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /App/Views/Pages/UpdaterDownloadProgressMainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml.Controls; 2 | using Coder.Desktop.App.ViewModels; 3 | 4 | namespace Coder.Desktop.App.Views.Pages; 5 | 6 | public sealed partial class UpdaterDownloadProgressMainPage : Page 7 | { 8 | public readonly UpdaterDownloadProgressViewModel ViewModel; 9 | public UpdaterDownloadProgressMainPage(UpdaterDownloadProgressViewModel viewModel) 10 | { 11 | ViewModel = viewModel; 12 | InitializeComponent(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /App/Views/Pages/UpdaterUpdateAvailableMainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml.Controls; 2 | using Coder.Desktop.App.ViewModels; 3 | 4 | namespace Coder.Desktop.App.Views.Pages; 5 | 6 | public sealed partial class UpdaterUpdateAvailableMainPage : Page 7 | { 8 | public readonly UpdaterUpdateAvailableViewModel ViewModel; 9 | 10 | public UpdaterUpdateAvailableMainPage(UpdaterUpdateAvailableViewModel viewModel) 11 | { 12 | ViewModel = viewModel; 13 | InitializeComponent(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /App/Converters/InverseBoolConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.UI.Xaml.Data; 3 | 4 | namespace Coder.Desktop.App.Converters; 5 | 6 | public class InverseBoolConverter : IValueConverter 7 | { 8 | public object Convert(object value, Type targetType, object parameter, string language) 9 | { 10 | return value is false; 11 | } 12 | 13 | public object ConvertBack(object value, Type targetType, object parameter, string language) 14 | { 15 | throw new NotImplementedException(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CoderSdk/Coder/Deployment.cs: -------------------------------------------------------------------------------- 1 | namespace Coder.Desktop.CoderSdk.Coder; 2 | 3 | public partial interface ICoderApiClient 4 | { 5 | public Task GetBuildInfo(CancellationToken ct = default); 6 | } 7 | 8 | public class BuildInfo 9 | { 10 | public string Version { get; set; } = ""; 11 | } 12 | 13 | public partial class CoderApiClient 14 | { 15 | public Task GetBuildInfo(CancellationToken ct = default) 16 | { 17 | return SendRequestNoBodyAsync(HttpMethod.Get, "/api/v2/buildinfo", ct); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /App/Properties/PublishProfiles/win-x64.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | FileSystem 8 | x64 9 | win-x64 10 | bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ 11 | 12 | 13 | -------------------------------------------------------------------------------- /App/Properties/PublishProfiles/win-x86.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | FileSystem 8 | x86 9 | win-x86 10 | bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ 11 | 12 | 13 | -------------------------------------------------------------------------------- /App/Properties/PublishProfiles/win-arm64.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | FileSystem 8 | ARM64 9 | win-arm64 10 | bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ 11 | 12 | 13 | -------------------------------------------------------------------------------- /App/Controls/ExpandChevron.xaml.cs: -------------------------------------------------------------------------------- 1 | using DependencyPropertyGenerator; 2 | using Microsoft.UI.Xaml.Controls; 3 | 4 | namespace Coder.Desktop.App.Controls; 5 | 6 | [DependencyProperty("IsOpen", DefaultValue = false)] 7 | public sealed partial class ExpandChevron : UserControl 8 | { 9 | public ExpandChevron() 10 | { 11 | InitializeComponent(); 12 | } 13 | 14 | partial void OnIsOpenChanged(bool oldValue, bool newValue) 15 | { 16 | var newState = newValue ? "NormalOn" : "NormalOff"; 17 | AnimatedIcon.SetState(ChevronIcon, newState); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CoderSdk/Coder/Users.cs: -------------------------------------------------------------------------------- 1 | namespace Coder.Desktop.CoderSdk.Coder; 2 | 3 | public partial interface ICoderApiClient 4 | { 5 | public Task GetUser(string user, CancellationToken ct = default); 6 | } 7 | 8 | public class User 9 | { 10 | public const string Me = "me"; 11 | 12 | public string Username { get; set; } = ""; 13 | } 14 | 15 | public partial class CoderApiClient 16 | { 17 | public Task GetUser(string user, CancellationToken ct = default) 18 | { 19 | return SendRequestNoBodyAsync(HttpMethod.Get, $"/api/v2/users/{user}", ct); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Vpn.Proto/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net8.0": { 5 | "Google.Protobuf": { 6 | "type": "Direct", 7 | "requested": "[3.29.3, )", 8 | "resolved": "3.29.3", 9 | "contentHash": "t7nZFFUFwigCwZ+nIXHDLweXvwIpsOXi+P7J7smPT/QjI3EKxnCzTQOhBqyEh6XEzc/pNH+bCFOOSjatrPt6Tw==" 10 | }, 11 | "Grpc.Tools": { 12 | "type": "Direct", 13 | "requested": "[2.69.0, )", 14 | "resolved": "2.69.0", 15 | "contentHash": "W5hW4R1h19FCzKb8ToqIJMI5YxnQqGmREEpV8E5XkfCtLPIK5MSHztwQ8gZUfG8qu9fg5MhItjzyPRqQBjnrbA==" 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /App/Controls/HorizontalRule.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /App/Utils/ForegroundWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using Microsoft.UI; 4 | using Microsoft.UI.Xaml; 5 | using WinRT.Interop; 6 | 7 | namespace Coder.Desktop.App.Utils; 8 | 9 | public static class ForegroundWindow 10 | { 11 | 12 | [DllImport("user32.dll")] 13 | private static extern bool SetForegroundWindow(IntPtr hwnd); 14 | 15 | public static void MakeForeground(Window window) 16 | { 17 | var hwnd = WindowNative.GetWindowHandle(window); 18 | var windowId = Win32Interop.GetWindowIdFromWindow(hwnd); 19 | _ = SetForegroundWindow(hwnd); 20 | // Not a big deal if it fails. 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /App/Views/SettingsWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.App.Utils; 2 | using Coder.Desktop.App.ViewModels; 3 | using Coder.Desktop.App.Views.Pages; 4 | using Microsoft.UI.Xaml.Media; 5 | using WinUIEx; 6 | 7 | namespace Coder.Desktop.App.Views; 8 | 9 | public sealed partial class SettingsWindow : WindowEx 10 | { 11 | public readonly SettingsViewModel ViewModel; 12 | 13 | public SettingsWindow(SettingsViewModel viewModel) 14 | { 15 | ViewModel = viewModel; 16 | InitializeComponent(); 17 | TitleBarIcon.SetTitlebarIcon(this); 18 | 19 | RootFrame.Content = new SettingsMainPage(ViewModel); 20 | 21 | this.CenterOnScreen(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /App/Views/SignInWindow.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Vpn.DebugClient/Vpn.DebugClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Coder.Desktop.Vpn.DebugClient 5 | Coder.Desktop.Vpn.DebugClient 6 | Exe 7 | net8.0-windows 8 | enable 9 | enable 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Vpn.Service/ManagerRpcService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | 3 | namespace Coder.Desktop.Vpn.Service; 4 | 5 | public class ManagerRpcService : BackgroundService 6 | { 7 | private readonly IManagerRpc _managerRpc; 8 | 9 | // ReSharper disable once ConvertToPrimaryConstructor 10 | public ManagerRpcService(IManagerRpc managerRpc) 11 | { 12 | _managerRpc = managerRpc; 13 | } 14 | 15 | public override async Task StopAsync(CancellationToken cancellationToken) 16 | { 17 | await _managerRpc.StopAsync(cancellationToken); 18 | } 19 | 20 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 21 | { 22 | await _managerRpc.ExecuteAsync(stoppingToken); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /App/Views/FileSyncListWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.App.Utils; 2 | using Coder.Desktop.App.ViewModels; 3 | using Coder.Desktop.App.Views.Pages; 4 | using Microsoft.UI.Xaml.Media; 5 | using WinUIEx; 6 | 7 | namespace Coder.Desktop.App.Views; 8 | 9 | public sealed partial class FileSyncListWindow : WindowEx 10 | { 11 | public readonly FileSyncListViewModel ViewModel; 12 | 13 | public FileSyncListWindow(FileSyncListViewModel viewModel) 14 | { 15 | ViewModel = viewModel; 16 | InitializeComponent(); 17 | TitleBarIcon.SetTitlebarIcon(this); 18 | 19 | ViewModel.Initialize(this, DispatcherQueue); 20 | RootFrame.Content = new FileSyncListMainPage(ViewModel); 21 | 22 | this.CenterOnScreen(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /App/Views/SettingsWindow.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /App/Views/FileSyncListWindow.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Vpn.Service/Delete-Service.ps1: -------------------------------------------------------------------------------- 1 | # Elevate to administrator 2 | if (-not ([Security.Principal.WindowsPrincipal]([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 3 | Write-Host "Elevating script to run as administrator..." 4 | Start-Process powershell.exe -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`"" -Verb RunAs 5 | exit 6 | } 7 | 8 | $name = "Coder Desktop (Debug)" 9 | 10 | try { 11 | Stop-Service -Name $name -Force -ErrorAction SilentlyContinue 12 | sc.exe delete $name 13 | Write-Host "Service '$name' deleted" 14 | } catch { 15 | Write-Host $_ -ForegroundColor Red 16 | Write-Host "Press Return to exit..." 17 | Read-Host 18 | } 19 | -------------------------------------------------------------------------------- /App/Views/DirectoryPickerWindow.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /App/Converters/BoolToObjectConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DependencyPropertyGenerator; 3 | using Microsoft.UI.Xaml; 4 | using Microsoft.UI.Xaml.Data; 5 | 6 | namespace Coder.Desktop.App.Converters; 7 | 8 | [DependencyProperty("TrueValue", DefaultValue = true)] 9 | [DependencyProperty("FalseValue", DefaultValue = true)] 10 | public partial class BoolToObjectConverter : DependencyObject, IValueConverter 11 | { 12 | public object Convert(object value, Type targetType, object parameter, string language) 13 | { 14 | return value is true ? TrueValue : FalseValue; 15 | } 16 | 17 | public object ConvertBack(object value, Type targetType, object parameter, string language) 18 | { 19 | throw new NotImplementedException(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /App/Utils/DisplayScale.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using Microsoft.UI.Xaml; 4 | using WinRT.Interop; 5 | 6 | namespace Coder.Desktop.App.Utils; 7 | 8 | /// 9 | /// A static utility class to house methods related to the visual scale of the display monitor. 10 | /// 11 | public static class DisplayScale 12 | { 13 | public static double WindowScale(Window win) 14 | { 15 | var hwnd = WindowNative.GetWindowHandle(win); 16 | var dpi = NativeApi.GetDpiForWindow(hwnd); 17 | if (dpi == 0) return 1; // assume scale of 1 18 | return dpi / 96.0; // 96 DPI == 1 19 | } 20 | 21 | public class NativeApi 22 | { 23 | [DllImport("user32.dll")] 24 | public static extern int GetDpiForWindow(IntPtr hwnd); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /App/Views/UpdaterDownloadProgressWindow.xaml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /App/Views/UpdaterUpdateAvailableWindow.xaml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Vpn/Vpn.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Coder.Desktop.Vpn 5 | Coder.Desktop.Vpn 6 | net8.0-windows 7 | enable 8 | enable 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /App/Views/Pages/DirectoryPickerMainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.App.ViewModels; 2 | using Microsoft.UI.Xaml.Controls; 3 | 4 | namespace Coder.Desktop.App.Views.Pages; 5 | 6 | public sealed partial class DirectoryPickerMainPage : Page 7 | { 8 | public readonly DirectoryPickerViewModel ViewModel; 9 | 10 | public DirectoryPickerMainPage(DirectoryPickerViewModel viewModel) 11 | { 12 | ViewModel = viewModel; 13 | InitializeComponent(); 14 | } 15 | 16 | private void TooltipText_IsTextTrimmedChanged(TextBlock sender, IsTextTrimmedChangedEventArgs e) 17 | { 18 | ToolTipService.SetToolTip(sender, null); 19 | if (!sender.IsTextTrimmed) return; 20 | 21 | var toolTip = new ToolTip 22 | { 23 | Content = sender.Text, 24 | }; 25 | ToolTipService.SetToolTip(sender, toolTip); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /App/Views/UpdaterCheckingForUpdatesWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Coder.Desktop.App.Utils; 3 | using NetSparkleUpdater.Interfaces; 4 | using WinUIEx; 5 | 6 | namespace Coder.Desktop.App.Views; 7 | 8 | public sealed partial class UpdaterCheckingForUpdatesWindow : WindowEx, ICheckingForUpdates 9 | { 10 | // Implements ICheckingForUpdates 11 | public event EventHandler? UpdatesUIClosing; 12 | 13 | public UpdaterCheckingForUpdatesWindow() 14 | { 15 | InitializeComponent(); 16 | TitleBarIcon.SetTitlebarIcon(this); 17 | AppWindow.Hide(); 18 | 19 | Closed += (_, _) => UpdatesUIClosing?.Invoke(this, EventArgs.Empty); 20 | } 21 | 22 | void ICheckingForUpdates.Show() 23 | { 24 | AppWindow.Show(); 25 | this.CenterOnScreen(); 26 | } 27 | 28 | void ICheckingForUpdates.Close() 29 | { 30 | Close(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /App/Views/Pages/FileSyncListMainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.App.ViewModels; 2 | using Microsoft.UI.Xaml.Controls; 3 | 4 | namespace Coder.Desktop.App.Views.Pages; 5 | 6 | public sealed partial class FileSyncListMainPage : Page 7 | { 8 | public FileSyncListViewModel ViewModel; 9 | 10 | public FileSyncListMainPage(FileSyncListViewModel viewModel) 11 | { 12 | ViewModel = viewModel; // already initialized 13 | InitializeComponent(); 14 | } 15 | 16 | // Adds a tooltip with the full text when it's ellipsized. 17 | private void TooltipText_IsTextTrimmedChanged(TextBlock sender, IsTextTrimmedChangedEventArgs e) 18 | { 19 | ToolTipService.SetToolTip(sender, null); 20 | if (!sender.IsTextTrimmed) return; 21 | 22 | var toolTip = new ToolTip 23 | { 24 | Content = sender.Text, 25 | }; 26 | ToolTipService.SetToolTip(sender, toolTip); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /App/Views/Pages/SignInUrlPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.App.ViewModels; 2 | using Microsoft.UI.Xaml.Controls; 3 | using Windows.System; 4 | 5 | namespace Coder.Desktop.App.Views.Pages; 6 | 7 | /// 8 | /// A login page to enter the Coder Server URL 9 | /// 10 | public sealed partial class SignInUrlPage : Page 11 | { 12 | public readonly SignInViewModel ViewModel; 13 | public readonly SignInWindow SignInWindow; 14 | 15 | public SignInUrlPage(SignInWindow parent, SignInViewModel viewModel) 16 | { 17 | InitializeComponent(); 18 | ViewModel = viewModel; 19 | SignInWindow = parent; 20 | } 21 | 22 | private void TextBox_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e) 23 | { 24 | if (e.Key == VirtualKey.Enter) 25 | { 26 | ViewModel.UrlPage_Next(SignInWindow); 27 | e.Handled = true; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /App/Views/Pages/TrayWindowLoadingPage.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 17 | 18 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Vpn.Proto/Vpn.Proto.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Coder.Desktop.Vpn.Proto 5 | Coder.Desktop.Vpn.Proto 6 | net8.0 7 | enable 8 | enable 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /App/Views/Pages/SignInTokenPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.App.ViewModels; 2 | using Microsoft.UI.Xaml.Controls; 3 | using Windows.System; 4 | 5 | namespace Coder.Desktop.App.Views.Pages; 6 | 7 | /// 8 | /// A sign in page to accept the user's Coder token. 9 | /// 10 | public sealed partial class SignInTokenPage : Page 11 | { 12 | public readonly SignInViewModel ViewModel; 13 | public readonly SignInWindow SignInWindow; 14 | 15 | public SignInTokenPage(SignInWindow parent, SignInViewModel viewModel) 16 | { 17 | InitializeComponent(); 18 | ViewModel = viewModel; 19 | SignInWindow = parent; 20 | } 21 | 22 | private async void PasswordBox_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e) 23 | { 24 | if (e.Key == VirtualKey.Enter) 25 | { 26 | await ViewModel.TokenPage_SignIn(SignInWindow); 27 | e.Handled = true; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Vpn.Service/ManagerConfig.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Coder.Desktop.Vpn.Service; 4 | 5 | // These values are the config option names used in the registry. Any option 6 | // here can be configured with `(Debug)?Manager:OptionName` in the registry. 7 | // 8 | // They should not be changed without backwards compatibility considerations. 9 | // If changed here, they should also be changed in the installer. 10 | public class ManagerConfig 11 | { 12 | [Required] 13 | [RegularExpression(@"^([a-zA-Z0-9_-]+\.)*[a-zA-Z0-9_-]+$")] 14 | public string ServiceRpcPipeName { get; set; } = "Coder.Desktop.Vpn"; 15 | 16 | [Required] public string TunnelBinaryPath { get; set; } = @"C:\coder-vpn.exe"; 17 | 18 | // If empty, signatures will not be verified. 19 | [Required] public string TunnelBinarySignatureSigner { get; set; } = "Coder Technologies Inc."; 20 | 21 | [Required] public bool TunnelBinaryAllowVersionMismatch { get; set; } = false; 22 | } 23 | -------------------------------------------------------------------------------- /Installer/Installer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Coder.Desktop.Installer 5 | Coder.Desktop.Installer 6 | Exe 7 | net481 8 | 13.0 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /App/App.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /App/Views/MessageWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.App.Utils; 2 | using Microsoft.UI.Xaml; 3 | using WinUIEx; 4 | 5 | namespace Coder.Desktop.App.Views; 6 | 7 | public sealed partial class MessageWindow : WindowEx 8 | { 9 | public string MessageTitle; 10 | public string MessageContent; 11 | 12 | public MessageWindow(string title, string content, string windowTitle = "Coder Desktop") 13 | { 14 | Title = windowTitle; 15 | MessageTitle = title; 16 | MessageContent = content; 17 | 18 | InitializeComponent(); 19 | TitleBarIcon.SetTitlebarIcon(this); 20 | this.CenterOnScreen(); 21 | AppWindow.Show(); 22 | 23 | // TODO: the window should resize to fit content and not be resizable 24 | // by the user, probably possible with SizedFrame and a Page, but 25 | // I didn't want to add a Page for this 26 | } 27 | 28 | public void CloseClicked(object? sender, RoutedEventArgs e) 29 | { 30 | Close(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /scripts/Create-AppCastSigningKey.ps1: -------------------------------------------------------------------------------- 1 | # This is mostly just here for reference. 2 | # 3 | # Usage: Create-AppCastSigningKey.ps1 -outputKeyPath 4 | param ( 5 | [Parameter(Mandatory = $true)] 6 | [string] $outputKeyPath 7 | ) 8 | 9 | $ErrorActionPreference = "Stop" 10 | 11 | & openssl.exe genpkey -algorithm ed25519 -out $outputKeyPath 12 | if ($LASTEXITCODE -ne 0) { throw "Failed to generate ED25519 private key" } 13 | 14 | # Export the public key in DER format 15 | $pubKeyDerPath = "$outputKeyPath.pub.der" 16 | & openssl.exe pkey -in $outputKeyPath -pubout -outform DER -out $pubKeyDerPath 17 | if ($LASTEXITCODE -ne 0) { throw "Failed to export ED25519 public key" } 18 | 19 | # Remove the DER header to get the actual key bytes 20 | $pubBytes = [System.IO.File]::ReadAllBytes($pubKeyDerPath)[-32..-1] 21 | Remove-Item $pubKeyDerPath 22 | 23 | # Base64 encode and print 24 | Write-Output "NetSparkle formatted public key:" 25 | Write-Output ([Convert]::ToBase64String($pubBytes)) 26 | Write-Output "" 27 | Write-Output "Private key written to $outputKeyPath" 28 | -------------------------------------------------------------------------------- /MutagenSdk/MutagenSdk.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Coder.Desktop.MutagenSdk 5 | Coder.Desktop.MutagenSdk 6 | net8.0 7 | enable 8 | enable 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Tests.App/Services/RdpConnectorTest.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.App.Services; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using Serilog; 5 | 6 | namespace Coder.Desktop.Tests.App.Services; 7 | 8 | [TestFixture] 9 | public class RdpConnectorTest 10 | { 11 | [Test(Description = "Spawns RDP for real")] 12 | [Ignore("Comment out to run manually")] 13 | [CancelAfter(30_000)] 14 | public async Task ConnectToRdp(CancellationToken ct) 15 | { 16 | var builder = Host.CreateApplicationBuilder(); 17 | builder.Services.AddSerilog(); 18 | builder.Services.AddSingleton(); 19 | var services = builder.Services.BuildServiceProvider(); 20 | 21 | var rdpConnector = (RdpConnector)services.GetService()!; 22 | var creds = new RdpCredentials("Administrator", "coderRDP!"); 23 | var workspace = "myworkspace.coder"; 24 | rdpConnector.WriteCredentials(workspace, creds); 25 | await rdpConnector.Connect(workspace, ct: ct); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /App/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PerMonitorV2 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /App/Views/TrayWindow.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Vpn.Service/ManagerService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace Coder.Desktop.Vpn.Service; 5 | 6 | /// 7 | /// Wraps Manager to provide a BackgroundService that informs the singleton Manager to shut down when stop is 8 | /// requested. 9 | /// 10 | public class ManagerService : BackgroundService 11 | { 12 | private readonly ILogger _logger; 13 | private readonly IManager _manager; 14 | 15 | // ReSharper disable once ConvertToPrimaryConstructor 16 | public ManagerService(ILogger logger, IManager manager) 17 | { 18 | _logger = logger; 19 | _manager = manager; 20 | } 21 | 22 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 23 | { 24 | // Block until the service is stopped. 25 | await Task.Delay(-1, stoppingToken); 26 | } 27 | 28 | public override async Task StopAsync(CancellationToken cancellationToken) 29 | { 30 | _logger.LogInformation("Informing Manager to stop"); 31 | await _manager.StopAsync(cancellationToken); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MutagenSdk/NamedPipesConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipes; 2 | using System.Security.Principal; 3 | 4 | namespace Coder.Desktop.MutagenSdk; 5 | 6 | public class NamedPipesConnectionFactory 7 | { 8 | private readonly string _pipeName; 9 | 10 | public NamedPipesConnectionFactory(string pipeName) 11 | { 12 | _pipeName = pipeName; 13 | } 14 | 15 | public async ValueTask ConnectAsync(SocketsHttpConnectionContext _, 16 | CancellationToken cancellationToken = default) 17 | { 18 | var client = new NamedPipeClientStream( 19 | ".", 20 | _pipeName, 21 | PipeDirection.InOut, 22 | PipeOptions.WriteThrough | PipeOptions.Asynchronous, 23 | TokenImpersonationLevel.Anonymous); 24 | 25 | try 26 | { 27 | // Set an upper limit of 2.5 seconds. MutagenSdk consumers can 28 | // retry if necessary. 29 | await client.ConnectAsync(2500, cancellationToken); 30 | return client; 31 | } 32 | catch 33 | { 34 | await client.DisposeAsync(); 35 | throw; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /App/Views/UpdaterCheckingForUpdatesWindow.xaml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /App/ViewModels/TrayWindowLoginRequiredViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Coder.Desktop.App.Views; 3 | using CommunityToolkit.Mvvm.Input; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.UI.Xaml; 6 | 7 | namespace Coder.Desktop.App.ViewModels; 8 | 9 | public partial class TrayWindowLoginRequiredViewModel 10 | { 11 | private readonly IServiceProvider _services; 12 | 13 | private SignInWindow? _signInWindow; 14 | 15 | public TrayWindowLoginRequiredViewModel(IServiceProvider services) 16 | { 17 | _services = services; 18 | } 19 | 20 | [RelayCommand] 21 | public void Login() 22 | { 23 | // This is safe against concurrent access since it all happens in the 24 | // UI thread. 25 | if (_signInWindow != null) 26 | { 27 | _signInWindow.Activate(); 28 | return; 29 | } 30 | 31 | _signInWindow = _services.GetRequiredService(); 32 | _signInWindow.Closed += (_, _) => _signInWindow = null; 33 | _signInWindow.Activate(); 34 | } 35 | 36 | [RelayCommand] 37 | public void Exit() 38 | { 39 | _ = ((App)Application.Current).ExitApplication(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests.App/Converters/FriendlyByteConverterTest.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.App.Converters; 2 | 3 | namespace Coder.Desktop.Tests.App.Converters; 4 | 5 | [TestFixture] 6 | public class FriendlyByteConverterTest 7 | { 8 | [Test] 9 | public void EndToEnd() 10 | { 11 | var cases = new List<(object, string)> 12 | { 13 | (0, "0 B"), 14 | ((uint)0, "0 B"), 15 | ((long)0, "0 B"), 16 | ((ulong)0, "0 B"), 17 | 18 | (1, "1 B"), 19 | (1024, "1 KB"), 20 | ((ulong)(1.1 * 1024), "1.1 KB"), 21 | (1024 * 1024, "1 MB"), 22 | (1024 * 1024 * 1024, "1 GB"), 23 | ((ulong)1024 * 1024 * 1024 * 1024, "1 TB"), 24 | ((ulong)1024 * 1024 * 1024 * 1024 * 1024, "1 PB"), 25 | ((ulong)1024 * 1024 * 1024 * 1024 * 1024 * 1024, "1 EB"), 26 | (ulong.MaxValue, "16 EB"), 27 | }; 28 | 29 | var converter = new FriendlyByteConverter(); 30 | foreach (var (input, expected) in cases) 31 | { 32 | var actual = converter.Convert(input, typeof(string), null!, null!); 33 | Assert.That(actual, Is.EqualTo(expected), $"case ({input.GetType()}){input}"); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests.Vpn.Service/testdata/self-signed.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDOzCCAiOgAwIBAgIUJAIgmuCtIc9yUfpTo3h0Srn3C88wDQYJKoZIhvcNAQEL 3 | BQAwHDEaMBgGA1UEAwwRQ29kZXIgU2VsZiBTaWduZWQwHhcNMjUwMzA3MDQ0NzQ2 4 | WhcNMzUwMzA1MDQ0NzQ2WjAcMRowGAYDVQQDDBFDb2RlciBTZWxmIFNpZ25lZDCC 5 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK6uYLDXy/C7TC4XaUbEmBiK 6 | ICUAz/XDFB19aJrvG6LQYmdbR8sCV4uvoZAXYfribD+AGrysEDnpny1BzNORARLd 7 | szBy0m1fOGbWqaBXg7Ot37rHWkU2iT2NEminHinx9UoJZLWxXsT1h1pnyJO4uzBW 8 | jroRgYbOzkaccoDSWebDjnzAy4LA+Sfdzqm4RvJFD+5dhg/EXyJyLQApN22NWOTP 9 | /8UXO0guUwSC+TGNFGRE6DkN96uX851HaCgrflz9zdLN5FSrSqvTsSStMTF9tHU3 10 | RlZFjTL+pVD6XSRMLn78xbch87sD3egaxaTvKd9Crx88GMwAnmrp8HqFAdUm4WUC 11 | AwEAAaN1MHMwHQYDVR0OBBYEFDJM3fCTrkRAMNKQhoH3mWyLXY0JMB8GA1UdIwQY 12 | MBaAFDJM3fCTrkRAMNKQhoH3mWyLXY0JMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0P 13 | BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQCU 14 | xdNbMynw3YexqDjzHaKFGp4p8pXH9lXAa50e2UsYQ2C3HjBRkDQ0VGwqZaWWidMo 15 | ZdL3TiKT3Gvd+eVEvZNVYAhOytevhztGa7RSIC54KbHM9pQiarQcCCpAVR3S1Ced 16 | zGExk2iVL/o4TZKipvv+lj9b+FmavtoWq9kDuO0Suja0rzBfk5/UpATWbVOSGzY9 17 | 1u0Rm2aIGgKxpOVPaxjD8JzJ+47r7Z6tSoYt4PScRy1kgf0VwKeUUdWLiGN4JxoS 18 | dX/onACConyPh4gK1fbHuKaoxVkIV3nFRAk18AwF4jThqFDkCQmUwh7A0DSyiPiD 19 | WeRW1iidZptmwzaOLnwz 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /App/Controls/ExpandChevron.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Tests.Vpn.Service/testdata/gen-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Generate a regular code signing certificate without the EV policy OID. 5 | openssl req \ 6 | -x509 \ 7 | -newkey rsa:2048 \ 8 | -keyout /dev/null \ 9 | -out self-signed.crt \ 10 | -days 3650 \ 11 | -nodes \ 12 | -subj "/CN=Coder Self Signed" \ 13 | -addext "keyUsage=digitalSignature" \ 14 | -addext "extendedKeyUsage=codeSigning" 15 | 16 | # Generate an EV code signing certificate by adding the EV policy OID. We add 17 | # a different OID before the EV OID to ensure the validator can handle multiple 18 | # policies. 19 | config=" 20 | [req] 21 | distinguished_name = req_distinguished_name 22 | x509_extensions = v3_req 23 | prompt = no 24 | 25 | [req_distinguished_name] 26 | CN = Coder Self Signed EV 27 | 28 | [v3_req] 29 | keyUsage = digitalSignature 30 | extendedKeyUsage = codeSigning 31 | certificatePolicies = @pol1,@pol2 32 | 33 | [pol1] 34 | policyIdentifier = 2.23.140.1.4.1 35 | CPS.1="https://coder.com" 36 | 37 | [pol2] 38 | policyIdentifier = 2.23.140.1.3 39 | CPS.1="https://coder.com" 40 | " 41 | 42 | openssl req \ 43 | -x509 \ 44 | -newkey rsa:2048 \ 45 | -keyout /dev/null \ 46 | -out self-signed-ev.crt \ 47 | -days 3650 \ 48 | -nodes \ 49 | -config <(echo "$config") 50 | -------------------------------------------------------------------------------- /Tests.Vpn.Service/TelemetryEnricherTest.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.Vpn.Proto; 2 | using Coder.Desktop.Vpn.Service; 3 | 4 | namespace Coder.Desktop.Tests.Vpn.Service; 5 | 6 | [TestFixture] 7 | public class TelemetryEnricherTest 8 | { 9 | [Test] 10 | public void EnrichStartRequest() 11 | { 12 | var req = new StartRequest 13 | { 14 | CoderUrl = "https://coder.example.com", 15 | }; 16 | var enricher = new TelemetryEnricher(); 17 | req = enricher.EnrichStartRequest(req); 18 | 19 | // quick sanity check that non-telemetry fields aren't lost or overwritten 20 | Assert.That(req.CoderUrl, Is.EqualTo("https://coder.example.com")); 21 | 22 | Assert.That(req.DeviceOs, Is.EqualTo("Windows")); 23 | // seems that test assemblies always set 1.0.0.0 24 | Assert.That(req.CoderDesktopVersion, Is.EqualTo("1.0.0.0")); 25 | Assert.That(req.DeviceId, Is.Not.Empty); 26 | var deviceId = req.DeviceId; 27 | 28 | // deviceId is different on different machines, but we can test that 29 | // each instance of the TelemetryEnricher produces the same value. 30 | enricher = new TelemetryEnricher(); 31 | req = enricher.EnrichStartRequest(new StartRequest()); 32 | Assert.That(req.DeviceId, Is.EqualTo(deviceId)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests.Vpn.Service/testdata/winres.json: -------------------------------------------------------------------------------- 1 | { 2 | "RT_MANIFEST": { 3 | "#1": { 4 | "0409": { 5 | "identity": {}, 6 | "description": "", 7 | "minimum-os": "win7", 8 | "execution-level": "", 9 | "ui-access": false, 10 | "auto-elevate": false, 11 | "dpi-awareness": "system", 12 | "disable-theming": false, 13 | "disable-window-filtering": false, 14 | "high-resolution-scrolling-aware": false, 15 | "ultra-high-resolution-scrolling-aware": false, 16 | "long-path-aware": false, 17 | "printer-driver-isolation": false, 18 | "gdi-scaling": false, 19 | "segment-heap": false, 20 | "use-common-controls-v6": false 21 | } 22 | } 23 | }, 24 | "RT_VERSION": { 25 | "#1": { 26 | "0409": { 27 | "fixed": { 28 | "file_version": "1.2.3.4", 29 | "product_version": "1.2.3.4" 30 | }, 31 | "info": { 32 | "0409": { 33 | "FileDescription": "Coder", 34 | "FileVersion": "1.2.3.4", 35 | "LegalCopyright": "Copyright 2025 Coder Technologies Inc.", 36 | "OriginalFilename": "coder.exe", 37 | "ProductName": "Coder", 38 | "ProductVersion": "1.2.3.4" 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /Tests.Vpn.Service/testdata/README.md: -------------------------------------------------------------------------------- 1 | # Tests.Vpn.Service testdata 2 | 3 | ### Executables 4 | 5 | `Build-Assets.ps1` creates `hello.exe` and derivatives. You need `go`, 6 | `go-winres` and Windows 10 SDK 10.0.19041.0 installed to run this. 7 | 8 | You must sign `hello-versioned-signed.exe` yourself with the Coder EV cert after 9 | the script completes. 10 | 11 | These files are checked into the repo so they shouldn't need to be built again. 12 | 13 | ### Certificates 14 | 15 | - `coder-ev.crt` is the Extended Validation Code Signing certificate used by 16 | Coder, extracted from a signed release binary on 2025-03-07 17 | - `google-llc-ev.crt` is the Extended Validation Code Signing certificate used 18 | by Google Chrome, extracted from an official binary on 2025-03-07 19 | - `mozilla-corporation.crt` is a regular Code Signing certificate used by 20 | Mozilla Firefox, extracted from an official binary on 2025-03-07 21 | - `self-signed-ev.crt` was generated with `gen-certs.sh` using Linux OpenSSL 22 | - `self-signed.crt` was generated with `gen-certs.sh` using Linux OpenSSL 23 | 24 | You can extract a certificate from an executable with the following PowerShell 25 | one-liner: 26 | 27 | ```powershell 28 | Get-AuthenticodeSignature binary.exe | Select-Object -ExpandProperty SignerCertificate | Export-Certificate -Type CERT -FilePath output.crt 29 | ``` 30 | -------------------------------------------------------------------------------- /Tests.Vpn.Service/testdata/self-signed-ev.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDcTCCAlmgAwIBAgIUEzhP7oxDynN4aXNmRqGZzPUCkxUwDQYJKoZIhvcNAQEL 3 | BQAwHzEdMBsGA1UEAwwUQ29kZXIgU2VsZiBTaWduZWQgRVYwHhcNMjUwMzA3MDQ0 4 | NzQ2WhcNMzUwMzA1MDQ0NzQ2WjAfMR0wGwYDVQQDDBRDb2RlciBTZWxmIFNpZ25l 5 | ZCBFVjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALzzLlfpFYwEfQb6 6 | eizMaUr9/Op2ELLBRjabDT17uXBTPFHVtHewIaZfYPv7aY3B3rQTzHfE0y9YYO0+ 7 | 1Zhd2WpNKYO3iQM9OOcd69XYuDeRh09m6vOwcK7gSkSr55D/dUe4+vnjQBG9O6Na 8 | fby/kcJuDVNnR9rTPJpXqfnlgjrO2WNbBn3K0xJcNjVpMqFm2Iw9eYCRTVIUp559 9 | +iUGnwM+NT0cGAMB8242Jyz6xgEaRnSmddmxLDkfWWfivamSpWaaopR2T5+6txFW 10 | C1vBeZ4Au+9FEne64NVefVDjdeIDU7pgwYxykPDf+Sc704L8Da5X0gEoa82pHOfw 11 | 1DNP94kCAwEAAaOBpDCBoTALBgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH 12 | AwMwXgYDVR0gBFcwVTApBgZngQwBBAEwHzAdBggrBgEFBQcCARYRaHR0cHM6Ly9j 13 | b2Rlci5jb20wKAYFZ4EMAQMwHzAdBggrBgEFBQcCARYRaHR0cHM6Ly9jb2Rlci5j 14 | b20wHQYDVR0OBBYEFL9MxyFpCw/CmWioihuEP6x8XW0zMA0GCSqGSIb3DQEBCwUA 15 | A4IBAQC19lsc4EEOD4H3VNtQpLaBPLmNU4dD4bpWpoqv2YIYFl0T2cSkZZ1ZmSKR 16 | PV+1D3w7HsdmOf+1wXQv8w4POy3Z/7m6pcy/Efw9ImYs5zwRr5AniFJxjRBkUYB2 17 | i2m3650v5OAab4qay0FWCY4/8MX866fiLrO0oyjFI6tU/Py8kWV7IgOa9RxJpNou 18 | oITfLXLZRgXULiaXaQRA4TdD5zI9Qe/wwvj6wJH3u8qpRq+m+vo0cxfQ47tisL11 19 | nMM59fUZrypxdOTRK0QiGz5rJlLmZXZO27RNT3ewpJsq4qjQ3CtJ946vdjDc8+kY 20 | ChQ9e6sS5mLBP4JXtuyG+P1Fdp5t 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /App/Models/CredentialModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Coder.Desktop.CoderSdk.Coder; 3 | 4 | namespace Coder.Desktop.App.Models; 5 | 6 | public enum CredentialState 7 | { 8 | // Unknown means "we haven't checked yet" 9 | Unknown, 10 | 11 | // Invalid means "we checked and there's either no saved credentials, or they are not valid" 12 | Invalid, 13 | 14 | // Valid means "we checked and there are saved credentials, and they are valid" 15 | Valid, 16 | } 17 | 18 | public class CredentialModel : ICoderApiClientCredentialProvider 19 | { 20 | public CredentialState State { get; init; } = CredentialState.Unknown; 21 | 22 | public Uri? CoderUrl { get; init; } 23 | public string? ApiToken { get; init; } 24 | 25 | public string? Username { get; init; } 26 | 27 | public CredentialModel Clone() 28 | { 29 | return new CredentialModel 30 | { 31 | State = State, 32 | CoderUrl = CoderUrl, 33 | ApiToken = ApiToken, 34 | Username = Username, 35 | }; 36 | } 37 | 38 | public CoderApiClientCredential? GetCoderApiClientCredential() 39 | { 40 | if (State != CredentialState.Valid) return null; 41 | return new CoderApiClientCredential 42 | { 43 | ApiToken = ApiToken!, 44 | CoderUrl = CoderUrl!, 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /App/Converters/VpnLifecycleToBoolConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Coder.Desktop.App.Models; 3 | using DependencyPropertyGenerator; 4 | using Microsoft.UI.Xaml; 5 | using Microsoft.UI.Xaml.Data; 6 | 7 | namespace Coder.Desktop.App.Converters; 8 | 9 | [DependencyProperty("Unknown", DefaultValue = false)] 10 | [DependencyProperty("Starting", DefaultValue = false)] 11 | [DependencyProperty("Started", DefaultValue = false)] 12 | [DependencyProperty("Stopping", DefaultValue = false)] 13 | [DependencyProperty("Stopped", DefaultValue = false)] 14 | public partial class VpnLifecycleToBoolConverter : DependencyObject, IValueConverter 15 | { 16 | public object Convert(object value, Type targetType, object parameter, string language) 17 | { 18 | if (value is not VpnLifecycle lifecycle) return Stopped; 19 | 20 | return lifecycle switch 21 | { 22 | VpnLifecycle.Unknown => Unknown, 23 | VpnLifecycle.Starting => Starting, 24 | VpnLifecycle.Started => Started, 25 | VpnLifecycle.Stopping => Stopping, 26 | VpnLifecycle.Stopped => Stopped, 27 | _ => Visibility.Collapsed, 28 | }; 29 | } 30 | 31 | public object ConvertBack(object value, Type targetType, object parameter, string language) 32 | { 33 | throw new NotImplementedException(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /App/Converters/FriendlyByteConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.UI.Xaml.Data; 3 | 4 | namespace Coder.Desktop.App.Converters; 5 | 6 | public class FriendlyByteConverter : IValueConverter 7 | { 8 | private static readonly string[] Suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]; 9 | 10 | public object Convert(object value, Type targetType, object parameter, string language) 11 | { 12 | switch (value) 13 | { 14 | case int i: 15 | if (i < 0) i = 0; 16 | return FriendlyBytes((ulong)i); 17 | case uint ui: 18 | return FriendlyBytes(ui); 19 | case long l: 20 | if (l < 0) l = 0; 21 | return FriendlyBytes((ulong)l); 22 | case ulong ul: 23 | return FriendlyBytes(ul); 24 | default: 25 | return FriendlyBytes(0); 26 | } 27 | } 28 | 29 | public object ConvertBack(object value, Type targetType, object parameter, string language) 30 | { 31 | throw new NotImplementedException(); 32 | } 33 | 34 | public static string FriendlyBytes(ulong bytes) 35 | { 36 | if (bytes == 0) 37 | return $"0 {Suffixes[0]}"; 38 | 39 | var place = System.Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); 40 | var num = Math.Round(bytes / Math.Pow(1024, place), 1); 41 | return $"{num} {Suffixes[place]}"; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests.Vpn.Proto/RpcMessageTest.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.Vpn.Proto; 2 | 3 | namespace Coder.Desktop.Tests.Vpn.Proto; 4 | 5 | [TestFixture] 6 | public class RpcRoleAttributeTest 7 | { 8 | [Test] 9 | public void Ok() 10 | { 11 | var role = new RpcRoleAttribute("manager"); 12 | Assert.That(role.Role, Is.EqualTo("manager")); 13 | role = new RpcRoleAttribute("tunnel"); 14 | Assert.That(role.Role, Is.EqualTo("tunnel")); 15 | role = new RpcRoleAttribute("service"); 16 | Assert.That(role.Role, Is.EqualTo("service")); 17 | role = new RpcRoleAttribute("client"); 18 | Assert.That(role.Role, Is.EqualTo("client")); 19 | } 20 | } 21 | 22 | [TestFixture] 23 | public class RpcMessageTest 24 | { 25 | [Test] 26 | public void GetRole() 27 | { 28 | // RpcMessage is not a supported message type and doesn't have an 29 | // RpcRoleAttribute 30 | var ex = Assert.Throws(() => _ = RpcMessage.GetRole()); 31 | Assert.That(ex.Message, 32 | Does.Contain("Message type 'Coder.Desktop.Vpn.Proto.RPC' does not have a RpcRoleAttribute")); 33 | 34 | Assert.That(ManagerMessage.GetRole(), Is.EqualTo("manager")); 35 | Assert.That(TunnelMessage.GetRole(), Is.EqualTo("tunnel")); 36 | Assert.That(ServiceMessage.GetRole(), Is.EqualTo("service")); 37 | Assert.That(ClientMessage.GetRole(), Is.EqualTo("client")); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests.App/Services/SettingsManagerTest.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.App.Models; 2 | using Coder.Desktop.App.Services; 3 | 4 | namespace Coder.Desktop.Tests.App.Services; 5 | [TestFixture] 6 | public sealed class SettingsManagerTests 7 | { 8 | private string _tempDir = string.Empty; 9 | private SettingsManager _sut = null!; 10 | 11 | [SetUp] 12 | public void SetUp() 13 | { 14 | _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); 15 | Directory.CreateDirectory(_tempDir); 16 | _sut = new SettingsManager(_tempDir); // inject isolated path 17 | } 18 | 19 | [TearDown] 20 | public void TearDown() 21 | { 22 | try { Directory.Delete(_tempDir, true); } catch { /* ignore */ } 23 | } 24 | 25 | [Test] 26 | public void Save_Persists() 27 | { 28 | var expected = true; 29 | var settings = new CoderConnectSettings 30 | { 31 | Version = 1, 32 | ConnectOnLaunch = expected 33 | }; 34 | _sut.Write(settings).GetAwaiter().GetResult(); 35 | var actual = _sut.Read().GetAwaiter().GetResult(); 36 | Assert.That(actual.ConnectOnLaunch, Is.EqualTo(expected)); 37 | } 38 | 39 | [Test] 40 | public void Read_MissingKey_ReturnsDefault() 41 | { 42 | var actual = _sut.Read().GetAwaiter().GetResult(); 43 | Assert.That(actual.ConnectOnLaunch, Is.False); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests.CoderSdk/Tests.CoderSdk.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Coder.Desktop.Tests.CoderSdk 5 | Coder.Desktop.Tests.CoderSdk 6 | net8.0 7 | enable 8 | enable 9 | 10 | false 11 | true 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coder Desktop for Windows 2 | 3 | Coder Desktop allows you to work on your Coder workspaces as though they're 4 | on your local network, with no port-forwarding required. It provides seamless 5 | access to your remote development environments through features like Coder 6 | Connect (VPN-like connectivity) and file synchronization between local and 7 | remote directories. 8 | 9 | Learn more about Coder Desktop in the 10 | [official documentation](https://coder.com/docs/user-guides/desktop). 11 | 12 | This repo contains the C# source code for Coder Desktop for Windows. You can 13 | download the latest version from the GitHub releases. 14 | 15 | ### Contributing 16 | 17 | You will need: 18 | 19 | - Visual Studio 2022 20 | - .NET desktop development 21 | - WinUI application development 22 | - Windows 10 SDK (10.0.19041.0) 23 | - Wix Toolset 5.0.2 (if building the installer) 24 | 25 | It's also recommended to use JetBrains Rider (or VS + ReSharper) for a better 26 | experience. 27 | 28 | ### License 29 | 30 | The Coder Desktop for Windows source is licensed under the GNU Affero General 31 | Public License v3.0 (AGPL-3.0). 32 | 33 | Some vendored files in this repo are licensed separately. The license for these 34 | files can be found in the same directory as the files. 35 | 36 | The binary distributions of Coder Desktop for Windows have some additional 37 | license disclaimers that can be found in 38 | [scripts/files/License.txt](scripts/files/License.txt) or during installation. -------------------------------------------------------------------------------- /App/ViewModels/TrayWindowDisconnectedViewModel.cs: -------------------------------------------------------------------------------- 1 | using Coder.Desktop.App.Models; 2 | using Coder.Desktop.App.Services; 3 | using CommunityToolkit.Mvvm.ComponentModel; 4 | using CommunityToolkit.Mvvm.Input; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | namespace Coder.Desktop.App.ViewModels; 9 | 10 | public partial class TrayWindowDisconnectedViewModel : ObservableObject 11 | { 12 | private readonly IRpcController _rpcController; 13 | 14 | [ObservableProperty] public partial bool ReconnectButtonEnabled { get; set; } = true; 15 | [ObservableProperty] public partial string ErrorMessage { get; set; } = string.Empty; 16 | [ObservableProperty] public partial bool ReconnectFailed { get; set; } = false; 17 | 18 | public TrayWindowDisconnectedViewModel(IRpcController rpcController) 19 | { 20 | _rpcController = rpcController; 21 | _rpcController.StateChanged += (_, rpcModel) => UpdateFromRpcModel(rpcModel); 22 | } 23 | 24 | private void UpdateFromRpcModel(RpcModel rpcModel) 25 | { 26 | ReconnectButtonEnabled = rpcModel.RpcLifecycle != RpcLifecycle.Disconnected; 27 | } 28 | 29 | [RelayCommand] 30 | public async Task Reconnect() 31 | { 32 | try 33 | { 34 | ReconnectFailed = false; 35 | ErrorMessage = string.Empty; 36 | await _rpcController.Reconnect(); 37 | } 38 | catch (Exception ex) 39 | { 40 | ErrorMessage = ex.Message; 41 | ReconnectFailed = true; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /App/Views/Pages/TrayWindowMainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Coder.Desktop.App.ViewModels; 3 | using Microsoft.UI.Xaml; 4 | using Microsoft.UI.Xaml.Controls; 5 | using Microsoft.UI.Xaml.Documents; 6 | 7 | namespace Coder.Desktop.App.Views.Pages; 8 | 9 | public sealed partial class TrayWindowMainPage : Page 10 | { 11 | public TrayWindowViewModel ViewModel { get; } 12 | 13 | public TrayWindowMainPage(TrayWindowViewModel viewModel) 14 | { 15 | InitializeComponent(); 16 | ViewModel = viewModel; 17 | ViewModel.Initialize(DispatcherQueue); 18 | } 19 | 20 | // HACK: using XAML to populate the text Runs results in an additional 21 | // whitespace Run being inserted between the ViewableHostname and the 22 | // ViewableHostnameSuffix. You might think, "OK let's populate the entire 23 | // TextBlock content from code then!", but this results in the ItemsRepeater 24 | // corrupting it and firing events off to the wrong AgentModel. 25 | // 26 | // This is the best solution I came up with that worked. 27 | public void AgentHostnameText_OnLoaded(object sender, RoutedEventArgs e) 28 | { 29 | if (sender is not TextBlock textBlock) return; 30 | 31 | var nonWhitespaceRuns = new List(); 32 | foreach (var inline in textBlock.Inlines) 33 | if (inline is Run run && run.Text != " ") 34 | nonWhitespaceRuns.Add(run); 35 | 36 | textBlock.Inlines.Clear(); 37 | foreach (var run in nonWhitespaceRuns) textBlock.Inlines.Add(run); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests.Vpn/Tests.Vpn.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Coder.Desktop.Tests.Vpn 5 | Coder.Desktop.Tests.Vpn 6 | net8.0-windows 7 | enable 8 | enable 9 | true 10 | 11 | false 12 | true 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /App/Controls/TrayIcon.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Windows.Input; 3 | using Windows.UI.ViewManagement; 4 | using DependencyPropertyGenerator; 5 | using Microsoft.UI.Xaml; 6 | using Microsoft.UI.Xaml.Controls; 7 | using Microsoft.UI.Xaml.Media.Imaging; 8 | 9 | namespace Coder.Desktop.App.Controls; 10 | 11 | [DependencyProperty("OpenCommand")] 12 | [DependencyProperty("ExitCommand")] 13 | [DependencyProperty("CheckForUpdatesCommand")] 14 | public sealed partial class TrayIcon : UserControl 15 | { 16 | private readonly UISettings _uiSettings = new(); 17 | 18 | public TrayIcon() 19 | { 20 | InitializeComponent(); 21 | _uiSettings.ColorValuesChanged += OnColorValuesChanged; 22 | UpdateTrayIconBasedOnTheme(); 23 | } 24 | 25 | private void OnColorValuesChanged(UISettings sender, object args) 26 | { 27 | DispatcherQueue.TryEnqueue(UpdateTrayIconBasedOnTheme); 28 | } 29 | 30 | private void UpdateTrayIconBasedOnTheme() 31 | { 32 | var currentTheme = Application.Current.RequestedTheme; 33 | Debug.WriteLine("Theme update requested, found theme: " + currentTheme); 34 | 35 | switch (currentTheme) 36 | { 37 | case ApplicationTheme.Dark: 38 | TaskbarIcon.IconSource = (BitmapImage)Resources["IconDarkTheme"]; 39 | break; 40 | case ApplicationTheme.Light: 41 | default: 42 | TaskbarIcon.IconSource = (BitmapImage)Resources["IconLightTheme"]; 43 | break; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Vpn.Service/Create-Service.ps1: -------------------------------------------------------------------------------- 1 | # Elevate to administrator 2 | if (-not ([Security.Principal.WindowsPrincipal]([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { 3 | Write-Host "Elevating script to run as administrator..." 4 | Start-Process powershell.exe -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`"" -Verb RunAs 5 | exit 6 | } 7 | 8 | $name = "Coder Desktop (Debug)" 9 | $binaryPath = Join-Path -Path $PSScriptRoot -ChildPath "bin/Debug/net8.0-windows/Vpn.Service.exe" 10 | 11 | try { 12 | Write-Host "Creating service..." 13 | New-Service -Name $name -BinaryPathName "`"$binaryPath`"" -DisplayName $name -StartupType Automatic 14 | 15 | $sddl = & sc.exe sdshow $name 16 | if (-not $sddl) { 17 | throw "Failed to retrieve security descriptor for service '$name'" 18 | } 19 | Write-Host "Current security descriptor: '$sddl'" 20 | $sddl = $sddl.Trim() -replace "D:", "D:(A;;RPWP;;;WD)" # allow everyone to start, stop, pause, and query the service 21 | Write-Host "Setting security descriptor: '$sddl'" 22 | & sc.exe sdset $name $sddl 23 | 24 | Write-Host "Starting service..." 25 | Start-Service -Name $name 26 | 27 | if ((Get-Service -Name $name -ErrorAction Stop).Status -ne "Running") { 28 | throw "Service '$name' is not running" 29 | } 30 | Write-Host "Service '$name' created and started successfully" 31 | } catch { 32 | Write-Host $_ -ForegroundColor Red 33 | Write-Host "Press Return to exit..." 34 | Read-Host 35 | } 36 | -------------------------------------------------------------------------------- /Tests.Vpn.Proto/Tests.Vpn.Proto.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Coder.Desktop.Tests.Vpn.Proto 5 | Coder.Desktop.Tests.Vpn.Proto 6 | net8.0 7 | enable 8 | enable 9 | true 10 | 11 | false 12 | true 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /App/Views/MessageWindow.xaml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 36 | 37 |