diff --git a/CHANGELOG.md b/CHANGELOG.md
index 94059852e..44a218e6d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -94,6 +94,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- [SIL.Core.Desktop] Implemented GetDefaultProgramForFileType (as trenamed) in a way that works on Windows 11, Mono (probably) and MacOS (untested).
- [SIL.Media] MediaInfo.HaveNecessaryComponents properly returns true if FFprobe is on the system path.
- [SIL.Media] Made MediaInfo.FFprobeFolder look for and return the folder when first accessed, even if no prior call to the setter or other action had caused it t be found.
+- [SIL.Core] Made GetSafeDirectories not crash and simply not return any subdirectory the user does not have permission to access.
+- [SIL.Core] In GetDirectoryDistributedWithApplication, prevented a failure in accessing one of the specified subfolders from allowing it to try the others.
### Removed
diff --git a/SIL.Core/IO/DirectoryHelper.cs b/SIL.Core/IO/DirectoryHelper.cs
index a9622a57f..014b038ee 100644
--- a/SIL.Core/IO/DirectoryHelper.cs
+++ b/SIL.Core/IO/DirectoryHelper.cs
@@ -1,10 +1,13 @@
// Copyright (c) 2024 SIL Global
// This software is licensed under the MIT License (http://opensource.org/licenses/MIT)
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Security;
using JetBrains.Annotations;
using SIL.PlatformUtilities;
+using SIL.Reporting;
using static System.Environment;
using static System.Environment.SpecialFolder;
using static System.IO.Path;
@@ -25,7 +28,7 @@ public static void Copy(string sourcePath, string destinationPath, bool overwrit
File.Copy(filepath, Combine(destinationPath, filename), overwrite);
}
- // Copy all the sub directories.
+ // Copy all the subdirectories.
foreach (var directoryPath in Directory.GetDirectories(sourcePath))
{
var directoryName = GetFileName(directoryPath);
@@ -106,21 +109,45 @@ public static bool IsEmpty(string path, bool onlyCheckForFiles = false)
///
/// Return subdirectories of that are not system or hidden.
+ /// Subdirectories which the user does not have permission to access will also be skipped.
/// There are some cases where our call to Directory.GetDirectories() throws.
/// For example, when the access permissions on a folder are set so that it can't be read.
/// Another possible example may be Windows Backup files, which apparently look like directories.
///
/// Directory path to look in.
- /// Zero or more directory names that are not system or hidden.
- /// E.g. when the user does not have
- /// read permission.
+ /// Zero or more directory names that are not system or hidden
+ /// The caller does not have the required
+ /// permission to access the subdirectories of .
+ ///
+ /// is a zero-length string, contains only white space, or contains one or more invalid characters. You can query for invalid characters by using the method.
+ ///
+ /// is .
+ /// The specified , file name,
+ /// or both exceed the system-defined maximum length.
+ /// is a file name.
+ /// The specified is
+ /// invalid (for example, it is on an unmapped drive).
public static string[] GetSafeDirectories(string path)
{
- return (from directoryName in Directory.GetDirectories(path)
- let dirInfo = new DirectoryInfo(directoryName)
- where (dirInfo.Attributes & FileAttributes.System) != FileAttributes.System
- where (dirInfo.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden
- select directoryName).ToArray();
+ var list = new List();
+ foreach (var directoryName in Directory.GetDirectories(path))
+ {
+ try
+ {
+ var dirInfo = new DirectoryInfo(directoryName);
+ if ((dirInfo.Attributes & FileAttributes.System) != FileAttributes.System)
+ {
+ if ((dirInfo.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden)
+ list.Add(directoryName);
+ }
+ }
+ catch (SecurityException e)
+ {
+ Logger.WriteError(e);
+ }
+ }
+
+ return list.ToArray();
}
#region Utilities that are specific to the Windows OS.
diff --git a/SIL.Core/IO/FileLocationUtilities.cs b/SIL.Core/IO/FileLocationUtilities.cs
index 9dfbc46c5..3533839fa 100644
--- a/SIL.Core/IO/FileLocationUtilities.cs
+++ b/SIL.Core/IO/FileLocationUtilities.cs
@@ -6,6 +6,7 @@
using System.Text;
using SIL.PlatformUtilities;
using SIL.Reflection;
+using SIL.Reporting;
namespace SIL.IO
{
@@ -63,7 +64,7 @@ private static string LocateExecutableDistributedWithApplication(string[] partsO
/// [applicationFolder]/[distFileFolderName]/[subPath1]/[subPathN] or
/// [applicationFolder]/[distFileFolderName]/[platform]/[subPath1]/[subPathN] or
/// [applicationFolder]/[subPath1]/[subPathN]. If the executable can't be found we
- /// search in the ProgramFiles folder ([programfiles]/[subPath1]/[subPathN]) on Windows,
+ /// search in the ProgramFiles folder ([ProgramFiles]/[subPath1]/[subPathN]) on Windows,
/// and in the folders included in the PATH environment variable on Linux.
/// When the executable has a prefix of ".exe" we're running on Linux we also
/// search for files without the prefix.
@@ -109,7 +110,7 @@ public static string LocateExecutable(params string[] partsOfTheSubPath)
}
private static string[] DirectoriesHoldingFiles => new[] {string.Empty, "DistFiles",
- "common" /*for wesay*/, "src" /*for Bloom*/};
+ "common" /*for WeSay*/, "src" /*for Bloom*/};
///
/// Find a file which, on a development machine, lives in [solution]/[distFileFolderName]/[subPath],
@@ -234,7 +235,7 @@ public static string GetDirectoryDistributedWithApplication(params string[] part
///
/// When subFoldersToSearch are not specified:
/// - If fallBackToDeepSearch is true, then the entire program files folder (and all
- /// (sub folders) is searched.
+ /// sub folders) is searched.
/// - If fallBackToDeepSearch is false, then only the top-level program files
/// folder is searched.
///
@@ -278,11 +279,11 @@ private static string LocateInProgramFilesFolder(string exeName, SearchOption sr
if (subFoldersToSearch.Length == 0)
subFoldersToSearch = new[] { string.Empty };
- foreach (var progFolder in Enumerable.Where(GetPossibleProgramFilesFolders(), Directory.Exists))
+ foreach (var progFolder in GetPossibleProgramFilesFolders().Where(Directory.Exists))
{
// calling Directory.GetFiles("C:\Program Files", exeName, SearchOption.AllDirectories) will fail
// if even one of the children of the Program Files doesn't allow you to search it.
- // So instead we first gather up all the children directories, and then search those.
+ // So instead we first gather all the child directories, and then search those.
// Some will give us access denied, and that's fine, we skip them.
// But we don't want to look in child directories on Linux because GetPossibleProgramFilesFolders()
// gives us the individual elements of the PATH environment variable, and these specify exactly
@@ -291,11 +292,24 @@ private static string LocateInProgramFilesFolder(string exeName, SearchOption sr
{
if (Platform.IsWindows)
{
- foreach (var subDir in DirectoryHelper.GetSafeDirectories(path))
+ string[] subDirectories = null;
+ try
{
- var tgtPath = GetFiles(exeName, srcOption, subDir);
- if (!string.IsNullOrEmpty(tgtPath))
- return tgtPath;
+ subDirectories = DirectoryHelper.GetSafeDirectories(path);
+ }
+ catch (Exception e)
+ {
+ Logger.WriteError(e);
+ }
+
+ if (subDirectories != null)
+ {
+ foreach (var subDir in subDirectories)
+ {
+ var tgtPath = GetFiles(exeName, srcOption, subDir);
+ if (!string.IsNullOrEmpty(tgtPath))
+ return tgtPath;
+ }
}
}
else
@@ -336,14 +350,18 @@ private static IEnumerable GetPossibleProgramFilesFolders()
{
if (!Platform.IsWindows)
{
- foreach (var dir in Environment.GetEnvironmentVariable("PATH").Split(':'))
- yield return dir;
+ var path = Environment.GetEnvironmentVariable("PATH");
+ if (!string.IsNullOrEmpty(path))
+ {
+ foreach (var dir in path.Split(':'))
+ yield return dir;
+ }
yield return "/opt"; // RAMP is installed in the /opt directory by default
}
- var pf = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
- yield return pf.Replace(" (x86)", string.Empty);
- yield return pf.Replace(" (x86)", string.Empty) + " (x86)";
+ yield return Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
+ if (Environment.Is64BitProcess)
+ yield return Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
}
}
}
\ No newline at end of file
diff --git a/SIL.Core/Reporting/Logger.cs b/SIL.Core/Reporting/Logger.cs
index 5c48b4036..723127c8a 100644
--- a/SIL.Core/Reporting/Logger.cs
+++ b/SIL.Core/Reporting/Logger.cs
@@ -4,18 +4,20 @@
using System.IO;
using System.Linq;
using System.Text;
+using JetBrains.Annotations;
using SIL.IO;
namespace SIL.Reporting
{
-
public interface ILogger
{
///
- /// This is something that should be listed in the source control checkin
+ /// This is something that should be listed in the source control check-in
///
void WriteConciseHistoricalEvent(string message, params object[] args);
}
+
+ [PublicAPI]
public class MultiLogger: ILogger
{
private readonly List _loggers= new List();
@@ -58,6 +60,8 @@ public void WriteConciseHistoricalEvent(string message, params object[] args)
{
_builder.Append(Logger.FormatMessage(message, args));
}
+
+ [PublicAPI]
public string GetLogText()
{
return _builder.ToString();
@@ -67,7 +71,7 @@ public string GetLogText()
/// ----------------------------------------------------------------------------------------
///
/// Logs stuff to a file created in
- /// c:\Documents and Settings\Username\Local Settings\Temp\Companyname\Productname\Log.txt
+ /// c:\Documents and Settings\Username\Local Settings\Temp\Companyname\ProductName\Log.txt
///
/// ----------------------------------------------------------------------------------------
public class Logger: IDisposable, ILogger
@@ -206,7 +210,7 @@ private void RestartMinorEvents()
public void CheckDisposed()
{
if (IsDisposed)
- throw new ObjectDisposedException(String.Format("'{0}' in use after being disposed.", GetType().Name));
+ throw new ObjectDisposedException($"'{GetType().Name}' in use after being disposed.");
}
///
@@ -217,10 +221,7 @@ public void CheckDisposed()
///
/// See if the object has been disposed.
///
- public bool IsDisposed
- {
- get { return m_isDisposed; }
- }
+ public bool IsDisposed => m_isDisposed;
///
/// Finalizer, in case client doesn't dispose it.
@@ -243,7 +244,7 @@ public void Dispose()
{
Dispose(true);
// This object will be cleaned up by the Dispose method.
- // Therefore, you should call GC.SupressFinalize to
+ // Therefore, you should call GC.SuppressFinalize to
// take this object off the finalization queue
// and prevent finalization code for this object
// from executing a second time.
@@ -258,7 +259,7 @@ public void Dispose()
/// Both managed and unmanaged resources can be disposed.
///
/// 2. If disposing is false, the method has been called by the
- /// runtime from inside the finalizer and you should not reference (access)
+ /// runtime from inside the finalizer, and you should not reference (access)
/// other managed objects, as they already have been garbage collected.
/// Only unmanaged resources can be disposed.
///
@@ -295,7 +296,7 @@ protected virtual void Dispose(bool disposing)
#endregion IDisposable & Co. implementation
///
- /// This is for version-control checkin descriptions. E.g. "Deleted foobar".
+ /// This is for version-control check-in descriptions. E.g. "Deleted foobar".
///
///
///
@@ -354,10 +355,7 @@ private string GetLogTextAndStartOver()
///
/// added this for a case of a catastrophic error so bad I couldn't get the means of finding out what just happened
///
- public static string MinorEventsLog
- {
- get { return Singleton.m_minorEvents.ToString(); }
- }
+ public static string MinorEventsLog => Singleton.m_minorEvents.ToString();
///
/// the place on disk where we are storing the log
@@ -373,10 +371,7 @@ public static string LogPath
return _actualLogPath;
}
}
- public static Logger Singleton
- {
- get { return _singleton; }
- }
+ public static Logger Singleton => _singleton;
private static void SetActualLogPath(string filename)
{
@@ -407,7 +402,7 @@ private void WriteEventCore(string message, params object[] args)
{
#endif
CheckDisposed();
- if (m_out != null && m_out.BaseStream.CanWrite)
+ if (m_out is { BaseStream: { CanWrite: true } })
{
m_out.Write(DateTime.Now.ToLongTimeString() + "\t");
m_out.WriteLine(FormatMessage(message, args));
@@ -455,7 +450,7 @@ public static void WriteError(string msg, Exception e)
}
///
- /// only a limitted number of the most recent of these events will show up in the log
+ /// only a limited number of the most recent of these events will show up in the log
///
public static void WriteMinorEvent(string message, params object[] args)
{
@@ -483,7 +478,7 @@ internal static string FormatMessage(string message, object[] args)
}
catch (Exception)
{
- return string.Format("Error formatting message with {0} args: {1}", args.Length, message);
+ return $"Error formatting message with {args.Length} args: {message}";
}
}
return message;
@@ -520,6 +515,7 @@ private void WriteMinorEventCore(string message, params object[] args)
}
}
+ [PublicAPI]
public static void ShowUserTheLogFile()
{
Singleton.m_out.Flush();
@@ -527,9 +523,10 @@ public static void ShowUserTheLogFile()
}
///
- /// if you're working with unmanaged code and get a System.AccessViolationException, well you're toast, and anything
- /// that requires UI is gonna freeze up. So call this instead
+ /// If you're working with unmanaged code and get a System.AccessViolationException, you're
+ /// toast, and anything that requires UI is going to freeze up. So call this instead.
///
+ [PublicAPI]
public static void ShowUserATextFileRelatedToCatastrophicError(Exception reallyBadException)
{
//did this special because we don't have an event loop to drive the error reporting dialog if Application.Run() dies