diff --git a/ShockOsc/Cli/CliOptions.cs b/ShockOsc/Cli/CliOptions.cs index 7829b28..fe22682 100644 --- a/ShockOsc/Cli/CliOptions.cs +++ b/ShockOsc/Cli/CliOptions.cs @@ -4,6 +4,12 @@ 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; } + [Option("headless", Required = false, Default = false, HelpText = "Run the application in headless mode.")] + public required bool Headless { get; init; } + + [Option('c', "console", Required = false, Default = false, HelpText = "Create console window for stdout/stderr.")] + public required bool Console { get; init; } + + [Option("uri", Required = false, HelpText = "Custom URI for callbacks")] + public required string Uri { get; init; } } \ No newline at end of file diff --git a/ShockOsc/Cli/Uri/UriParameter.cs b/ShockOsc/Cli/Uri/UriParameter.cs new file mode 100644 index 0000000..f9491b4 --- /dev/null +++ b/ShockOsc/Cli/Uri/UriParameter.cs @@ -0,0 +1,7 @@ +namespace OpenShock.ShockOsc.Cli.Uri; + +public class UriParameter +{ + public required UriParameterType Type { get; set; } + public IReadOnlyCollection Arguments { get; set; } = Array.Empty(); +} \ No newline at end of file diff --git a/ShockOsc/Cli/Uri/UriParameterType.cs b/ShockOsc/Cli/Uri/UriParameterType.cs new file mode 100644 index 0000000..d0c4d8c --- /dev/null +++ b/ShockOsc/Cli/Uri/UriParameterType.cs @@ -0,0 +1,6 @@ +namespace OpenShock.ShockOsc.Cli.Uri; + +public enum UriParameterType +{ + Token +} \ No newline at end of file diff --git a/ShockOsc/Cli/Uri/UriParser.cs b/ShockOsc/Cli/Uri/UriParser.cs new file mode 100644 index 0000000..64fe6b3 --- /dev/null +++ b/ShockOsc/Cli/Uri/UriParser.cs @@ -0,0 +1,17 @@ +namespace OpenShock.ShockOsc.Cli.Uri; + +public static class UriParser +{ + public static UriParameter Parse(string uri) + { + ReadOnlySpan uriSpan = uri; + var dePrefixed = uriSpan[9..]; + var type = dePrefixed[..dePrefixed.IndexOf('/')]; + + return new UriParameter + { + Type = Enum.Parse(type, true), + Arguments = dePrefixed[(type.Length + 1)..].ToString().Split('/') + }; + } +} \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/PipeHelper.cs b/ShockOsc/Platforms/Windows/PipeHelper.cs new file mode 100644 index 0000000..4cf5606 --- /dev/null +++ b/ShockOsc/Platforms/Windows/PipeHelper.cs @@ -0,0 +1,32 @@ +using System.Collections; + +namespace OpenShock.ShockOsc; + +public static class PipeHelper +{ + public static IEnumerable EnumeratePipes() { + bool MoveNextSafe(IEnumerator enumerator) { + + // Pipes might have illegal characters in path. Seen one from IAR containing < and >. + // The FileSystemEnumerable.MoveNext source code indicates that another call to MoveNext will return + // the next entry. + // Pose a limit in case the underlying implementation changes somehow. This also means that no more than 10 + // pipes with bad names may occur in sequence. + const int retries = 10; + for (int i = 0; i < retries; i++) { + try { + return enumerator.MoveNext(); + } catch (ArgumentException) { + } + } + Console.WriteLine("Pipe enumeration: Retry limit due to bad names reached."); + return false; + } + + using (var enumerator = Directory.EnumerateFiles(@"\\.\pipe\").GetEnumerator()) { + while (MoveNextSafe(enumerator)) { + yield return enumerator.Current; + } + } + } +} \ No newline at end of file diff --git a/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs b/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs index eb7f8c8..ba3b88e 100644 --- a/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs +++ b/ShockOsc/Platforms/Windows/WindowsEntryPoint.cs @@ -1,22 +1,37 @@ #if WINDOWS +using System.Diagnostics; +using System.IO.Pipes; using System.Runtime.InteropServices; +using System.Text.Json; using CommandLine; using Microsoft.Extensions.Hosting; using Microsoft.UI.Dispatching; +using Microsoft.Windows.AppLifecycle; using OpenShock.ShockOsc.Cli; +using OpenShock.ShockOsc.Cli.Uri; using OpenShock.ShockOsc.Services; +using OpenShock.ShockOsc.Services.Pipes; using OpenShock.ShockOsc.Utils; using WinRT; using Application = Microsoft.UI.Xaml.Application; +using UriParser = OpenShock.ShockOsc.Cli.Uri.UriParser; namespace OpenShock.ShockOsc.Platforms.Windows; public static class WindowsEntryPoint { + private const int ATTACH_PARENT_PROCESS = -1; + [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] [DllImport("Microsoft.ui.xaml.dll")] private static extern void XamlCheckProcessRequirements(); + [DllImport("kernel32.dll")] + private static extern bool AllocConsole(); + + [DllImport("kernel32.dll")] + private static extern bool AttachConsole(int pid); + [STAThread] private static void Main(string[] args) { @@ -31,6 +46,45 @@ private static void Main(string[] args) private static void Start(CliOptions config) { + if (config.Console) + { + // Command line given, display console + if (!AttachConsole(ATTACH_PARENT_PROCESS)) + AllocConsole(); + } + + const string pipeName = @"\\.\pipe\OpenShock.ShockOSC"; + + if (PipeHelper.EnumeratePipes().Any(x => x.Equals(pipeName, StringComparison.InvariantCultureIgnoreCase))) + { + if (!string.IsNullOrEmpty(config.Uri)) + { + using var pipeClientStream = new NamedPipeClientStream(".", "OpenShock.ShockOsc", PipeDirection.Out); + pipeClientStream.Connect(500); + + using var writer = new StreamWriter(pipeClientStream); + writer.AutoFlush = true; + + var parsedUri = UriParser.Parse(config.Uri); + + if (parsedUri.Type == UriParameterType.Token) + { + writer.WriteLine(JsonSerializer.Serialize(new PipeMessage + { + Type = PipeMessageType.Token, + Data = parsedUri.Arguments + })); + } + + return; + } + + Console.WriteLine("Another instance of ShockOSC is already running."); + Environment.Exit(1); + return; + } + + if (config.Headless) { Console.WriteLine("Running in headless mode."); @@ -38,10 +92,10 @@ private static void Start(CliOptions config) var host = HeadlessProgram.SetupHeadlessHost(); OsTask.Run(host.Services.GetRequiredService().Authenticate); host.Run(); - + return; } - + XamlCheckProcessRequirements(); ComWrappersSupport.InitializeComWrappers(); Application.Start(delegate diff --git a/ShockOsc/Platforms/Windows/WindowsTrayService.cs b/ShockOsc/Platforms/Windows/WindowsTrayService.cs index 142bd99..091c6e7 100644 --- a/ShockOsc/Platforms/Windows/WindowsTrayService.cs +++ b/ShockOsc/Platforms/Windows/WindowsTrayService.cs @@ -89,7 +89,13 @@ private static void OnMainClick(object? sender, EventArgs eventArgs) private static void OnQuitClick(object? sender, EventArgs eventArgs) { - Application.Current?.Quit(); + if (Application.Current != null) + { + Application.Current.Quit(); + return; + } + + Environment.Exit(0); } } diff --git a/ShockOsc/Services/Pipes/PipeMessage.cs b/ShockOsc/Services/Pipes/PipeMessage.cs new file mode 100644 index 0000000..4271760 --- /dev/null +++ b/ShockOsc/Services/Pipes/PipeMessage.cs @@ -0,0 +1,7 @@ +namespace OpenShock.ShockOsc.Services.Pipes; + +public sealed class PipeMessage +{ + public required PipeMessageType Type { get; set; } + public object? Data { get; set; } +} \ No newline at end of file diff --git a/ShockOsc/Services/Pipes/PipeMessageType.cs b/ShockOsc/Services/Pipes/PipeMessageType.cs new file mode 100644 index 0000000..e99a79b --- /dev/null +++ b/ShockOsc/Services/Pipes/PipeMessageType.cs @@ -0,0 +1,6 @@ +namespace OpenShock.ShockOsc.Services.Pipes; + +public enum PipeMessageType +{ + Token +} \ No newline at end of file diff --git a/ShockOsc/Services/Pipes/PipeServerService.cs b/ShockOsc/Services/Pipes/PipeServerService.cs new file mode 100644 index 0000000..1f89726 --- /dev/null +++ b/ShockOsc/Services/Pipes/PipeServerService.cs @@ -0,0 +1,76 @@ +using System.Collections.Concurrent; +using System.IO.Pipes; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using OpenShock.SDK.CSharp.Utils; +using OpenShock.ShockOsc.Utils; + +namespace OpenShock.ShockOsc.Services.Pipes; + +public sealed class PipeServerService +{ + private readonly ILogger _logger; + private uint _clientCount = 0; + + public PipeServerService(ILogger logger) + { + _logger = logger; + } + + public ConcurrentQueue MessageQueue { get; } = new(); + public event Func? OnMessageReceived; + + public void StartServer() + { + OsTask.Run(ServerLoop); + } + + private async Task ServerLoop() + { + var id = _clientCount++; + + await using var pipeServerStream = new NamedPipeServerStream("OpenShock.ShockOsc", PipeDirection.In, 20, + PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + + + _logger.LogInformation("[{Id}] Starting new server loop", id); + + await pipeServerStream.WaitForConnectionAsync(); +#pragma warning disable CS4014 + OsTask.Run(ServerLoop); +#pragma warning restore CS4014 + + _logger.LogInformation("[{Id}] Pipe connected!", id); + + using var reader = new StreamReader(pipeServerStream); + while (pipeServerStream.IsConnected && !reader.EndOfStream) + { + var line = await reader.ReadLineAsync(); + if (string.IsNullOrEmpty(line)) + { + _logger.LogWarning("[{Id}] Received empty pipe message. Skipping...", id); + continue; + } + + try + { + var jsonObj = JsonSerializer.Deserialize(line); + if (jsonObj is null) + { + _logger.LogWarning("[{Id}] Failed to deserialize pipe message. Skipping...", id); + continue; + } + + MessageQueue.Enqueue(jsonObj); + await OnMessageReceived.Raise(); + _logger.LogInformation("[{Id}], Received pipe message of type: {Type}", id, jsonObj.Type); + } + catch (JsonException ex) + { + _logger.LogError(ex, "[{Id}] Failed to deserialize pipe message. Skipping...", id); + } + } + + _logger.LogInformation("[{Id}] Pipe disconnected. Stopping server loop...", id); + } +} \ No newline at end of file diff --git a/ShockOsc/ShockOscBootstrap.cs b/ShockOsc/ShockOscBootstrap.cs index 9fa1634..3341838 100644 --- a/ShockOsc/ShockOscBootstrap.cs +++ b/ShockOsc/ShockOscBootstrap.cs @@ -7,6 +7,7 @@ using OpenShock.ShockOsc.Logging; using OpenShock.ShockOsc.OscQueryLibrary; using OpenShock.ShockOsc.Services; +using OpenShock.ShockOsc.Services.Pipes; using OpenShock.ShockOsc.Utils; using Serilog; @@ -45,6 +46,8 @@ public static void AddShockOscServices(this IServiceCollection services) services.AddMemoryCache(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); @@ -107,6 +110,7 @@ public static void StartShockOscServices(this IServiceProvider services, bool he // <---- Warmup ----> services.GetRequiredService(); services.GetRequiredService().Start(); + services.GetRequiredService().StartServer(); var updater = services.GetRequiredService(); OsTask.Run(updater.CheckUpdate);