379 lines
13 KiB
C#
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
|
|
}
|
|
}
|