yavsc/src/PostIt/PostIt/App.axaml.cs

86 lines
2.7 KiB
C#
Raw Normal View History

postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
using System;
2026-05-29 01:29:36 +01:00
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
using PostIt.Services;
2026-05-29 01:29:36 +01:00
using PostIt.ViewModels;
using PostIt.Views;
namespace PostIt;
public partial class App : Application
{
2026-06-10 16:59:23 +01:00
public App()
{
}
postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
2026-05-29 01:29:36 +01:00
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
// Single-instance hand-off: if we were launched with a
// custom-scheme URL on the command line, we are a 2nd
// instance whose job is to forward the OAuth2 callback
// URL to the running PostIt process and exit. The first
// instance is parked inside CustomSchemeBrowser.InvokeAsync
// waiting on the named pipe for exactly this message.
if (TryHandOffCustomSchemeUrl())
{
return;
}
2026-05-29 01:29:36 +01:00
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
2026-06-11 00:14:51 +01:00
DataContext = new MainPageViewModel()
2026-05-29 01:29:36 +01:00
};
}
else if (ApplicationLifetime is IActivityApplicationLifetime singleViewFactoryApplicationLifetime)
{
2026-06-11 00:14:51 +01:00
singleViewFactoryApplicationLifetime.MainViewFactory = () => new MainPage { DataContext = new MainPageViewModel() };
2026-05-29 01:29:36 +01:00
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
2026-06-11 00:14:51 +01:00
singleViewPlatform.MainView = new MainPage
2026-05-29 01:29:36 +01:00
{
2026-06-11 00:14:51 +01:00
DataContext = new MainPageViewModel()
2026-05-29 01:29:36 +01:00
};
}
base.OnFrameworkInitializationCompleted();
}
postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
private bool TryHandOffCustomSchemeUrl()
{
var args = Environment.GetCommandLineArgs();
var scheme = Platform.CustomScheme;
foreach (var arg in args)
{
if (arg.StartsWith(scheme + "://", StringComparison.OrdinalIgnoreCase))
{
// Best-effort: try to send the URL to the running
// instance via the named pipe. If the pipe isn't
// answering, just exit — there's no 1st instance
// to forward to (e.g. user double-clicked the link
// after closing PostIt). Falling through with a
// normal startup would be confusing.
SingleInstance.TryHandOffAsync(arg).GetAwaiter().GetResult();
if (ApplicationLifetime is IControlledApplicationLifetime lifetime)
{
lifetime.Shutdown(0);
}
else
{
Environment.Exit(0);
}
return true;
}
}
return false;
}
2026-06-10 16:59:23 +01:00
}