From 2a8cbd7079d4abd5c173a518e711fbc8090693f8 Mon Sep 17 00:00:00 2001 From: Luca Date: Mon, 6 May 2024 09:27:16 +0200 Subject: [PATCH] Headless mode --- ShockOsc/Cli/CliOptions.cs | 9 ++ ShockOsc/HeadlessProgram.cs | 24 +++ ShockOsc/MauiProgram.cs | 145 ++---------------- .../Linux/{LinuxApp.cs => LinuxEntryPoint.cs} | 4 +- ShockOsc/Platforms/Windows/App.xaml.cs | 5 +- .../Platforms/Windows/WindowsEntryPoint.cs | 56 +++++++ ShockOsc/Platforms/Windows/WindowsServices.cs | 13 ++ .../Platforms/Windows/WindowsTrayService.cs | 6 +- ShockOsc/Properties/launchSettings.json | 7 +- ShockOsc/Services/AuthService.cs | 36 +++++ ShockOsc/Services/BackendControlService.cs | 14 -- ShockOsc/ShockOsc.csproj | 8 +- ShockOsc/ShockOscBootstrap.cs | 114 ++++++++++++++ .../Pages/Authentication/Authenticate.razor | 19 +-- 14 files changed, 289 insertions(+), 171 deletions(-) create mode 100644 ShockOsc/Cli/CliOptions.cs create mode 100644 ShockOsc/HeadlessProgram.cs rename ShockOsc/Platforms/Linux/{LinuxApp.cs => LinuxEntryPoint.cs} (58%) create mode 100644 ShockOsc/Platforms/Windows/WindowsEntryPoint.cs create mode 100644 ShockOsc/Platforms/Windows/WindowsServices.cs create mode 100644 ShockOsc/Services/AuthService.cs delete mode 100644 ShockOsc/Services/BackendControlService.cs create mode 100644 ShockOsc/ShockOscBootstrap.cs diff --git a/ShockOsc/Cli/CliOptions.cs b/ShockOsc/Cli/CliOptions.cs new file mode 100644 index 0000000..7829b28 --- /dev/null +++ b/ShockOsc/Cli/CliOptions.cs @@ -0,0 +1,9 @@ +using CommandLine; + +namespace OpenShock.ShockOsc.Cli; + +public sealed class CliOptions +{ + [Option('h', "headless", Required = false, Default = false, HelpText = "Run the application in headless mode.")] + public bool Headless { get; set; } +} \ No newline at end of file diff --git a/ShockOsc/HeadlessProgram.cs b/ShockOsc/HeadlessProgram.cs new file mode 100644 index 0000000..be5f6e5 --- /dev/null +++ b/ShockOsc/HeadlessProgram.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Hosting; + +namespace OpenShock.ShockOsc; + +public static class HeadlessProgram +{ + public static IHost SetupHeadlessHost() + { + var builder = Host.CreateDefaultBuilder(); + builder.ConfigureServices(services => + { + services.AddShockOscServices(); + +#if WINDOWS + services.AddWindowsServices(); +#endif + }); + + var app = builder.Build(); + app.Services.StartShockOscServices(true); + + return app; + } +} \ No newline at end of file diff --git a/ShockOsc/MauiProgram.cs b/ShockOsc/MauiProgram.cs index 1c4857b..7ab4e45 100644 --- a/ShockOsc/MauiProgram.cs +++ b/ShockOsc/MauiProgram.cs @@ -1,40 +1,13 @@ -#if WINDOWS -using System.Diagnostics; -using System.Net; +#if MAUI using Microsoft.Maui.LifecycleEvents; -using MudBlazor.Services; -using OpenShock.SDK.CSharp.Hub; -using OpenShock.ShockOsc.Backend; using OpenShock.ShockOsc.Config; -using OpenShock.ShockOsc.Logging; -using OpenShock.ShockOsc.OscQueryLibrary; -using OpenShock.ShockOsc.Services; -using OpenShock.ShockOsc.Utils; -using Serilog; using MauiApp = OpenShock.ShockOsc.Ui.MauiApp; -using Rect = OpenShock.ShockOsc.Utils.Rect; - - using Microsoft.UI; - -#if M - -#endif - namespace OpenShock.ShockOsc; public static class MauiProgram { - private const int WS_CAPTION = 0x00C00000; - private const int WS_BORDER = 0x00800000; - private const int WS_SYSMENU = 0x00080000; - private const int WS_SIZEBOX = 0x00040000; - private const int WS_MINIMIZEBOX = 0x00020000; - private const int WS_MAXIMIZEBOX = 0x00010000; - private const int WS_THICKFRAME = 0x00040000; - - private static ShockOscConfig? _config; public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() @@ -43,96 +16,21 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() // <---- Services ----> - var loggerConfiguration = new LoggerConfiguration() - .MinimumLevel.Information() - .Filter.ByExcluding(ev => - ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")).Filter - .ByExcluding(x => x.MessageTemplate.Text.StartsWith("Failed to find handler for")) - .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Information) - .WriteTo.UiLogSink() - .WriteTo.Console( - outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"); - - // ReSharper disable once RedundantAssignment - var isDebug = Environment.GetCommandLineArgs() - .Any(x => x.Equals("--debug", StringComparison.InvariantCultureIgnoreCase)); - -#if DEBUG - isDebug = true; -#endif - if (isDebug) - { - Console.WriteLine("Debug mode enabled"); - loggerConfiguration.MinimumLevel.Verbose(); - } - - Log.Logger = loggerConfiguration.CreateLogger(); - - builder.Services.AddSerilog(Log.Logger); - - builder.Services.AddMemoryCache(); - - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(provider => - { - var config = provider.GetRequiredService(); - var listenAddress = config.Config.Osc.QuestSupport ? IPAddress.Any : IPAddress.Loopback; - return new OscQueryServer("ShockOsc", listenAddress, config); - }); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - + builder.Services.AddShockOscServices(); + +#if WINDOWS + builder.Services.AddWindowsServices(); + builder.ConfigureLifecycleEvents(lifecycleBuilder => { lifecycleBuilder.AddWindows(windowsLifecycleBuilder => { windowsLifecycleBuilder.OnWindowCreated(window => { - //use Microsoft.UI.Windowing functions for window var handle = WinRT.Interop.WindowNative.GetWindowHandle(window); - var id = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(handle); + var id = Win32Interop.GetWindowIdFromWindow(handle); var appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(id); - // var style = WindowUtils.GetWindowLongPtrA(handle, (int)WindowLongFlags.GWL_STYLE); - // - // style &= ~WS_CAPTION; // Remove the title bar - // style |= WS_THICKFRAME; // Add thick frame for resizing - // - // WindowUtils.SetWindowLongPtrA(handle, (int)WindowLongFlags.GWL_STYLE, style); - // - // var reff = new Rect(); - // WindowUtils.AdjustWindowRectEx(ref reff, style, false, 0); - // reff.top = 6000; - // reff.left *= -1; - // - // var margins = new Margins - // { - // cxLeftWidth = 0, - // cxRightWidth = 0, - // cyTopHeight = 0, - // cyBottomHeight = 0 - // }; - // - // WindowUtils.DwmExtendFrameIntoClientArea(handle, ref margins); - // - // WindowUtils.SetWindowPos(handle, 0, 0, 0, 0, 0x0040 | 0x0002 | 0x0001 | 0x0020); - // - //When user execute the closing method, we can push a display alert. If user click Yes, close this application, if click the cancel, display alert will dismiss. appWindow.Closing += async (s, e) => { @@ -155,39 +53,20 @@ public static Microsoft.Maui.Hosting.MauiApp CreateMauiApp() }); }); }); - - - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - - builder.Services.AddMudServices(); - builder.Services.AddMauiBlazorWebView(); +#endif // <---- App ----> builder .UseMauiApp() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); - - -#if DEBUG - builder.Services.AddBlazorWebViewDeveloperTools(); -#endif - + var app = builder.Build(); - + _config = app.Services.GetRequiredService().Config; - app.Services.GetService()?.Initialize(); - - // <---- Warmup ----> - app.Services.GetRequiredService(); - app.Services.GetRequiredService().Start(); - - var updater = app.Services.GetRequiredService(); - OsTask.Run(updater.CheckUpdate); - + app.Services.StartShockOscServices(false); + return app; } } diff --git a/ShockOsc/Platforms/Linux/LinuxApp.cs b/ShockOsc/Platforms/Linux/LinuxEntryPoint.cs similarity index 58% rename from ShockOsc/Platforms/Linux/LinuxApp.cs rename to ShockOsc/Platforms/Linux/LinuxEntryPoint.cs index a609cec..27db742 100644 --- a/ShockOsc/Platforms/Linux/LinuxApp.cs +++ b/ShockOsc/Platforms/Linux/LinuxEntryPoint.cs @@ -1,6 +1,6 @@ -namespace OpenShock.ShockOsc.Linux; +namespace OpenShock.ShockOsc.Platforms.Linux; #if !WINDOWS -public static class LinuxApp +public static class LinuxEntryPoint { public static void Main(string[] args) { diff --git a/ShockOsc/Platforms/Windows/App.xaml.cs b/ShockOsc/Platforms/Windows/App.xaml.cs index d144b91..11f6bcc 100644 --- a/ShockOsc/Platforms/Windows/App.xaml.cs +++ b/ShockOsc/Platforms/Windows/App.xaml.cs @@ -1,8 +1,9 @@ - +#if WINDOWS // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. -#if WINDOWS + +using Microsoft.Windows.AppLifecycle; namespace OpenShock.ShockOsc.Platforms.Windows; /// diff --git a/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs b/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs new file mode 100644 index 0000000..eb7f8c8 --- /dev/null +++ b/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs @@ -0,0 +1,56 @@ +#if WINDOWS +using System.Runtime.InteropServices; +using CommandLine; +using Microsoft.Extensions.Hosting; +using Microsoft.UI.Dispatching; +using OpenShock.ShockOsc.Cli; +using OpenShock.ShockOsc.Services; +using OpenShock.ShockOsc.Utils; +using WinRT; +using Application = Microsoft.UI.Xaml.Application; + +namespace OpenShock.ShockOsc.Platforms.Windows; + +public static class WindowsEntryPoint +{ + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] + [DllImport("Microsoft.ui.xaml.dll")] + private static extern void XamlCheckProcessRequirements(); + + [STAThread] + private static void Main(string[] args) + { + var parsed = Parser.Default.ParseArguments(args); + parsed.WithParsed(Start); + parsed.WithNotParsed(errors => + { + errors.Output(); + Environment.Exit(1); + }); + } + + private static void Start(CliOptions config) + { + if (config.Headless) + { + Console.WriteLine("Running in headless mode."); + + var host = HeadlessProgram.SetupHeadlessHost(); + OsTask.Run(host.Services.GetRequiredService().Authenticate); + host.Run(); + + return; + } + + XamlCheckProcessRequirements(); + ComWrappersSupport.InitializeComWrappers(); + Application.Start(delegate + { + var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()); + SynchronizationContext.SetSynchronizationContext(context); + // ReSharper disable once ObjectCreationAsStatement + new App(); + }); + } +} +#endif \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/WindowsServices.cs b/ShockOsc/Platforms/Windows/WindowsServices.cs new file mode 100644 index 0000000..e1abe29 --- /dev/null +++ b/ShockOsc/Platforms/Windows/WindowsServices.cs @@ -0,0 +1,13 @@ +#if WINDOWS +using OpenShock.ShockOsc.Services; + +namespace OpenShock.ShockOsc; + +public static class WindowsServices +{ + public static void AddWindowsServices(this IServiceCollection services) + { + services.AddSingleton(); + } +} +#endif \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/WindowsTrayService.cs b/ShockOsc/Platforms/Windows/WindowsTrayService.cs index 861a924..142bd99 100644 --- a/ShockOsc/Platforms/Windows/WindowsTrayService.cs +++ b/ShockOsc/Platforms/Windows/WindowsTrayService.cs @@ -49,10 +49,14 @@ public void Initialize() menu.Items.Add(new ToolStripSeparator()); menu.Items.Add("Restart", null, Restart); menu.Items.Add("Quit ShockOSC", null, OnQuitClick); - + tray.ContextMenuStrip = menu; tray.Click += OnMainClick; + menu.Opened += async (sender, args) => + { + var aa = menu; + }; tray.Visible = true; } diff --git a/ShockOsc/Properties/launchSettings.json b/ShockOsc/Properties/launchSettings.json index de9182a..36a0c0e 100644 --- a/ShockOsc/Properties/launchSettings.json +++ b/ShockOsc/Properties/launchSettings.json @@ -1,8 +1,13 @@ { "profiles": { - "Windows Machine": { + "ShockOsc": { "commandName": "Project", "nativeDebugging": false + }, + "ShockOscHeadless": { + "commandName": "Project", + "nativeDebugging": false, + "commandLineArgs": "--headless" } } } diff --git a/ShockOsc/Services/AuthService.cs b/ShockOsc/Services/AuthService.cs new file mode 100644 index 0000000..c5a7ccf --- /dev/null +++ b/ShockOsc/Services/AuthService.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.Logging; +using OpenShock.SDK.CSharp.Hub; +using OpenShock.ShockOsc.Backend; + +namespace OpenShock.ShockOsc.Services; + +public sealed class AuthService +{ + private readonly ILogger _logger; + private readonly BackendHubManager _backendHubManager; + private readonly OpenShockHubClient _hubClient; + private readonly LiveControlManager _liveControlManager; + private readonly OpenShockApi _apiClient; + + public AuthService(ILogger logger, BackendHubManager backendHubManager, OpenShockHubClient hubClient, LiveControlManager liveControlManager, OpenShockApi apiClient) + { + _logger = logger; + _backendHubManager = backendHubManager; + _hubClient = hubClient; + _liveControlManager = liveControlManager; + _apiClient = apiClient; + } + + public async Task Authenticate() + { + _logger.LogInformation("Setting up live client"); + await _backendHubManager.SetupLiveClient(); + _logger.LogInformation("Starting live client"); + await _hubClient.StartAsync(); + + _logger.LogInformation("Refreshing shockers"); + await _apiClient.RefreshShockers(); + + await _liveControlManager.RefreshConnections(); + } +} \ No newline at end of file diff --git a/ShockOsc/Services/BackendControlService.cs b/ShockOsc/Services/BackendControlService.cs deleted file mode 100644 index b357be0..0000000 --- a/ShockOsc/Services/BackendControlService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using OpenShock.ShockOsc.Backend; -using OpenShock.ShockOsc.Ui.Components; - -namespace OpenShock.ShockOsc.Services; - -public sealed class BackendControlService -{ - private readonly BackendHubManager _backendHubManager; - - public BackendControlService(BackendHubManager backendHubManager) - { - _backendHubManager = backendHubManager; - } -} \ No newline at end of file diff --git a/ShockOsc/ShockOsc.csproj b/ShockOsc/ShockOsc.csproj index 4d533c8..b8ffa9e 100644 --- a/ShockOsc/ShockOsc.csproj +++ b/ShockOsc/ShockOsc.csproj @@ -25,6 +25,8 @@ en en-US;en false + + DISABLE_XAML_GENERATED_MAIN @@ -32,10 +34,12 @@ 10.0.17763.0 10.0.17763.0 None + MAUI + OpenShock.ShockOsc.Platforms.Windows.WindowsEntryPoint - OpenShock.ShockOsc.Linux.LinuxApp + OpenShock.ShockOsc.Platforms.Linux.LinuxEntryPoint @@ -61,8 +65,10 @@ + + diff --git a/ShockOsc/ShockOscBootstrap.cs b/ShockOsc/ShockOscBootstrap.cs new file mode 100644 index 0000000..9fa1634 --- /dev/null +++ b/ShockOsc/ShockOscBootstrap.cs @@ -0,0 +1,114 @@ +using System.Net; +using Microsoft.Extensions.DependencyInjection; +using MudBlazor.Services; +using OpenShock.SDK.CSharp.Hub; +using OpenShock.ShockOsc.Backend; +using OpenShock.ShockOsc.Config; +using OpenShock.ShockOsc.Logging; +using OpenShock.ShockOsc.OscQueryLibrary; +using OpenShock.ShockOsc.Services; +using OpenShock.ShockOsc.Utils; +using Serilog; + +namespace OpenShock.ShockOsc; + +public static class ShockOscBootstrap +{ + public static void AddShockOscServices(this IServiceCollection services) + { + var loggerConfiguration = new LoggerConfiguration() + .MinimumLevel.Information() + .Filter.ByExcluding(ev => + ev.Exception is InvalidDataException a && a.Message.StartsWith("Invocation provides")).Filter + .ByExcluding(x => x.MessageTemplate.Text.StartsWith("Failed to find handler for")) + .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Information) + .WriteTo.UiLogSink() + .WriteTo.Console( + outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"); + + // ReSharper disable once RedundantAssignment + var isDebug = Environment.GetCommandLineArgs() + .Any(x => x.Equals("--debug", StringComparison.InvariantCultureIgnoreCase)); + +#if DEBUG + isDebug = true; +#endif + if (isDebug) + { + Console.WriteLine("Debug mode enabled"); + loggerConfiguration.MinimumLevel.Verbose(); + } + + Log.Logger = loggerConfiguration.CreateLogger(); + + services.AddSerilog(Log.Logger); + + services.AddMemoryCache(); + + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(provider => + { + var config = provider.GetRequiredService(); + var listenAddress = config.Config.Osc.QuestSupport ? IPAddress.Any : IPAddress.Loopback; + return new OscQueryServer("ShockOsc", listenAddress, config); + }); + + services.AddSingleton(); + services.AddSingleton(); + +#if DEBUG + services.AddBlazorWebViewDeveloperTools(); +#endif + + services.AddSingleton(); + + services.AddMudServices(); + services.AddMauiBlazorWebView(); + } + + public static void StartShockOscServices(this IServiceProvider services, bool headless) + { + #region SystemTray + +#if WINDOWS + if (headless) + { + var applicationThread = new Thread(() => + { + services.GetService()?.Initialize(); + System.Windows.Forms.Application.Run(); + }); + applicationThread.Start(); + } + else services.GetService()?.Initialize(); +#else + services.GetService()?.Initialize(); +#endif + + #endregion + + + // <---- Warmup ----> + services.GetRequiredService(); + services.GetRequiredService().Start(); + + var updater = services.GetRequiredService(); + OsTask.Run(updater.CheckUpdate); + } +} \ No newline at end of file diff --git a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor index ac24ab0..7af406e 100644 --- a/ShockOsc/Ui/Pages/Authentication/Authenticate.razor +++ b/ShockOsc/Ui/Pages/Authentication/Authenticate.razor @@ -1,16 +1,9 @@ -@using OpenShock.ShockOsc.Backend @using OpenShock.ShockOsc.Config -@using Microsoft.Extensions.Logging -@using OpenShock.SDK.CSharp.Hub @using OpenShock.ShockOsc.Services @inject ConfigManager ConfigManager @inject NavigationManager NavigationManager -@inject BackendHubManager HubManager -@inject OpenShockHubClient HubClient -@inject OpenShockApi ApiClient -@inject ILogger Logger -@inject LiveControlManager LiveControlManager @inject ISnackbar Snackbar +@inject AuthService AuthService @layout NotAuthedLayout @inherits LayoutComponentBase @@ -83,15 +76,7 @@ try { - Logger.LogInformation("Setting up live client"); - await HubManager.SetupLiveClient(); - Logger.LogInformation("Starting live client"); - await HubClient.StartAsync(); - - Logger.LogInformation("Refreshing shockers"); - await ApiClient.RefreshShockers(); - - await LiveControlManager.RefreshConnections(); + await AuthService.Authenticate(); _currentState = State.Authenticated; await InvokeAsync(StateHasChanged);