Skip to content
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

Switch to using named pipes for IPC #6442

Merged
merged 9 commits into from
Dec 6, 2024
42 changes: 0 additions & 42 deletions osu.Framework.Tests/Platform/HeadlessGameHostTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@

using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Tests.IO;

namespace osu.Framework.Tests.Platform
{
Expand Down Expand Up @@ -86,46 +84,6 @@ public void TestThreadSafetyResetOnEnteringThread()
}
}

[Test]
public void TestIpc()
{
using (var server = new BackgroundGameHeadlessGameHost(@"server", new HostOptions { IPCPort = 45356 }))
using (var client = new HeadlessGameHost(@"client", new HostOptions { IPCPort = 45356 }))
{
Assert.IsTrue(server.IsPrimaryInstance, @"Server wasn't able to bind");
Assert.IsFalse(client.IsPrimaryInstance, @"Client was able to bind when it shouldn't have been able to");

var serverChannel = new IpcChannel<Foobar>(server);
var clientChannel = new IpcChannel<Foobar>(client);

async Task waitAction()
{
using (var received = new SemaphoreSlim(0))
{
serverChannel.MessageReceived += message =>
{
Assert.AreEqual("example", message.Bar);
// ReSharper disable once AccessToDisposedClosure
received.Release();
return null;
};

await clientChannel.SendMessageAsync(new Foobar { Bar = "example" }).ConfigureAwait(false);

if (!await received.WaitAsync(10000).ConfigureAwait(false))
throw new TimeoutException("Message was not received in a timely fashion");
}
}

Assert.IsTrue(Task.Run(waitAction).Wait(10000), @"Message was not received in a timely fashion");
}
}

private class Foobar
{
public string Bar;
}

public class ExceptionDuringSetupGameHost : TestRunHeadlessGameHost
{
public ExceptionDuringSetupGameHost(string gameName)
Expand Down
155 changes: 155 additions & 0 deletions osu.Framework.Tests/Platform/IPCTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

#nullable disable

using System;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Platform;
using osu.Framework.Tests.IO;

namespace osu.Framework.Tests.Platform
{
[TestFixture]
public partial class IPCTest
{
[Test]
public void TestNoPipeNameSpecifiedCoexist()
{
using (var host1 = new BackgroundGameHeadlessGameHost(@"server", new HostOptions { IPCPipeName = null }))
using (var host2 = new HeadlessGameHost(@"client", new HostOptions { IPCPipeName = null }))
{
Assert.IsTrue(host1.IsPrimaryInstance, @"Host 1 wasn't able to bind");
Assert.IsTrue(host2.IsPrimaryInstance, @"Host 2 wasn't able to bind");
}
}

[Test]
public void TestDifferentPipeNamesCoexist()
{
using (var host1 = new BackgroundGameHeadlessGameHost(@"server", new HostOptions { IPCPipeName = "test-app" }))
using (var host2 = new HeadlessGameHost(@"client", new HostOptions { IPCPipeName = "test-app-2" }))
{
Assert.IsTrue(host1.IsPrimaryInstance, @"Host 1 wasn't able to bind");
Assert.IsTrue(host2.IsPrimaryInstance, @"Host 2 wasn't able to bind");
}
}

[Test]
public void TestOneWay()
{
using (var server = new BackgroundGameHeadlessGameHost(@"server", new HostOptions { IPCPipeName = "test-app" }))
using (var client = new HeadlessGameHost(@"client", new HostOptions { IPCPipeName = "test-app" }))
{
Assert.IsTrue(server.IsPrimaryInstance, @"Server wasn't able to bind");
Assert.IsFalse(client.IsPrimaryInstance, @"Client was able to bind when it shouldn't have been able to");

var serverChannel = new IpcChannel<Foobar>(server);
var clientChannel = new IpcChannel<Foobar>(client);

async Task waitAction()
{
using (var received = new SemaphoreSlim(0))
{
serverChannel.MessageReceived += message =>
{
Assert.AreEqual("example", message.Bar);
// ReSharper disable once AccessToDisposedClosure
received.Release();
return null;
};

await clientChannel.SendMessageAsync(new Foobar { Bar = "example" }).ConfigureAwait(false);

if (!await received.WaitAsync(10000).ConfigureAwait(false))
throw new TimeoutException("Message was not received in a timely fashion");
}
}

Assert.IsTrue(Task.Run(waitAction).Wait(10000), @"Message was not received in a timely fashion");
}
}

[Test]
public void TestTwoWay()
{
using (var server = new BackgroundGameHeadlessGameHost(@"server", new HostOptions { IPCPipeName = "test-app" }))
using (var client = new HeadlessGameHost(@"client", new HostOptions { IPCPipeName = "test-app" }))
{
Assert.IsTrue(server.IsPrimaryInstance, @"Server wasn't able to bind");
Assert.IsFalse(client.IsPrimaryInstance, @"Client was able to bind when it shouldn't have been able to");

var serverChannel = new IpcChannel<Foobar, Foobar>(server);
var clientChannel = new IpcChannel<Foobar, Foobar>(client);

async Task waitAction()
{
using (var received = new SemaphoreSlim(0))
{
serverChannel.MessageReceived += message =>
{
Assert.AreEqual("example", message.Bar);
// ReSharper disable once AccessToDisposedClosure
received.Release();

return new Foobar { Bar = "test response" };
};

var response = await clientChannel.SendMessageWithResponseAsync(new Foobar { Bar = "example" }).ConfigureAwait(false);

if (!await received.WaitAsync(10000).ConfigureAwait(false))
throw new TimeoutException("Message was not received in a timely fashion");

Assert.That(response?.Bar, Is.EqualTo("test response"));
}
}

Assert.IsTrue(Task.Run(waitAction).Wait(10000), @"Message was not received in a timely fashion");
}
}

[Test]
public void TestIpcLegacyPortSupport()
{
#pragma warning disable CS0618 // Type or member is obsolete
using (var server = new BackgroundGameHeadlessGameHost(@"server", new HostOptions { IPCPort = 45356 }))
using (var client = new HeadlessGameHost(@"client", new HostOptions { IPCPort = 45356 }))
#pragma warning restore CS0618 // Type or member is obsolete
{
Assert.IsTrue(server.IsPrimaryInstance, @"Server wasn't able to bind");
Assert.IsFalse(client.IsPrimaryInstance, @"Client was able to bind when it shouldn't have been able to");

var serverChannel = new IpcChannel<Foobar, object>(server);
var clientChannel = new IpcChannel<Foobar, object>(client);

async Task waitAction()
{
using (var received = new SemaphoreSlim(0))
{
serverChannel.MessageReceived += message =>
{
Assert.AreEqual("example", message.Bar);
// ReSharper disable once AccessToDisposedClosure
received.Release();
return null;
};

await clientChannel.SendMessageAsync(new Foobar { Bar = "example" }).ConfigureAwait(false);

if (!await received.WaitAsync(10000).ConfigureAwait(false))
throw new TimeoutException("Message was not received in a timely fashion");
}
}

Assert.IsTrue(Task.Run(waitAction).Wait(10000), @"Message was not received in a timely fashion");
}
}

private class Foobar
{
public string Bar;
}
}
}
6 changes: 3 additions & 3 deletions osu.Framework.Tests/Platform/UserInputManagerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public partial class UserInputManagerTest
[Test]
public void IsAliveTest()
{
using (var client = new TestHeadlessGameHost(@"client", 45356))
using (var client = new TestHeadlessGameHost(@"client"))
{
var testGame = new TestTestGame();
client.Run(testGame);
Expand All @@ -25,8 +25,8 @@ private class TestHeadlessGameHost : TestRunHeadlessGameHost
{
public Drawable CurrentRoot => Root;

public TestHeadlessGameHost(string gameName, int? ipcPort)
: base(gameName, new HostOptions { IPCPort = ipcPort })
public TestHeadlessGameHost(string gameName)
: base(gameName, new HostOptions { IPCPipeName = gameName })
{
}
}
Expand Down
18 changes: 14 additions & 4 deletions osu.Framework/HostOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Globalization;
using osu.Framework.Platform;

namespace osu.Framework
Expand All @@ -11,12 +13,20 @@ namespace osu.Framework
public class HostOptions
{
/// <summary>
/// The IPC port to bind. This port should be between 1024 and 49151,
/// should be shared by all instances of a given osu!framework app,
/// but be distinct from IPC ports specified by other osu!framework apps.
/// Use <see cref="IPCPipeName"/> instead.
/// </summary>
[Obsolete("Use IPCPipeName instead.")] // can be removed 20250603.
public int? IPCPort
{
set => IPCPipeName = value?.ToString(CultureInfo.InvariantCulture);
}

/// <summary>
/// The IPC pipe name to bind. This should be shared by all instances of
/// an osu!framework app that want to perform inter-process communications.
/// See <see cref="IIpcHost"/> for more details on usage.
/// </summary>
public int? IPCPort { get; set; }
public string? IPCPipeName { get; set; }

/// <summary>
/// Whether this is a portable installation. Will cause all game files to be placed alongside the executable, rather than in the standard data directory.
Expand Down
19 changes: 13 additions & 6 deletions osu.Framework/Platform/DesktopGameHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ namespace osu.Framework.Platform
{
public abstract class DesktopGameHost : SDLGameHost
{
private TcpIpcProvider ipcProvider;
private readonly int? ipcPort;
private NamedPipeIpcProvider ipcProvider;
private readonly string ipcPipeName;

protected DesktopGameHost(string gameName, HostOptions options = null)
: base(gameName, options)
{
ipcPort = Options.IPCPort;
ipcPipeName = Options.IPCPipeName;
IsPortableInstallation = Options.PortableInstallation;
}

Expand Down Expand Up @@ -55,13 +55,13 @@ protected override void SetupForRun()

private void ensureIPCReady()
{
if (ipcPort == null)
if (ipcPipeName == null)
return;

if (ipcProvider != null)
return;

ipcProvider = new TcpIpcProvider(ipcPort.Value);
ipcProvider = new NamedPipeIpcProvider(ipcPipeName);
ipcProvider.MessageReceived += OnMessageReceived;

IsPrimaryInstance = ipcProvider.Bind();
Expand Down Expand Up @@ -97,7 +97,7 @@ public override bool PresentFileExternally(string filename)
return true;
}

private void openUsingShellExecute(string path) => Process.Start(new ProcessStartInfo
private static void openUsingShellExecute(string path) => Process.Start(new ProcessStartInfo
{
FileName = path,
UseShellExecute = true //see https://github.com/dotnet/corefx/issues/10361
Expand All @@ -110,6 +110,13 @@ public override Task SendMessageAsync(IpcMessage message)
return ipcProvider.SendMessageAsync(message);
}

public override Task<IpcMessage> SendMessageWithResponseAsync(IpcMessage message)
{
ensureIPCReady();

return ipcProvider.SendMessageWithResponseAsync(message);
}

protected override void Dispose(bool isDisposing)
{
ipcProvider?.Dispose();
Expand Down
1 change: 1 addition & 0 deletions osu.Framework/Platform/GameHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public abstract class GameHost : IIpcHost, IDisposable
protected IpcMessage OnMessageReceived(IpcMessage message) => MessageReceived?.Invoke(message);

public virtual Task SendMessageAsync(IpcMessage message) => throw new NotSupportedException("This platform does not implement IPC.");
public virtual Task<IpcMessage> SendMessageWithResponseAsync(IpcMessage message) => throw new NotSupportedException("This platform does not implement IPC.");

/// <summary>
/// Requests that a file or folder be opened externally with an associated application, if available.
Expand Down
11 changes: 9 additions & 2 deletions osu.Framework/Platform/IIpcHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@ public interface IIpcHost
/// Invoked when a message is received by this IPC server.
/// Returns either a response in the form of an <see cref="IpcMessage"/>, or <c>null</c> for no response.
/// </summary>
event Func<IpcMessage, IpcMessage> MessageReceived;
event Func<IpcMessage, IpcMessage?>? MessageReceived;

/// <summary>
/// Send a message to the IPC server.
/// Asynchronously send a message to the IPC server.
/// </summary>
/// <param name="ipcMessage">The message to send.</param>
Task SendMessageAsync(IpcMessage ipcMessage);

/// <summary>
/// Asynchronously send a message to the IPC server and receive a response.
/// </summary>
/// <param name="message">The message to send.</param>
/// <returns>The response from the server.</returns>
Task<IpcMessage?> SendMessageWithResponseAsync(IpcMessage message);
}
}
Loading
Loading