mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
Merge branch 'api14' into AddonLifecycleRefactor
# Conflicts: # Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
This commit is contained in:
commit
0480693f92
34 changed files with 498 additions and 261 deletions
|
|
@ -108,6 +108,11 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
|
||||||
config.LogName = json.value("LogName", config.LogName);
|
config.LogName = json.value("LogName", config.LogName);
|
||||||
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
|
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
|
||||||
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
|
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
|
||||||
|
|
||||||
|
if (json.contains("TempDirectory") && !json["TempDirectory"].is_null()) {
|
||||||
|
config.TempDirectory = json.value("TempDirectory", config.TempDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
config.Language = json.value("Language", config.Language);
|
config.Language = json.value("Language", config.Language);
|
||||||
config.Platform = json.value("Platform", config.Platform);
|
config.Platform = json.value("Platform", config.Platform);
|
||||||
config.GameVersion = json.value("GameVersion", config.GameVersion);
|
config.GameVersion = json.value("GameVersion", config.GameVersion);
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ struct DalamudStartInfo {
|
||||||
std::string ConfigurationPath;
|
std::string ConfigurationPath;
|
||||||
std::string LogPath;
|
std::string LogPath;
|
||||||
std::string LogName;
|
std::string LogName;
|
||||||
|
std::string TempDirectory;
|
||||||
std::string PluginDirectory;
|
std::string PluginDirectory;
|
||||||
std::string AssetDirectory;
|
std::string AssetDirectory;
|
||||||
ClientLanguage Language = ClientLanguage::English;
|
ClientLanguage Language = ClientLanguage::English;
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ static DalamudExpected<void> append_injector_launch_args(std::vector<std::wstrin
|
||||||
args.emplace_back(L"--logname=\"" + unicode::convert<std::wstring>(g_startInfo.LogName) + L"\"");
|
args.emplace_back(L"--logname=\"" + unicode::convert<std::wstring>(g_startInfo.LogName) + L"\"");
|
||||||
args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert<std::wstring>(g_startInfo.PluginDirectory) + L"\"");
|
args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert<std::wstring>(g_startInfo.PluginDirectory) + L"\"");
|
||||||
args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert<std::wstring>(g_startInfo.AssetDirectory) + L"\"");
|
args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert<std::wstring>(g_startInfo.AssetDirectory) + L"\"");
|
||||||
|
args.emplace_back(L"--dalamud-temp-directory=\"" + unicode::convert<std::wstring>(g_startInfo.TempDirectory) + L"\"");
|
||||||
args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast<int>(g_startInfo.Language)));
|
args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast<int>(g_startInfo.Language)));
|
||||||
args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs));
|
args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs));
|
||||||
// NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler
|
// NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,12 @@ public record DalamudStartInfo
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ConfigurationPath { get; set; }
|
public string? ConfigurationPath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the directory for temporary files. This directory needs to exist and be writable to the user.
|
||||||
|
/// It should also be predictable and easy for launchers to find.
|
||||||
|
/// </summary>
|
||||||
|
public string? TempDirectory { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the path of the log files.
|
/// Gets or sets the path of the log files.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -291,6 +291,7 @@ namespace Dalamud.Injector
|
||||||
var configurationPath = startInfo.ConfigurationPath;
|
var configurationPath = startInfo.ConfigurationPath;
|
||||||
var pluginDirectory = startInfo.PluginDirectory;
|
var pluginDirectory = startInfo.PluginDirectory;
|
||||||
var assetDirectory = startInfo.AssetDirectory;
|
var assetDirectory = startInfo.AssetDirectory;
|
||||||
|
var tempDirectory = startInfo.TempDirectory;
|
||||||
var delayInitializeMs = startInfo.DelayInitializeMs;
|
var delayInitializeMs = startInfo.DelayInitializeMs;
|
||||||
var logName = startInfo.LogName;
|
var logName = startInfo.LogName;
|
||||||
var logPath = startInfo.LogPath;
|
var logPath = startInfo.LogPath;
|
||||||
|
|
@ -321,6 +322,10 @@ namespace Dalamud.Injector
|
||||||
{
|
{
|
||||||
assetDirectory = args[i][key.Length..];
|
assetDirectory = args[i][key.Length..];
|
||||||
}
|
}
|
||||||
|
else if (args[i].StartsWith(key = "--dalamud-temp-directory="))
|
||||||
|
{
|
||||||
|
tempDirectory = args[i][key.Length..];
|
||||||
|
}
|
||||||
else if (args[i].StartsWith(key = "--dalamud-delay-initialize="))
|
else if (args[i].StartsWith(key = "--dalamud-delay-initialize="))
|
||||||
{
|
{
|
||||||
delayInitializeMs = int.Parse(args[i][key.Length..]);
|
delayInitializeMs = int.Parse(args[i][key.Length..]);
|
||||||
|
|
@ -433,6 +438,7 @@ namespace Dalamud.Injector
|
||||||
startInfo.ConfigurationPath = configurationPath;
|
startInfo.ConfigurationPath = configurationPath;
|
||||||
startInfo.PluginDirectory = pluginDirectory;
|
startInfo.PluginDirectory = pluginDirectory;
|
||||||
startInfo.AssetDirectory = assetDirectory;
|
startInfo.AssetDirectory = assetDirectory;
|
||||||
|
startInfo.TempDirectory = tempDirectory;
|
||||||
startInfo.Language = clientLanguage;
|
startInfo.Language = clientLanguage;
|
||||||
startInfo.Platform = platform;
|
startInfo.Platform = platform;
|
||||||
startInfo.DelayInitializeMs = delayInitializeMs;
|
startInfo.DelayInitializeMs = delayInitializeMs;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
using Dalamud.Networking.Pipes;
|
using Dalamud.Networking.Rpc.Model;
|
||||||
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Dalamud.Test.Pipes
|
namespace Dalamud.Test.Rpc
|
||||||
{
|
{
|
||||||
public class DalamudUriTests
|
public class DalamudUriTests
|
||||||
{
|
{
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
namespace Dalamud.Game.Addon.Events;
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
namespace Dalamud.Game.Addon.Events;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// AddonEventManager memory address resolver.
|
/// AddonEventManager memory address resolver.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
namespace Dalamud.Game;
|
namespace Dalamud.Game;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
namespace Dalamud.Game.ClientState;
|
namespace Dalamud.Game.ClientState;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
using Dalamud.IoC;
|
using Dalamud.IoC;
|
||||||
using Dalamud.IoC.Internal;
|
using Dalamud.IoC.Internal;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
namespace Dalamud.Game.Config;
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
namespace Dalamud.Game.Config;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Game config system address resolver.
|
/// Game config system address resolver.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
namespace Dalamud.Game.DutyState;
|
namespace Dalamud.Game.DutyState;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
namespace Dalamud.Game.Gui;
|
namespace Dalamud.Game.Gui;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
namespace Dalamud.Game.Gui.NamePlate;
|
namespace Dalamud.Game.Gui.NamePlate;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
namespace Dalamud.Game.Network;
|
namespace Dalamud.Game.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
namespace Dalamud.Game.Network.Internal;
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
namespace Dalamud.Game.Network.Internal;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Internal address resolver for the network handlers.
|
/// Internal address resolver for the network handlers.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
using Iced.Intel;
|
using Iced.Intel;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
using Dalamud.IoC;
|
using Dalamud.IoC;
|
||||||
using Dalamud.IoC.Internal;
|
using Dalamud.IoC.Internal;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
namespace Dalamud.Game;
|
namespace Dalamud.Game;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,167 +0,0 @@
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO.Pipes;
|
|
||||||
using System.Security.AccessControl;
|
|
||||||
using System.Security.Principal;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Dalamud.Logging.Internal;
|
|
||||||
using Dalamud.Utility;
|
|
||||||
|
|
||||||
namespace Dalamud.Networking.Pipes.Rpc;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Simple multi-client JSON-RPC named pipe host using StreamJsonRpc.
|
|
||||||
/// </summary>
|
|
||||||
internal class PipeRpcHost : IDisposable
|
|
||||||
{
|
|
||||||
private readonly ModuleLog log = new("RPC/Host");
|
|
||||||
|
|
||||||
private readonly RpcServiceRegistry registry = new();
|
|
||||||
private readonly CancellationTokenSource cts = new();
|
|
||||||
private readonly ConcurrentDictionary<Guid, RpcConnection> sessions = new();
|
|
||||||
private Task? acceptLoopTask;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="PipeRpcHost"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="pipeName">The pipe name to create.</param>
|
|
||||||
public PipeRpcHost(string? pipeName = null)
|
|
||||||
{
|
|
||||||
// Default pipe name based on current process ID for uniqueness per Dalamud instance.
|
|
||||||
this.PipeName = pipeName ?? $"DalamudRPC.{Environment.ProcessId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the name of the named pipe this RPC host is using.
|
|
||||||
/// </summary>
|
|
||||||
public string PipeName { get; }
|
|
||||||
|
|
||||||
/// <summary>Adds a local object exposing RPC methods callable by clients.</summary>
|
|
||||||
/// <param name="service">An arbitrary service object that will be introspected to add to RPC.</param>
|
|
||||||
public void AddService(object service) => this.registry.AddService(service);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds a standalone JSON-RPC method callable by clients.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">The name to add.</param>
|
|
||||||
/// <param name="handler">The delegate that acts as the handler.</param>
|
|
||||||
public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler);
|
|
||||||
|
|
||||||
/// <summary>Starts accepting client connections.</summary>
|
|
||||||
public void Start()
|
|
||||||
{
|
|
||||||
if (this.acceptLoopTask != null) return;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a list of connected client IDs.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Connected client IDs.</returns>
|
|
||||||
public IReadOnlyCollection<Guid> GetClientIds() => this.sessions.Keys.AsReadOnlyCollection();
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
this.cts.Cancel();
|
|
||||||
this.acceptLoopTask?.Wait(1000);
|
|
||||||
|
|
||||||
foreach (var kv in this.sessions)
|
|
||||||
{
|
|
||||||
kv.Value.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sessions.Clear();
|
|
||||||
this.cts.Dispose();
|
|
||||||
this.log.Information("PipeRpcHost disposed ({Pipe})", this.PipeName);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PipeSecurity BuildPipeSecurity()
|
|
||||||
{
|
|
||||||
var ps = new PipeSecurity();
|
|
||||||
ps.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().User!, PipeAccessRights.FullControl, AccessControlType.Allow));
|
|
||||||
|
|
||||||
return ps;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AcceptLoopAsync()
|
|
||||||
{
|
|
||||||
this.log.Information("PipeRpcHost starting on pipe {Pipe}", this.PipeName);
|
|
||||||
var token = this.cts.Token;
|
|
||||||
var security = this.BuildPipeSecurity();
|
|
||||||
|
|
||||||
while (!token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
NamedPipeServerStream? server = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
server = NamedPipeServerStreamAcl.Create(
|
|
||||||
this.PipeName,
|
|
||||||
PipeDirection.InOut,
|
|
||||||
NamedPipeServerStream.MaxAllowedServerInstances,
|
|
||||||
PipeTransmissionMode.Message,
|
|
||||||
PipeOptions.Asynchronous,
|
|
||||||
65536,
|
|
||||||
65536,
|
|
||||||
security);
|
|
||||||
|
|
||||||
await server.WaitForConnectionAsync(token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var session = new RpcConnection(server, 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)
|
|
||||||
{
|
|
||||||
server?.Dispose();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
server?.Dispose();
|
|
||||||
this.log.Error(ex, "Error in pipe accept loop");
|
|
||||||
await Task.Delay(500, token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
using Dalamud.Logging.Internal;
|
|
||||||
|
|
||||||
namespace Dalamud.Networking.Pipes.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 PipeRpcHost host;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="RpcHostService"/> class.
|
|
||||||
/// </summary>
|
|
||||||
[ServiceManager.ServiceConstructor]
|
|
||||||
public RpcHostService()
|
|
||||||
{
|
|
||||||
this.host = new PipeRpcHost();
|
|
||||||
this.host.Start();
|
|
||||||
|
|
||||||
this.log.Information("RpcHostService started on pipe {Pipe}", this.host.PipeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the RPC host to drill down.
|
|
||||||
/// </summary>
|
|
||||||
public PipeRpcHost Host => this.host;
|
|
||||||
|
|
||||||
/// <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.host.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.host.AddMethod(name, handler);
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void DisposeService()
|
|
||||||
{
|
|
||||||
this.host.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
|
|
||||||
namespace Dalamud.Networking.Pipes;
|
namespace Dalamud.Networking.Rpc.Model;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A Dalamud Uri, in the format:
|
/// A Dalamud Uri, in the format:
|
||||||
|
|
@ -1,34 +1,37 @@
|
||||||
using System.IO.Pipes;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using Dalamud.Networking.Rpc.Service;
|
||||||
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
using StreamJsonRpc;
|
using StreamJsonRpc;
|
||||||
|
|
||||||
namespace Dalamud.Networking.Pipes.Rpc;
|
namespace Dalamud.Networking.Rpc;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A single RPC client session connected via named pipe.
|
/// A single RPC client session connected via a stream (named pipe or Unix socket).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class RpcConnection : IDisposable
|
internal class RpcConnection : IDisposable
|
||||||
{
|
{
|
||||||
private readonly NamedPipeServerStream pipe;
|
private readonly Stream stream;
|
||||||
private readonly RpcServiceRegistry registry;
|
private readonly RpcServiceRegistry registry;
|
||||||
private readonly CancellationTokenSource cts = new();
|
private readonly CancellationTokenSource cts = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="RpcConnection"/> class.
|
/// Initializes a new instance of the <see cref="RpcConnection"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pipe">The named pipe that this connection will handle.</param>
|
/// <param name="stream">The stream that this connection will handle.</param>
|
||||||
/// <param name="registry">A registry of RPC services.</param>
|
/// <param name="registry">A registry of RPC services.</param>
|
||||||
public RpcConnection(NamedPipeServerStream pipe, RpcServiceRegistry registry)
|
public RpcConnection(Stream stream, RpcServiceRegistry registry)
|
||||||
{
|
{
|
||||||
this.Id = Guid.CreateVersion7();
|
this.Id = Guid.CreateVersion7();
|
||||||
this.pipe = pipe;
|
this.stream = stream;
|
||||||
this.registry = registry;
|
this.registry = registry;
|
||||||
|
|
||||||
var formatter = new JsonMessageFormatter();
|
var formatter = new JsonMessageFormatter();
|
||||||
var handler = new HeaderDelimitedMessageHandler(pipe, pipe, formatter);
|
var handler = new HeaderDelimitedMessageHandler(stream, stream, formatter);
|
||||||
|
|
||||||
this.Rpc = new JsonRpc(handler);
|
this.Rpc = new JsonRpc(handler);
|
||||||
this.Rpc.AllowModificationWhileListening = true;
|
this.Rpc.AllowModificationWhileListening = true;
|
||||||
|
|
@ -72,11 +75,11 @@ internal class RpcConnection : IDisposable
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.pipe.Dispose();
|
this.stream.Dispose();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Debug(ex, "Error disposing pipe for client {Id}", this.Id);
|
Log.Debug(ex, "Error disposing stream for client {Id}", this.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cts.Dispose();
|
this.cts.Dispose();
|
||||||
91
Dalamud/Networking/Rpc/RpcHostService.cs
Normal file
91
Dalamud/Networking/Rpc/RpcHostService.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ using System.Threading;
|
||||||
|
|
||||||
using StreamJsonRpc;
|
using StreamJsonRpc;
|
||||||
|
|
||||||
namespace Dalamud.Networking.Pipes.Rpc;
|
namespace Dalamud.Networking.Rpc;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session.
|
/// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session.
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
using System.Threading.Tasks;
|
using System.Diagnostics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Dalamud.Data;
|
using Dalamud.Data;
|
||||||
using Dalamud.Game;
|
using Dalamud.Game;
|
||||||
using Dalamud.Game.ClientState;
|
using Dalamud.Game.ClientState;
|
||||||
using Dalamud.Networking.Pipes.Rpc;
|
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
|
|
||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
|
|
||||||
namespace Dalamud.Networking.Pipes.Internal;
|
namespace Dalamud.Networking.Rpc.Service;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A minimal service to respond with information about this client.
|
/// A minimal service to respond with information about this client.
|
||||||
|
|
@ -40,7 +40,9 @@ internal sealed class ClientHelloService : IInternalDisposableService
|
||||||
ApiVersion = "1.0",
|
ApiVersion = "1.0",
|
||||||
DalamudVersion = Util.GetScmVersion(),
|
DalamudVersion = Util.GetScmVersion(),
|
||||||
GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown",
|
GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown",
|
||||||
ClientIdentifier = await this.GetClientIdentifier(),
|
ProcessId = Environment.ProcessId,
|
||||||
|
ProcessStartTime = new DateTimeOffset(Process.GetCurrentProcess().StartTime).ToUnixTimeSeconds(),
|
||||||
|
ClientState = await this.GetClientIdentifier(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,7 +117,17 @@ internal record ClientHelloResponse
|
||||||
public string? GameVersion { get; init; }
|
public string? GameVersion { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets an identifier for this client.
|
/// Gets the process ID of this client.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ClientIdentifier { get; init; }
|
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; }
|
||||||
}
|
}
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
using Dalamud.Logging.Internal;
|
using Dalamud.Logging.Internal;
|
||||||
using Dalamud.Networking.Pipes.Rpc;
|
using Dalamud.Networking.Rpc.Model;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
|
|
||||||
namespace Dalamud.Networking.Pipes.Internal;
|
namespace Dalamud.Networking.Rpc.Service;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A service responsible for handling Dalamud URIs and dispatching them accordingly.
|
/// A service responsible for handling Dalamud URIs and dispatching them accordingly.
|
||||||
67
Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs
Normal file
67
Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
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
|
||||||
|
|
@ -3,12 +3,13 @@
|
||||||
using Dalamud.Console;
|
using Dalamud.Console;
|
||||||
using Dalamud.IoC;
|
using Dalamud.IoC;
|
||||||
using Dalamud.IoC.Internal;
|
using Dalamud.IoC.Internal;
|
||||||
using Dalamud.Networking.Pipes.Internal;
|
using Dalamud.Networking.Rpc.Model;
|
||||||
using Dalamud.Plugin.Internal.Types;
|
using Dalamud.Plugin.Internal.Types;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
#pragma warning disable DAL_RPC
|
#pragma warning disable DAL_RPC
|
||||||
|
|
||||||
namespace Dalamud.Networking.Pipes.Api;
|
namespace Dalamud.Networking.Rpc.Service.Links;
|
||||||
|
|
||||||
/// <inheritdoc cref="IPluginLinkHandler" />
|
/// <inheritdoc cref="IPluginLinkHandler" />
|
||||||
[PluginInterface]
|
[PluginInterface]
|
||||||
|
|
@ -44,7 +45,7 @@ public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler
|
||||||
|
|
||||||
private void HandleUri(DalamudUri uri)
|
private void HandleUri(DalamudUri uri)
|
||||||
{
|
{
|
||||||
var target = uri.Path.Split("/").FirstOrDefault();
|
var target = uri.Path.Split("/").ElementAtOrDefault(1);
|
||||||
var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName);
|
var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName);
|
||||||
if (target == null || !string.Equals(target, thisPlugin, StringComparison.OrdinalIgnoreCase))
|
if (target == null || !string.Equals(target, thisPlugin, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
32
Dalamud/Networking/Rpc/Transport/IRpcTransport.cs
Normal file
32
Dalamud/Networking/Rpc/Transport/IRpcTransport.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
207
Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
Normal file
207
Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
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,6 +1,6 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
using Dalamud.Networking.Pipes;
|
using Dalamud.Networking.Rpc.Model;
|
||||||
|
|
||||||
namespace Dalamud.Plugin.Services;
|
namespace Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.SelfTest;
|
||||||
|
|
||||||
namespace Dalamud.Plugin.SelfTest;
|
namespace Dalamud.Plugin.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface for registering and unregistering self-test steps from plugins.
|
/// Interface for registering and unregistering self-test steps from plugins.
|
||||||
|
|
@ -2,9 +2,7 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
using Dalamud.Plugin.Services;
|
namespace Dalamud.Plugin.Services;
|
||||||
|
|
||||||
namespace Dalamud.Game;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A SigScanner facilitates searching for memory signatures in a given ProcessModule.
|
/// A SigScanner facilitates searching for memory signatures in a given ProcessModule.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
namespace Dalamud.Game.ClientState.Objects;
|
namespace Dalamud.Plugin.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get and set various kinds of targets for the player.
|
/// Get and set various kinds of targets for the player.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue