mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-29 20:03:41 +01:00
fix: Remove RPC (#2526)
Some checks are pending
Some checks are pending
This commit is contained in:
parent
c7dd694a53
commit
bc2eac6006
14 changed files with 0 additions and 1112 deletions
|
|
@ -1,108 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Dalamud.Networking.Rpc.Model;
|
|
||||||
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Dalamud.Test.Rpc
|
|
||||||
{
|
|
||||||
public class DalamudUriTests
|
|
||||||
{
|
|
||||||
[Theory]
|
|
||||||
[InlineData("https://www.google.com/", false)]
|
|
||||||
[InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", true)]
|
|
||||||
public void ValidatesScheme(string uri, bool valid)
|
|
||||||
{
|
|
||||||
Action act = () => { _ = DalamudUri.FromUri(uri); };
|
|
||||||
|
|
||||||
var ex = Record.Exception(act);
|
|
||||||
if (valid)
|
|
||||||
{
|
|
||||||
Assert.Null(ex);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Assert.NotNull(ex);
|
|
||||||
Assert.IsType<ArgumentOutOfRangeException>(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", "plugininstaller")]
|
|
||||||
[InlineData("dalamud://Plugin/Dalamud.FindAnything/OpenWindow", "plugin")]
|
|
||||||
[InlineData("dalamud://Test", "test")]
|
|
||||||
public void ExtractsNamespace(string uri, string expectedNamespace)
|
|
||||||
{
|
|
||||||
var dalamudUri = DalamudUri.FromUri(uri);
|
|
||||||
Assert.Equal(expectedNamespace, dalamudUri.Namespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/")]
|
|
||||||
[InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux")]
|
|
||||||
[InlineData("dalamud://foo/bar/baz", "/bar/baz")]
|
|
||||||
[InlineData("dalamud://foo/bar", "/bar")]
|
|
||||||
[InlineData("dalamud://foo/bar/", "/bar/")]
|
|
||||||
[InlineData("dalamud://foo/", "/")]
|
|
||||||
public void ExtractsPath(string uri, string expectedPath)
|
|
||||||
{
|
|
||||||
var dalamudUri = DalamudUri.FromUri(uri);
|
|
||||||
Assert.Equal(expectedPath, dalamudUri.Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo#frag", "/bar/baz/qux/?cow=moo#frag")]
|
|
||||||
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/?cow=moo")]
|
|
||||||
[InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux?cow=moo")]
|
|
||||||
[InlineData("dalamud://foo/bar/baz", "/bar/baz")]
|
|
||||||
[InlineData("dalamud://foo/bar?cow=moo", "/bar?cow=moo")]
|
|
||||||
[InlineData("dalamud://foo/bar", "/bar")]
|
|
||||||
[InlineData("dalamud://foo/bar/?cow=moo", "/bar/?cow=moo")]
|
|
||||||
[InlineData("dalamud://foo/bar/", "/bar/")]
|
|
||||||
[InlineData("dalamud://foo/?cow=moo#chicken", "/?cow=moo#chicken")]
|
|
||||||
[InlineData("dalamud://foo/?cow=moo", "/?cow=moo")]
|
|
||||||
[InlineData("dalamud://foo/", "/")]
|
|
||||||
public void ExtractsData(string uri, string expectedData)
|
|
||||||
{
|
|
||||||
var dalamudUri = DalamudUri.FromUri(uri);
|
|
||||||
|
|
||||||
Assert.Equal(expectedData, dalamudUri.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("dalamud://foo/bar", 0)]
|
|
||||||
[InlineData("dalamud://foo/bar?cow=moo", 1)]
|
|
||||||
[InlineData("dalamud://foo/bar?cow=moo&wolf=awoo", 2)]
|
|
||||||
[InlineData("dalamud://foo/bar?cow=moo&wolf=awoo&cat", 3)]
|
|
||||||
public void ExtractsQueryParams(string uri, int queryCount)
|
|
||||||
{
|
|
||||||
var dalamudUri = DalamudUri.FromUri(uri);
|
|
||||||
Assert.Equal(queryCount, dalamudUri.QueryParams.Count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("dalamud://foo/bar/baz/qux/meh/?foo=bar", 5, true)]
|
|
||||||
[InlineData("dalamud://foo/bar/baz/qux/meh/", 5, true)]
|
|
||||||
[InlineData("dalamud://foo/bar/baz/qux/meh", 5)]
|
|
||||||
[InlineData("dalamud://foo/bar/baz/qux", 4)]
|
|
||||||
[InlineData("dalamud://foo/bar/baz", 3)]
|
|
||||||
[InlineData("dalamud://foo/bar/", 2)]
|
|
||||||
[InlineData("dalamud://foo/bar", 2)]
|
|
||||||
public void ExtractsSegments(string uri, int segmentCount, bool finalSegmentEndsWithSlash = false)
|
|
||||||
{
|
|
||||||
var dalamudUri = DalamudUri.FromUri(uri);
|
|
||||||
var segments = dalamudUri.Segments;
|
|
||||||
|
|
||||||
// First segment must always be `/`
|
|
||||||
Assert.Equal("/", segments[0]);
|
|
||||||
|
|
||||||
Assert.Equal(segmentCount, segments.Length);
|
|
||||||
|
|
||||||
if (finalSegmentEndsWithSlash)
|
|
||||||
{
|
|
||||||
Assert.EndsWith("/", segments.Last());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -79,7 +79,6 @@
|
||||||
<PackageReference Include="Serilog.Sinks.Console" />
|
<PackageReference Include="Serilog.Sinks.Console" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" />
|
<PackageReference Include="Serilog.Sinks.File" />
|
||||||
<PackageReference Include="sqlite-net-pcl" />
|
<PackageReference Include="sqlite-net-pcl" />
|
||||||
<PackageReference Include="StreamJsonRpc" />
|
|
||||||
<PackageReference Include="StyleCop.Analyzers">
|
<PackageReference Include="StyleCop.Analyzers">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Specialized;
|
|
||||||
using System.Web;
|
|
||||||
|
|
||||||
namespace Dalamud.Networking.Rpc.Model;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A Dalamud Uri, in the format:
|
|
||||||
/// <code>dalamud://{NAMESPACE}/{ARBITRARY}</code>
|
|
||||||
/// </summary>
|
|
||||||
public record DalamudUri
|
|
||||||
{
|
|
||||||
private readonly Uri rawUri;
|
|
||||||
|
|
||||||
private DalamudUri(Uri uri)
|
|
||||||
{
|
|
||||||
if (uri.Scheme != "dalamud")
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(uri), "URI must be of scheme dalamud.");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rawUri = uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the namespace that this URI should be routed to. Generally a high level component like "PluginInstaller".
|
|
||||||
/// </summary>
|
|
||||||
public string Namespace => this.rawUri.Authority;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the raw (untargeted) path and query params for this URI.
|
|
||||||
/// </summary>
|
|
||||||
public string Data =>
|
|
||||||
this.rawUri.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.UriEscaped);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the raw (untargeted) path for this URI.
|
|
||||||
/// </summary>
|
|
||||||
public string Path => this.rawUri.AbsolutePath;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a list of segments based on the provided Data element.
|
|
||||||
/// </summary>
|
|
||||||
public string[] Segments => this.GetDataSegments();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the raw query parameters for this URI, if any.
|
|
||||||
/// </summary>
|
|
||||||
public string Query => this.rawUri.Query;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the query params (as a parsed NameValueCollection) in this URI.
|
|
||||||
/// </summary>
|
|
||||||
public NameValueCollection QueryParams => HttpUtility.ParseQueryString(this.Query);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the fragment (if one is specified) in this URI.
|
|
||||||
/// </summary>
|
|
||||||
public string Fragment => this.rawUri.Fragment;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override string ToString() => this.rawUri.ToString();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Build a DalamudURI from a given URI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uri">The URI to convert to a Dalamud URI.</param>
|
|
||||||
/// <returns>Returns a DalamudUri.</returns>
|
|
||||||
public static DalamudUri FromUri(Uri uri)
|
|
||||||
{
|
|
||||||
return new DalamudUri(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Build a DalamudURI from a URI in string format.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uri">The URI to convert to a Dalamud URI.</param>
|
|
||||||
/// <returns>Returns a DalamudUri.</returns>
|
|
||||||
public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri));
|
|
||||||
|
|
||||||
private string[] GetDataSegments()
|
|
||||||
{
|
|
||||||
// reimplementation of the System.URI#Segments, under MIT license.
|
|
||||||
var path = this.Path;
|
|
||||||
|
|
||||||
var segments = new List<string>();
|
|
||||||
var current = 0;
|
|
||||||
while (current < path.Length)
|
|
||||||
{
|
|
||||||
var next = path.IndexOf('/', current);
|
|
||||||
if (next == -1)
|
|
||||||
{
|
|
||||||
next = path.Length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
segments.Add(path.Substring(current, (next - current) + 1));
|
|
||||||
current = next + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return segments.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Dalamud.Networking.Rpc.Service;
|
|
||||||
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
using StreamJsonRpc;
|
|
||||||
|
|
||||||
namespace Dalamud.Networking.Rpc;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A single RPC client session connected via a stream (named pipe or Unix socket).
|
|
||||||
/// </summary>
|
|
||||||
internal class RpcConnection : IDisposable
|
|
||||||
{
|
|
||||||
private readonly Stream stream;
|
|
||||||
private readonly RpcServiceRegistry registry;
|
|
||||||
private readonly CancellationTokenSource cts = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="RpcConnection"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stream">The stream that this connection will handle.</param>
|
|
||||||
/// <param name="registry">A registry of RPC services.</param>
|
|
||||||
public RpcConnection(Stream stream, RpcServiceRegistry registry)
|
|
||||||
{
|
|
||||||
this.Id = Guid.CreateVersion7();
|
|
||||||
this.stream = stream;
|
|
||||||
this.registry = registry;
|
|
||||||
|
|
||||||
var formatter = new JsonMessageFormatter();
|
|
||||||
var handler = new HeaderDelimitedMessageHandler(stream, stream, formatter);
|
|
||||||
|
|
||||||
this.Rpc = new JsonRpc(handler);
|
|
||||||
this.Rpc.AllowModificationWhileListening = true;
|
|
||||||
this.Rpc.Disconnected += this.OnDisconnected;
|
|
||||||
this.registry.Attach(this.Rpc);
|
|
||||||
|
|
||||||
this.Rpc.StartListening();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the GUID for this connection.
|
|
||||||
/// </summary>
|
|
||||||
public Guid Id { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the JsonRpc instance for this connection.
|
|
||||||
/// </summary>
|
|
||||||
public JsonRpc Rpc { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a task that's called on RPC completion.
|
|
||||||
/// </summary>
|
|
||||||
public Task Completion => this.Rpc.Completion;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (!this.cts.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
this.cts.Cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
this.Rpc.Dispose();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Debug(ex, "Error disposing JsonRpc for client {Id}", this.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
this.stream.Dispose();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Debug(ex, "Error disposing stream for client {Id}", this.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cts.Dispose();
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDisconnected(object? sender, JsonRpcDisconnectedEventArgs e)
|
|
||||||
{
|
|
||||||
Log.Debug("RPC client {Id} disconnected: {Reason}", this.Id, e.Description);
|
|
||||||
this.registry.Detach(this.Rpc);
|
|
||||||
this.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Dalamud.Logging.Internal;
|
|
||||||
using Dalamud.Networking.Rpc.Transport;
|
|
||||||
|
|
||||||
namespace Dalamud.Networking.Rpc;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The Dalamud service repsonsible for hosting the RPC.
|
|
||||||
/// </summary>
|
|
||||||
[ServiceManager.EarlyLoadedService]
|
|
||||||
internal class RpcHostService : IServiceType, IInternalDisposableService
|
|
||||||
{
|
|
||||||
private readonly ModuleLog log = new("RPC");
|
|
||||||
private readonly RpcServiceRegistry registry = new();
|
|
||||||
private readonly List<IRpcTransport> transports = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="RpcHostService"/> class.
|
|
||||||
/// </summary>
|
|
||||||
[ServiceManager.ServiceConstructor]
|
|
||||||
public RpcHostService()
|
|
||||||
{
|
|
||||||
this.StartUnixTransport();
|
|
||||||
|
|
||||||
if (this.transports.Count == 0)
|
|
||||||
{
|
|
||||||
this.log.Warning("No RPC hosts could be started on this platform");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all active RPC transports.
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyList<IRpcTransport> Transports => this.transports;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a new service Object to the RPC host.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="service">The object to add.</param>
|
|
||||||
public void AddService(object service) => this.registry.AddService(service);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add a new standalone method to the RPC host.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">The method name to add.</param>
|
|
||||||
/// <param name="handler">The handler to add.</param>
|
|
||||||
public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler);
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void DisposeService()
|
|
||||||
{
|
|
||||||
foreach (var host in this.transports)
|
|
||||||
{
|
|
||||||
host.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.transports.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IRpcTransport.InvokeClientAsync"/>
|
|
||||||
public async Task<T> InvokeClientAsync<T>(Guid clientId, string method, params object[] arguments)
|
|
||||||
{
|
|
||||||
var clients = this.transports.SelectMany(t => t.Connections).ToImmutableDictionary();
|
|
||||||
|
|
||||||
if (!clients.TryGetValue(clientId, out var session))
|
|
||||||
throw new KeyNotFoundException($"No client {clientId}");
|
|
||||||
|
|
||||||
return await session.Rpc.InvokeAsync<T>(method, arguments).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IRpcTransport.BroadcastNotifyAsync"/>
|
|
||||||
public async Task BroadcastNotifyAsync(string method, params object[] arguments)
|
|
||||||
{
|
|
||||||
await foreach (var transport in this.transports.ToAsyncEnumerable().ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
await transport.BroadcastNotifyAsync(method, arguments).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartUnixTransport()
|
|
||||||
{
|
|
||||||
var transport = new UnixRpcTransport(this.registry);
|
|
||||||
this.transports.Add(transport);
|
|
||||||
transport.Start();
|
|
||||||
this.log.Information("RpcHostService listening to UNIX socket: {Socket}", transport.SocketPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
using StreamJsonRpc;
|
|
||||||
|
|
||||||
namespace Dalamud.Networking.Rpc;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session.
|
|
||||||
/// New sessions get all previously registered targets; newly added targets are attached to all active sessions.
|
|
||||||
/// </summary>
|
|
||||||
internal class RpcServiceRegistry
|
|
||||||
{
|
|
||||||
private readonly Lock sync = new();
|
|
||||||
private readonly List<object> targets = [];
|
|
||||||
private readonly List<(string Name, Delegate Handler)> methods = [];
|
|
||||||
private readonly List<JsonRpc> activeRpcs = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registers a new local RPC target object. Its public JSON-RPC methods become callable by clients.
|
|
||||||
/// Adds <paramref name="service"/> to the registry and attaches it to all active RPC sessions.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="service">The service instance containing JSON-RPC callable methods to expose.</param>
|
|
||||||
public void AddService(object service)
|
|
||||||
{
|
|
||||||
lock (this.sync)
|
|
||||||
{
|
|
||||||
this.targets.Add(service);
|
|
||||||
foreach (var rpc in this.activeRpcs)
|
|
||||||
{
|
|
||||||
rpc.AddLocalRpcTarget(service);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registers a new standalone JSON-RPC method.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">The name of the method to add.</param>
|
|
||||||
/// <param name="handler">The handler to add.</param>
|
|
||||||
public void AddMethod(string name, Delegate handler)
|
|
||||||
{
|
|
||||||
lock (this.sync)
|
|
||||||
{
|
|
||||||
this.methods.Add((name, handler));
|
|
||||||
foreach (var rpc in this.activeRpcs)
|
|
||||||
{
|
|
||||||
rpc.AddLocalRpcMethod(name, handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attaches a JsonRpc instance <paramref name="rpc"/> to the registry so it receives all existing service targets.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="rpc">The JsonRpc instance to attach and populate with current targets.</param>
|
|
||||||
internal void Attach(JsonRpc rpc)
|
|
||||||
{
|
|
||||||
lock (this.sync)
|
|
||||||
{
|
|
||||||
this.activeRpcs.Add(rpc);
|
|
||||||
foreach (var t in this.targets)
|
|
||||||
{
|
|
||||||
rpc.AddLocalRpcTarget(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var m in this.methods)
|
|
||||||
{
|
|
||||||
rpc.AddLocalRpcMethod(m.Name, m.Handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Detaches a JsonRpc instance <paramref name="rpc"/> from the registry (e.g. when a client disconnects).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="rpc">The JsonRpc instance being detached.</param>
|
|
||||||
internal void Detach(JsonRpc rpc)
|
|
||||||
{
|
|
||||||
lock (this.sync)
|
|
||||||
{
|
|
||||||
this.activeRpcs.Remove(rpc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Dalamud.Data;
|
|
||||||
using Dalamud.Game;
|
|
||||||
using Dalamud.Game.ClientState;
|
|
||||||
using Dalamud.Utility;
|
|
||||||
|
|
||||||
using Lumina.Excel.Sheets;
|
|
||||||
|
|
||||||
namespace Dalamud.Networking.Rpc.Service;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A minimal service to respond with information about this client.
|
|
||||||
/// </summary>
|
|
||||||
[ServiceManager.EarlyLoadedService]
|
|
||||||
internal sealed class ClientHelloService : IInternalDisposableService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ClientHelloService"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="rpcHostService">Injected host service.</param>
|
|
||||||
[ServiceManager.ServiceConstructor]
|
|
||||||
public ClientHelloService(RpcHostService rpcHostService)
|
|
||||||
{
|
|
||||||
rpcHostService.AddMethod("hello", this.HandleHello);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handle a hello request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">.</param>
|
|
||||||
/// <returns>Respond with information.</returns>
|
|
||||||
public async Task<ClientHelloResponse> HandleHello(ClientHelloRequest request)
|
|
||||||
{
|
|
||||||
var dalamud = await Service<Dalamud>.GetAsync();
|
|
||||||
|
|
||||||
return new ClientHelloResponse
|
|
||||||
{
|
|
||||||
ApiVersion = "1.0",
|
|
||||||
DalamudVersion = Versioning.GetScmVersion(),
|
|
||||||
GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown",
|
|
||||||
ProcessId = Environment.ProcessId,
|
|
||||||
ProcessStartTime = new DateTimeOffset(Process.GetCurrentProcess().StartTime).ToUnixTimeSeconds(),
|
|
||||||
ClientState = await this.GetClientIdentifier(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void DisposeService()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> GetClientIdentifier()
|
|
||||||
{
|
|
||||||
var framework = await Service<Framework>.GetAsync();
|
|
||||||
var clientState = await Service<ClientState>.GetAsync();
|
|
||||||
var dataManager = await Service<DataManager>.GetAsync();
|
|
||||||
|
|
||||||
var clientIdentifier = $"FFXIV Process ${Environment.ProcessId}";
|
|
||||||
|
|
||||||
await framework.RunOnFrameworkThread(() =>
|
|
||||||
{
|
|
||||||
if (clientState.IsLoggedIn)
|
|
||||||
{
|
|
||||||
var player = clientState.LocalPlayer;
|
|
||||||
if (player != null)
|
|
||||||
{
|
|
||||||
var world = dataManager.GetExcelSheet<World>().GetRow(player.HomeWorld.RowId);
|
|
||||||
clientIdentifier = $"Logged in as {player.Name.TextValue} @ {world.Name.ExtractText()}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
clientIdentifier = "On login screen";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return clientIdentifier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A request from a client to say hello.
|
|
||||||
/// </summary>
|
|
||||||
internal record ClientHelloRequest
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the API version this client is expecting.
|
|
||||||
/// </summary>
|
|
||||||
public string ApiVersion { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the user agent of the client.
|
|
||||||
/// </summary>
|
|
||||||
public string UserAgent { get; init; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A response from Dalamud to a hello request.
|
|
||||||
/// </summary>
|
|
||||||
internal record ClientHelloResponse
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the API version this server has offered.
|
|
||||||
/// </summary>
|
|
||||||
public string? ApiVersion { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current Dalamud version.
|
|
||||||
/// </summary>
|
|
||||||
public string? DalamudVersion { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current game version.
|
|
||||||
/// </summary>
|
|
||||||
public string? GameVersion { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the process ID of this client.
|
|
||||||
/// </summary>
|
|
||||||
public int? ProcessId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the time this process started.
|
|
||||||
/// </summary>
|
|
||||||
public long? ProcessStartTime { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a state for this client for user display.
|
|
||||||
/// </summary>
|
|
||||||
public string? ClientState { get; init; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
using Dalamud.Logging.Internal;
|
|
||||||
using Dalamud.Networking.Rpc.Model;
|
|
||||||
using Dalamud.Utility;
|
|
||||||
|
|
||||||
namespace Dalamud.Networking.Rpc.Service;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A service responsible for handling Dalamud URIs and dispatching them accordingly.
|
|
||||||
/// </summary>
|
|
||||||
[ServiceManager.EarlyLoadedService]
|
|
||||||
internal class LinkHandlerService : IInternalDisposableService
|
|
||||||
{
|
|
||||||
private readonly ModuleLog log = new("LinkHandler");
|
|
||||||
|
|
||||||
// key: namespace (e.g. "plugin" or "PluginInstaller") -> list of handlers
|
|
||||||
private readonly ConcurrentDictionary<string, List<Action<DalamudUri>>> handlers
|
|
||||||
= new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="LinkHandlerService"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="rpcHostService">The injected RPC host service.</param>
|
|
||||||
[ServiceManager.ServiceConstructor]
|
|
||||||
public LinkHandlerService(RpcHostService rpcHostService)
|
|
||||||
{
|
|
||||||
rpcHostService.AddMethod("handleLink", this.HandleLinkCall);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void DisposeService()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Register a handler for a namespace. All URIs with this namespace will be dispatched to the handler.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ns">The namespace to use for this subscription.</param>
|
|
||||||
/// <param name="handler">The command handler.</param>
|
|
||||||
public void Register(string ns, Action<DalamudUri> handler)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(ns))
|
|
||||||
throw new ArgumentNullException(nameof(ns));
|
|
||||||
|
|
||||||
var list = this.handlers.GetOrAdd(ns, _ => []);
|
|
||||||
lock (list)
|
|
||||||
{
|
|
||||||
list.Add(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log.Verbose("Registered handler for {Namespace}", ns);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unregister a handler.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ns">The namespace to use for this subscription.</param>
|
|
||||||
/// <param name="handler">The command handler.</param>
|
|
||||||
public void Unregister(string ns, Action<DalamudUri> handler)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(ns))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!this.handlers.TryGetValue(ns, out var list))
|
|
||||||
return;
|
|
||||||
|
|
||||||
list.RemoveAll(x => x == handler);
|
|
||||||
|
|
||||||
if (list.Count == 0)
|
|
||||||
this.handlers.TryRemove(ns, out _);
|
|
||||||
|
|
||||||
this.log.Verbose("Unregistered handler for {Namespace}", ns);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dispatch a URI to matching handlers.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uri">The URI to parse and dispatch.</param>
|
|
||||||
public void Dispatch(DalamudUri uri)
|
|
||||||
{
|
|
||||||
this.log.Information("Received URI: {Uri}", uri.ToString());
|
|
||||||
|
|
||||||
var ns = uri.Namespace;
|
|
||||||
if (!this.handlers.TryGetValue(ns, out var actions))
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var h in actions)
|
|
||||||
{
|
|
||||||
h.InvokeSafely(uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The RPC-invokable link handler.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uri">A plain-text URI to parse.</param>
|
|
||||||
public void HandleLinkCall(string uri)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(uri))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var du = DalamudUri.FromUri(uri);
|
|
||||||
this.Dispatch(du);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
using Dalamud.Game.Gui.Toast;
|
|
||||||
using Dalamud.Interface.ImGuiNotification;
|
|
||||||
using Dalamud.Interface.ImGuiNotification.Internal;
|
|
||||||
using Dalamud.Networking.Rpc.Model;
|
|
||||||
|
|
||||||
namespace Dalamud.Networking.Rpc.Service.Links;
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A debug controller for link handling.
|
|
||||||
/// </summary>
|
|
||||||
[ServiceManager.EarlyLoadedService]
|
|
||||||
internal sealed class DebugLinkHandler : IInternalDisposableService
|
|
||||||
{
|
|
||||||
private readonly LinkHandlerService linkHandlerService;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="DebugLinkHandler"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="linkHandler">Injected LinkHandler.</param>
|
|
||||||
[ServiceManager.ServiceConstructor]
|
|
||||||
public DebugLinkHandler(LinkHandlerService linkHandler)
|
|
||||||
{
|
|
||||||
this.linkHandlerService = linkHandler;
|
|
||||||
|
|
||||||
this.linkHandlerService.Register("debug", this.HandleLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void DisposeService()
|
|
||||||
{
|
|
||||||
this.linkHandlerService.Unregister("debug", this.HandleLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleLink(DalamudUri uri)
|
|
||||||
{
|
|
||||||
var action = uri.Path.Split("/").GetValue(1)?.ToString();
|
|
||||||
switch (action)
|
|
||||||
{
|
|
||||||
case "toast":
|
|
||||||
this.ShowToast(uri);
|
|
||||||
break;
|
|
||||||
case "notification":
|
|
||||||
this.ShowNotification(uri);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowToast(DalamudUri uri)
|
|
||||||
{
|
|
||||||
var message = uri.QueryParams.Get("message") ?? "Hello, world!";
|
|
||||||
Service<ToastGui>.Get().ShowNormal(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowNotification(DalamudUri uri)
|
|
||||||
{
|
|
||||||
Service<NotificationManager>.Get().AddNotification(
|
|
||||||
new Notification
|
|
||||||
{
|
|
||||||
Title = uri.QueryParams.Get("title"),
|
|
||||||
Content = uri.QueryParams.Get("content") ?? "Hello, world!",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Dalamud.Console;
|
|
||||||
using Dalamud.IoC;
|
|
||||||
using Dalamud.IoC.Internal;
|
|
||||||
using Dalamud.Networking.Rpc.Model;
|
|
||||||
using Dalamud.Plugin.Internal.Types;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
|
|
||||||
#pragma warning disable DAL_RPC
|
|
||||||
|
|
||||||
namespace Dalamud.Networking.Rpc.Service.Links;
|
|
||||||
|
|
||||||
/// <inheritdoc cref="IPluginLinkHandler" />
|
|
||||||
[PluginInterface]
|
|
||||||
[ServiceManager.ScopedService]
|
|
||||||
[ResolveVia<IPluginLinkHandler>]
|
|
||||||
public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler
|
|
||||||
{
|
|
||||||
private readonly LinkHandlerService linkHandler;
|
|
||||||
private readonly LocalPlugin localPlugin;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="PluginLinkHandler"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localPlugin">The plugin to bind this service to.</param>
|
|
||||||
/// <param name="linkHandler">The central link handler.</param>
|
|
||||||
internal PluginLinkHandler(LocalPlugin localPlugin, LinkHandlerService linkHandler)
|
|
||||||
{
|
|
||||||
this.linkHandler = linkHandler;
|
|
||||||
this.localPlugin = localPlugin;
|
|
||||||
|
|
||||||
this.linkHandler.Register("plugin", this.HandleUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public event IPluginLinkHandler.PluginUriReceived? OnUriReceived;
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void DisposeService()
|
|
||||||
{
|
|
||||||
this.OnUriReceived = null;
|
|
||||||
this.linkHandler.Unregister("plugin", this.HandleUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleUri(DalamudUri uri)
|
|
||||||
{
|
|
||||||
var target = uri.Path.Split("/").ElementAtOrDefault(1);
|
|
||||||
var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName);
|
|
||||||
if (target == null || !string.Equals(target, thisPlugin, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.OnUriReceived?.Invoke(uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Dalamud.Networking.Rpc.Transport;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Interface for RPC host implementations (named pipes or Unix sockets).
|
|
||||||
/// </summary>
|
|
||||||
internal interface IRpcTransport : IDisposable
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a list of active RPC connections.
|
|
||||||
/// </summary>
|
|
||||||
IReadOnlyDictionary<Guid, RpcConnection> Connections { get; }
|
|
||||||
|
|
||||||
/// <summary>Starts accepting client connections.</summary>
|
|
||||||
void Start();
|
|
||||||
|
|
||||||
/// <summary>Invoke an RPC request on a specific client expecting a result.</summary>
|
|
||||||
/// <param name="clientId">The client ID to invoke.</param>
|
|
||||||
/// <param name="method">The method to invoke.</param>
|
|
||||||
/// <param name="arguments">Any arguments to invoke.</param>
|
|
||||||
/// <returns>An optional return based on the specified RPC.</returns>
|
|
||||||
/// <typeparam name="T">The expected response type.</typeparam>
|
|
||||||
Task<T> InvokeClientAsync<T>(Guid clientId, string method, params object[] arguments);
|
|
||||||
|
|
||||||
/// <summary>Send a notification to all connected clients (no response expected).</summary>
|
|
||||||
/// <param name="method">The method name to broadcast.</param>
|
|
||||||
/// <param name="arguments">The arguments to broadcast.</param>
|
|
||||||
/// <returns>Returns a Task when completed.</returns>
|
|
||||||
Task BroadcastNotifyAsync(string method, params object[] arguments);
|
|
||||||
}
|
|
||||||
|
|
@ -1,207 +0,0 @@
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Dalamud.Logging.Internal;
|
|
||||||
using Dalamud.Utility;
|
|
||||||
|
|
||||||
namespace Dalamud.Networking.Rpc.Transport;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Simple multi-client JSON-RPC Unix socket host using StreamJsonRpc.
|
|
||||||
/// </summary>
|
|
||||||
internal class UnixRpcTransport : IRpcTransport
|
|
||||||
{
|
|
||||||
private readonly ModuleLog log = new("RPC/Transport/UnixSocket");
|
|
||||||
|
|
||||||
private readonly RpcServiceRegistry registry;
|
|
||||||
private readonly CancellationTokenSource cts = new();
|
|
||||||
private readonly ConcurrentDictionary<Guid, RpcConnection> sessions = new();
|
|
||||||
private readonly string? cleanupSocketDirectory;
|
|
||||||
|
|
||||||
private Task? acceptLoopTask;
|
|
||||||
private Socket? listenSocket;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="UnixRpcTransport"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="registry">The RPC service registry to use.</param>
|
|
||||||
/// <param name="socketDirectory">The Unix socket directory to use. If null, defaults to Dalamud home directory.</param>
|
|
||||||
/// <param name="socketName">The name of the socket to create.</param>
|
|
||||||
public UnixRpcTransport(RpcServiceRegistry registry, string? socketDirectory = null, string? socketName = null)
|
|
||||||
{
|
|
||||||
this.registry = registry;
|
|
||||||
socketName ??= $"DalamudRPC.{Environment.ProcessId}.sock";
|
|
||||||
|
|
||||||
if (!socketDirectory.IsNullOrEmpty())
|
|
||||||
{
|
|
||||||
this.SocketPath = Path.Combine(socketDirectory, socketName);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
socketDirectory = Service<Dalamud>.Get().StartInfo.TempDirectory;
|
|
||||||
|
|
||||||
if (socketDirectory == null)
|
|
||||||
{
|
|
||||||
this.SocketPath = Path.Combine(Path.GetTempPath(), socketName);
|
|
||||||
this.log.Warning("Temp dir was not set in StartInfo; using system temp for unix socket.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.SocketPath = Path.Combine(socketDirectory, socketName);
|
|
||||||
this.cleanupSocketDirectory = socketDirectory;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the path of the Unix socket this RPC host is using.
|
|
||||||
/// </summary>
|
|
||||||
public string SocketPath { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public IReadOnlyDictionary<Guid, RpcConnection> Connections => this.sessions;
|
|
||||||
|
|
||||||
/// <summary>Starts accepting client connections.</summary>
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
if (this.acceptLoopTask != null) return;
|
|
||||||
|
|
||||||
// Make the directory for the socket if it doesn't exist
|
|
||||||
var socketDir = Path.GetDirectoryName(this.SocketPath);
|
|
||||||
if (!string.IsNullOrEmpty(socketDir) && !Directory.Exists(socketDir))
|
|
||||||
{
|
|
||||||
this.log.Error("Directory for unix socket does not exist: {Path}", socketDir);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete existing socket for this PID, if it exists.
|
|
||||||
if (File.Exists(this.SocketPath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Delete(this.SocketPath);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
this.log.Warning(ex, "Failed to delete existing socket file: {Path}", this.SocketPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Invoke an RPC request on a specific client expecting a result.</summary>
|
|
||||||
/// <param name="clientId">The client ID to invoke.</param>
|
|
||||||
/// <param name="method">The method to invoke.</param>
|
|
||||||
/// <param name="arguments">Any arguments to invoke.</param>
|
|
||||||
/// <returns>An optional return based on the specified RPC.</returns>
|
|
||||||
/// <typeparam name="T">The expected response type.</typeparam>
|
|
||||||
public Task<T> InvokeClientAsync<T>(Guid clientId, string method, params object[] arguments)
|
|
||||||
{
|
|
||||||
if (!this.sessions.TryGetValue(clientId, out var session))
|
|
||||||
throw new KeyNotFoundException($"No client {clientId}");
|
|
||||||
|
|
||||||
return session.Rpc.InvokeAsync<T>(method, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Send a notification to all connected clients (no response expected).</summary>
|
|
||||||
/// <param name="method">The method name to broadcast.</param>
|
|
||||||
/// <param name="arguments">The arguments to broadcast.</param>
|
|
||||||
/// <returns>Returns a Task when completed.</returns>
|
|
||||||
public Task BroadcastNotifyAsync(string method, params object[] arguments)
|
|
||||||
{
|
|
||||||
var list = this.sessions.Values;
|
|
||||||
var tasks = new List<Task>(list.Count);
|
|
||||||
foreach (var s in list)
|
|
||||||
{
|
|
||||||
tasks.Add(s.Rpc.NotifyAsync(method, arguments));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.WhenAll(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
this.cts.Cancel();
|
|
||||||
this.acceptLoopTask?.Wait(1000);
|
|
||||||
|
|
||||||
foreach (var kv in this.sessions)
|
|
||||||
{
|
|
||||||
kv.Value.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sessions.Clear();
|
|
||||||
|
|
||||||
this.listenSocket?.Dispose();
|
|
||||||
|
|
||||||
if (File.Exists(this.SocketPath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Delete(this.SocketPath);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
this.log.Warning(ex, "Failed to delete socket file on dispose: {Path}", this.SocketPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cts.Dispose();
|
|
||||||
this.log.Information("UnixRpcHost disposed ({Socket})", this.SocketPath);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AcceptLoopAsync()
|
|
||||||
{
|
|
||||||
var token = this.cts.Token;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var endpoint = new UnixDomainSocketEndPoint(this.SocketPath);
|
|
||||||
this.listenSocket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
|
|
||||||
this.listenSocket.Bind(endpoint);
|
|
||||||
this.listenSocket.Listen(128);
|
|
||||||
|
|
||||||
while (!token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
Socket? clientSocket = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
clientSocket = await this.listenSocket.AcceptAsync(token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var stream = new NetworkStream(clientSocket, ownsSocket: true);
|
|
||||||
var session = new RpcConnection(stream, this.registry);
|
|
||||||
this.sessions.TryAdd(session.Id, session);
|
|
||||||
|
|
||||||
this.log.Debug("RPC connection created: {Id}", session.Id);
|
|
||||||
|
|
||||||
_ = session.Completion.ContinueWith(t =>
|
|
||||||
{
|
|
||||||
this.sessions.TryRemove(session.Id, out _);
|
|
||||||
this.log.Debug("RPC connection removed: {Id}", session.Id);
|
|
||||||
}, TaskScheduler.Default);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
clientSocket?.Dispose();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
clientSocket?.Dispose();
|
|
||||||
this.log.Error(ex, "Error in socket accept loop");
|
|
||||||
await Task.Delay(500, token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
this.log.Error(ex, "Fatal error in Unix socket accept loop");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
|
|
||||||
using Dalamud.Networking.Rpc.Model;
|
|
||||||
|
|
||||||
namespace Dalamud.Plugin.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the
|
|
||||||
/// <c>dalamud://plugin/{PLUGIN_INTERNAL_NAME}/...</c> namespace.
|
|
||||||
/// </summary>
|
|
||||||
[Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")]
|
|
||||||
public interface IPluginLinkHandler : IDalamudService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A delegate containing the received URI.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uri">The URI opened by the user.</param>
|
|
||||||
public delegate void PluginUriReceived(DalamudUri uri);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The event fired when a URI targeting this plugin is received.
|
|
||||||
/// </summary>
|
|
||||||
event PluginUriReceived OnUriReceived;
|
|
||||||
}
|
|
||||||
|
|
@ -46,9 +46,6 @@
|
||||||
<PackageVersion Include="Reloaded.Memory" Version="7.0.0" />
|
<PackageVersion Include="Reloaded.Memory" Version="7.0.0" />
|
||||||
<PackageVersion Include="Reloaded.Memory.Buffers" Version="2.0.0" />
|
<PackageVersion Include="Reloaded.Memory.Buffers" Version="2.0.0" />
|
||||||
|
|
||||||
<!-- Named Pipes / RPC -->
|
|
||||||
<PackageVersion Include="StreamJsonRpc" Version="2.22.23" />
|
|
||||||
|
|
||||||
<!-- Unit Testing -->
|
<!-- Unit Testing -->
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue