Skip to content

Commit

Permalink
Named pipes for. custom uri handling
Browse files Browse the repository at this point in the history
  • Loading branch information
LucHeart committed May 7, 2024
1 parent 2b97528 commit f8a6df3
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 5 deletions.
10 changes: 8 additions & 2 deletions ShockOsc/Cli/CliOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
7 changes: 7 additions & 0 deletions ShockOsc/Cli/Uri/UriParameter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace OpenShock.ShockOsc.Cli.Uri;

public class UriParameter
{
public required UriParameterType Type { get; set; }
public IReadOnlyCollection<string> Arguments { get; set; } = Array.Empty<string>();
}
6 changes: 6 additions & 0 deletions ShockOsc/Cli/Uri/UriParameterType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace OpenShock.ShockOsc.Cli.Uri;

public enum UriParameterType
{
Token
}
17 changes: 17 additions & 0 deletions ShockOsc/Cli/Uri/UriParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace OpenShock.ShockOsc.Cli.Uri;

public static class UriParser
{
public static UriParameter Parse(string uri)
{
ReadOnlySpan<char> uriSpan = uri;
var dePrefixed = uriSpan[9..];
var type = dePrefixed[..dePrefixed.IndexOf('/')];

return new UriParameter
{
Type = Enum.Parse<UriParameterType>(type, true),
Arguments = dePrefixed[(type.Length + 1)..].ToString().Split('/')
};
}
}
32 changes: 32 additions & 0 deletions ShockOsc/Platforms/Windows/PipeHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Collections;

namespace OpenShock.ShockOsc;

public static class PipeHelper
{
public static IEnumerable<string> 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;
}
}
}
}
58 changes: 56 additions & 2 deletions ShockOsc/Platforms/Windows/WindowsEntryPoint.cs
Original file line number Diff line number Diff line change
@@ -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)
{
Expand All @@ -31,17 +46,56 @@ 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.");

var host = HeadlessProgram.SetupHeadlessHost();
OsTask.Run(host.Services.GetRequiredService<AuthService>().Authenticate);
host.Run();

return;
}

XamlCheckProcessRequirements();
ComWrappersSupport.InitializeComWrappers();
Application.Start(delegate
Expand Down
8 changes: 7 additions & 1 deletion ShockOsc/Platforms/Windows/WindowsTrayService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
7 changes: 7 additions & 0 deletions ShockOsc/Services/Pipes/PipeMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace OpenShock.ShockOsc.Services.Pipes;

public sealed class PipeMessage
{
public required PipeMessageType Type { get; set; }
public object? Data { get; set; }
}
6 changes: 6 additions & 0 deletions ShockOsc/Services/Pipes/PipeMessageType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace OpenShock.ShockOsc.Services.Pipes;

public enum PipeMessageType
{
Token
}
76 changes: 76 additions & 0 deletions ShockOsc/Services/Pipes/PipeServerService.cs
Original file line number Diff line number Diff line change
@@ -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<PipeServerService> _logger;
private uint _clientCount = 0;

public PipeServerService(ILogger<PipeServerService> logger)
{
_logger = logger;
}

public ConcurrentQueue<PipeMessage> MessageQueue { get; } = new();
public event Func<Task>? 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<PipeMessage>(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);
}
}
4 changes: 4 additions & 0 deletions ShockOsc/ShockOscBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -45,6 +46,8 @@ public static void AddShockOscServices(this IServiceCollection services)

services.AddMemoryCache();

services.AddSingleton<PipeServerService>();

services.AddSingleton<ShockOscData>();

services.AddSingleton<ConfigManager>();
Expand Down Expand Up @@ -107,6 +110,7 @@ public static void StartShockOscServices(this IServiceProvider services, bool he
// <---- Warmup ---->
services.GetRequiredService<Services.ShockOsc>();
services.GetRequiredService<OscQueryServer>().Start();
services.GetRequiredService<PipeServerService>().StartServer();

var updater = services.GetRequiredService<Updater>();
OsTask.Run(updater.CheckUpdate);
Expand Down

0 comments on commit f8a6df3

Please sign in to comment.