From 00528d31b43ff92d2051aa5b27eee4633aac8e7b Mon Sep 17 00:00:00 2001 From: tombogle Date: Thu, 7 Nov 2024 22:53:56 -0600 Subject: [PATCH] Renamed GetFromRegistryProgramThatOpensFileType to GetDefaultProgramForFileType and implemented it in a way that works on Windows 11, Mono (probably) and MacOS (untested) --- CHANGELOG.md | 2 + SIL.Archiving/ArchivingPrograms.cs | 2 +- SIL.Core.Desktop.Tests/IO/FileLocatorTests.cs | 18 +- SIL.Core.Desktop/IO/FileLocator.cs | 159 +++++++++++++----- 4 files changed, 122 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48ee9b792..2d741422e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,10 +75,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [SIL.Archiving] Made MetaTranscript.WriteCorpusImdiFile asynchronous, changing its signature to return Task. - [SIL.Archiving] Changed the name of the third parameter in ArchivingDlgViewModel.AddFileGroup from progressMessage to addingToArchiveProgressMessage. - [SIL.Windows.Forms.Archiving] Changed Cancel Button to say Close instead in IMDIArchivingDlg. +- [SIL.Core.Desktop] Renamed GetFromRegistryProgramThatOpensFileType to GetDefaultProgramForFileType. ### Fixed - [SIL.Archiving] Fixed typo in RampArchivingDlgViewModel for Ethnomusicology performance collection. - [SIL.Archiving] Changed URLs that used http: to https: in resource EmptyMets.xml. +- [SIL.Core.Desktop] Implemented GetDefaultProgramForFileType (as trenamed) in a way that works on Windows 11, Mono (probably) and MacOS (untested). ### Removed diff --git a/SIL.Archiving/ArchivingPrograms.cs b/SIL.Archiving/ArchivingPrograms.cs index 5e6b60089..796fc37e3 100644 --- a/SIL.Archiving/ArchivingPrograms.cs +++ b/SIL.Archiving/ArchivingPrograms.cs @@ -31,7 +31,7 @@ public static string GetRampExeFileLocation() if (ArchivingDlgViewModel.IsMono) exeFile = FileLocationUtilities.LocateInProgramFiles("ramp", true); else - exeFile = FileLocator.GetFromRegistryProgramThatOpensFileType(rampFileExtension) ?? + exeFile = FileLocator.GetDefaultProgramForFileType(rampFileExtension) ?? FileLocationUtilities.LocateInProgramFiles("ramp.exe", true, "ramp"); // make sure the file exists diff --git a/SIL.Core.Desktop.Tests/IO/FileLocatorTests.cs b/SIL.Core.Desktop.Tests/IO/FileLocatorTests.cs index 4ecaf0210..d21b09be2 100644 --- a/SIL.Core.Desktop.Tests/IO/FileLocatorTests.cs +++ b/SIL.Core.Desktop.Tests/IO/FileLocatorTests.cs @@ -30,27 +30,21 @@ public void LocateFile_FileNoteFound_ReturnsEmptyString() } [Test] - [Platform(Exclude="Unix")] - [Category("KnownMonoIssue")] - public void GetFromRegistryProgramThatOpensFileType_SendInvalidType_ReturnsNull() + public void GetDefaultProgramForFileType_SendInvalidType_ReturnsNull() { - Assert.IsNull(FileLocator.GetFromRegistryProgramThatOpensFileType(".blah")); + Assert.IsNull(FileLocator.GetDefaultProgramForFileType(".blah")); } [Test] - [Platform(Exclude="Unix")] - [Category("KnownMonoIssue")] - public void GetFromRegistryProgramThatOpensFileType_SendValidType_ReturnsProgramPath() + public void GetDefaultProgramForFileType_SendValidType_ReturnsProgramPath() { - Assert.IsNotNull(FileLocator.GetFromRegistryProgramThatOpensFileType(".txt")); + Assert.IsNotNull(FileLocator.GetDefaultProgramForFileType(".txt")); } [Test] - [Platform(Exclude="Unix")] - [Category("KnownMonoIssue")] - public void GetFromRegistryProgramThatOpensFileType_SendExtensionWithoutPeriod_ReturnsProgramPath() + public void GetDefaultProgramForFileType_SendExtensionWithoutPeriod_ReturnsProgramPath() { - Assert.IsNotNull(FileLocator.GetFromRegistryProgramThatOpensFileType("txt")); + Assert.IsNotNull(FileLocator.GetDefaultProgramForFileType("txt")); } } } diff --git a/SIL.Core.Desktop/IO/FileLocator.cs b/SIL.Core.Desktop/IO/FileLocator.cs index 2451f2c4c..41bc58d45 100644 --- a/SIL.Core.Desktop/IO/FileLocator.cs +++ b/SIL.Core.Desktop/IO/FileLocator.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.InteropServices; +using System.Text; using JetBrains.Annotations; -using Microsoft.Win32; using SIL.PlatformUtilities; using SIL.Reporting; @@ -150,69 +152,134 @@ public virtual IFileLocator CloneAndCustomize(IEnumerable addedSearchPat return new FileLocator(new List(SearchPaths.Concat(addedSearchPaths))); } - #region Methods for locating file in program files folders + #region Methods for locating program file associated with a file /// ------------------------------------------------------------------------------------ /// - /// Searches the registry and returns the full path to the application program used to - /// open files having the specified extension. The fileExtension can be with or without - /// the preceding period. If the command cannot be found in the registry, then null is - /// returned. If a command in the registry is found, but it refers to a program file - /// that does not exist, null is returned. + /// returns the full path to the application program used to open files having the + /// specified extension/type. The fileExtension can be with or without + /// the preceding period. If no associated application can be found or the associated + /// program does not actually exist, null is returned. /// /// ------------------------------------------------------------------------------------ - public static string GetFromRegistryProgramThatOpensFileType(string fileExtension) + public static string GetDefaultProgramForFileType(string fileExtension) { - if (!Platform.IsWindows) - { - //------------------------------------------------------------------------------------ - // The following command will output the mime type of an existing file, Phil.html: - // file -b --mime-type ~/Phil.html - // - // This command will tell you the default application to open the file Phil.html: - // ext=$(grep "$(file -b --mime-type ~/Phil.html)" /etc/mime.types - // | awk '{print $1}') && xdg-mime query default $ext - // - // This command will open the file Phil.html using the default application: - // xdg-open ~/Page.html - //------------------------------------------------------------------------------------ - - throw new NotImplementedException( - "GetFromRegistryProgramThatOpensFileType not implemented on Mono yet."); - } + if (!fileExtension.StartsWith(".")) + fileExtension = "." + fileExtension; - var ext = fileExtension.Trim(); - if (!ext.StartsWith(".")) - ext = "." + ext; + if (Platform.IsWindows) + return GetDefaultWindowsProgramForFileType(fileExtension); - var key = Registry.ClassesRoot.OpenSubKey(ext); - if (key == null) - return null; + if (Platform.IsMac) + return GetDefaultMacProgramForFileType(fileExtension); + + if (Platform.IsLinux) + return GetDefaultLinuxProgramForFileType(fileExtension); + + throw new PlatformNotSupportedException("This operating system is not supported."); + } + + [DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)] + private static extern uint AssocQueryString( + uint flags, + int str, + string pszAssoc, + string pszExtra, + [Out] StringBuilder pszOut, + ref uint pcchOut); + + private static string GetDefaultWindowsProgramForFileType(string fileExtension) + { + const int assocStrExecutable = 2; + uint length = 260; + var sb = new StringBuilder((int)length); - var value = key.GetValue(string.Empty) as string; - key.Dispose(); + var result = AssocQueryString(0, assocStrExecutable, fileExtension, null, sb, ref length); - if (value == null) + if (result != 0 || sb.Length == 0) return null; - key = Registry.ClassesRoot.OpenSubKey($"{value}\\shell\\open\\command"); + var path = sb.ToString(); + return Path.GetFileName(path) != "OpenWith.exe" && File.Exists(path) ? path : null; + } - if (key == null && value.ToLower() == "ramp.package") + private static string GetDefaultMacProgramForFileType(string fileExtension) + { + try { - key = Registry.ClassesRoot.OpenSubKey("ramp\\shell\\open\\command"); - if (key == null) - return null; - } + string filePath = $"/tmp/dummy{fileExtension}"; + Process.Start("touch", filePath)?.WaitForExit(); - value = key?.GetValue(string.Empty) as string; - key?.Dispose(); + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "open", + Arguments = "-Ra " + filePath, + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; - if (value == null) - return null; + process.Start(); + var output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); - value = value.Trim('\"', '%', '1', ' '); - return (!File.Exists(value) ? null : value); + return string.IsNullOrEmpty(output) ? null : output; + } + catch + { + return null; + } } + private static string GetDefaultLinuxProgramForFileType(string fileExtension) + { + try + { + var mimeProcess = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "xdg-mime", + Arguments = "query default " + fileExtension, + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + mimeProcess.Start(); + var desktopEntry = mimeProcess.StandardOutput.ReadToEnd().Trim(); + mimeProcess.WaitForExit(); + + if (string.IsNullOrEmpty(desktopEntry)) + return null; + + // Check if the executable associated with the desktop entry exists + var whichProcess = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = "which", + Arguments = desktopEntry, + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + } + }; + + whichProcess.Start(); + string executablePath = whichProcess.StandardOutput.ReadToEnd().Trim(); + whichProcess.WaitForExit(); + + return string.IsNullOrEmpty(executablePath) ? null : executablePath; + } + catch + { + return null; + } + } #endregion public virtual void AddPath(string path)