Skip to content

feat: add logging to App #78

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 30, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions App/App.csproj
Original file line number Diff line number Diff line change
@@ -61,10 +61,13 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.4" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.250108002" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="WinUIEx" Version="2.5.1" />
</ItemGroup>

94 changes: 75 additions & 19 deletions App/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -16,6 +16,9 @@
using Microsoft.Win32;
using Microsoft.Windows.AppLifecycle;
using Windows.ApplicationModel.Activation;
using Microsoft.Extensions.Logging;
using Serilog;
using System.Collections.Generic;

namespace Coder.Desktop.App;

@@ -24,22 +27,39 @@ public partial class App : Application
private readonly IServiceProvider _services;

private bool _handleWindowClosed = true;
private const string MutagenControllerConfigSection = "MutagenController";

#if !DEBUG
private const string MutagenControllerConfigSection = "AppMutagenController";
private const string ConfigSubKey = @"SOFTWARE\Coder Desktop\App";
private const string logFilename = "app.log";
#else
private const string MutagenControllerConfigSection = "DebugAppMutagenController";
private const string ConfigSubKey = @"SOFTWARE\Coder Desktop\DebugApp";
private const string logFilename = "debug-app.log";
#endif

private readonly ILogger<App> _logger;

public App()
{
var builder = Host.CreateApplicationBuilder();
var configBuilder = builder.Configuration as IConfigurationBuilder;

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

var services = builder.Services;

// Logging
builder.Services.AddSerilog((_, loggerConfig) =>
{
loggerConfig.ReadFrom.Configuration(builder.Configuration);
});

services.AddSingleton<ICredentialManager, CredentialManager>();
services.AddSingleton<IRpcController, RpcController>();

@@ -69,12 +89,14 @@ public App()
services.AddTransient<TrayWindow>();

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

InitializeComponent();
}

public async Task ExitApplication()
{
_logger.LogDebug("exiting app");
_handleWindowClosed = false;
Exit();
var syncController = _services.GetRequiredService<ISyncSessionController>();
@@ -87,36 +109,39 @@ public async Task ExitApplication()

protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
_logger.LogInformation("new instance launched");
// Start connecting to the manager in the background.
var rpcController = _services.GetRequiredService<IRpcController>();
if (rpcController.GetState().RpcLifecycle == RpcLifecycle.Disconnected)
// Passing in a CT with no cancellation is desired here, because
// the named pipe open will block until the pipe comes up.
// TODO: log
_ = rpcController.Reconnect(CancellationToken.None).ContinueWith(t =>
_logger.LogDebug("reconnecting with VPN service");
_ = rpcController.Reconnect(CancellationToken.None).ContinueWith(t =>
{
if (t.Exception != null)
{
_logger.LogError(t.Exception, "failed to connect to VPN service");
#if DEBUG
if (t.Exception != null)
{
Debug.WriteLine(t.Exception);
Debugger.Break();
}
Debug.WriteLine(t.Exception);
Debugger.Break();
#endif
});
}
});

// Load the credentials in the background.
var credentialManagerCts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var credentialManager = _services.GetRequiredService<ICredentialManager>();
_ = credentialManager.LoadCredentials(credentialManagerCts.Token).ContinueWith(t =>
{
// TODO: log
#if DEBUG
if (t.Exception != null)
{
_logger.LogError(t.Exception, "failed to load credentials");
#if DEBUG
Debug.WriteLine(t.Exception);
Debugger.Break();
}
#endif
}

credentialManagerCts.Dispose();
}, CancellationToken.None);

@@ -125,10 +150,14 @@ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs ar
var syncSessionController = _services.GetRequiredService<ISyncSessionController>();
_ = syncSessionController.RefreshState(syncSessionCts.Token).ContinueWith(t =>
{
// TODO: log
if (t.IsCanceled || t.Exception != null)
{
_logger.LogError(t.Exception, "failed to refresh sync state (canceled = {canceled})", t.IsCanceled);
#if DEBUG
if (t.IsCanceled || t.Exception != null) Debugger.Break();
Debugger.Break();
#endif
}

syncSessionCts.Dispose();
}, CancellationToken.None);

@@ -148,17 +177,44 @@ public void OnActivated(object? sender, AppActivationArguments args)
{
case ExtendedActivationKind.Protocol:
var protoArgs = args.Data as IProtocolActivatedEventArgs;
if (protoArgs == null)
{
_logger.LogWarning("URI activation with null data");
return;
}

HandleURIActivation(protoArgs.Uri);
break;

default:
// TODO: log
_logger.LogWarning("activation for {kind}, which is unhandled", args.Kind);
break;
}
}

public void HandleURIActivation(Uri uri)
{
// TODO: handle
// don't log the query string as that's where we include some sensitive information like passwords
_logger.LogInformation("handling URI activation for {path}", uri.AbsolutePath);
}

private static void AddDefaultConfig(IConfigurationBuilder builder)
{
var logPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"CoderDesktop",
logFilename);
builder.AddInMemoryCollection(new Dictionary<string, string?>
{
[MutagenControllerConfigSection + ":MutagenExecutablePath"] = @"C:\mutagen.exe",
["Serilog:Using:0"] = "Serilog.Sinks.File",
["Serilog:MinimumLevel"] = "Information",
["Serilog:Enrich:0"] = "FromLogContext",
["Serilog:WriteTo:0:Name"] = "File",
["Serilog:WriteTo:0:Args:path"] = logPath,
["Serilog:WriteTo:0:Args:outputTemplate"] =
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}",
["Serilog:WriteTo:0:Args:rollingInterval"] = "Day",
});
}
}
420 changes: 238 additions & 182 deletions App/packages.lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Installer/Installer.csproj
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
<PackageReference Include="WixSharp_wix4" Version="2.6.0" />
<PackageReference Include="WixSharp_wix4.bin" Version="2.6.0" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
26 changes: 17 additions & 9 deletions Installer/Program.cs
Original file line number Diff line number Diff line change
@@ -2,7 +2,9 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using CommandLine;
using Microsoft.Extensions.Configuration;
using WixSharp;
using WixSharp.Bootstrapper;
using WixSharp.CommonTasks;
@@ -128,7 +130,8 @@ public class BootstrapperOptions : SharedOptions
if (!SystemFile.Exists(MsiPath))
throw new ArgumentException($"MSI package not found at '{MsiPath}'", nameof(MsiPath));
if (!SystemFile.Exists(WindowsAppSdkPath))
throw new ArgumentException($"Windows App Sdk package not found at '{WindowsAppSdkPath}'", nameof(WindowsAppSdkPath));
throw new ArgumentException($"Windows App Sdk package not found at '{WindowsAppSdkPath}'",
nameof(WindowsAppSdkPath));
}
}

@@ -138,6 +141,8 @@ public class Program
private const string Manufacturer = "Coder Technologies Inc.";
private const string HelpUrl = "https://coder.com/docs";
private const string RegistryKey = @"SOFTWARE\Coder Desktop";
private const string AppConfigRegistryKey = RegistryKey + @"\App";
private const string VpnServiceConfigRegistryKey = RegistryKey + @"\VpnService";

private const string DotNetCheckName = "DOTNET_RUNTIME_CHECK";
private const RollForward DotNetCheckRollForward = RollForward.minor;
@@ -258,18 +263,21 @@ private static int BuildMsiPackage(MsiOptions opts)

project.AddRegValues(
// Add registry values that are consumed by the manager. Note that these
// should not be changed. See Vpn.Service/Program.cs and
// should not be changed. See Vpn.Service/Program.cs (AddDefaultConfig) and
// Vpn.Service/ManagerConfig.cs for more details.
new RegValue(RegistryHive, RegistryKey, "Manager:ServiceRpcPipeName", "Coder.Desktop.Vpn"),
new RegValue(RegistryHive, RegistryKey, "Manager:TunnelBinaryPath",
new RegValue(RegistryHive, VpnServiceConfigRegistryKey, "Manager:ServiceRpcPipeName", "Coder.Desktop.Vpn"),
new RegValue(RegistryHive, VpnServiceConfigRegistryKey, "Manager:TunnelBinaryPath",
$"[INSTALLFOLDER]{opts.VpnDir}\\coder-vpn.exe"),
new RegValue(RegistryHive, RegistryKey, "Manager:LogFileLocation",
new RegValue(RegistryHive, VpnServiceConfigRegistryKey, "Manager:TunnelBinarySignatureSigner",
"Coder Technologies Inc."),
new RegValue(RegistryHive, VpnServiceConfigRegistryKey, "Manager:TunnelBinaryAllowVersionMismatch",
"false"),
new RegValue(RegistryHive, VpnServiceConfigRegistryKey, "Serilog:WriteTo:0:Args:path",
@"[INSTALLFOLDER]coder-desktop-service.log"),
new RegValue(RegistryHive, RegistryKey, "Manager:TunnelBinarySignatureSigner", "Coder Technologies Inc."),
new RegValue(RegistryHive, RegistryKey, "Manager:TunnelBinaryAllowVersionMismatch", "false"),

// Add registry values that are consumed by the App MutagenController. See App/Services/MutagenController.cs
new RegValue(RegistryHive, RegistryKey, "AppMutagenController:MutagenExecutablePath",
@"[INSTALLFOLDER]mutagen.exe")
new RegValue(RegistryHive, AppConfigRegistryKey, "MutagenController:MutagenExecutablePath",
@"[INSTALLFOLDER]vpn\mutagen.exe")
);

// Note: most of this control panel info will not be visible as this
384 changes: 202 additions & 182 deletions Tests.Vpn.Service/packages.lock.json

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions Vpn.Service/ManagerConfig.cs
Original file line number Diff line number Diff line change
@@ -15,8 +15,6 @@ public class ManagerConfig

[Required] public string TunnelBinaryPath { get; set; } = @"C:\coder-vpn.exe";

[Required] public string LogFileLocation { get; set; } = @"C:\coder-desktop-service.log";

// If empty, signatures will not be verified.
[Required] public string TunnelBinarySignatureSigner { get; set; } = "Coder Technologies Inc.";

67 changes: 43 additions & 24 deletions Vpn.Service/Program.cs
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Win32;
using Serilog;
using ILogger = Serilog.ILogger;

namespace Coder.Desktop.Vpn.Service;

@@ -14,29 +15,22 @@ public static class Program
// installer.
#if !DEBUG
private const string ServiceName = "Coder Desktop";
private const string ManagerConfigSection = "Manager";
private const string ConfigSubKey = @"SOFTWARE\Coder Desktop\VpnService";
#else
// This value matches Create-Service.ps1.
private const string ServiceName = "Coder Desktop (Debug)";
private const string ManagerConfigSection = "DebugManager";
private const string ConfigSubKey = @"SOFTWARE\Coder Desktop\DebugVpnService";
#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 const string ManagerConfigSection = "Manager";

private static ILogger MainLogger => Log.ForContext("SourceContext", "Coder.Desktop.Vpn.Service.Program");

private static LoggerConfiguration BaseLogConfig => new LoggerConfiguration()
.Enrich.FromLogContext()
.MinimumLevel.Debug()
.WriteTo.Console(outputTemplate: ConsoleOutputTemplate);

public static async Task<int> Main(string[] args)
{
Log.Logger = BaseLogConfig.CreateLogger();
// This logger will only be used until we load our full logging configuration and replace it.
Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console()
.CreateLogger();
MainLogger.Information("Application is starting");
try
{
@@ -58,27 +52,26 @@ public static async Task<int> Main(string[] args)
private static async Task BuildAndRun(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
var configBuilder = builder.Configuration as IConfigurationBuilder;

// Configuration sources
builder.Configuration.Sources.Clear();
(builder.Configuration as IConfigurationBuilder).Add(
new RegistryConfigurationSource(Registry.LocalMachine, @"SOFTWARE\Coder Desktop"));
AddDefaultConfig(configBuilder);
configBuilder.Add(
new RegistryConfigurationSource(Registry.LocalMachine, ConfigSubKey));
builder.Configuration.AddEnvironmentVariables("CODER_MANAGER_");
builder.Configuration.AddCommandLine(args);

// Options types (these get registered as IOptions<T> singletons)
builder.Services.AddOptions<ManagerConfig>()
.Bind(builder.Configuration.GetSection(ManagerConfigSection))
.ValidateDataAnnotations()
.PostConfigure(config =>
{
Log.Logger = BaseLogConfig
.WriteTo.File(config.LogFileLocation, outputTemplate: FileOutputTemplate)
.CreateLogger();
});
.ValidateDataAnnotations();

// Logging
builder.Services.AddSerilog();
builder.Services.AddSerilog((_, loggerConfig) =>
{
loggerConfig.ReadFrom.Configuration(builder.Configuration);
});

// Singletons
builder.Services.AddSingleton<IDownloader, Downloader>();
@@ -101,6 +94,32 @@ private static async Task BuildAndRun(string[] args)
builder.Services.AddHostedService<ManagerService>();
builder.Services.AddHostedService<ManagerRpcService>();

await builder.Build().RunAsync();
var host = builder.Build();
Log.Logger = (ILogger)host.Services.GetService(typeof(ILogger))!;
MainLogger.Information("Application is starting");

await host.RunAsync();
}

private static void AddDefaultConfig(IConfigurationBuilder builder)
{
builder.AddInMemoryCollection(new Dictionary<string, string?>
{
["Serilog:Using:0"] = "Serilog.Sinks.File",
["Serilog:Using:1"] = "Serilog.Sinks.Console",

["Serilog:MinimumLevel"] = "Information",
["Serilog:Enrich:0"] = "FromLogContext",

["Serilog:WriteTo:0:Name"] = "File",
["Serilog:WriteTo:0:Args:path"] = @"C:\coder-desktop-service.log",
["Serilog:WriteTo:0:Args:outputTemplate"] =
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}",
["Serilog:WriteTo:0:Args:rollingInterval"] = "Day",

["Serilog:WriteTo:1:Name"] = "Console",
["Serilog:WriteTo:1:Args:outputTemplate"] =
"[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}",
});
}
}
7 changes: 4 additions & 3 deletions Vpn.Service/Vpn.Service.csproj
Original file line number Diff line number Diff line change
@@ -25,12 +25,13 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.4" />
<PackageReference Include="Microsoft.Security.Extensions" Version="1.3.0" />
<PackageReference Include="Semver" Version="3.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
</ItemGroup>
384 changes: 202 additions & 182 deletions Vpn.Service/packages.lock.json

Large diffs are not rendered by default.


Unchanged files with check annotations Beta

var converter = new FriendlyByteConverter();
foreach (var (input, expected) in cases)
{
var actual = converter.Convert(input, typeof(string), null, null);

Check warning on line 32 in Tests.App/Converters/FriendlyByteConverterTest.cs

GitHub Actions / test

Cannot convert null literal to non-nullable reference type.

Check warning on line 32 in Tests.App/Converters/FriendlyByteConverterTest.cs

GitHub Actions / test

Cannot convert null literal to non-nullable reference type.
Assert.That(actual, Is.EqualTo(expected), $"case ({input.GetType()}){input}");
}
}