From adf2a70b20ab76f26ff85b82c056d0c9f367afeb Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 13 Feb 2025 00:00:15 +1100 Subject: [PATCH 1/4] chore: various finish line tasks --- .github/workflows/ci.yaml | 19 ++-- .github/workflows/release.yaml | 0 .gitignore | 4 +- App/App.csproj | 32 +++++-- App/App.xaml.cs | 21 ++++- App/Images/SplashScreen.scale-200.png | Bin 5372 -> 0 bytes App/Images/Square150x150Logo.scale-200.png | Bin 1755 -> 0 bytes App/Images/Square44x44Logo.scale-200.png | Bin 637 -> 0 bytes App/Program.cs | 83 +++++++++++++++++ .../PublishProfiles/win-arm64.pubxml | 4 +- App/Properties/PublishProfiles/win-x64.pubxml | 4 +- App/Properties/PublishProfiles/win-x86.pubxml | 4 +- App/Services/CredentialManager.cs | 22 +++-- App/Services/RpcController.cs | 9 +- App/ViewModels/TrayWindowViewModel.cs | 88 ++++++++++++++---- App/Views/Pages/SignInTokenPage.xaml | 5 +- App/Views/Pages/TrayWindowMainPage.xaml | 6 +- App/Views/SignInWindow.xaml.cs | 12 +++ App/Views/TrayWindow.xaml.cs | 5 +- App/packages.lock.json | 6 ++ Publish-Alpha.ps1 | 60 +++++++++--- Vpn.Service/Manager.cs | 24 +++-- Vpn.Service/ManagerConfig.cs | 6 +- Vpn.Service/Program.cs | 38 ++++---- 24 files changed, 349 insertions(+), 103 deletions(-) create mode 100644 .github/workflows/release.yaml delete mode 100644 App/Images/SplashScreen.scale-200.png delete mode 100644 App/Images/Square150x150Logo.scale-200.png delete mode 100644 App/Images/Square44x44Logo.scale-200.png create mode 100644 App/Program.cs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8c42c13..b5059b2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,10 +51,15 @@ jobs: cache-dependency-path: '**/packages.lock.json' - name: dotnet restore run: dotnet restore --locked-mode - #- name: dotnet publish - # run: dotnet publish --no-restore --configuration Release --output .\publish - #- name: Upload artifact - # uses: actions/upload-artifact@v4 - # with: - # name: publish - # path: .\publish\ + # This doesn't call `dotnet publish` on the entire solution, just the + # projects we care about building. Doing a full publish includes test + # libraries and stuff which is pointless. + - name: dotnet publish Coder.Desktop.Vpn.Service + run: dotnet publish .\Vpn.Service\Vpn.Service.csproj --configuration Release --output .\publish\Vpn.Service + - name: dotnet publish Coder.Desktop.App + run: dotnet publish .\App\App.csproj --configuration Release --output .\publish\App + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: publish + path: .\publish\ diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore index 4ea0881..54c47a0 100644 --- a/.gitignore +++ b/.gitignore @@ -403,5 +403,7 @@ FodyWeavers.xsd .idea/**/shelf publish -WindowsAppRuntimeInstall-x64.exe +WindowsAppRuntimeInstall-*.exe +windowsdesktop-runtime-*.exe wintun.dll +wintun-*.dll diff --git a/App/App.csproj b/App/App.csproj index f6e3c0d..c28256a 100644 --- a/App/App.csproj +++ b/App/App.csproj @@ -10,22 +10,42 @@ Properties\PublishProfiles\win-$(Platform).pubxml true enable - false + true None true preview + + DISABLE_XAML_GENERATED_MAIN + + + + true + CopyUsed + true + true - - - - - + + + + + + + + + + + + diff --git a/App/App.xaml.cs b/App/App.xaml.cs index 515d404..af4217e 100644 --- a/App/App.xaml.cs +++ b/App/App.xaml.cs @@ -1,5 +1,5 @@ using System; -using System.Diagnostics; +using System.Threading.Tasks; using Coder.Desktop.App.Services; using Coder.Desktop.App.ViewModels; using Coder.Desktop.App.Views; @@ -13,6 +13,8 @@ public partial class App : Application { private readonly IServiceProvider _services; + private bool _handleWindowClosed = true; + public App() { var services = new ServiceCollection(); @@ -36,18 +38,27 @@ public App() _services = services.BuildServiceProvider(); -#if DEBUG - UnhandledException += (_, e) => { Debug.WriteLine(e.Exception.ToString()); }; -#endif - InitializeComponent(); } + public async Task ExitApplication() + { + _handleWindowClosed = false; + Exit(); + var rpcManager = _services.GetRequiredService(); + // TODO: send a StopRequest if we're connected??? + await rpcManager.DisposeAsync(); + Environment.Exit(0); + } + protected override void OnLaunched(LaunchActivatedEventArgs args) { var trayWindow = _services.GetRequiredService(); + + // Prevent the TrayWindow from closing, just hide it. trayWindow.Closed += (sender, args) => { + if (!_handleWindowClosed) return; args.Handled = true; trayWindow.AppWindow.Hide(); }; diff --git a/App/Images/SplashScreen.scale-200.png b/App/Images/SplashScreen.scale-200.png deleted file mode 100644 index 32f486a86792a5e34cd9a8261b394c49b48f86be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5372 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 diff --git a/App/Images/Square44x44Logo.scale-200.png b/App/Images/Square44x44Logo.scale-200.png deleted file mode 100644 index f713bba67f551ef91020b75716a4dc8ebd744b1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 637 zcmeAS@N?(olHy`uVBq!ia0vp^5g^RL1|$oo8kjIJFu8cTIEGZ*dUI*J;2{SImxtDO zm%3!R$UazoY}x{$j0P5ABYXWr(l=jxJ6ps1W{tV=^>{Dl><3nv3A}sm=EZ)#l3`NR zpZda3^rNox*D1%NC98Z~L*6zipLw~Gxn&(Y-;KmJ+aR6eLabU-L#y8HW%7P-E_-VlLqIabbHPHKT*)fT@9iWJ7iWgOT9%0}Lrj>lztPxWq6sPw3pi z#-<=#$jjrP_DD*i!RLsn0mIA=>4~N)IMYWIf=j%-zuKCdMG%tHYot70D1| zvWa0wMhauW#S>1CnI_;>!1Q3zMA17@DOVq{MQ+{U7^a&yA+%dMCG;WNPV0i;w$tu; zX^b}UKziPM)(<;)ruW;-`)bBN+rQNM*Zs_>?n$|FVFo-e*PZb*@U7VAd+tHb4e + { + var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()); + SynchronizationContext.SetSynchronizationContext(context); + + app = new App(); + app.UnhandledException += (_, e) => + { + e.Handled = true; + ShowExceptionAndCrash(e.Exception); + }; + }); + } + catch (Exception e) + { + ShowExceptionAndCrash(e); + } + } + + [STAThread] + private static bool CheckSingleInstance() + { +#if !DEBUG + const string appInstanceName = "Coder.Desktop.App"; +#else + const string appInstanceName = "Coder.Desktop.App.Debug"; +#endif + + var instance = AppInstance.FindOrRegisterForKey(appInstanceName); + return instance.IsCurrent; + } + + [STAThread] + private static void ShowExceptionAndCrash(Exception e) + { + const string title = "Coder Desktop Fatal Error"; + var message = + "Coder Desktop has encountered a fatal error and must exit.\n\n" + + e + "\n\n" + + Environment.StackTrace; + MessageBoxW(IntPtr.Zero, message, title, 0); + + if (app != null) + app.Exit(); + + // This will log the exception to the Windows Event Log. +#if DEBUG + // And, if in DEBUG mode, it will also log to the console window. + AllocConsole(); +#endif + Environment.FailFast("Coder Desktop has encountered a fatal error and must exit.", e); + } +} +#endif diff --git a/App/Properties/PublishProfiles/win-arm64.pubxml b/App/Properties/PublishProfiles/win-arm64.pubxml index 5b906a9..ac9753e 100644 --- a/App/Properties/PublishProfiles/win-arm64.pubxml +++ b/App/Properties/PublishProfiles/win-arm64.pubxml @@ -1,4 +1,4 @@ - + @@ -8,7 +8,5 @@ https://go.microsoft.com/fwlink/?LinkID=208121. ARM64 win-arm64 bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ - true - False diff --git a/App/Properties/PublishProfiles/win-x64.pubxml b/App/Properties/PublishProfiles/win-x64.pubxml index d6e3ca5..942523b 100644 --- a/App/Properties/PublishProfiles/win-x64.pubxml +++ b/App/Properties/PublishProfiles/win-x64.pubxml @@ -1,4 +1,4 @@ - + @@ -8,7 +8,5 @@ https://go.microsoft.com/fwlink/?LinkID=208121. x64 win-x64 bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ - true - False diff --git a/App/Properties/PublishProfiles/win-x86.pubxml b/App/Properties/PublishProfiles/win-x86.pubxml index 084c7fe..e763481 100644 --- a/App/Properties/PublishProfiles/win-x86.pubxml +++ b/App/Properties/PublishProfiles/win-x86.pubxml @@ -1,4 +1,4 @@ - + @@ -8,7 +8,5 @@ https://go.microsoft.com/fwlink/?LinkID=208121. x86 win-x86 bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ - true - False diff --git a/App/Services/CredentialManager.cs b/App/Services/CredentialManager.cs index 05dceec..4e47f47 100644 --- a/App/Services/CredentialManager.cs +++ b/App/Services/CredentialManager.cs @@ -2,6 +2,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Coder.Desktop.App.Models; @@ -10,6 +11,17 @@ namespace Coder.Desktop.App.Services; +public class RawCredentials +{ + public required string CoderUrl { get; set; } + public required string ApiToken { get; set; } +} + +[JsonSerializable(typeof(RawCredentials))] +public partial class RawCredentialsJsonContext : JsonSerializerContext +{ +} + public interface ICredentialManager { public event EventHandler CredentialsChanged; @@ -123,7 +135,7 @@ private void UpdateState(CredentialModel newModel) RawCredentials? credentials; try { - credentials = JsonSerializer.Deserialize(raw); + credentials = JsonSerializer.Deserialize(raw, RawCredentialsJsonContext.Default.RawCredentials); } catch (JsonException) { @@ -138,16 +150,10 @@ private void UpdateState(CredentialModel newModel) private static void WriteCredentials(RawCredentials credentials) { - var raw = JsonSerializer.Serialize(credentials); + var raw = JsonSerializer.Serialize(credentials, RawCredentialsJsonContext.Default.RawCredentials); NativeApi.WriteCredentials(CredentialsTargetName, raw); } - private class RawCredentials - { - public required string CoderUrl { get; set; } - public required string ApiToken { get; set; } - } - private static class NativeApi { private const int CredentialTypeGeneric = 1; diff --git a/App/Services/RpcController.cs b/App/Services/RpcController.cs index 07ae38e..a02347f 100644 --- a/App/Services/RpcController.cs +++ b/App/Services/RpcController.cs @@ -32,7 +32,7 @@ public VpnLifecycleException(string message) : base(message) } } -public interface IRpcController +public interface IRpcController : IAsyncDisposable { public event EventHandler StateChanged; @@ -224,6 +224,13 @@ public async Task StopVpn(CancellationToken ct = default) new InvalidOperationException($"Service reported failure: {reply.Stop.ErrorMessage}")); } + public async ValueTask DisposeAsync() + { + if (_speaker != null) + await _speaker.DisposeAsync(); + GC.SuppressFinalize(this); + } + private void MutateState(Action mutator) { RpcModel newState; diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index 5fcd84e..7044770 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -3,6 +3,7 @@ using System.Linq; using Coder.Desktop.App.Models; using Coder.Desktop.App.Services; +using Coder.Desktop.Vpn.Proto; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Google.Protobuf; @@ -15,19 +16,19 @@ namespace Coder.Desktop.App.ViewModels; public partial class TrayWindowViewModel : ObservableObject { private const int MaxAgents = 5; + private const string DefaultDashboardUrl = "https://coder.com"; private readonly IRpcController _rpcController; private readonly ICredentialManager _credentialManager; + private ToggleSwitch? _vpnActiveSwitch; + private bool _isProgrammaticStateChange; + private DispatcherQueue? _dispatcherQueue; [ObservableProperty] public partial VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Unknown; - // VpnSwitchOn needs to be its own property as it is a two-way binding - [ObservableProperty] - public partial bool VpnSwitchOn { get; set; } = false; - [ObservableProperty] public partial string? VpnFailedMessage { get; set; } = null; @@ -82,13 +83,26 @@ private void UpdateFromRpcModel(RpcModel rpcModel) if (rpcModel.RpcLifecycle is RpcLifecycle.Disconnected) { VpnLifecycle = VpnLifecycle.Unknown; - VpnSwitchOn = false; + SetVpnSwitch(false); Agents = []; return; } VpnLifecycle = rpcModel.VpnLifecycle; - VpnSwitchOn = rpcModel.VpnLifecycle is VpnLifecycle.Starting or VpnLifecycle.Started; + SetVpnSwitch(rpcModel.VpnLifecycle is VpnLifecycle.Starting or VpnLifecycle.Started); + + // Get the current dashboard URL. + var credentialModel = _credentialManager.GetCredentials(); + Uri? coderUri = null; + if (credentialModel.State == CredentialState.Valid && !string.IsNullOrWhiteSpace(credentialModel.CoderUrl)) + try + { + coderUri = new Uri(credentialModel.CoderUrl, UriKind.Absolute); + } + catch + { + // Ignore + } // Add every known agent. HashSet workspacesWithAgents = []; @@ -114,6 +128,8 @@ private void UpdateFromRpcModel(RpcModel rpcModel) var lastHandshakeAgo = DateTime.UtcNow.Subtract(agent.LastHandshake.ToDateTime()); workspacesWithAgents.Add(agent.WorkspaceId); + var workspace = rpcModel.Workspaces.FirstOrDefault(w => w.Id == agent.WorkspaceId); + agents.Add(new AgentViewModel { Hostname = fqdnPrefix, @@ -121,26 +137,21 @@ private void UpdateFromRpcModel(RpcModel rpcModel) ConnectionStatus = lastHandshakeAgo < TimeSpan.FromMinutes(5) ? AgentConnectionStatus.Green : AgentConnectionStatus.Red, - // TODO: we don't actually have any way of crafting a dashboard - // URL without the owner's username - DashboardUrl = "https://coder.com", + DashboardUrl = WorkspaceUri(coderUri, workspace?.Name), }); } - // For every workspace that doesn't have an agent, add a dummy agent. - foreach (var workspace in rpcModel.Workspaces.Where(w => !workspacesWithAgents.Contains(w.Id))) - { + // For every stopped workspace that doesn't have any agents, add a + // dummy agent row. + foreach (var workspace in rpcModel.Workspaces.Where(w => w.Status == Workspace.Types.Status.Stopped && !workspacesWithAgents.Contains(w.Id))) agents.Add(new AgentViewModel { // We just assume that it's a single-agent workspace. Hostname = workspace.Name, HostnameSuffix = ".coder", ConnectionStatus = AgentConnectionStatus.Gray, - // TODO: we don't actually have any way of crafting a dashboard - // URL without the owner's username - DashboardUrl = "https://coder.com", + DashboardUrl = WorkspaceUri(coderUri, workspace.Name), }); - } // Sort by status green, red, gray, then by hostname. agents.Sort((a, b) => @@ -154,25 +165,61 @@ private void UpdateFromRpcModel(RpcModel rpcModel) if (Agents.Count < MaxAgents) ShowAllAgents = false; } + private string WorkspaceUri(Uri? baseUri, string? workspaceName) + { + if (baseUri == null) return DefaultDashboardUrl; + if (string.IsNullOrWhiteSpace(workspaceName)) return baseUri.ToString(); + try + { + return new Uri(baseUri, $"/@me/{workspaceName}").ToString(); + } + catch + { + return DefaultDashboardUrl; + } + } + private void UpdateFromCredentialsModel(CredentialModel credentialModel) { // HACK: the HyperlinkButton crashes the whole app if the initial URI // or this URI is invalid. CredentialModel.CoderUrl should never be // null while the Page is active as the Page is only displayed when // CredentialModel.Status == Valid. - DashboardUrl = credentialModel.CoderUrl ?? "https://coder.com"; + DashboardUrl = credentialModel.CoderUrl ?? DefaultDashboardUrl; + } + + private void SetVpnSwitch(bool value) + { + if (_vpnActiveSwitch == null) return; + _isProgrammaticStateChange = true; + _vpnActiveSwitch.IsOn = value; + _isProgrammaticStateChange = false; + } + + // HACK: using a two-way bool to store the VPN active state results in + // erroneous events being sent (even outside our change handlers). This + // sucks and breaks the ViewModel separation but is necessary for the + // switch to function correctly. + public void VpnSwitch_Loaded(object sender, RoutedEventArgs e) + { + if (sender is not ToggleSwitch toggleSwitch) return; + _vpnActiveSwitch = toggleSwitch; + SetVpnSwitch(VpnLifecycle is VpnLifecycle.Starting or VpnLifecycle.Started); } - // VpnSwitch_Toggled is handled separately than just listening to the - // property change as we need to be able to tell the difference between the - // user toggling the switch and the switch being toggled by code. public void VpnSwitch_Toggled(object sender, RoutedEventArgs e) { if (sender is not ToggleSwitch toggleSwitch) return; + // HACK: the toggled event gets fired even when the switch state is + // changed from code, so we ignore all events while we're performing + // changes. + if (_isProgrammaticStateChange) return; + VpnFailedMessage = ""; try { + // The start/stop methods will call back to update the state. if (toggleSwitch.IsOn) _rpcController.StartVpn(); else @@ -180,6 +227,7 @@ public void VpnSwitch_Toggled(object sender, RoutedEventArgs e) } catch { + // TODO: display error VpnFailedMessage = e.ToString(); } } diff --git a/App/Views/Pages/SignInTokenPage.xaml b/App/Views/Pages/SignInTokenPage.xaml index a09efb8..93a1796 100644 --- a/App/Views/Pages/SignInTokenPage.xaml +++ b/App/Views/Pages/SignInTokenPage.xaml @@ -57,14 +57,13 @@ HorizontalAlignment="Right" Padding="10" /> - + Password="{x:Bind ViewModel.ApiToken, Mode=TwoWay}" /> - + + @@ -56,8 +57,8 @@ Grid.Column="2" OnContent="" OffContent="" - IsOn="{x:Bind ViewModel.VpnSwitchOn, Mode=TwoWay}" IsEnabled="{x:Bind ViewModel.VpnLifecycle, Converter={StaticResource NotConnectingBoolConverter}, Mode=OneWay}" + Loaded="{x:Bind ViewModel.VpnSwitch_Loaded}" Toggled="{x:Bind ViewModel.VpnSwitch_Toggled}" Margin="0,0,-110,0" HorizontalAlignment="Right" /> @@ -204,6 +205,7 @@ diff --git a/App/Views/SignInWindow.xaml.cs b/App/Views/SignInWindow.xaml.cs index 5549e7c..2d866e7 100644 --- a/App/Views/SignInWindow.xaml.cs +++ b/App/Views/SignInWindow.xaml.cs @@ -1,7 +1,9 @@ using Windows.Graphics; using Coder.Desktop.App.ViewModels; using Coder.Desktop.App.Views.Pages; +using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; +using WinRT.Interop; namespace Coder.Desktop.App.Views; @@ -24,6 +26,7 @@ public SignInWindow(SignInViewModel viewModel) NavigateToUrlPage(); ResizeWindow(); + MoveWindowToCenterOfDisplay(); } public void NavigateToTokenPage() @@ -43,4 +46,13 @@ private void ResizeWindow() var width = (int)(WIDTH * scale); AppWindow.Resize(new SizeInt32(width, height)); } + + private void MoveWindowToCenterOfDisplay() + { + var workArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Primary).WorkArea; + var x = (workArea.Width - AppWindow.Size.Width) / 2; + var y = (workArea.Height - AppWindow.Size.Height) / 2; + if (x < 0 || y < 0) return; + AppWindow.Move(new PointInt32(x, y)); + } } diff --git a/App/Views/TrayWindow.xaml.cs b/App/Views/TrayWindow.xaml.cs index 224fae2..b528723 100644 --- a/App/Views/TrayWindow.xaml.cs +++ b/App/Views/TrayWindow.xaml.cs @@ -247,10 +247,11 @@ private void Tray_Open() [RelayCommand] private void Tray_Exit() { - Application.Current.Exit(); + // It's fine that this happens in the background. + _ = ((App)Application.Current).ExitApplication(); } - public class NativeApi + public static class NativeApi { [DllImport("dwmapi.dll")] public static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int value, int size); diff --git a/App/packages.lock.json b/App/packages.lock.json index ca5e679..78dfa1b 100644 --- a/App/packages.lock.json +++ b/App/packages.lock.json @@ -35,6 +35,12 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1" } }, + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[8.0.12, )", + "resolved": "8.0.12", + "contentHash": "FV4HnQ3JI15PHnJ5PGTbz+rYvrih42oLi/7UMIshNwCwUZhTq13UzrggtXk4ygrcMcN+4jsS6hhshx2p/Zd0ig==" + }, "Microsoft.WindowsAppSDK": { "type": "Direct", "requested": "[1.6.250108002, )", diff --git a/Publish-Alpha.ps1 b/Publish-Alpha.ps1 index 79032b3..86ce8e7 100644 --- a/Publish-Alpha.ps1 +++ b/Publish-Alpha.ps1 @@ -1,8 +1,15 @@ +# Usage: Publish-Alpha.ps1 [-arch ] +param ( + [ValidateSet("x64", "arm64")] + [string] $arch = "x64" +) + # CD to the directory of this PS script Push-Location $PSScriptRoot # Create a publish directory -$publishDir = Join-Path $PSScriptRoot "publish" +New-Item -ItemType Directory -Path "publish" -Force +$publishDir = Join-Path $PSScriptRoot "publish/$arch" if (Test-Path $publishDir) { # prompt the user to confirm the deletion $confirm = Read-Host "The directory $publishDir already exists. Do you want to delete it? (y/n)" @@ -17,39 +24,57 @@ New-Item -ItemType Directory -Path $publishDir # Build in release mode dotnet.exe clean -dotnet.exe publish .\Vpn.Service\Vpn.Service.csproj -c Release -a x64 -o $publishDir\service +$servicePublishDir = Join-Path $publishDir "service" +dotnet.exe publish .\Vpn.Service\Vpn.Service.csproj -c Release -a $arch -o $servicePublishDir +# App needs to be built with msbuild +$appPublishDir = Join-Path $publishDir "app" $msbuildBinary = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe -& $msbuildBinary .\App\App.csproj /p:Configuration=Release /p:Platform=x64 /p:OutputPath=..\publish\app /p:GenerateAppxPackageOnBuild=true +& $msbuildBinary .\App\App.csproj /p:Configuration=Release /p:Platform=$arch /p:OutputPath=$appPublishDir $scriptsDir = Join-Path $publishDir "scripts" New-Item -ItemType Directory -Path $scriptsDir -# Download the 1.6.250108002 redistributable zip from here and drop the x64 -# version in the root of the repo: +# Download 8.0.12 Desktop runtime from here for both amd64 and arm64: +# https://dotnet.microsoft.com/en-us/download/dotnet/8.0 +$dotnetRuntimeInstaller = Join-Path $PSScriptRoot "windowsdesktop-runtime-8.0.12-win-$($arch).exe" +Copy-Item $dotnetRuntimeInstaller $scriptsDir + +# Download the 1.6.250108002 redistributable zip from here and drop the executables +# in the root of the repo: # https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/downloads -$windowsAppSdkInstaller = Join-Path $PSScriptRoot "WindowsAppRuntimeInstall-x64.exe" +$windowsAppSdkInstaller = Join-Path $PSScriptRoot "WindowsAppRuntimeInstall-$($arch).exe" Copy-Item $windowsAppSdkInstaller $scriptsDir -# Acquire wintun.dll and put it in the root of the repo. -$wintunDll = Join-Path $PSScriptRoot "wintun.dll" +# Download wintun DLLs from https://www.wintun.net and place wintun-x64.dll and +# wintun-arm64.dll in the root of the repo. +$wintunDll = Join-Path $PSScriptRoot "wintun-$arch.dll" Copy-Item $wintunDll $scriptsDir # Add a PS1 script for installing the service $installScript = Join-Path $scriptsDir "Install.ps1" $installScriptContent = @" try { + # Install .NET Desktop Runtime + `$dotNetInstallerPath = Join-Path `$PSScriptRoot "windowsdesktop-runtime-8.0.12-win-$($arch).exe" + Write-Host "Installing .NET Desktop Runtime from `$dotNetInstallerPath" + Start-Process `$dotNetInstallerPath -ArgumentList "/install /quiet /norestart" -Wait + # Install Windows App SDK - `$installerPath = Join-Path `$PSScriptRoot "WindowsAppRuntimeInstall-x64.exe" - Start-Process `$installerPath -ArgumentList "/silent" -Wait + Write-Host "Installing Windows App SDK from `$sdkInstallerPath" + `$sdkInstallerPath = Join-Path `$PSScriptRoot "WindowsAppRuntimeInstall-$($arch).exe" + Start-Process `$sdkInstallerPath -ArgumentList "--quiet" -Wait # Install wintun.dll - `$wintunPath = Join-Path `$PSScriptRoot "wintun.dll" + Write-Host "Installing wintun.dll from `$wintunPath" + `$wintunPath = Join-Path `$PSScriptRoot "wintun-$($arch).dll" Copy-Item `$wintunPath "C:\wintun.dll" # Install and start the service `$name = "Coder Desktop (Debug)" `$binaryPath = Join-Path `$PSScriptRoot "..\service\Vpn.Service.exe" | Resolve-Path + Write-Host "Installing service" New-Service -Name `$name -BinaryPathName `$binaryPath -StartupType Automatic + Write-Host "Starting service" Start-Service -Name `$name } catch { Write-Host "" @@ -76,10 +101,13 @@ $uninstallScriptContent = @" try { # Uninstall the service `$name = "Coder Desktop (Debug)" + Write-Host "Stopping service" Stop-Service -Name `$name + Write-Host "Deleting service" sc.exe delete `$name # Delete wintun.dll + Write-Host "Deleting wintun.dll" Remove-Item "C:\wintun.dll" # Maybe delete C:\coder-vpn.exe and C:\CoderDesktop.log @@ -127,6 +155,11 @@ $readmeContent = @" selecting "Exit". 2. Uninstall the service by double clicking `Uninstall.bat`. +## Troubleshooting +1. Try installing `scripts/windowsdesktop-runtime-8.0.12-win-$($arch).exe`. +2. Try installing `scripts/WindowsAppRuntimeInstall-$($arch).exe`. +3. Ensure `C:\wintun.dll` exists. + ## Notes - During install and uninstall a User Account Control popup will appear asking for admin permissions. This is normal. @@ -138,3 +171,8 @@ $readmeContent = @" by double clicking `StartTrayApp.bat`. "@ Set-Content -Path $readme -Value $readmeContent + +# Zip everything in the publish directory and drop it into publish. +$zipFile = Join-Path $PSScriptRoot "publish\CoderDesktop-preview-$($arch).zip" +Remove-Item -Path $zipFile -ErrorAction SilentlyContinue +Compress-Archive -Path "$($publishDir)\*" -DestinationPath $zipFile diff --git a/Vpn.Service/Manager.cs b/Vpn.Service/Manager.cs index 6ed7b82..f2e6ddd 100644 --- a/Vpn.Service/Manager.cs +++ b/Vpn.Service/Manager.cs @@ -16,6 +16,12 @@ public enum TunnelStatus Stopped, } +public class ServerVersion +{ + public required string String { get; set; } + public required SemVersion SemVersion { get; set; } +} + public interface IManager : IDisposable { public Task StopAsync(CancellationToken ct = default); @@ -40,7 +46,7 @@ public class Manager : IManager // TunnelSupervisor already has protections against concurrent operations, // but all the other stuff before starting the tunnel does not. private readonly RaiiSemaphoreSlim _tunnelOperationLock = new(1, 1); - private SemVersion? _lastServerVersion; + private ServerVersion? _lastServerVersion; private StartRequest? _lastStartRequest; private readonly RaiiSemaphoreSlim _statusLock = new(1, 1); @@ -132,11 +138,9 @@ private async ValueTask HandleClientMessageStart(ClientMessage me { try { - var serverVersion = - await CheckServerVersionAndCredentials(message.Start.CoderUrl, message.Start.ApiToken, - ct); + var serverVersion = await CheckServerVersionAndCredentials(message.Start.CoderUrl, message.Start.ApiToken, ct); if (_status == TunnelStatus.Started && _lastStartRequest != null && - _lastStartRequest.Equals(message.Start) && _lastServerVersion == serverVersion) + _lastStartRequest.Equals(message.Start) && _lastServerVersion?.String == serverVersion.String) { // The client is requesting to start an identical tunnel while // we're already running it. @@ -156,7 +160,7 @@ await CheckServerVersionAndCredentials(message.Start.CoderUrl, message.Start.Api // Stop the tunnel if it's running so we don't have to worry about // permissions issues when replacing the binary. await _tunnelSupervisor.StopAsync(ct); - await DownloadTunnelBinaryAsync(message.Start.CoderUrl, serverVersion, ct); + await DownloadTunnelBinaryAsync(message.Start.CoderUrl, serverVersion.SemVersion, ct); await _tunnelSupervisor.StartAsync(_config.TunnelBinaryPath, HandleTunnelRpcMessage, HandleTunnelRpcError, ct); @@ -361,7 +365,7 @@ private static string SystemArchitecture() /// Cancellation token /// The server version /// The server version is not within the required range - private async ValueTask CheckServerVersionAndCredentials(string baseUrl, string apiToken, + private async ValueTask CheckServerVersionAndCredentials(string baseUrl, string apiToken, CancellationToken ct = default) { var client = new CoderApiClient(baseUrl, apiToken); @@ -377,7 +381,11 @@ private async ValueTask CheckServerVersionAndCredentials(string base var user = await client.GetUser(User.Me, ct); _logger.LogInformation("Authenticated to server as '{Username}'", user.Username); - return serverVersion; + return new ServerVersion + { + String = buildInfo.Version, + SemVersion = serverVersion, + }; } /// diff --git a/Vpn.Service/ManagerConfig.cs b/Vpn.Service/ManagerConfig.cs index 906a0b8..3cbdb89 100644 --- a/Vpn.Service/ManagerConfig.cs +++ b/Vpn.Service/ManagerConfig.cs @@ -1,16 +1,16 @@ using System.ComponentModel.DataAnnotations; -using System.Diagnostics.CodeAnalysis; namespace Coder.Desktop.Vpn.Service; -[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")] public class ManagerConfig { [Required] [RegularExpression(@"^([a-zA-Z0-9_-]+\.)*[a-zA-Z0-9_-]+$")] public string ServiceRpcPipeName { get; set; } = "Coder.Desktop.Vpn"; - // TODO: pick a better default path [Required] public string TunnelBinaryPath { get; set; } = @"C:\coder-vpn.exe"; + + [Required] + public string LogFileLocation { get; set; } = @"C:\coder-desktop-service.log"; } diff --git a/Vpn.Service/Program.cs b/Vpn.Service/Program.cs index c2a1037..3d963a6 100644 --- a/Vpn.Service/Program.cs +++ b/Vpn.Service/Program.cs @@ -8,29 +8,27 @@ namespace Coder.Desktop.Vpn.Service; public static class Program { -#if DEBUG - private const string ServiceName = "Coder Desktop (Debug)"; +#if !DEBUG + private const string ServiceName = "Coder Desktop"; #else - const string ServiceName = "Coder Desktop"; + private const string ServiceName = "Coder Desktop (Debug)"; #endif + private const string ConsoleOutputTemplate = + "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}"; + private const string FileOutputTemplate = + "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}"; + private static readonly ILogger MainLogger = Log.ForContext("SourceContext", "Coder.Desktop.Vpn.Service.Program"); + private static LoggerConfiguration LogConfig = new LoggerConfiguration() + .Enrich.FromLogContext() + .MinimumLevel.Debug() + .WriteTo.Console(outputTemplate: ConsoleOutputTemplate); + public static async Task Main(string[] args) { - // Configure Serilog. - Log.Logger = new LoggerConfiguration() - .Enrich.FromLogContext() - // TODO: configurable level - .MinimumLevel.Debug() - .WriteTo.Console( - outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}") - // TODO: better location - .WriteTo.File(@"C:\CoderDesktop.log", - outputTemplate: - "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}") - .CreateLogger(); - + Log.Logger = LogConfig.CreateLogger(); try { await BuildAndRun(args); @@ -61,7 +59,13 @@ private static async Task BuildAndRun(string[] args) // Options types (these get registered as IOptions singletons) builder.Services.AddOptions() .Bind(builder.Configuration.GetSection("Manager")) - .ValidateDataAnnotations(); + .ValidateDataAnnotations() + .PostConfigure(config => + { + LogConfig = LogConfig + .WriteTo.File(config.LogFileLocation, outputTemplate: FileOutputTemplate); + Log.Logger = LogConfig.CreateLogger(); + }); // Logging builder.Services.AddSerilog(); From 4819b794c7ece66cea08cec864700be3d6d15535 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Thu, 13 Feb 2025 00:01:23 +1100 Subject: [PATCH 2/4] fixup! chore: various finish line tasks --- .github/workflows/release.yaml | 61 ++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e69de29..b9810fd 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -0,0 +1,61 @@ +name: Release + +on: + push: + tags: + - '*' + +permissions: + contents: write + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Get version from tag + id: version + shell: pwsh + run: | + $tag = $env:GITHUB_REF -replace 'refs/tags/','' + if ($tag -notmatch '^v\d+\.\d+\.\d+$') { + throw "Tag must be in format v1.2.3" + } + $version = $tag -replace '^v','' + $assemblyVersion = "$version.0" + echo "VERSION=$version" >> $env:GITHUB_OUTPUT + echo "ASSEMBLY_VERSION=$assemblyVersion" >> $env:GITHUB_OUTPUT + + - name: Build and publish x64 + run: | + dotnet publish src/App/App.csproj -c Release -r win-x64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/x64 + dotnet publish src/Vpn.Service/Vpn.Service.csproj -c Release -r win-x64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/x64 + + - name: Build and publish arm64 + run: | + dotnet publish src/App/App.csproj -c Release -r win-arm64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/arm64 + dotnet publish src/Vpn.Service/Vpn.Service.csproj -c Release -r win-arm64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/arm64 + + - name: Create ZIP archives + shell: pwsh + run: | + Compress-Archive -Path "publish/x64/*" -DestinationPath "./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-x64.zip" + Compress-Archive -Path "publish/arm64/*" -DestinationPath "./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-arm64.zip" + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: | + ./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-x64.zip + ./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-arm64.zip + name: Release ${{ steps.version.outputs.VERSION }} + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 6af0b7b7366913ed0f4d3c6030bc364ab2a0e088 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Mon, 17 Feb 2025 06:44:17 +1100 Subject: [PATCH 3/4] PR comments --- App/ViewModels/TrayWindowViewModel.cs | 41 ++++++------------------- App/Views/Pages/TrayWindowMainPage.xaml | 2 +- App/packages.lock.json | 6 ---- CoderSdk/CoderApiClient.cs | 15 +++++++-- CoderSdk/Deployment.cs | 2 +- CoderSdk/Users.cs | 2 +- 6 files changed, 25 insertions(+), 43 deletions(-) diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index 7044770..6133dbc 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -21,14 +21,15 @@ public partial class TrayWindowViewModel : ObservableObject private readonly IRpcController _rpcController; private readonly ICredentialManager _credentialManager; - private ToggleSwitch? _vpnActiveSwitch; - private bool _isProgrammaticStateChange; - private DispatcherQueue? _dispatcherQueue; [ObservableProperty] public partial VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Unknown; + // This is a separate property because we need the switch to be 2-way. + [ObservableProperty] + public partial bool VpnSwitchActive { get; set; } = false; + [ObservableProperty] public partial string? VpnFailedMessage { get; set; } = null; @@ -83,13 +84,13 @@ private void UpdateFromRpcModel(RpcModel rpcModel) if (rpcModel.RpcLifecycle is RpcLifecycle.Disconnected) { VpnLifecycle = VpnLifecycle.Unknown; - SetVpnSwitch(false); + VpnSwitchActive = false; Agents = []; return; } VpnLifecycle = rpcModel.VpnLifecycle; - SetVpnSwitch(rpcModel.VpnLifecycle is VpnLifecycle.Starting or VpnLifecycle.Started); + VpnSwitchActive = rpcModel.VpnLifecycle is VpnLifecycle.Starting or VpnLifecycle.Started; // Get the current dashboard URL. var credentialModel = _credentialManager.GetCredentials(); @@ -188,42 +189,20 @@ private void UpdateFromCredentialsModel(CredentialModel credentialModel) DashboardUrl = credentialModel.CoderUrl ?? DefaultDashboardUrl; } - private void SetVpnSwitch(bool value) - { - if (_vpnActiveSwitch == null) return; - _isProgrammaticStateChange = true; - _vpnActiveSwitch.IsOn = value; - _isProgrammaticStateChange = false; - } - - // HACK: using a two-way bool to store the VPN active state results in - // erroneous events being sent (even outside our change handlers). This - // sucks and breaks the ViewModel separation but is necessary for the - // switch to function correctly. - public void VpnSwitch_Loaded(object sender, RoutedEventArgs e) - { - if (sender is not ToggleSwitch toggleSwitch) return; - _vpnActiveSwitch = toggleSwitch; - SetVpnSwitch(VpnLifecycle is VpnLifecycle.Starting or VpnLifecycle.Started); - } - public void VpnSwitch_Toggled(object sender, RoutedEventArgs e) { if (sender is not ToggleSwitch toggleSwitch) return; - // HACK: the toggled event gets fired even when the switch state is - // changed from code, so we ignore all events while we're performing - // changes. - if (_isProgrammaticStateChange) return; - VpnFailedMessage = ""; try { // The start/stop methods will call back to update the state. - if (toggleSwitch.IsOn) + if (toggleSwitch.IsOn && VpnLifecycle is VpnLifecycle.Stopped) _rpcController.StartVpn(); - else + else if (!toggleSwitch.IsOn && VpnLifecycle is VpnLifecycle.Started) _rpcController.StopVpn(); + else + toggleSwitch.IsOn = VpnLifecycle is VpnLifecycle.Starting or VpnLifecycle.Started; } catch { diff --git a/App/Views/Pages/TrayWindowMainPage.xaml b/App/Views/Pages/TrayWindowMainPage.xaml index d6e419e..bd05aaf 100644 --- a/App/Views/Pages/TrayWindowMainPage.xaml +++ b/App/Views/Pages/TrayWindowMainPage.xaml @@ -57,8 +57,8 @@ Grid.Column="2" OnContent="" OffContent="" + IsOn="{x:Bind ViewModel.VpnSwitchActive, Mode=TwoWay}" IsEnabled="{x:Bind ViewModel.VpnLifecycle, Converter={StaticResource NotConnectingBoolConverter}, Mode=OneWay}" - Loaded="{x:Bind ViewModel.VpnSwitch_Loaded}" Toggled="{x:Bind ViewModel.VpnSwitch_Toggled}" Margin="0,0,-110,0" HorizontalAlignment="Right" /> diff --git a/App/packages.lock.json b/App/packages.lock.json index 78dfa1b..ca5e679 100644 --- a/App/packages.lock.json +++ b/App/packages.lock.json @@ -35,12 +35,6 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1" } }, - "Microsoft.NET.ILLink.Tasks": { - "type": "Direct", - "requested": "[8.0.12, )", - "resolved": "8.0.12", - "contentHash": "FV4HnQ3JI15PHnJ5PGTbz+rYvrih42oLi/7UMIshNwCwUZhTq13UzrggtXk4ygrcMcN+4jsS6hhshx2p/Zd0ig==" - }, "Microsoft.WindowsAppSDK": { "type": "Direct", "requested": "[1.6.250108002, )", diff --git a/CoderSdk/CoderApiClient.cs b/CoderSdk/CoderApiClient.cs index 34863f1..8624e9d 100644 --- a/CoderSdk/CoderApiClient.cs +++ b/CoderSdk/CoderApiClient.cs @@ -17,6 +17,12 @@ public override string ConvertName(string name) } } +[JsonSerializable(typeof(BuildInfo))] +[JsonSerializable(typeof(User))] +public partial class CoderSdkJsonContext : JsonSerializerContext +{ +} + /// /// Provides a limited selection of API methods for a Coder instance. /// @@ -37,6 +43,7 @@ public CoderApiClient(Uri baseUrl) _httpClient.BaseAddress = baseUrl; _jsonOptions = new JsonSerializerOptions { + TypeInfoResolver = CoderSdkJsonContext.Default, PropertyNameCaseInsensitive = true, PropertyNamingPolicy = new SnakeCaseNamingPolicy(), DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, @@ -53,9 +60,11 @@ public void SetSessionToken(string token) _httpClient.DefaultRequestHeaders.Remove("Coder-Session-Token"); _httpClient.DefaultRequestHeaders.Add("Coder-Session-Token", token); } + private async Task SendRequestNoBodyAsync(HttpMethod method, string path, CancellationToken ct = default) => + await SendRequestAsync(method, path, null, ct); - private async Task SendRequestAsync(HttpMethod method, string path, - object? payload, CancellationToken ct = default) + private async Task SendRequestAsync(HttpMethod method, string path, + TRequest? payload, CancellationToken ct = default) { try { @@ -63,7 +72,7 @@ private async Task SendRequestAsync(HttpMethod method, str if (payload is not null) { - var json = JsonSerializer.Serialize(payload, _jsonOptions); + var json = JsonSerializer.Serialize(payload, typeof(TRequest), _jsonOptions); request.Content = new StringContent(json, Encoding.UTF8, "application/json"); } diff --git a/CoderSdk/Deployment.cs b/CoderSdk/Deployment.cs index b00d49f..d85a458 100644 --- a/CoderSdk/Deployment.cs +++ b/CoderSdk/Deployment.cs @@ -17,6 +17,6 @@ public partial class CoderApiClient { public Task GetBuildInfo(CancellationToken ct = default) { - return SendRequestAsync(HttpMethod.Get, "/api/v2/buildinfo", null, ct); + return SendRequestNoBodyAsync(HttpMethod.Get, "/api/v2/buildinfo", ct); } } diff --git a/CoderSdk/Users.cs b/CoderSdk/Users.cs index 58ff474..2d99e02 100644 --- a/CoderSdk/Users.cs +++ b/CoderSdk/Users.cs @@ -12,6 +12,6 @@ public partial class CoderApiClient { public Task GetUser(string user, CancellationToken ct = default) { - return SendRequestAsync(HttpMethod.Get, $"/api/v2/users/{user}", null, ct); + return SendRequestNoBodyAsync(HttpMethod.Get, $"/api/v2/users/{user}", ct); } } From 9bda625932c3123d47e6ffbbaa0d31bf2f20a056 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Mon, 17 Feb 2025 06:45:18 +1100 Subject: [PATCH 4/4] fixup! PR comments --- App/ViewModels/TrayWindowViewModel.cs | 3 ++- App/Views/Pages/TrayWindowMainPage.xaml | 3 ++- App/Views/SignInWindow.xaml.cs | 1 - CoderSdk/CoderApiClient.cs | 8 ++++++-- Vpn.Service/Manager.cs | 3 ++- Vpn.Service/Program.cs | 1 + 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index 6133dbc..c643d2f 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -144,7 +144,8 @@ private void UpdateFromRpcModel(RpcModel rpcModel) // For every stopped workspace that doesn't have any agents, add a // dummy agent row. - foreach (var workspace in rpcModel.Workspaces.Where(w => w.Status == Workspace.Types.Status.Stopped && !workspacesWithAgents.Contains(w.Id))) + foreach (var workspace in rpcModel.Workspaces.Where(w => + w.Status == Workspace.Types.Status.Stopped && !workspacesWithAgents.Contains(w.Id))) agents.Add(new AgentViewModel { // We just assume that it's a single-agent workspace. diff --git a/App/Views/Pages/TrayWindowMainPage.xaml b/App/Views/Pages/TrayWindowMainPage.xaml index bd05aaf..66ec273 100644 --- a/App/Views/Pages/TrayWindowMainPage.xaml +++ b/App/Views/Pages/TrayWindowMainPage.xaml @@ -15,7 +15,8 @@ - + diff --git a/App/Views/SignInWindow.xaml.cs b/App/Views/SignInWindow.xaml.cs index 2d866e7..771dda0 100644 --- a/App/Views/SignInWindow.xaml.cs +++ b/App/Views/SignInWindow.xaml.cs @@ -3,7 +3,6 @@ using Coder.Desktop.App.Views.Pages; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; -using WinRT.Interop; namespace Coder.Desktop.App.Views; diff --git a/CoderSdk/CoderApiClient.cs b/CoderSdk/CoderApiClient.cs index 8624e9d..016998d 100644 --- a/CoderSdk/CoderApiClient.cs +++ b/CoderSdk/CoderApiClient.cs @@ -60,8 +60,12 @@ public void SetSessionToken(string token) _httpClient.DefaultRequestHeaders.Remove("Coder-Session-Token"); _httpClient.DefaultRequestHeaders.Add("Coder-Session-Token", token); } - private async Task SendRequestNoBodyAsync(HttpMethod method, string path, CancellationToken ct = default) => - await SendRequestAsync(method, path, null, ct); + + private async Task SendRequestNoBodyAsync(HttpMethod method, string path, + CancellationToken ct = default) + { + return await SendRequestAsync(method, path, null, ct); + } private async Task SendRequestAsync(HttpMethod method, string path, TRequest? payload, CancellationToken ct = default) diff --git a/Vpn.Service/Manager.cs b/Vpn.Service/Manager.cs index f2e6ddd..93c08dd 100644 --- a/Vpn.Service/Manager.cs +++ b/Vpn.Service/Manager.cs @@ -138,7 +138,8 @@ private async ValueTask HandleClientMessageStart(ClientMessage me { try { - var serverVersion = await CheckServerVersionAndCredentials(message.Start.CoderUrl, message.Start.ApiToken, ct); + var serverVersion = + await CheckServerVersionAndCredentials(message.Start.CoderUrl, message.Start.ApiToken, ct); if (_status == TunnelStatus.Started && _lastStartRequest != null && _lastStartRequest.Equals(message.Start) && _lastServerVersion?.String == serverVersion.String) { diff --git a/Vpn.Service/Program.cs b/Vpn.Service/Program.cs index 3d963a6..e5447bc 100644 --- a/Vpn.Service/Program.cs +++ b/Vpn.Service/Program.cs @@ -16,6 +16,7 @@ public static class Program private const string ConsoleOutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}"; + private const string FileOutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}"; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy