/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* 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.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Policy;
using Python.Runtime;
using QuantConnect.Interfaces;
using QuantConnect.Logging;
using QuantConnect.AlgorithmFactory.Python.Wrappers;
using QuantConnect.Configuration;
using QuantConnect.Python;
using QuantConnect.Util;
namespace QuantConnect.AlgorithmFactory
{
///
/// Loader creates and manages the memory and exception space of the algorithm, ensuring if it explodes the Lean Engine is intact.
///
[ClassInterface(ClassInterfaceType.AutoDual)]
public class Loader : MarshalByRefObject
{
// True if we are in a debugging session
private readonly bool _debugging;
// Defines the maximum amount of time we will allow for instantiating an instance of IAlgorithm
private readonly TimeSpan _loaderTimeLimit;
// Language of the loader class.
private readonly Language _language;
// Defines how we resolve a list of type names into a single type name to be instantiated
private readonly Func, string> _multipleTypeNameResolverFunction;
// The worker thread instance the loader will use if not null
private readonly WorkerThread _workerThread;
///
/// Memory space of the user algorithm
///
public AppDomain appDomain { get; set; }
///
/// The algorithm's interface type that we'll be trying to load
///
private static readonly Type AlgorithmInterfaceType = typeof (IAlgorithm);
///
/// The full type name of QCAlgorithm, this is so we don't pick him up when querying for types
///
private const string AlgorithmBaseTypeFullName = "QuantConnect.Algorithm.QCAlgorithm";
///
/// The full type name of QCAlgorithmFramework, this is so we don't pick him up when querying for types
///
private const string FrameworkBaseTypeFullName = "QuantConnect.Algorithm.Framework.QCAlgorithmFramework";
///
/// Creates a new loader with a 10 second maximum load time that forces exactly one derived type to be found
///
public Loader()
: this(false, Language.CSharp, TimeSpan.FromSeconds(10), names => names.SingleOrDefault())
{
}
///
/// Creates a new loader with the specified configuration
///
/// True if we are debugging
/// Which language are we trying to load
///
/// Used to limit how long it takes to create a new instance
///
///
/// Used to resolve multiple type names found in assembly to a single type name, if null, defaults to names => names.SingleOrDefault()
///
/// When we search an assembly for derived types of IAlgorithm, sometimes the assembly will contain multiple matching types. This is the case
/// for the QuantConnect.Algorithm assembly in this solution. In order to pick the correct type, consumers must specify how to pick the type,
/// that's what this function does, it picks the correct type from the list of types found within the assembly.
///
/// The worker thread instance the loader should use
public Loader(bool debugging, Language language, TimeSpan loaderTimeLimit, Func, string> multipleTypeNameResolverFunction, WorkerThread workerThread = null)
{
_debugging = debugging;
_language = language;
_workerThread = workerThread;
if (multipleTypeNameResolverFunction == null)
{
throw new ArgumentNullException(nameof(multipleTypeNameResolverFunction));
}
_loaderTimeLimit = loaderTimeLimit;
_multipleTypeNameResolverFunction = multipleTypeNameResolverFunction;
}
///
/// Creates a new instance of the specified class in the library, safely.
///
/// Location of the DLL
/// Output algorithm instance
/// Output error message on failure
/// Bool true on successfully loading the class.
public bool TryCreateAlgorithmInstance(string assemblyPath, out IAlgorithm algorithmInstance, out string errorMessage)
{
//Default initialisation of Assembly.
algorithmInstance = null;
errorMessage = "";
//First most basic check:
if (!File.Exists(assemblyPath))
{
return false;
}
switch (_language)
{
case Language.Python:
TryCreatePythonAlgorithm(assemblyPath, out algorithmInstance, out errorMessage);
break;
default:
TryCreateILAlgorithm(assemblyPath, out algorithmInstance, out errorMessage);
break;
}
//Successful load.
return algorithmInstance != null;
}
///
/// Create a new instance of a python algorithm
///
///
///
///
///
private bool TryCreatePythonAlgorithm(string assemblyPath, out IAlgorithm algorithmInstance, out string errorMessage)
{
algorithmInstance = null;
errorMessage = string.Empty;
//File does not exist.
if (!File.Exists(assemblyPath))
{
errorMessage = $"Loader.TryCreatePythonAlgorithm(): Unable to find py file: {assemblyPath}";
return false;
}
var pythonFile = new FileInfo(assemblyPath);
var moduleName = pythonFile.Name.Replace(".pyc", "").Replace(".py", "");
try
{
PythonInitializer.Initialize();
algorithmInstance = new AlgorithmPythonWrapper(moduleName);
// we need stdout for debugging
if (!_debugging && Config.GetBool("mute-python-library-logging", true))
{
using (Py.GIL())
{
PythonEngine.Exec(
@"
import logging, os, sys
sys.stdout = open(os.devnull, 'w')
logging.captureWarnings(True)"
);
}
}
}
catch (Exception e)
{
Log.Error(e);
errorMessage = $"Loader.TryCreatePythonAlgorithm(): Unable to import python module {assemblyPath}. {e.Message}";
return false;
}
//Successful load.
return true;
}
///
/// Create a generic IL algorithm
///
///
///
///
///
private bool TryCreateILAlgorithm(string assemblyPath, out IAlgorithm algorithmInstance, out string errorMessage)
{
errorMessage = "";
algorithmInstance = null;
try
{
byte[] debugInformationBytes = null;
// if the assembly is located in the base directory then don't bother loading the pdbs
// manually, they'll be loaded automatically by the .NET runtime.
var directoryName = new FileInfo(assemblyPath).DirectoryName;
if (directoryName != null && directoryName.TrimEnd(Path.DirectorySeparatorChar) != AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar))
{
// see if the pdb exists
var mdbFilename = assemblyPath + ".mdb";
var pdbFilename = assemblyPath.Substring(0, assemblyPath.Length - 4) + ".pdb";
if (File.Exists(pdbFilename))
{
debugInformationBytes = File.ReadAllBytes(pdbFilename);
}
// see if the mdb exists
if (File.Exists(mdbFilename))
{
debugInformationBytes = File.ReadAllBytes(mdbFilename);
}
}
//Load the assembly:
Assembly assembly;
if (debugInformationBytes == null)
{
Log.Trace("Loader.TryCreateILAlgorithm(): Loading only the algorithm assembly");
assembly = Assembly.LoadFrom(assemblyPath);
}
else
{
Log.Trace("Loader.TryCreateILAlgorithm(): Loading debug information with algorithm");
var assemblyBytes = File.ReadAllBytes(assemblyPath);
assembly = Assembly.Load(assemblyBytes, debugInformationBytes);
}
//Get the list of extention classes in the library:
var types = GetExtendedTypeNames(assembly);
Log.Debug("Loader.TryCreateILAlgorithm(): Assembly types: " + string.Join(",", types));
//No extensions, nothing to load.
if (types.Count == 0)
{
errorMessage = "Algorithm type was not found.";
Log.Error("Loader.TryCreateILAlgorithm(): Types array empty, no algorithm type found.");
return false;
}
if (types.Count > 1)
{
// reshuffle type[0] to the resolved typename
types[0] = _multipleTypeNameResolverFunction.Invoke(types);
if (string.IsNullOrEmpty(types[0]))
{
errorMessage = "Algorithm type name not found, or unable to resolve multiple algorithm types to a single type. Please verify algorithm type name matches the algorithm name in the configuration file and that there is one and only one class derived from QCAlgorithm.";
Log.Error($"Loader.TryCreateILAlgorithm(): {errorMessage}");
return false;
}
}
//Load the assembly into this AppDomain:
algorithmInstance = (IAlgorithm)assembly.CreateInstance(types[0], true);
if (algorithmInstance != null)
{
Log.Trace("Loader.TryCreateILAlgorithm(): Loaded " + algorithmInstance.GetType().Name);
}
}
catch (ReflectionTypeLoadException err)
{
Log.Error(err);
Log.Error("Loader.TryCreateILAlgorithm(1): " + err.LoaderExceptions[0]);
if (err.InnerException != null) errorMessage = err.InnerException.Message;
}
catch (Exception err)
{
errorMessage = "Algorithm type name not found, or unable to resolve multiple algorithm types to a single type. Please verify algorithm type name matches the algorithm name in the configuration file and that there is one and only one class derived from QCAlgorithm.";
Log.Error($"Loader.TryCreateILAlgorithm(): {errorMessage}\n{err.InnerException ?? err}");
return false;
}
return true;
}
///
/// Get a list of all the matching type names in this DLL assembly:
///
/// Assembly dll we're loading.
/// String list of types available.
public static List GetExtendedTypeNames(Assembly assembly)
{
var types = new List();
try
{
Type[] assemblyTypes;
try
{
assemblyTypes = assembly.GetTypes();
}
catch (ReflectionTypeLoadException e)
{
// We may want to exclude possible null values
// See https://stackoverflow.com/questions/7889228/how-to-prevent-reflectiontypeloadexception-when-calling-assembly-gettypes
assemblyTypes = e.Types.Where(t => t != null).ToArray();
var countTypesNotLoaded = e.LoaderExceptions.Length;
Log.Error($"Loader.GetExtendedTypeNames(): Unable to load {countTypesNotLoaded} of the requested types, " +
"see below for more details on what causes an issue:");
foreach (Exception inner in e.LoaderExceptions)
{
Log.Error($"Loader.GetExtendedTypeNames(): {inner.Message}");
}
}
if (assemblyTypes != null && assemblyTypes.Length > 0)
{
types = (from t in assemblyTypes
where t.IsClass // require class
where !t.IsAbstract // require concrete impl
where AlgorithmInterfaceType.IsAssignableFrom(t) // require derived from IAlgorithm
where t.FullName != AlgorithmBaseTypeFullName // require not equal to QuantConnect.QCAlgorithm
where t.FullName != FrameworkBaseTypeFullName // require not equal to QuantConnect.QCAlgorithmFramework
where t.GetConstructor(Type.EmptyTypes) != null // require default ctor
select t.FullName).ToList();
}
else
{
Log.Error("API.GetExtendedTypeNames(): No types found in assembly.");
}
}
catch (Exception err)
{
Log.Error(err);
}
return types;
}
///
/// Creates a new instance of the class in the library, safely.
///
/// Location of the DLL
/// Limit of the RAM for this process
/// Output algorithm instance
/// Output error message on failure
/// bool success
public bool TryCreateAlgorithmInstanceWithIsolator(string assemblyPath, int ramLimit, out IAlgorithm algorithmInstance, out string errorMessage)
{
IAlgorithm instance = null;
var error = string.Empty;
var success = false;
var isolator = new Isolator();
var complete = isolator.ExecuteWithTimeLimit(_loaderTimeLimit, () =>
{
success = TryCreateAlgorithmInstance(assemblyPath, out instance, out error);
}, ramLimit, sleepIntervalMillis:100, workerThread:_workerThread);
algorithmInstance = instance;
errorMessage = error;
// if the isolator stopped us early add that to our error message
if (!complete)
{
errorMessage = "Failed to create algorithm instance within 10 seconds. Try re-building algorithm. " + error;
}
return complete && success && algorithmInstance != null;
}
#pragma warning disable CS1574
///
/// Unload this factory's appDomain.
///
/// Not used in lean engine. Running the library in an app domain is 10x slower.
///
#pragma warning restore CS1574
public void Unload() {
if (appDomain != null)
{
AppDomain.Unload(appDomain);
appDomain = null;
}
}
} // End Algorithm Factory Class
} // End QC Namespace.