Skip to content

Commit

Permalink
Rework protocol registration
Browse files Browse the repository at this point in the history
  • Loading branch information
erri120 committed Jun 26, 2024
1 parent f2bbcb4 commit 3a73ded
Show file tree
Hide file tree
Showing 12 changed files with 178 additions and 173 deletions.
1 change: 1 addition & 0 deletions NexusMods.App.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EA/@EntryIndexedValue">EA</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GOG/@EntryIndexedValue">GOG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=JWT/@EntryIndexedValue">JWT</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MIME/@EntryIndexedValue">MIME</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=VM/@EntryIndexedValue">VM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/Abbreviations/=LG/@EntryIndexedValue">LG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/Abbreviations/=MD/@EntryIndexedValue">MD</s:String>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NexusMods.CrossPlatform.ProtocolRegistration;

namespace NexusMods.Networking.NexusWebApi;

internal class HandlerRegistration : IHostedService
{
private readonly ILogger _logger;
private readonly IProtocolRegistration _protocolRegistration;

public HandlerRegistration(
ILogger<HandlerRegistration> logger,
IProtocolRegistration protocolRegistration)
{
_logger = logger;
_protocolRegistration = protocolRegistration;
}

public Task StartAsync(CancellationToken cancellationToken)
{
_ = Task.Run(async () =>
{
try
{
await _protocolRegistration.RegisterHandler(uriScheme: "nxm", cancellationToken: cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, "Exception while registering handler for nxm links");
}
}, cancellationToken);

return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,6 @@ public LoginManager(
/// <param name="token"></param>
public async Task LoginAsync(CancellationToken token = default)
{
// temporary but if we want oauth to work we _have_ to be registered as the nxm handler
await _protocolRegistration.RegisterSelf("nxm");

JwtTokenReply? jwtToken;
try
{
Expand All @@ -137,7 +134,7 @@ public async Task LoginAsync(CancellationToken token = default)

using var tx = _conn.BeginTransaction();

var newTokenEntity = JWTToken.Create(_conn.Db, tx, jwtToken!);
var newTokenEntity = JWTToken.Create(_conn.Db, tx, jwtToken);
if (newTokenEntity is null)
{
_logger.LogError("Invalid new token data");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,4 @@
<ProjectReference Include="..\..\NexusMods.ProxyConsole.Abstractions\NexusMods.ProxyConsole.Abstractions.csproj" />
<PackageReference Include="NexusMods.MnemonicDB.SourceGenerator" PrivateAssets="all" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
<Folder Include="Messages\" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public static IServiceCollection AddNexusWebApi(this IServiceCollection collecti
return collection
.AddAllSingleton<ILoginManager, LoginManager>()
.AddAllSingleton<INexusApiClient, NexusApiClient>()
.AddHostedService<HandlerRegistration>()
.AddNexusApiVerbs();
}
}
7 changes: 3 additions & 4 deletions src/NexusMods.App.Cli/ProtocolVerbs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@ public static IServiceCollection AddProtocolVerbs(this IServiceCollection servic


[Verb("associate-nxm", "Associate the nxm:// protocol with this application")]
private static Task<int> AssociateNxm([Injected] IProtocolRegistration protocolRegistration)
private static async Task<int> AssociateNxm([Injected] IProtocolRegistration protocolRegistration)
{
protocolRegistration.RegisterSelf("nxm");
return Task.FromResult(0);
await protocolRegistration.RegisterHandler("nxm");
return 0;
}


[Verb("download-and-install-mod", "Download a mod and install it in one step")]
private static async Task<int> DownloadAndInstallMod([Injected] IRenderer renderer,
[Option("u","url", "The url of the mod to download")] Uri uri,
Expand Down
4 changes: 4 additions & 0 deletions src/NexusMods.CrossPlatform/NexusMods.CrossPlatform.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
<InternalsVisibleTo Include="NexusMods.CrossPlatform.Tests" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="../NexusMods.App/com.nexusmods.app.desktop" />
</ItemGroup>

<ItemGroup>
<Compile Update="Process\AOSInterop.cs">
<DependentUpon>IOSInterop.cs</DependentUpon>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,12 @@
using NexusMods.Paths;

namespace NexusMods.CrossPlatform.ProtocolRegistration;

/// <summary>
/// deals with protocol registration, that is: setting up this application system wide
/// as the default handler for a custom protocol (e.g. nxm://...)
/// For the actual code to be invoked when such a url is received, see NexusMods.Cli.ProtocolInvokation
///
/// This is platform dependent functionality
/// Abstracts OS-specific protocol registration logic.
/// </summary>
public interface IProtocolRegistration
{
/// <summary>
/// register this application as a handler for a protocol (e.g. nxm://...).
/// This should be called every time the application runs for every protocol it handles
/// </summary>
/// <param name="protocol">The protocol to register for</param>
/// <returns>the previous handler, if any</returns>
Task<string?> RegisterSelf(string protocol);

/// <summary>
/// register an arbitrary command line as the handler for a protocol.
/// The primary usecase for this is to unregister the current application, potentially
/// restoring a previous handler
/// </summary>
/// <param name="protocol">The protocol to register for</param>
/// <param name="friendlyName">Arbitrary friendly name for the protocol</param>
/// <param name="workingDirectory">The directory inside which the program is executed</param>
/// <param name="commandLine">The full commandline</param>
/// <returns>the previous handler, if any</returns>
Task<string?> Register(string protocol, string friendlyName, string workingDirectory, string commandLine);

/// <summary>
/// determine if this application is the handler for a protocol. This is based on the full url
/// of the calling process so another installation of the same application would _not_ count
/// Registers the App as a protocol handler for <paramref name="uriScheme"/>.
/// </summary>
/// <param name="protocol">The protocol to check for</param>
Task<bool> IsSelfHandler(string protocol);
Task RegisterHandler(string uriScheme, bool setAsDefaultHandler = true, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Runtime.Versioning;
using System.Text;
using CliWrap;
using Microsoft.Extensions.Logging;
using NexusMods.App.BuildInfo;
using NexusMods.CrossPlatform.Process;
using NexusMods.Paths;

Expand All @@ -12,81 +14,119 @@ namespace NexusMods.CrossPlatform.ProtocolRegistration;
[SupportedOSPlatform("linux")]
public class ProtocolRegistrationLinux : IProtocolRegistration
{
private const string BaseId = "nexusmods-app";
private const string ApplicationId = "com.nexusmods.app";
private const string DesktopFile = $"{ApplicationId}.desktop";
private const string DesktopFileResourceName = $"NexusMods.CrossPlatform.{DesktopFile}";
private const string ExecutablePathPlaceholder = "${INSTALL_EXEC}";

private readonly ILogger _logger;
private readonly IProcessFactory _processFactory;
private readonly IFileSystem _fileSystem;
private readonly IOSInterop _osInterop;

/// <summary>
/// Constructor.
/// </summary>
public ProtocolRegistrationLinux(IProcessFactory processFactory, IFileSystem fileSystem, IOSInterop osInterop)
public ProtocolRegistrationLinux(
ILogger<ProtocolRegistrationLinux> logger,
IProcessFactory processFactory,
IFileSystem fileSystem,
IOSInterop osInterop)
{
_logger = logger;
_processFactory = processFactory;
_fileSystem = fileSystem;
_osInterop = osInterop;
}

/// <inheritdoc/>
public async Task<string?> RegisterSelf(string protocol)
public async Task RegisterHandler(string uriScheme, bool setAsDefaultHandler = true, CancellationToken cancellationToken = default)
{
var executable = _osInterop.GetOwnExe();

return await Register(
protocol,
friendlyName: $"{BaseId}-{protocol}.desktop",
workingDirectory: executable.Directory,
commandLine: $"{EscapeWhitespaceForCli(executable)} %u"
);
if (CompileConstants.InstallationMethod != InstallationMethod.PackageManager)
{
var applicationsDirectory = _fileSystem.GetKnownPath(KnownPath.XDG_DATA_HOME).Combine("applications");
_logger.LogInformation("Using applications directory `{Path}`", applicationsDirectory);

try
{
await CreateDesktopFile(applicationsDirectory, cancellationToken: cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, "Exception while creating desktop file, the handler for `{Scheme}` might not work", uriScheme);
return;
}

try
{
await UpdateMIMECacheDatabase(applicationsDirectory, cancellationToken: cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, "Exception while updating MIME cache database, see process logs for more details");
return;
}
}

if (setAsDefaultHandler)
{
try
{
await SetAsDefaultHandler(uriScheme, cancellationToken: cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, "Exception while setting the default handler for `{Scheme}`, see the process logs for more details", uriScheme);
}
}
}

private static string EscapeWhitespaceForCli(AbsolutePath path) => path.ToString().Replace(" ", @"\ ");

/// <inheritdoc/>
public async Task<string?> Register(string protocol, string friendlyName, string workingDirectory, string commandLine)
private async Task CreateDesktopFile(AbsolutePath applicationsDirectory, CancellationToken cancellationToken = default)
{
var applicationsFolder = _fileSystem.GetKnownPath(KnownPath.HomeDirectory)
.Combine(".local/share/applications");
var filePath = applicationsDirectory.Combine(DesktopFile);
var backupPath = filePath.AppendExtension(new Extension(".bak"));

applicationsFolder.CreateDirectory();
if (filePath.FileExists)
{
_logger.LogInformation("Moving existing desktop file from `{From}` to `{To}`", filePath, backupPath);
}

var desktopEntryFile = applicationsFolder.Combine(friendlyName);
_logger.LogInformation("Creating desktop file at `{Path}`", filePath);

var sb = new StringBuilder();
sb.AppendLine("[Desktop Entry]");
sb.AppendLine($"Name=NexusMods.App {protocol.ToUpper()} Handler");
sb.AppendLine("Terminal=false");
sb.AppendLine("Type=Application");
sb.AppendLine($"Path={workingDirectory}");
sb.AppendLine($"Exec={commandLine}");
sb.AppendLine($"MimeType=x-scheme-handler/{protocol}");
sb.AppendLine("NoDisplay=true");
await using var stream = typeof(ProtocolRegistrationLinux).Assembly.GetManifestResourceStream(DesktopFileResourceName);
if (stream is null)
{
_logger.LogError($"Manifest resource Stream for `{DesktopFileResourceName}` is null!");
return;
}

await desktopEntryFile.WriteAllTextAsync(sb.ToString());
using var sr = new StreamReader(stream, encoding: Encoding.UTF8);

var command = Cli.Wrap("update-desktop-database")
.WithArguments(applicationsFolder.ToString());
var text = await sr.ReadToEndAsync(cancellationToken);
text = text.Replace(ExecutablePathPlaceholder, EscapeWhitespaceForCli(_osInterop.GetOwnExe()));

await _processFactory.ExecuteAsync(command);
return null;
await filePath.WriteAllTextAsync(text, cancellationToken);
}

/// <inheritdoc/>
public async Task<bool> IsSelfHandler(string protocol)
private async Task UpdateMIMECacheDatabase(AbsolutePath applicationsDirectory, CancellationToken cancellationToken = default)
{
var stdOutBuffer = new StringBuilder();
_logger.LogInformation("Updating MIME cache database");

var command = Cli.Wrap("xdg-settings")
.WithArguments($"check default-url-scheme-handler {protocol} {BaseId}-{protocol}.desktop")
.WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOutBuffer));
var command = Cli
.Wrap("update-desktop-database")
.WithArguments(EscapeWhitespaceForCli(applicationsDirectory));

var res = await _processFactory.ExecuteAsync(command);
if (res.ExitCode != 0) return false;
await _processFactory.ExecuteAsync(command, cancellationToken: cancellationToken);
}

var stdOut = stdOutBuffer.ToString();
private async Task SetAsDefaultHandler(string uriScheme, CancellationToken cancellationToken = default)
{
var command = Cli
.Wrap("xdg-settings")
.WithArguments($"set default-url-scheme-handler {uriScheme} {DesktopFile}");

// might end with 0xA (LF)
return stdOut.StartsWith("yes", StringComparison.InvariantCultureIgnoreCase);
await _processFactory.ExecuteAsync(command, cancellationToken: cancellationToken);
}

private string EscapeWhitespaceForCli(AbsolutePath path) => path.ToNativeSeparators(_fileSystem.OS).Replace(" ", @"\ ");
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
using NexusMods.Paths;

namespace NexusMods.CrossPlatform.ProtocolRegistration;

/// <summary>
/// Protocol registration of OSX
/// Protocol registration of OSX.
/// </summary>
public class ProtocolRegistrationOSX : IProtocolRegistration
{
/// <inheritdoc />
public Task<string?> RegisterSelf(string protocol)
{
throw new NotImplementedException();
}

/// <inheritdoc />
public Task<string?> Register(string protocol, string friendlyName, string workingDirectory, string commandLine)
{
throw new NotImplementedException();
}

/// <inheritdoc />
public Task<bool> IsSelfHandler(string protocol)
/// <inheritdoc/>
public Task RegisterHandler(string uriScheme, bool setAsDefaultHandler = true, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
Expand Down
Loading

0 comments on commit 3a73ded

Please sign in to comment.