// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using Duplicati.Library.AutoUpdater; using Duplicati.Library.Interface; namespace Duplicati.Library.DynamicLoader { /// /// This class supports dynamic loading of instances of a given interface /// /// The interface that the class loads internal abstract class DynamicLoader where T : class, IDynamicModule { /// /// The tag used for logging /// private static readonly string LOGTAG = Logging.Log.LogTagFromType>(); /// /// A lock used to guarantee threadsafe access to the interface lookup table /// protected readonly object m_lock = new object(); /// /// A cached list of interfaces /// protected Dictionary m_interfaces; /// /// List of supported commands /// protected Dictionary> m_supportedCommands = new(); /// /// Function to extract the key value from the interface /// /// The interface to extract the key from /// The key for the interface protected abstract string GetInterfaceKey(T item); /// /// Gets a list of subfolders to search for interfaces /// protected abstract string[] Subfolders { get; } /// /// The list of statically included modules /// protected abstract IEnumerable BuiltInModules { get; } /// /// Construct a new instance of the dynamic loader, /// does not load anything /// protected DynamicLoader() { } /// /// Provides threadsafe doublelocking loading of the interface table /// protected void LoadInterfaces() { if (m_interfaces == null) lock (m_lock) if (m_interfaces == null) { var interfaces = new Dictionary(); // When loading, inject the built-ins first, so they can be replaced by subfolder matches foreach (T b in BuiltInModules) interfaces[GetInterfaceKey(b)] = b; // When loading, the subfolder matches are placed last in the // resulting list, and thus applied last to the lookup, // meaning that they can replace the built-in versions foreach (T b in FindInterfaceImplementors(Subfolders)) interfaces[GetInterfaceKey(b)] = b; m_interfaces = interfaces; } } /// /// Searches the base folder of this dll for classes in other assemblies that /// implements a certain interface, and has a default constructor /// /// Any additional folders besides the assembly path to search in /// A list of instanciated classes which implements the interface private IEnumerable FindInterfaceImplementors(string[] additionalfolders) { var interfaces = new List(); // Search in these folders for modules var root_paths = new[] { Path.Combine(UpdaterManager.INSTALLATIONDIR, "modules"), Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) ?? string.Empty, AutoUpdateSettings.AppName, "modules" ), Environment.GetEnvironmentVariable($"{AutoUpdateSettings.AppName}_MODULE_PATH") } .Where(x => !string.IsNullOrEmpty(x) && Directory.Exists(x)); // In each folder we look, search only in the subfolders containing the modules var files = new List(); foreach (var path in root_paths) { if (additionalfolders != null) foreach (string s in additionalfolders) { string subpath = System.IO.Path.Combine(path, s); if (System.IO.Directory.Exists(subpath)) files.AddRange(System.IO.Directory.GetFiles(subpath, "*.dll")); } } foreach (string s in files) { try { //NOTE: This is pretty nifty, due to the use of assembly redirect and LoadFile, we can // actually end up loading multiple versions of the same file (if it is present). //Since the lookup dictionary applies the modules in the order returned // and the subfolders are probed last, a module in the subfolder // will take the place of a stock module, if both use same key Assembly asm = Assembly.LoadFile(s); if (asm != Assembly.GetExecutingAssembly()) { foreach (Type t in asm.GetExportedTypes()) try { if (typeof(T).IsAssignableFrom(t) && t != typeof(T)) { //TODO: Figure out how to support types with no default constructors if (t.GetConstructor(Type.EmptyTypes) != null) { T i = Activator.CreateInstance(t) as T; if (i != null) interfaces.Add(i); } } } catch (Exception ex) { ex = GetActualException(ex); Duplicati.Library.Logging.Log.WriteWarningMessage(LOGTAG, "SoftError", ex, Strings.DynamicLoader.DynamicTypeLoadError(t.FullName, s, ex.Message)); } } } catch (Exception ex) { ex = GetActualException(ex); // Since this is locating the assemblies that have the proper interface, it isn't an error to not. // This was loading the log with errors about additional DLL's that are not plugins and do not have manifests. Duplicati.Library.Logging.Log.WriteExplicitMessage(LOGTAG, "HardError", ex, Strings.DynamicLoader.DynamicAssemblyLoadError(s, ex.Message)); } } return interfaces; } private Exception GetActualException(Exception ex) { if (ex is TargetInvocationException) ex = (ex as TargetInvocationException).InnerException; return ex; } /// /// Gets a list of loaded interfaces /// public T[] Interfaces { get { LoadInterfaces(); lock (m_lock) return new List(m_interfaces.Values).ToArray(); } } /// /// Gets a list of the keys of loaded interfaces /// public string[] Keys { get { LoadInterfaces(); lock (m_lock) return new List(m_interfaces.Keys).ToArray(); } } /// /// Returns the supported commands from the item, applying caching /// /// The item to get the supported commands for /// The list of supported commands protected IEnumerable GetSupportedCommandsCached(T item) { var type = item.GetType(); lock (m_lock) { if (m_supportedCommands.TryGetValue(type, out var commands)) return commands; return m_supportedCommands[type] = item.SupportedCommands.ToList().AsReadOnly(); } } /// /// Helper method to register a module /// /// The module to register public void AddModule(T module) { LoadInterfaces(); lock (m_lock) m_interfaces[GetInterfaceKey(module)] = module; } } }