JD2022-TU1/main/tools/framework/JD.TeaBoxAddinSystem/Addins/Management/AddinRepository.cs

379 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using JD.Collections.Extensions;
using JD.Kernel.StaticHelpers;
using JD.Kernel.Structs;
using JD.LogService;
using JD.Serializers.Xml;
using JD.TeaBoxAddinSystem.Addins.Internal;
namespace JD.TeaBoxAddinSystem.Addins.Management
{
/// <summary>
/// An addin repository is a collection of addins, defined by one <see cref="AddinRepository.ApplicationPath"/> and optionally some user paths.
/// </summary>
/// <remarks>
/// An addin repository is also responsible for persisting its state throughout sessions (i.e. what addins are enabled/disabled).
/// It is NOT an addin cache.
/// The addin repository is automatically persisted through a file named <see cref="AddinRepository.RepositoryFilename"/> in the <see cref="AddinRepository.ApplicationPath"/>.
/// A repository can be used by more than one application.
/// </remarks>
public class AddinRepository
{
#region Constants
/// <summary>
/// Addin file extension.
/// </summary>
public const string AddinExtension = "addin";
/// <summary>
/// Partial Addin file extension.
/// </summary>
public const string PartialAddinExtension = "partial.addin";
/// <summary>
/// Repository filename.
/// </summary>
public const string RepositoryFilename = "addin_repository";
/// <summary>
/// Directory in which addins resides.
/// </summary>
private const string AddinDirectory = "Addins/";
#endregion
#region Variables
private readonly string m_applicationPath;
private readonly List<string> m_userPaths = new List<string>();
private readonly List<Addin> m_addins = new List<Addin>();
#endregion
#region Constructor
/// <summary>
/// Constructor.
/// </summary>
/// <param name="applicationPath">Application Path (see <see cref="ApplicationPath"/>).</param>
public AddinRepository(string applicationPath)
{
m_applicationPath = applicationPath;
if (string.IsNullOrEmpty(m_applicationPath))
{
m_applicationPath = Environment.CurrentDirectory;
}
else if (!FileUtility.IsAbsolutePath(m_applicationPath))
{
m_applicationPath = FileUtility.Combine(Environment.CurrentDirectory, m_applicationPath);
}
m_applicationPath = FileUtility.FormatPath(m_applicationPath);
System.Diagnostics.Debug.Assert(!string.IsNullOrEmpty(ApplicationPath));
LoadFromFile();
}
#endregion
#region Properties
/// <summary>
/// The application path is the main path used to scan for addin files.
/// </summary>
public string ApplicationPath
{
get
{
return m_applicationPath;
}
}
/// <summary>
/// User paths are additional paths used to scan for addin files.
/// </summary>
public IList<string> UserPaths
{
get
{
return m_userPaths;
}
}
/// <summary>
/// All the addins contained in this repository.
/// </summary>
public IList<Addin> Addins
{
get
{
return m_addins.AsReadOnly();
}
}
private IEnumerable<string> AllAddinPaths
{
get
{
yield return Path.Combine(ApplicationPath, AddinDirectory);
foreach (string path in UserPaths)
{
yield return path;
}
}
}
private string FullRepositoryFilename
{
get
{
return FileUtility.Combine(ApplicationPath, RepositoryFilename);
}
}
#endregion
#region Methods
#region Addins management
/// <summary>
/// Loads all the addins in this repository.
/// </summary>
public void Load()
{
foreach (Addin addin in m_addins)
{
AddinManager.LoadAddin(addin);
}
}
/// <summary>
/// Locate all addins defined as code (<see cref="IXmlAddin"/>),
/// instantiates them and generate .addin files dynamically.
/// The advantage of this approach is to have addins defined as C# and strongly typed references which makes refactoring much easier.
/// </summary>
private void ComposeCsharpAddins(ILogger logger)
{
//Load all assemblies from application path that have not been loaded yet, to be able to find the addins as code
var dlls = System.IO.Directory.EnumerateFiles(ApplicationPath, "*.dll");
var assembliesToInclude = new[] { "JD.", "TeaBox." };
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
var dllsToLoad = dlls.Where(dll => loadedAssemblies.All(x => x.Location != dll) && assembliesToInclude.Any(dll.Contains)).ToList();
foreach (var dllToLoad in dllsToLoad)
{
try
{
AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName(dllToLoad));
}
catch (Exception ex)
{
logger.LogError($"Unable to load assembly {dllToLoad}. Exception:{ex}.");
throw;
}
}
//Find all the instances of addins
var xmlAddins = new List<IXmlAddin>();
var assemblies = EnumerableExtensions.Union(loadedAssemblies, AppDomain.CurrentDomain.GetAssemblies().Where(dll => assembliesToInclude.Any(x => dll.FullName.Contains(x)))).ToList();
foreach (var assembly in assemblies)
{
try
{
var exportedTypes = assembly.GetExportedTypes();
var xmlAddinTypes = exportedTypes.Where(type => typeof(IXmlAddin).IsAssignableFrom(type) && !type.IsAbstract);
foreach (var xmlAddinType in xmlAddinTypes)
{
var xmlAddinInstance = (IXmlAddin) Activator.CreateInstance(xmlAddinType);
xmlAddins.Add(xmlAddinInstance);
logger.LogMessage($"Addin found in type {xmlAddinType.Name} from assembly {assembly.FullName}.");
}
}
catch (Exception ex)
{
logger.LogError($"Unable to locate addins in {assembly}. Exception:{ex}.");
throw;
}
}
logger.LogMessage($"{xmlAddins.Count} addin(s) loaded.");
//Generate all .addin files in output Addins folder
var addinFolder = Path.Combine(ApplicationPath, AddinDirectory);
if (!Directory.Exists(addinFolder))
{
Directory.CreateDirectory(addinFolder);
}
System.IO.Directory.EnumerateFiles(addinFolder, "*.addin").ToList().ForEach(File.Delete);
foreach (var xmlAddin in xmlAddins)
{
var addinFilePath = Path.Combine(addinFolder, xmlAddin.FileName);
xmlAddin.RootElement.Save(addinFilePath);
logger.LogMessage($"Saved addin {xmlAddin} at \"{addinFilePath}\".");
}
}
/// <summary>
/// Updates the repository. This will also update the addin repository file.
/// </summary>
public void Update(ILogger loggingContext)
{
ComposeCsharpAddins(loggingContext);
Throw.IfNull(loggingContext, "loggingContext");
const string addinPattern = "*." + AddinExtension;
Dictionary<string, Addin> addinsPerFilename = new Dictionary<string, Addin>();
Dictionary<string, Addin> addinsPerFullId = new Dictionary<string, Addin>();
Dictionary<string, DateTime> addinsDate = new Dictionary<string, DateTime>();
foreach (string path in AllAddinPaths)
{
foreach (string addinFileName in Directory.EnumerateFiles(FileUtility.CleanPath(path), addinPattern, SearchOption.AllDirectories))
{
if (addinFileName.EndsWith(PartialAddinExtension))
{
continue;
}
string fileName = FileUtility.FormatPath(addinFileName);
FileInfo fileInfo = new FileInfo(fileName);
if (!addinsPerFilename.ContainsKey(fileName))
{
Addin addin = LoadAddin(fileName, loggingContext);
if (addin != null)
{
if (addinsPerFullId.ContainsKey(addin.FullId))
{
DateTime alreadyLoadedAddinLastWriteTime = addinsDate[addin.FullId];
Error error = new Error(LogCategory.Error)
{
Origin = new FileLocation(fileName)
};
if (alreadyLoadedAddinLastWriteTime >= fileInfo.LastWriteTime)
{
error.Message =
$"Addin {addin.FullId} was already declared in {addinsPerFullId[addin.FullId].FileLocation.GetValueOrDefault().FileName}.";
loggingContext.Log(error);
continue;
}
error.Message =
$"Addin {addin.FileLocation.GetValueOrDefault().FileName} is newer than {addinsPerFullId[addin.FullId].FileLocation.GetValueOrDefault().FileName} and will replace it. You might need to clean the output path";
loggingContext.Log(error);
}
if (addin.Manifest.X86Only && IntPtr.Size != 4)
{
loggingContext.LogMessage("Addin {0} will be ignored because it is only compatible on x86 platforms", addin.FullId);
continue;
}
addinsPerFullId[addin.FullId] = addin;
addinsPerFilename.Add(fileName, addin);
addinsDate[addin.FullId] = fileInfo.LastWriteTime;
}
}
}
}
// Sort them out
m_addins.Clear();
m_addins.AddRange(new AddinTopologicalSort(addinsPerFullId.Values).Sort());
}
private static Addin LoadAddin(string filename, ILogger loggingContext)
{
System.Diagnostics.Debug.Assert(File.Exists(filename));
const string AddinNode = "Addin";
try
{
using (XmlSerializerReader reader = new XmlSerializerReader(filename))
{
using (reader.StartElement(AddinNode))
{
return AddinXmlSerializer.ReadAddin(reader, loggingContext);
}
}
}
catch (Exception e)
{
LogService.LogService.LogException(e, "Could not load addin {0}.", filename);
return null;
}
}
#endregion
#region Serialization
/// <summary>
/// Loads the repository.
/// </summary>
private void LoadFromFile()
{
if (File.Exists(FullRepositoryFilename))
{
using (XmlSerializerReader reader = new XmlSerializerReader(FullRepositoryFilename))
{
ReadFromXml(reader);
}
}
}
/// <summary>
/// Clears the repository.
/// </summary>
public void Clear()
{
UserPaths.Clear();
if (File.Exists(FullRepositoryFilename))
{
File.Delete(FullRepositoryFilename);
}
}
private const string PathsNode = "Paths";
private const string UserPathNode = "UserPath";
private void ReadFromXml(XmlSerializerReader reader)
{
System.Diagnostics.Debug.Assert(reader != null);
// User paths
UserPaths.Clear();
using (reader.StartElement(PathsNode))
{
foreach (string element in reader)
{
if (string.Equals(element, UserPathNode, StringComparison.OrdinalIgnoreCase))
{
UserPaths.Add(reader.ReadInnerData());
}
}
}
}
#endregion
#endregion
}
}