/*
* Copyright 2018 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
using CommonUtil;
namespace PluginCommon {
///
/// Manages loaded plugins, in the "remote" AppDomain.
///
public sealed class PluginManager : MarshalByRefObject {
///
/// Collection of instances of active plugins, keyed by script identifier. Other
/// plugin assemblies may be present in the AppDomain, but have not been identified
/// by the application as being of interest.
///
private Dictionary mActivePlugins = new Dictionary();
///
/// Reference to file data.
///
private byte[] mFileData;
///
/// Constructor, invoked from CreateInstanceAndUnwrap().
///
public PluginManager() {
Debug.WriteLine("PluginManager ctor (id=" + AppDomain.CurrentDomain.Id + ")");
// Seems to require [SecurityCritical]
//Type lsc = Type.GetType("System.Runtime.Remoting.Lifetime.LifetimeServices");
//PropertyInfo prop = lsc.GetProperty("LeaseTime");
//prop.SetValue(null, TimeSpan.FromSeconds(30));
}
~PluginManager() {
Debug.WriteLine("~PluginManager (id=" + AppDomain.CurrentDomain.Id + ")");
}
///
/// Sets the file data to use for all plugins.
///
/// The file data argument will be an AppDomain-local copy of the data, made by the
/// argument marshalling code. So plugins can scribble all over it without trashing
/// the original. We want to store it in PluginManager so we don't make a new copy
/// for each individual plugin.
///
/// 65xx code and data.
public void SetFileData(byte[] fileData) {
mFileData = fileData;
}
///
/// Tests simple round-trip communication. This may be called from an arbitrary thread.
///
public int Ping(int val) {
Debug.WriteLine("PluginManager Ping tid=" + Thread.CurrentThread.ManagedThreadId +
" (id=" + AppDomain.CurrentDomain.Id + "): " + val);
return val + 1;
}
///
/// Creates a plugin instance from a compiled assembly. Pass in the script identifier
/// for future lookups. If the plugin has already been instantiated, that object
/// will be returned.
///
/// Full path to compiled assembly.
/// Identifier to use in e.g. GetPlugin().
/// Reference to plugin instance.
public IPlugin LoadPlugin(string dllPath, string scriptIdent, out string failMsg) {
if (mActivePlugins.TryGetValue(dllPath, out IPlugin ip)) {
Debug.WriteLine("PM: returning cached plugin for " + dllPath);
failMsg = string.Empty;
return ip;
}
Assembly asm = Assembly.LoadFile(dllPath);
foreach (Type type in asm.GetExportedTypes()) {
// Using a System.Linq extension method.
if (type.IsClass && !type.IsAbstract &&
type.GetInterfaces().Contains(typeof(IPlugin))) {
ConstructorInfo ctor = type.GetConstructor(Type.EmptyTypes);
IPlugin iplugin;
try {
iplugin = (IPlugin)ctor.Invoke(null);
} catch (Exception ex) {
if (ex.InnerException != null) {
failMsg = ex.InnerException.Message;
} else {
failMsg = ex.Message;
}
Debug.WriteLine("LoadPlugin: failed to load '" + scriptIdent + "': "
+ failMsg);
return null;
}
Debug.WriteLine("PM created instance: " + iplugin);
mActivePlugins.Add(scriptIdent, iplugin);
failMsg = string.Empty;
return iplugin;
}
}
throw new Exception("No IPlugin class found in " + dllPath);
}
///
/// Gets an instance of a previously-loaded plugin.
///
/// Script identifier that was passed to LoadPlugin().
/// Reference to instance of plugin.
public IPlugin GetPlugin(string scriptIdent) {
if (mActivePlugins.TryGetValue(scriptIdent, out IPlugin plugin)) {
return plugin;
}
return null;
}
///
/// Returns a string with the assembly's location.
///
public string GetPluginAssemblyLocation(IPlugin plugin) {
return plugin.GetType().Assembly.Location;
}
///
/// Generates a list of references to instances of active plugins.
///
/// Newly-created list of plugin references.
public Dictionary GetActivePlugins() {
Dictionary dict =
new Dictionary(mActivePlugins.Count);
foreach (KeyValuePair kvp in mActivePlugins) {
// copy the contents; probably not necessary across AppDomain
dict.Add(kvp.Key, kvp.Value);
}
Debug.WriteLine("PluginManager: returning " + dict.Count + " plugins (id=" +
AppDomain.CurrentDomain.Id + ")");
return dict;
}
///
/// Clears the list of loaded plugins. This does not unload the assemblies from
/// the AppDomain.
///
public void ClearPluginList() {
mActivePlugins.Clear();
}
///
/// Invokes the Prepare() method on all active plugins.
///
/// Reference to host object providing app services.
/// Serialized AddressMap entries.
/// SymbolTable contents, converted to PlSymbol.
public void PreparePlugins(IApplication appRef,
List addrEntries, List plSyms) {
AddressMap addrMap = new AddressMap(addrEntries);
AddressTranslate addrTrans = new AddressTranslate(addrMap);
foreach (KeyValuePair kvp in mActivePlugins) {
IPlugin ipl = kvp.Value;
ipl.Prepare(appRef, mFileData, addrTrans);
if (ipl is IPlugin_SymbolList) {
((IPlugin_SymbolList)ipl).UpdateSymbolList(plSyms);
}
}
}
///
/// Invokes the Unprepare() method on all active plugins.
///
public void UnpreparePlugins() {
foreach (KeyValuePair kvp in mActivePlugins) {
IPlugin ipl = kvp.Value;
ipl.Unprepare();
}
}
///
/// Returns true if any of the plugins report that the before or after label is
/// significant.
///
public bool IsLabelSignificant(string labelBefore, string labelAfter) {
foreach (KeyValuePair kvp in mActivePlugins) {
IPlugin ipl = kvp.Value;
if (ipl is IPlugin_SymbolList &&
((IPlugin_SymbolList)ipl).IsLabelSignificant(labelBefore,
labelAfter)) {
return true;
}
}
return false;
}
#if false
///
/// DEBUG ONLY: establish a fast lease timeout. Normally the lease
/// is five minutes; this reduces it to a few seconds. (The actual time is
/// also affected by LifetimeServices.LeaseManagerPollTime, which defaults
/// to 10 seconds.)
///
/// Unfortunately this must be tagged [SecurityCritical] to match the method being
/// overridden, but in a partially-trusted sandbox that's not allowed. You have
/// to relax security entirely for this to work.
///
//[SecurityPermissionAttribute(SecurityAction.Demand,
// Flags = SecurityPermissionFlag.Infrastructure)]
[System.Security.SecurityCritical]
public override object InitializeLifetimeService() {
object lease = base.InitializeLifetimeService();
// netstandard2.0 doesn't have System.Runtime.Remoting.Lifetime, so use reflection
PropertyInfo leaseState = lease.GetType().GetProperty("CurrentState");
PropertyInfo initialLeaseTime = lease.GetType().GetProperty("InitialLeaseTime");
PropertyInfo sponsorshipTimeout = lease.GetType().GetProperty("SponsorshipTimeout");
PropertyInfo renewOnCallTime = lease.GetType().GetProperty("RenewOnCallTime");
Console.WriteLine("Default lease: ini=" +
initialLeaseTime.GetValue(lease) + " spon=" +
sponsorshipTimeout.GetValue(lease) + " renOC=" +
renewOnCallTime.GetValue(lease));
if ((int)leaseState.GetValue(lease) == 1 /*LeaseState.Initial*/) {
// Initial lease duration.
initialLeaseTime.SetValue(lease, TimeSpan.FromSeconds(8));
// How long we will wait for the sponsor to respond
// with a lease renewal time.
sponsorshipTimeout.SetValue(lease, TimeSpan.FromSeconds(5));
// Each call to the remote object extends the lease so that
// it has at least this much time left.
renewOnCallTime.SetValue(lease, TimeSpan.FromSeconds(2));
}
return lease;
}
#endif
}
}