diff --git a/Dalamud.Test/Pipes/DalamudUriTests.cs b/Dalamud.Test/Rpc/DalamudUriTests.cs similarity index 98% rename from Dalamud.Test/Pipes/DalamudUriTests.cs rename to Dalamud.Test/Rpc/DalamudUriTests.cs index 4977f3814..b371a5698 100644 --- a/Dalamud.Test/Pipes/DalamudUriTests.cs +++ b/Dalamud.Test/Rpc/DalamudUriTests.cs @@ -1,10 +1,11 @@ using System; using System.Linq; -using Dalamud.Networking.Pipes; +using Dalamud.Networking.Rpc.Model; + using Xunit; -namespace Dalamud.Test.Pipes +namespace Dalamud.Test.Rpc { public class DalamudUriTests { diff --git a/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs b/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs deleted file mode 100644 index 78df27323..000000000 --- a/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Dalamud.Logging.Internal; - -namespace Dalamud.Networking.Pipes.Rpc; - -/// -/// The Dalamud service repsonsible for hosting the RPC. -/// -[ServiceManager.EarlyLoadedService] -internal class RpcHostService : IServiceType, IInternalDisposableService -{ - private readonly ModuleLog log = new("RPC"); - private readonly PipeRpcHost host; - - /// - /// Initializes a new instance of the class. - /// - [ServiceManager.ServiceConstructor] - public RpcHostService() - { - this.host = new PipeRpcHost(); - this.host.Start(); - - this.log.Information("RpcHostService started on pipe {Pipe}", this.host.PipeName); - } - - /// - /// Gets the RPC host to drill down. - /// - public PipeRpcHost Host => this.host; - - /// - /// Add a new service Object to the RPC host. - /// - /// The object to add. - public void AddService(object service) => this.host.AddService(service); - - /// - /// Add a new standalone method to the RPC host. - /// - /// The method name to add. - /// The handler to add. - public void AddMethod(string name, Delegate handler) => this.host.AddMethod(name, handler); - - /// - public void DisposeService() - { - this.host.Dispose(); - } -} diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs similarity index 93% rename from Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs rename to Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs index 78fbb0d82..e9372bf0e 100644 --- a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs +++ b/Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs @@ -3,12 +3,14 @@ using Dalamud.Console; using Dalamud.IoC; using Dalamud.IoC.Internal; -using Dalamud.Networking.Pipes.Internal; +using Dalamud.Networking.Rpc.Model; +using Dalamud.Networking.Rpc.Service; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; + #pragma warning disable DAL_RPC -namespace Dalamud.Networking.Pipes.Api; +namespace Dalamud.Networking.Rpc.Api; /// [PluginInterface] diff --git a/Dalamud/Networking/Pipes/DalamudUri.cs b/Dalamud/Networking/Rpc/Model/DalamudUri.cs similarity index 98% rename from Dalamud/Networking/Pipes/DalamudUri.cs rename to Dalamud/Networking/Rpc/Model/DalamudUri.cs index 7e639cbbe..852478762 100644 --- a/Dalamud/Networking/Pipes/DalamudUri.cs +++ b/Dalamud/Networking/Rpc/Model/DalamudUri.cs @@ -2,7 +2,7 @@ using System.Collections.Specialized; using System.Web; -namespace Dalamud.Networking.Pipes; +namespace Dalamud.Networking.Rpc.Model; /// /// A Dalamud Uri, in the format: diff --git a/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs b/Dalamud/Networking/Rpc/RpcConnection.cs similarity index 76% rename from Dalamud/Networking/Pipes/Rpc/RpcConnection.cs rename to Dalamud/Networking/Rpc/RpcConnection.cs index 8e1c3a085..5288948eb 100644 --- a/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs +++ b/Dalamud/Networking/Rpc/RpcConnection.cs @@ -1,34 +1,37 @@ -using System.IO.Pipes; +using System.IO; using System.Threading; using System.Threading.Tasks; +using Dalamud.Networking.Rpc.Service; + using Serilog; + using StreamJsonRpc; -namespace Dalamud.Networking.Pipes.Rpc; +namespace Dalamud.Networking.Rpc; /// -/// A single RPC client session connected via named pipe. +/// A single RPC client session connected via a stream (named pipe or Unix socket). /// internal class RpcConnection : IDisposable { - private readonly NamedPipeServerStream pipe; + private readonly Stream stream; private readonly RpcServiceRegistry registry; private readonly CancellationTokenSource cts = new(); /// /// Initializes a new instance of the class. /// - /// The named pipe that this connection will handle. + /// The stream that this connection will handle. /// A registry of RPC services. - public RpcConnection(NamedPipeServerStream pipe, RpcServiceRegistry registry) + public RpcConnection(Stream stream, RpcServiceRegistry registry) { this.Id = Guid.CreateVersion7(); - this.pipe = pipe; + this.stream = stream; this.registry = registry; 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.AllowModificationWhileListening = true; @@ -72,11 +75,11 @@ internal class RpcConnection : IDisposable try { - this.pipe.Dispose(); + this.stream.Dispose(); } 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(); diff --git a/Dalamud/Networking/Rpc/RpcHostService.cs b/Dalamud/Networking/Rpc/RpcHostService.cs new file mode 100644 index 000000000..f164992eb --- /dev/null +++ b/Dalamud/Networking/Rpc/RpcHostService.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; + +using Dalamud.Logging.Internal; +using Dalamud.Networking.Rpc.Transport; +using Dalamud.Utility; + +namespace Dalamud.Networking.Rpc; + +/// +/// The Dalamud service repsonsible for hosting the RPC. +/// +[ServiceManager.EarlyLoadedService] +internal class RpcHostService : IServiceType, IInternalDisposableService +{ + private readonly ModuleLog log = new("RPC"); + private readonly RpcServiceRegistry registry = new(); + private readonly List transports = []; + + /// + /// Initializes a new instance of the class. + /// + [ServiceManager.ServiceConstructor] + public RpcHostService() + { + this.StartUnixTransport(); + this.StartPipeTransport(); + + if (this.transports.Count == 0) + { + this.log.Warning("No RPC hosts could be started on this platform"); + } + } + + /// + /// Gets all active RPC transports. + /// + public IReadOnlyList Transports => this.transports; + + /// + /// Add a new service Object to the RPC host. + /// + /// The object to add. + public void AddService(object service) => this.registry.AddService(service); + + /// + /// Add a new standalone method to the RPC host. + /// + /// The method name to add. + /// The handler to add. + public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler); + + /// + public void DisposeService() + { + foreach (var host in this.transports) + { + host.Dispose(); + } + + this.transports.Clear(); + } + + /// + public async Task InvokeClientAsync(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(method, arguments).ConfigureAwait(false); + } + + /// + 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 started Unix socket host: {Socket}", transport.SocketPath); + } + + private void StartPipeTransport() + { + // Wine doesn't support named pipes. + if (Util.IsWine()) + return; + + var transport = new PipeRpcTransport(this.registry); + this.transports.Add(transport); + transport.Start(); + this.log.Information("RpcHostService started named pipe host: {Pipe}", transport.PipeName); + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs b/Dalamud/Networking/Rpc/RpcServiceRegistry.cs similarity index 98% rename from Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs rename to Dalamud/Networking/Rpc/RpcServiceRegistry.cs index 71037d45e..6daea14bf 100644 --- a/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs +++ b/Dalamud/Networking/Rpc/RpcServiceRegistry.cs @@ -3,7 +3,7 @@ using System.Threading; using StreamJsonRpc; -namespace Dalamud.Networking.Pipes.Rpc; +namespace Dalamud.Networking.Rpc; /// /// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session. diff --git a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs similarity index 97% rename from Dalamud/Networking/Pipes/Internal/ClientHelloService.cs rename to Dalamud/Networking/Rpc/Service/ClientHelloService.cs index 9c182561e..041bc135f 100644 --- a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs +++ b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs @@ -3,12 +3,11 @@ using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; -using Dalamud.Networking.Pipes.Rpc; using Dalamud.Utility; using Lumina.Excel.Sheets; -namespace Dalamud.Networking.Pipes.Internal; +namespace Dalamud.Networking.Rpc.Service; /// /// A minimal service to respond with information about this client. diff --git a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs b/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs similarity index 97% rename from Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs rename to Dalamud/Networking/Rpc/Service/LinkHandlerService.cs index 3cc4af9f4..9fa311ede 100644 --- a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs +++ b/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using Dalamud.Logging.Internal; -using Dalamud.Networking.Pipes.Rpc; +using Dalamud.Networking.Rpc.Model; using Dalamud.Utility; -namespace Dalamud.Networking.Pipes.Internal; +namespace Dalamud.Networking.Rpc.Service; /// /// A service responsible for handling Dalamud URIs and dispatching them accordingly. diff --git a/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs new file mode 100644 index 000000000..ad7578eb4 --- /dev/null +++ b/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Dalamud.Networking.Rpc.Transport; + +/// +/// Interface for RPC host implementations (named pipes or Unix sockets). +/// +internal interface IRpcTransport : IDisposable +{ + /// + /// Gets a list of active RPC connections. + /// + IReadOnlyDictionary Connections { get; } + + /// Starts accepting client connections. + void Start(); + + /// Invoke an RPC request on a specific client expecting a result. + /// The client ID to invoke. + /// The method to invoke. + /// Any arguments to invoke. + /// An optional return based on the specified RPC. + /// The expected response type. + Task InvokeClientAsync(Guid clientId, string method, params object[] arguments); + + /// Send a notification to all connected clients (no response expected). + /// The method name to broadcast. + /// The arguments to broadcast. + /// Returns a Task when completed. + Task BroadcastNotifyAsync(string method, params object[] arguments); +} diff --git a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs similarity index 81% rename from Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs rename to Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs index ad1cc72cd..0cefeb853 100644 --- a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs +++ b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs @@ -9,26 +9,28 @@ using System.Threading.Tasks; using Dalamud.Logging.Internal; using Dalamud.Utility; -namespace Dalamud.Networking.Pipes.Rpc; +namespace Dalamud.Networking.Rpc.Transport; /// /// Simple multi-client JSON-RPC named pipe host using StreamJsonRpc. /// -internal class PipeRpcHost : IDisposable +internal class PipeRpcTransport : IRpcTransport { private readonly ModuleLog log = new("RPC/Host"); - private readonly RpcServiceRegistry registry = new(); + private readonly RpcServiceRegistry registry; private readonly CancellationTokenSource cts = new(); private readonly ConcurrentDictionary sessions = new(); private Task? acceptLoopTask; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// + /// The RPC service registry to use. /// The pipe name to create. - public PipeRpcHost(string? pipeName = null) + public PipeRpcTransport(RpcServiceRegistry registry, string? pipeName = null) { + this.registry = registry; // Default pipe name based on current process ID for uniqueness per Dalamud instance. this.PipeName = pipeName ?? $"DalamudRPC.{Environment.ProcessId}"; } @@ -38,16 +40,8 @@ internal class PipeRpcHost : IDisposable /// public string PipeName { get; } - /// Adds a local object exposing RPC methods callable by clients. - /// An arbitrary service object that will be introspected to add to RPC. - public void AddService(object service) => this.registry.AddService(service); - - /// - /// Adds a standalone JSON-RPC method callable by clients. - /// - /// The name to add. - /// The delegate that acts as the handler. - public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler); + /// + public IReadOnlyDictionary Connections => this.sessions; /// Starts accepting client connections. public void Start() @@ -86,12 +80,6 @@ internal class PipeRpcHost : IDisposable return Task.WhenAll(tasks); } - /// - /// Gets a list of connected client IDs. - /// - /// Connected client IDs. - public IReadOnlyCollection GetClientIds() => this.sessions.Keys.AsReadOnlyCollection(); - /// public void Dispose() { diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs new file mode 100644 index 000000000..3019f5aaf --- /dev/null +++ b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs @@ -0,0 +1,223 @@ +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; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Networking.Rpc.Transport; + +/// +/// Simple multi-client JSON-RPC Unix socket host using StreamJsonRpc. +/// +internal class UnixRpcTransport : IRpcTransport +{ + private readonly ModuleLog log = new("RPC/UnixHost"); + + private readonly RpcServiceRegistry registry; + private readonly CancellationTokenSource cts = new(); + private readonly ConcurrentDictionary sessions = new(); + private readonly string? cleanupSocketDirectory; + + private Task? acceptLoopTask; + private Socket? listenSocket; + + /// + /// Initializes a new instance of the class. + /// + /// The RPC service registry to use. + /// The Unix socket path to create. If null, defaults to a path based on process ID. + public UnixRpcTransport(RpcServiceRegistry registry, string? socketPath = null) + { + this.registry = registry; + + if (socketPath != null) + { + this.SocketPath = socketPath; + } + else + { + var dalamudConfigPath = Service.Get().StartInfo.ConfigurationPath; + var dalamudHome = Path.GetDirectoryName(dalamudConfigPath); + var socketName = $"DalamudRPC.{Environment.ProcessId}.sock"; + + if (dalamudHome == null) + { + this.SocketPath = Path.Combine(Path.GetTempPath(), socketName); + this.log.Warning("Dalamud home is empty! UDS socket will be in temp."); + } + else + { + this.SocketPath = Path.Combine(dalamudHome, socketName); + this.cleanupSocketDirectory = dalamudHome; + } + } + } + + /// + /// Gets the path of the Unix socket this RPC host is using. + /// + public string SocketPath { get; } + + /// + public IReadOnlyDictionary Connections => this.sessions; + + /// Starts accepting client connections. + 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)) + { + try + { + Directory.CreateDirectory(socketDir); + } + catch (Exception ex) + { + this.log.Error(ex, "Failed to create socket directory: {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); + + // note: needs to be run _after_ we're alive so that we don't delete our own socket. + if (this.cleanupSocketDirectory != null) + { + Task.Run(async () => await UnixSocketUtil.CleanStaleSockets(this.cleanupSocketDirectory)); + } + } + + /// Invoke an RPC request on a specific client expecting a result. + /// The client ID to invoke. + /// The method to invoke. + /// Any arguments to invoke. + /// An optional return based on the specified RPC. + /// The expected response type. + public Task InvokeClientAsync(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(method, arguments); + } + + /// Send a notification to all connected clients (no response expected). + /// The method name to broadcast. + /// The arguments to broadcast. + /// Returns a Task when completed. + public Task BroadcastNotifyAsync(string method, params object[] arguments) + { + var list = this.sessions.Values; + var tasks = new List(list.Count); + foreach (var s in list) + { + tasks.Add(s.Rpc.NotifyAsync(method, arguments)); + } + + return Task.WhenAll(tasks); + } + + /// + 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() + { + this.log.Information("UnixRpcHost starting on socket {Socket}", this.SocketPath); + 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"); + } + } +} diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs index 5d2d32728..bff5c8ba2 100644 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; -using Dalamud.Networking.Pipes; +using Dalamud.Networking.Rpc.Model; namespace Dalamud.Plugin.Services; diff --git a/Dalamud/Utility/UnixSocketUtil.cs b/Dalamud/Utility/UnixSocketUtil.cs new file mode 100644 index 000000000..46bb05c74 --- /dev/null +++ b/Dalamud/Utility/UnixSocketUtil.cs @@ -0,0 +1,92 @@ +using System.IO; +using System.Net.Sockets; +using System.Threading.Tasks; + +using Serilog; + +namespace Dalamud.Utility; + +/// +/// A set of utilities to help manage Unix sockets. +/// +internal static class UnixSocketUtil +{ + // Default probe timeout in milliseconds. + private const int DefaultProbeMs = 200; + + /// + /// Test whether a Unix socket is alive/listening. + /// + /// The path to test. + /// How long to wait for a connection success. + /// A task result representing if a socket is alive or not. + public static async Task IsSocketAlive(string path, int timeoutMs = DefaultProbeMs) + { + if (string.IsNullOrEmpty(path)) return false; + var endpoint = new UnixDomainSocketEndPoint(path); + using var client = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + + var connectTask = client.ConnectAsync(endpoint); + var completed = await Task.WhenAny(connectTask, Task.Delay(timeoutMs)).ConfigureAwait(false); + + if (completed == connectTask) + { + // Connected or failed very quickly. If the task is successful, the socket is alive. + if (connectTask.IsCompletedSuccessfully) + { + try + { + client.Shutdown(SocketShutdown.Both); + } + catch + { + // ignored + } + + return true; + } + } + + return false; + } + + /// + /// Find and remove stale Dalamud RPC sockets. + /// + /// The directory to scan for stale sockets. + /// The timeout to wait for a connection attempt to succeed. + /// A task that executes when sockets are purged. + public static async Task CleanStaleSockets(string directory, int probeTimeoutMs = DefaultProbeMs) + { + if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) return; + + foreach (var file in Directory.EnumerateFiles(directory, "DalamudRPC.*.sock", SearchOption.TopDirectoryOnly)) + { + // we don't need to check ourselves. + if (file.Contains(Environment.ProcessId.ToString())) continue; + + bool shouldDelete; + + try + { + shouldDelete = !await IsSocketAlive(file, probeTimeoutMs); + } + catch + { + shouldDelete = true; + } + + if (shouldDelete) + { + try + { + File.Delete(file); + } + catch (Exception ex) + { + Log.Error(ex, "Could not delete stale socket file: {File}", file); + } + } + } + } +}