FileAssociationHelper.cs
1 using Microsoft.Win32; 2 using Ryujinx.Common; 3 using Ryujinx.Common.Logging; 4 using System; 5 using System.Diagnostics; 6 using System.IO; 7 using System.Runtime.InteropServices; 8 using System.Runtime.Versioning; 9 10 namespace Ryujinx.UI.Common.Helper 11 { 12 public static partial class FileAssociationHelper 13 { 14 private static readonly string[] _fileExtensions = { ".nca", ".nro", ".nso", ".nsp", ".xci" }; 15 16 [SupportedOSPlatform("linux")] 17 private static readonly string _mimeDbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "mime"); 18 19 private const int SHCNE_ASSOCCHANGED = 0x8000000; 20 private const int SHCNF_FLUSH = 0x1000; 21 22 [LibraryImport("shell32.dll", SetLastError = true)] 23 public static partial void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2); 24 25 public static bool IsTypeAssociationSupported => (OperatingSystem.IsLinux() || OperatingSystem.IsWindows()) && !ReleaseInformation.IsFlatHubBuild; 26 27 [SupportedOSPlatform("linux")] 28 private static bool AreMimeTypesRegisteredLinux() => File.Exists(Path.Combine(_mimeDbPath, "packages", "Ryujinx.xml")); 29 30 [SupportedOSPlatform("linux")] 31 private static bool InstallLinuxMimeTypes(bool uninstall = false) 32 { 33 string installKeyword = uninstall ? "uninstall" : "install"; 34 35 if ((uninstall && AreMimeTypesRegisteredLinux()) || (!uninstall && !AreMimeTypesRegisteredLinux())) 36 { 37 string mimeTypesFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "mime", "Ryujinx.xml"); 38 string additionalArgs = !uninstall ? "--novendor" : ""; 39 40 using Process mimeProcess = new(); 41 42 mimeProcess.StartInfo.FileName = "xdg-mime"; 43 mimeProcess.StartInfo.Arguments = $"{installKeyword} {additionalArgs} --mode user {mimeTypesFile}"; 44 45 mimeProcess.Start(); 46 mimeProcess.WaitForExit(); 47 48 if (mimeProcess.ExitCode != 0) 49 { 50 Logger.Error?.PrintMsg(LogClass.Application, $"Unable to {installKeyword} mime types. Make sure xdg-utils is installed. Process exited with code: {mimeProcess.ExitCode}"); 51 52 return false; 53 } 54 55 using Process updateMimeProcess = new(); 56 57 updateMimeProcess.StartInfo.FileName = "update-mime-database"; 58 updateMimeProcess.StartInfo.Arguments = _mimeDbPath; 59 60 updateMimeProcess.Start(); 61 updateMimeProcess.WaitForExit(); 62 63 if (updateMimeProcess.ExitCode != 0) 64 { 65 Logger.Error?.PrintMsg(LogClass.Application, $"Could not update local mime database. Process exited with code: {updateMimeProcess.ExitCode}"); 66 } 67 } 68 69 return true; 70 } 71 72 [SupportedOSPlatform("windows")] 73 private static bool AreMimeTypesRegisteredWindows() 74 { 75 static bool CheckRegistering(string ext) 76 { 77 RegistryKey key = Registry.CurrentUser.OpenSubKey(@$"Software\Classes\{ext}"); 78 79 if (key is null) 80 { 81 return false; 82 } 83 84 var openCmd = key.OpenSubKey(@"shell\open\command"); 85 86 string keyValue = (string)openCmd.GetValue(""); 87 88 return keyValue is not null && (keyValue.Contains("Ryujinx") || keyValue.Contains(AppDomain.CurrentDomain.FriendlyName)); 89 } 90 91 bool registered = false; 92 93 foreach (string ext in _fileExtensions) 94 { 95 registered |= CheckRegistering(ext); 96 } 97 98 return registered; 99 } 100 101 [SupportedOSPlatform("windows")] 102 private static bool InstallWindowsMimeTypes(bool uninstall = false) 103 { 104 static bool RegisterExtension(string ext, bool uninstall = false) 105 { 106 string keyString = @$"Software\Classes\{ext}"; 107 108 if (uninstall) 109 { 110 // If the types don't already exist, there's nothing to do and we can call this operation successful. 111 if (!AreMimeTypesRegisteredWindows()) 112 { 113 return true; 114 } 115 Logger.Debug?.Print(LogClass.Application, $"Removing type association {ext}"); 116 Registry.CurrentUser.DeleteSubKeyTree(keyString); 117 Logger.Debug?.Print(LogClass.Application, $"Removed type association {ext}"); 118 } 119 else 120 { 121 using var key = Registry.CurrentUser.CreateSubKey(keyString); 122 123 if (key is null) 124 { 125 return false; 126 } 127 128 Logger.Debug?.Print(LogClass.Application, $"Adding type association {ext}"); 129 using var openCmd = key.CreateSubKey(@"shell\open\command"); 130 openCmd.SetValue("", $"\"{Environment.ProcessPath}\" \"%1\""); 131 Logger.Debug?.Print(LogClass.Application, $"Added type association {ext}"); 132 133 } 134 135 return true; 136 } 137 138 bool registered = false; 139 140 foreach (string ext in _fileExtensions) 141 { 142 registered |= RegisterExtension(ext, uninstall); 143 } 144 145 // Notify Explorer the file association has been changed. 146 SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_FLUSH, IntPtr.Zero, IntPtr.Zero); 147 148 return registered; 149 } 150 151 public static bool AreMimeTypesRegistered() 152 { 153 if (OperatingSystem.IsLinux()) 154 { 155 return AreMimeTypesRegisteredLinux(); 156 } 157 158 if (OperatingSystem.IsWindows()) 159 { 160 return AreMimeTypesRegisteredWindows(); 161 } 162 163 // TODO: Add macOS support. 164 165 return false; 166 } 167 168 public static bool Install() 169 { 170 if (OperatingSystem.IsLinux()) 171 { 172 return InstallLinuxMimeTypes(); 173 } 174 175 if (OperatingSystem.IsWindows()) 176 { 177 return InstallWindowsMimeTypes(); 178 } 179 180 // TODO: Add macOS support. 181 182 return false; 183 } 184 185 public static bool Uninstall() 186 { 187 if (OperatingSystem.IsLinux()) 188 { 189 return InstallLinuxMimeTypes(true); 190 } 191 192 if (OperatingSystem.IsWindows()) 193 { 194 return InstallWindowsMimeTypes(true); 195 } 196 197 // TODO: Add macOS support. 198 199 return false; 200 } 201 } 202 }