diff --git a/NexusMods.App.sln.DotSettings b/NexusMods.App.sln.DotSettings
index d5f1baf00b..de3541f942 100644
--- a/NexusMods.App.sln.DotSettings
+++ b/NexusMods.App.sln.DotSettings
@@ -41,6 +41,7 @@
EA
GOG
JWT
+ MIME
VM
LG
MD
diff --git a/src/Networking/NexusMods.Networking.NexusWebApi/HandlerRegistration.cs b/src/Networking/NexusMods.Networking.NexusWebApi/HandlerRegistration.cs
new file mode 100644
index 0000000000..10053e5953
--- /dev/null
+++ b/src/Networking/NexusMods.Networking.NexusWebApi/HandlerRegistration.cs
@@ -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 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;
+ }
+}
diff --git a/src/Networking/NexusMods.Networking.NexusWebApi/LoginManager.cs b/src/Networking/NexusMods.Networking.NexusWebApi/LoginManager.cs
index edfb0b3aaf..1a46b94814 100644
--- a/src/Networking/NexusMods.Networking.NexusWebApi/LoginManager.cs
+++ b/src/Networking/NexusMods.Networking.NexusWebApi/LoginManager.cs
@@ -110,9 +110,6 @@ public LoginManager(
///
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
{
@@ -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");
diff --git a/src/Networking/NexusMods.Networking.NexusWebApi/NexusMods.Networking.NexusWebApi.csproj b/src/Networking/NexusMods.Networking.NexusWebApi/NexusMods.Networking.NexusWebApi.csproj
index 14e7f9dc20..50ea30d993 100644
--- a/src/Networking/NexusMods.Networking.NexusWebApi/NexusMods.Networking.NexusWebApi.csproj
+++ b/src/Networking/NexusMods.Networking.NexusWebApi/NexusMods.Networking.NexusWebApi.csproj
@@ -19,8 +19,4 @@
-
-
-
-
diff --git a/src/Networking/NexusMods.Networking.NexusWebApi/Services.cs b/src/Networking/NexusMods.Networking.NexusWebApi/Services.cs
index 66d3b04646..602dd01ef8 100644
--- a/src/Networking/NexusMods.Networking.NexusWebApi/Services.cs
+++ b/src/Networking/NexusMods.Networking.NexusWebApi/Services.cs
@@ -46,6 +46,7 @@ public static IServiceCollection AddNexusWebApi(this IServiceCollection collecti
return collection
.AddAllSingleton()
.AddAllSingleton()
+ .AddHostedService()
.AddNexusApiVerbs();
}
}
diff --git a/src/NexusMods.App.Cli/ProtocolVerbs.cs b/src/NexusMods.App.Cli/ProtocolVerbs.cs
index c80fc82ce2..98fad4ff4d 100644
--- a/src/NexusMods.App.Cli/ProtocolVerbs.cs
+++ b/src/NexusMods.App.Cli/ProtocolVerbs.cs
@@ -30,13 +30,12 @@ public static IServiceCollection AddProtocolVerbs(this IServiceCollection servic
[Verb("associate-nxm", "Associate the nxm:// protocol with this application")]
- private static Task AssociateNxm([Injected] IProtocolRegistration protocolRegistration)
+ private static async Task 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 DownloadAndInstallMod([Injected] IRenderer renderer,
[Option("u","url", "The url of the mod to download")] Uri uri,
diff --git a/src/NexusMods.CrossPlatform/NexusMods.CrossPlatform.csproj b/src/NexusMods.CrossPlatform/NexusMods.CrossPlatform.csproj
index 1c129ad085..9e01986953 100644
--- a/src/NexusMods.CrossPlatform/NexusMods.CrossPlatform.csproj
+++ b/src/NexusMods.CrossPlatform/NexusMods.CrossPlatform.csproj
@@ -12,6 +12,10 @@
+
+
+
+
IOSInterop.cs
diff --git a/src/NexusMods.CrossPlatform/ProtocolRegistration/IProtocolRegistration.cs b/src/NexusMods.CrossPlatform/ProtocolRegistration/IProtocolRegistration.cs
index e987c17545..d1d1a44094 100644
--- a/src/NexusMods.CrossPlatform/ProtocolRegistration/IProtocolRegistration.cs
+++ b/src/NexusMods.CrossPlatform/ProtocolRegistration/IProtocolRegistration.cs
@@ -1,40 +1,12 @@
-using NexusMods.Paths;
-
namespace NexusMods.CrossPlatform.ProtocolRegistration;
///
-/// 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.
///
public interface IProtocolRegistration
{
///
- /// 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
- ///
- /// The protocol to register for
- /// the previous handler, if any
- Task RegisterSelf(string protocol);
-
- ///
- /// 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
- ///
- /// The protocol to register for
- /// Arbitrary friendly name for the protocol
- /// The directory inside which the program is executed
- /// The full commandline
- /// the previous handler, if any
- Task Register(string protocol, string friendlyName, string workingDirectory, string commandLine);
-
- ///
- /// 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 .
///
- /// The protocol to check for
- Task IsSelfHandler(string protocol);
+ Task RegisterHandler(string uriScheme, bool setAsDefaultHandler = true, CancellationToken cancellationToken = default);
}
diff --git a/src/NexusMods.CrossPlatform/ProtocolRegistration/ProtocolRegistrationLinux.cs b/src/NexusMods.CrossPlatform/ProtocolRegistration/ProtocolRegistrationLinux.cs
index d3ed127397..20c9b819e7 100644
--- a/src/NexusMods.CrossPlatform/ProtocolRegistration/ProtocolRegistrationLinux.cs
+++ b/src/NexusMods.CrossPlatform/ProtocolRegistration/ProtocolRegistrationLinux.cs
@@ -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;
@@ -12,8 +14,12 @@ 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;
@@ -21,72 +27,106 @@ public class ProtocolRegistrationLinux : IProtocolRegistration
///
/// Constructor.
///
- public ProtocolRegistrationLinux(IProcessFactory processFactory, IFileSystem fileSystem, IOSInterop osInterop)
+ public ProtocolRegistrationLinux(
+ ILogger logger,
+ IProcessFactory processFactory,
+ IFileSystem fileSystem,
+ IOSInterop osInterop)
{
+ _logger = logger;
_processFactory = processFactory;
_fileSystem = fileSystem;
_osInterop = osInterop;
}
///
- public async Task 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(" ", @"\ ");
-
- ///
- public async Task 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);
}
- ///
- public async Task 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(" ", @"\ ");
}
diff --git a/src/NexusMods.CrossPlatform/ProtocolRegistration/ProtocolRegistrationOSX.cs b/src/NexusMods.CrossPlatform/ProtocolRegistration/ProtocolRegistrationOSX.cs
index c62d8e747b..f5fcd3f145 100644
--- a/src/NexusMods.CrossPlatform/ProtocolRegistration/ProtocolRegistrationOSX.cs
+++ b/src/NexusMods.CrossPlatform/ProtocolRegistration/ProtocolRegistrationOSX.cs
@@ -1,26 +1,12 @@
-using NexusMods.Paths;
-
namespace NexusMods.CrossPlatform.ProtocolRegistration;
///
-/// Protocol registration of OSX
+/// Protocol registration of OSX.
///
public class ProtocolRegistrationOSX : IProtocolRegistration
{
- ///
- public Task RegisterSelf(string protocol)
- {
- throw new NotImplementedException();
- }
-
- ///
- public Task Register(string protocol, string friendlyName, string workingDirectory, string commandLine)
- {
- throw new NotImplementedException();
- }
-
- ///
- public Task IsSelfHandler(string protocol)
+ ///
+ public Task RegisterHandler(string uriScheme, bool setAsDefaultHandler = true, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
diff --git a/src/NexusMods.CrossPlatform/ProtocolRegistration/ProtocolRegistrationWindows.cs b/src/NexusMods.CrossPlatform/ProtocolRegistration/ProtocolRegistrationWindows.cs
index b7492932f7..88befbc427 100644
--- a/src/NexusMods.CrossPlatform/ProtocolRegistration/ProtocolRegistrationWindows.cs
+++ b/src/NexusMods.CrossPlatform/ProtocolRegistration/ProtocolRegistrationWindows.cs
@@ -1,4 +1,5 @@
using System.Runtime.Versioning;
+using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using NexusMods.CrossPlatform.Process;
using NexusMods.Paths;
@@ -11,54 +12,65 @@ namespace NexusMods.CrossPlatform.ProtocolRegistration;
[SupportedOSPlatform("windows")]
public class ProtocolRegistrationWindows : IProtocolRegistration
{
+ private readonly ILogger _logger;
private readonly IOSInterop _osInterop;
+ private readonly IFileSystem _fileSystem;
///
- /// constructor
+ /// Constructor.
///
- public ProtocolRegistrationWindows(IOSInterop osInterop)
+ public ProtocolRegistrationWindows(
+ ILogger logger,
+ IOSInterop osInterop,
+ IFileSystem fileSystem)
{
+ _logger = logger;
_osInterop = osInterop;
+ _fileSystem = fileSystem;
}
///
- public Task IsSelfHandler(string protocol)
+ public Task RegisterHandler(string uriScheme, bool setAsDefaultHandler = true, CancellationToken cancellationToken = default)
{
- using var key = GetClassKey(protocol);
- using var commandKey = GetCommandKey(key);
+ try
+ {
+ UpdateRegistry(uriScheme);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Exception while updating registry to register protocol handler for `{Scheme}`", uriScheme);
+ }
+
+ if (setAsDefaultHandler)
+ {
+ // On Windows, this is done via the "UserChoice" registry key.
+ // We can't set this automatically without much hassle. See this for details:
+ // https://www.winhelponline.com/blog/set-default-browser-file-associations-command-line-windows-10/
+ _logger.LogDebug("Skipping setting default handler for `{Scheme}`", uriScheme);
+ }
- return Task.FromResult(((string?)commandKey.GetValue("") ?? "").Contains(_osInterop.GetOwnExe().ToString()));
+ return Task.CompletedTask;
}
- ///
- public Task Register(string protocol, string friendlyName, string workingDirectory, string commandLine)
+ private void UpdateRegistry(string uriScheme)
{
- using var key = GetClassKey(protocol);
- key.SetValue("", "URL:" + friendlyName);
+ // https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85)
+
+ using var key = GetClassKey(uriScheme);
+ key.SetValue("", "URL:Nexus Mods App");
key.SetValue("URL Protocol", "");
using var commandKey = GetCommandKey(key);
- var res = (string?)commandKey.GetValue("");
- commandKey.SetValue("", commandLine);
- commandKey.SetValue("WorkingDirectory", workingDirectory);
+ var executable = _osInterop.GetOwnExe();
- return Task.FromResult(res);
+ commandKey.SetValue("", $"\"{executable.ToNativeSeparators(_fileSystem.OS)}\" \"%1\"");
+ commandKey.SetValue("WorkingDirectory", $"\"{executable.Parent.ToNativeSeparators(_fileSystem.OS)}\"");
}
- ///
- public Task RegisterSelf(string protocol)
- {
- var exePath = _osInterop.GetOwnExe();
- var osInfo = FileSystem.Shared.OS;
- return Register(protocol, "NMA", "\""+exePath.Parent.ToNativeSeparators(osInfo) + "\"",
- "\""+exePath.ToNativeSeparators(osInfo)+"\"" +
- " protocol-invoke --url \"%1\"");
- }
-
private static RegistryKey GetClassKey(string protocol)
{
- return Registry.CurrentUser.CreateSubKey("SOFTWARE\\Classes\\" + protocol);
+ return Registry.CurrentUser.CreateSubKey(@"SOFTWARE\Classes\" + protocol);
}
private static RegistryKey GetCommandKey(RegistryKey parent)
diff --git a/tests/NexusMods.CrossPlatform.Tests/ProtocolRegistrationTests.Linux.cs b/tests/NexusMods.CrossPlatform.Tests/ProtocolRegistrationTests.Linux.cs
deleted file mode 100644
index b5a8444855..0000000000
--- a/tests/NexusMods.CrossPlatform.Tests/ProtocolRegistrationTests.Linux.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System.Runtime.Versioning;
-using FluentAssertions;
-using NexusMods.CrossPlatform.Process;
-using NexusMods.CrossPlatform.ProtocolRegistration;
-using NexusMods.Paths;
-
-namespace NexusMods.CrossPlatform.Tests;
-
-public class ProtocolRegistrationTests(IOSInterop interop)
-{
- [SkippableFact]
- [SupportedOSPlatform("linux")]
- public async Task ShouldWork_IsSelfHandler_Linux()
- {
- Skip.If(!OperatingSystem.IsLinux());
-
- const string protocol = "foo";
- var processFactory = new FakeProcessFactory(0)
- {
- StandardOutput = "yes\n"
- };
-
- var protocolRegistration = new ProtocolRegistrationLinux(processFactory, FileSystem.Shared, interop);
- var res = await protocolRegistration.IsSelfHandler(protocol);
- res.Should().BeTrue();
- }
-
- [SkippableFact]
- [SupportedOSPlatform("linux")]
- public async Task ShouldError_IsSelfHandler_Linux()
- {
- Skip.If(!OperatingSystem.IsLinux());
-
- const string protocol = "foo";
- var processFactory = new FakeProcessFactory(0)
- {
- StandardOutput = "no\n",
- };
-
- var protocolRegistration = new ProtocolRegistrationLinux(processFactory, FileSystem.Shared, interop);
- var res = await protocolRegistration.IsSelfHandler(protocol);
- res.Should().BeFalse();
- }
-}