Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
Expand Down
1 change: 1 addition & 0 deletions src/XIVLauncher.Common.Windows/WindowsDalamudRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public WindowsDalamudRunner(DirectoryInfo dotnetRuntimePath)
DalamudInjectorArgs.LoggingPath(dalamudStartInfo.LoggingPath),
DalamudInjectorArgs.PluginDirectory(dalamudStartInfo.PluginDirectory),
DalamudInjectorArgs.AssetDirectory(dalamudStartInfo.AssetDirectory),
DalamudInjectorArgs.TempDirectory(dalamudStartInfo.TempDirectory),
DalamudInjectorArgs.ClientLanguage((int)dalamudStartInfo.Language),
DalamudInjectorArgs.DelayInitialize(dalamudStartInfo.DelayInitializeMs),
DalamudInjectorArgs.TsPackB64(Convert.ToBase64String(Encoding.UTF8.GetBytes(dalamudStartInfo.TroubleshootingPackData))),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworks>net9.0;netstandard2.0</TargetFrameworks>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Deterministic>true</Deterministic>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
Expand Down
3 changes: 2 additions & 1 deletion src/XIVLauncher.Common/Dalamud/DalamudInjectorArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ public static class DalamudInjectorArgs
public static string ConfigurationPath(string path) => $"--dalamud-configuration-path=\"{path}\"";
public static string PluginDirectory(string path) => $"--dalamud-plugin-directory=\"{path}\"";
public static string AssetDirectory(string path) => $"--dalamud-asset-directory=\"{path}\"";
public static string TempDirectory(string path) => $"--dalamud-temp-directory=\"{path}\"";
public static string ClientLanguage(int language) => $"--dalamud-client-language={language}";
public static string DelayInitialize(int delay) => $"--dalamud-delay-initialize={delay}";
public static string TsPackB64(string data) => $"--dalamud-tspack-b64={data}";
}
}
}
5 changes: 4 additions & 1 deletion src/XIVLauncher.Common/Dalamud/DalamudLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ public Process Run(FileInfo gameExe, string gameArgs, IDictionary<string, string
Log.Information("[HOOKS] DalamudLauncher::Run(gp:{0}, cl:{1})", this.gamePath.FullName, this.language);

var ingamePluginPath = Path.Combine(this.configDirectory.FullName, "installedPlugins");

Directory.CreateDirectory(ingamePluginPath);

var tempDir = Path.Combine(Path.GetTempPath(), "XIVLauncher");
Directory.CreateDirectory(tempDir);

var startInfo = new DalamudStartInfo
{
Language = language,
Expand All @@ -98,6 +100,7 @@ public Process Run(FileInfo gameExe, string gameArgs, IDictionary<string, string
AssetDirectory = this.updater.AssetDirectory.FullName,
GameVersion = Repository.Ffxiv.GetVer(gamePath),
WorkingDirectory = this.updater.Runner.Directory?.FullName,
TempDirectory = tempDir,
DelayInitializeMs = this.injectionDelay,
TroubleshootingPackData = this.troubleshootingData,
};
Expand Down
3 changes: 2 additions & 1 deletion src/XIVLauncher.Common/Dalamud/DalamudStartInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ public sealed class DalamudStartInfo

public string PluginDirectory;
public string AssetDirectory;
public string TempDirectory;
public ClientLanguage Language;
public int DelayInitializeMs;

public string GameVersion;
public string TroubleshootingPackData;
}
}
}
47 changes: 47 additions & 0 deletions src/XIVLauncher.Common/Dalamud/Rpc/DalamudRpcClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using StreamJsonRpc;

namespace XIVLauncher.Common.Dalamud.Rpc;

public class DalamudRpcClient : IAsyncDisposable
{
private readonly Socket socket;
private readonly Stream stream;
private readonly JsonRpc rpc;

public IDalamudRpc Proxy { get; }

public string SocketPath { get; }

private DalamudRpcClient(string socketPath, Socket socket)
{
this.SocketPath = socketPath;
this.socket = socket;

this.stream = new NetworkStream(socket, ownsSocket: false);
var handler = new HeaderDelimitedMessageHandler(this.stream, this.stream, new JsonMessageFormatter());
this.rpc = new JsonRpc(handler);
this.rpc.StartListening();
this.Proxy = this.rpc.Attach<IDalamudRpc>();
}

public static async Task<DalamudRpcClient> ConnectAsync(string socketPath, CancellationToken cancellationToken = default)
{
var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
await socket.ConnectAsync(new UnixDomainSocketEndPoint(socketPath), cancellationToken).ConfigureAwait(false);

return new DalamudRpcClient(socketPath, socket);
}

public async ValueTask DisposeAsync()
{
this.rpc.Dispose();
await this.stream.DisposeAsync().ConfigureAwait(false);

this.socket.Dispose();
}
}
106 changes: 106 additions & 0 deletions src/XIVLauncher.Common/Dalamud/Rpc/DalamudRpcDiscovery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
using XIVLauncher.Common.Dalamud.Rpc.Types;

namespace XIVLauncher.Common.Dalamud.Rpc;

public class DalamudRpcDiscovery(string? searchPath = null, int connectionTimeoutMs = 100)
{
private readonly string searchPath = searchPath ?? Path.Combine(Path.GetTempPath(), "XIVLauncher");

public async IAsyncEnumerable<DiscoveredClient> SearchAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (!Directory.Exists(this.searchPath))
{
yield break;
}

// Find candidate socket files using known pattern
var candidates = Directory.GetFiles(this.searchPath, "DalamudRPC.*.sock", SearchOption.TopDirectoryOnly);

foreach (var socketPath in candidates)
{
cancellationToken.ThrowIfCancellationRequested();

Log.Information("Found candidate socket: {SocketPath}", socketPath);
var discoveredClient = await this.TryConnectClientAsync(socketPath, cancellationToken).ConfigureAwait(false);

if (discoveredClient != null)
{
yield return discoveredClient;
}
}
}

private async Task<DiscoveredClient?> TryConnectClientAsync(string socketPath, CancellationToken cancellationToken)
{
DalamudRpcClient? client = null;

try
{
client = await DalamudRpcClient.ConnectAsync(socketPath, cancellationToken).ConfigureAwait(false);

var helloRequest = new ClientHelloRequest();
var response = await HelloWithTimeoutAsync(client.Proxy, helloRequest, connectionTimeoutMs, cancellationToken).ConfigureAwait(false);
Log.Debug("Received hello response from socket {SocketPath}: {@Response}", socketPath, response);

if (!string.IsNullOrEmpty(response.ClientState))
{
var discoveredClient = new DiscoveredClient(response, client);
client = null;

return discoveredClient;
}

return null;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
Log.Debug(ex, "Failed to attach to socket: {SocketPath}", socketPath);
return null;
}
finally
{
if (client != null)
{
await client.DisposeAsync().ConfigureAwait(false);
}
}
}

private static async Task<ClientHelloResponse> HelloWithTimeoutAsync(
IDalamudRpc rpcProxy,
ClientHelloRequest request,
int timeoutMs,
CancellationToken cancellationToken)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(timeoutMs);

try
{
return await rpcProxy.HelloAsync(request).ConfigureAwait(false);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
throw new TimeoutException($"Hello request timed out after {timeoutMs}ms");
}
}
}

public record DiscoveredClient(ClientHelloResponse HelloResponse, DalamudRpcClient Client) : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await this.Client.DisposeAsync().ConfigureAwait(false);
}
}
16 changes: 16 additions & 0 deletions src/XIVLauncher.Common/Dalamud/Rpc/IDalamudRpc.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Threading.Tasks;
using StreamJsonRpc;
using XIVLauncher.Common.Dalamud.Rpc.Types;

namespace XIVLauncher.Common.Dalamud.Rpc;

// WARNING: Do not alter this file without coordinating with the Dalamud team.

public interface IDalamudRpc
{
[JsonRpcMethod("hello")]
Task<ClientHelloResponse> HelloAsync(ClientHelloRequest request);

[JsonRpcMethod("handleLink")]
Task HandleLinkAsync(string link);
}
49 changes: 49 additions & 0 deletions src/XIVLauncher.Common/Dalamud/Rpc/Types/ClientHello.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace XIVLauncher.Common.Dalamud.Rpc.Types;

// WARNING: Do not alter this file without coordinating with the Dalamud team.

public record ClientHelloRequest
{
/// <summary>
/// Gets the API version this client is expecting.
/// </summary>
public string ApiVersion { get; init; } = "1.0";

/// <summary>
/// Gets the user agent of the client.
/// </summary>
public string UserAgent { get; init; } = "XIVLauncher/1.0";
}

public 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 an identifier for this client.
/// </summary>
public string? ClientState { get; init; }
}
94 changes: 94 additions & 0 deletions src/XIVLauncher.Common/Util/SocketHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading.Tasks;
using Serilog;

namespace XIVLauncher.Common.Util;

/// <summary>
/// A set of utilities to help manage Unix sockets.
/// </summary>
internal static class SocketHelpers
{
// Default probe timeout in milliseconds.
private const int DefaultProbeMs = 200;

/// <summary>
/// Test whether a Unix socket is alive/listening.
/// </summary>
/// <param name="path">The path to test.</param>
/// <param name="timeoutMs">How long to wait for a connection success.</param>
/// <returns>A task result representing if a socket is alive or not.</returns>
public static async Task<bool> 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;
}

/// <summary>
/// Find and remove stale Dalamud RPC sockets.
/// </summary>
/// <param name="directory">The directory to scan for stale sockets.</param>
/// <param name="searchPattern">The search pattern to find socket files.</param>
/// <param name="probeTimeoutMs">The timeout to wait for a connection attempt to succeed.</param>
/// <returns>A task that executes when sockets are purged.</returns>
public static async Task CleanStaleSockets(string directory, string searchPattern = "DalamudRPC.*.sock", int probeTimeoutMs = DefaultProbeMs)
{
if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) return;

foreach (var file in Directory.EnumerateFiles(directory, searchPattern, 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);
}
}
}
}
}
Loading