Lean  $LEAN_TAG$
Loader.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using System.Collections.Generic;
18 using System.IO;
19 using System.Linq;
20 using System.Reflection;
21 using System.Runtime.InteropServices;
22 using System.Security.Policy;
23 using Python.Runtime;
25 using QuantConnect.Logging;
28 using QuantConnect.Python;
29 using QuantConnect.Util;
30 
32 {
33  /// <summary>
34  /// Loader creates and manages the memory and exception space of the algorithm, ensuring if it explodes the Lean Engine is intact.
35  /// </summary>
36  [ClassInterface(ClassInterfaceType.AutoDual)]
37  public class Loader : MarshalByRefObject
38  {
39  // True if we are in a debugging session
40  private readonly bool _debugging;
41 
42  // Defines the maximum amount of time we will allow for instantiating an instance of IAlgorithm
43  private readonly TimeSpan _loaderTimeLimit;
44 
45  // Language of the loader class.
46  private readonly Language _language;
47 
48  // Defines how we resolve a list of type names into a single type name to be instantiated
49  private readonly Func<List<string>, string> _multipleTypeNameResolverFunction;
50 
51  // The worker thread instance the loader will use if not null
52  private readonly WorkerThread _workerThread;
53 
54  /// <summary>
55  /// Memory space of the user algorithm
56  /// </summary>
57  public AppDomain appDomain { get; set; }
58 
59  /// <summary>
60  /// The algorithm's interface type that we'll be trying to load
61  /// </summary>
62  private static readonly Type AlgorithmInterfaceType = typeof (IAlgorithm);
63 
64  /// <summary>
65  /// The full type name of QCAlgorithm, this is so we don't pick him up when querying for types
66  /// </summary>
67  private const string AlgorithmBaseTypeFullName = "QuantConnect.Algorithm.QCAlgorithm";
68 
69  /// <summary>
70  /// The full type name of QCAlgorithmFramework, this is so we don't pick him up when querying for types
71  /// </summary>
72  private const string FrameworkBaseTypeFullName = "QuantConnect.Algorithm.Framework.QCAlgorithmFramework";
73 
74  /// <summary>
75  /// Creates a new loader with a 10 second maximum load time that forces exactly one derived type to be found
76  /// </summary>
77  public Loader()
78  : this(false, Language.CSharp, TimeSpan.FromSeconds(10), names => names.SingleOrDefault())
79  {
80  }
81 
82  /// <summary>
83  /// Creates a new loader with the specified configuration
84  /// </summary>
85  /// <param name="debugging">True if we are debugging</param>
86  /// <param name="language">Which language are we trying to load</param>
87  /// <param name="loaderTimeLimit">
88  /// Used to limit how long it takes to create a new instance
89  /// </param>
90  /// <param name="multipleTypeNameResolverFunction">
91  /// Used to resolve multiple type names found in assembly to a single type name, if null, defaults to names => names.SingleOrDefault()
92  ///
93  /// When we search an assembly for derived types of IAlgorithm, sometimes the assembly will contain multiple matching types. This is the case
94  /// for the QuantConnect.Algorithm assembly in this solution. In order to pick the correct type, consumers must specify how to pick the type,
95  /// that's what this function does, it picks the correct type from the list of types found within the assembly.
96  /// </param>
97  /// <param name="workerThread">The worker thread instance the loader should use</param>
98  public Loader(bool debugging, Language language, TimeSpan loaderTimeLimit, Func<List<string>, string> multipleTypeNameResolverFunction, WorkerThread workerThread = null)
99  {
100  _debugging = debugging;
101  _language = language;
102  _workerThread = workerThread;
103  if (multipleTypeNameResolverFunction == null)
104  {
105  throw new ArgumentNullException(nameof(multipleTypeNameResolverFunction));
106  }
107 
108  _loaderTimeLimit = loaderTimeLimit;
109  _multipleTypeNameResolverFunction = multipleTypeNameResolverFunction;
110  }
111 
112 
113  /// <summary>
114  /// Creates a new instance of the specified class in the library, safely.
115  /// </summary>
116  /// <param name="assemblyPath">Location of the DLL</param>
117  /// <param name="algorithmInstance">Output algorithm instance</param>
118  /// <param name="errorMessage">Output error message on failure</param>
119  /// <returns>Bool true on successfully loading the class.</returns>
120  public bool TryCreateAlgorithmInstance(string assemblyPath, out IAlgorithm algorithmInstance, out string errorMessage)
121  {
122  //Default initialisation of Assembly.
123  algorithmInstance = null;
124  errorMessage = "";
125 
126  //First most basic check:
127  if (!File.Exists(assemblyPath))
128  {
129  return false;
130  }
131 
132  switch (_language)
133  {
134  case Language.Python:
135  TryCreatePythonAlgorithm(assemblyPath, out algorithmInstance, out errorMessage);
136  break;
137 
138  default:
139  TryCreateILAlgorithm(assemblyPath, out algorithmInstance, out errorMessage);
140  break;
141  }
142 
143  //Successful load.
144  return algorithmInstance != null;
145  }
146 
147  /// <summary>
148  /// Create a new instance of a python algorithm
149  /// </summary>
150  /// <param name="assemblyPath"></param>
151  /// <param name="algorithmInstance"></param>
152  /// <param name="errorMessage"></param>
153  /// <returns></returns>
154  private bool TryCreatePythonAlgorithm(string assemblyPath, out IAlgorithm algorithmInstance, out string errorMessage)
155  {
156  algorithmInstance = null;
157  errorMessage = string.Empty;
158 
159  //File does not exist.
160  if (!File.Exists(assemblyPath))
161  {
162  errorMessage = $"Loader.TryCreatePythonAlgorithm(): Unable to find py file: {assemblyPath}";
163  return false;
164  }
165 
166  var pythonFile = new FileInfo(assemblyPath);
167  var moduleName = pythonFile.Name.Replace(".pyc", "").Replace(".py", "");
168 
169  try
170  {
172 
173  algorithmInstance = new AlgorithmPythonWrapper(moduleName);
174 
175  // we need stdout for debugging
176  if (!_debugging && Config.GetBool("mute-python-library-logging", true))
177  {
178  using (Py.GIL())
179  {
180  PythonEngine.Exec(
181  @"
182 import logging, os, sys
183 sys.stdout = open(os.devnull, 'w')
184 logging.captureWarnings(True)"
185  );
186  }
187  }
188  }
189  catch (Exception e)
190  {
191  Log.Error(e);
192  errorMessage = $"Loader.TryCreatePythonAlgorithm(): Unable to import python module {assemblyPath}. {e.Message}";
193  return false;
194  }
195 
196  //Successful load.
197  return true;
198  }
199 
200  /// <summary>
201  /// Create a generic IL algorithm
202  /// </summary>
203  /// <param name="assemblyPath"></param>
204  /// <param name="algorithmInstance"></param>
205  /// <param name="errorMessage"></param>
206  /// <returns></returns>
207  private bool TryCreateILAlgorithm(string assemblyPath, out IAlgorithm algorithmInstance, out string errorMessage)
208  {
209  errorMessage = "";
210  algorithmInstance = null;
211 
212  try
213  {
214  byte[] debugInformationBytes = null;
215 
216  // if the assembly is located in the base directory then don't bother loading the pdbs
217  // manually, they'll be loaded automatically by the .NET runtime.
218  var directoryName = new FileInfo(assemblyPath).DirectoryName;
219  if (directoryName != null && directoryName.TrimEnd(Path.DirectorySeparatorChar) != AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar))
220  {
221  // see if the pdb exists
222  var mdbFilename = assemblyPath + ".mdb";
223  var pdbFilename = assemblyPath.Substring(0, assemblyPath.Length - 4) + ".pdb";
224  if (File.Exists(pdbFilename))
225  {
226  debugInformationBytes = File.ReadAllBytes(pdbFilename);
227  }
228  // see if the mdb exists
229  if (File.Exists(mdbFilename))
230  {
231  debugInformationBytes = File.ReadAllBytes(mdbFilename);
232  }
233  }
234 
235  //Load the assembly:
236  Assembly assembly;
237  if (debugInformationBytes == null)
238  {
239  Log.Trace("Loader.TryCreateILAlgorithm(): Loading only the algorithm assembly");
240  assembly = Assembly.LoadFrom(assemblyPath);
241  }
242  else
243  {
244  Log.Trace("Loader.TryCreateILAlgorithm(): Loading debug information with algorithm");
245  var assemblyBytes = File.ReadAllBytes(assemblyPath);
246  assembly = Assembly.Load(assemblyBytes, debugInformationBytes);
247  }
248 
249  //Get the list of extention classes in the library:
250  var types = GetExtendedTypeNames(assembly);
251  Log.Debug("Loader.TryCreateILAlgorithm(): Assembly types: " + string.Join(",", types));
252 
253  //No extensions, nothing to load.
254  if (types.Count == 0)
255  {
256  errorMessage = "Algorithm type was not found.";
257  Log.Error("Loader.TryCreateILAlgorithm(): Types array empty, no algorithm type found.");
258  return false;
259  }
260 
261  if (types.Count > 1)
262  {
263  // reshuffle type[0] to the resolved typename
264  types[0] = _multipleTypeNameResolverFunction.Invoke(types);
265 
266  if (string.IsNullOrEmpty(types[0]))
267  {
268  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.";
269  Log.Error($"Loader.TryCreateILAlgorithm(): {errorMessage}");
270  return false;
271  }
272  }
273  //Load the assembly into this AppDomain:
274  algorithmInstance = (IAlgorithm)assembly.CreateInstance(types[0], true);
275 
276  if (algorithmInstance != null)
277  {
278  Log.Trace("Loader.TryCreateILAlgorithm(): Loaded " + algorithmInstance.GetType().Name);
279  }
280 
281  }
282  catch (ReflectionTypeLoadException err)
283  {
284  Log.Error(err);
285  Log.Error("Loader.TryCreateILAlgorithm(1): " + err.LoaderExceptions[0]);
286  if (err.InnerException != null) errorMessage = err.InnerException.Message;
287  }
288  catch (Exception err)
289  {
290  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.";
291  Log.Error($"Loader.TryCreateILAlgorithm(): {errorMessage}\n{err.InnerException ?? err}");
292  return false;
293  }
294 
295  return true;
296  }
297 
298  /// <summary>
299  /// Get a list of all the matching type names in this DLL assembly:
300  /// </summary>
301  /// <param name="assembly">Assembly dll we're loading.</param>
302  /// <returns>String list of types available.</returns>
303  public static List<string> GetExtendedTypeNames(Assembly assembly)
304  {
305  var types = new List<string>();
306  try
307  {
308  Type[] assemblyTypes;
309  try
310  {
311  assemblyTypes = assembly.GetTypes();
312  }
313  catch (ReflectionTypeLoadException e)
314  {
315  // We may want to exclude possible null values
316  // See https://stackoverflow.com/questions/7889228/how-to-prevent-reflectiontypeloadexception-when-calling-assembly-gettypes
317  assemblyTypes = e.Types.Where(t => t != null).ToArray();
318 
319  var countTypesNotLoaded = e.LoaderExceptions.Length;
320  Log.Error($"Loader.GetExtendedTypeNames(): Unable to load {countTypesNotLoaded} of the requested types, " +
321  "see below for more details on what causes an issue:");
322 
323  foreach (Exception inner in e.LoaderExceptions)
324  {
325  Log.Error($"Loader.GetExtendedTypeNames(): {inner.Message}");
326  }
327  }
328 
329  if (assemblyTypes != null && assemblyTypes.Length > 0)
330  {
331  types = (from t in assemblyTypes
332  where t.IsClass // require class
333  where !t.IsAbstract // require concrete impl
334  where AlgorithmInterfaceType.IsAssignableFrom(t) // require derived from IAlgorithm
335  where t.FullName != AlgorithmBaseTypeFullName // require not equal to QuantConnect.QCAlgorithm
336  where t.FullName != FrameworkBaseTypeFullName // require not equal to QuantConnect.QCAlgorithmFramework
337  where t.GetConstructor(Type.EmptyTypes) != null // require default ctor
338  select t.FullName).ToList();
339  }
340  else
341  {
342  Log.Error("API.GetExtendedTypeNames(): No types found in assembly.");
343  }
344  }
345  catch (Exception err)
346  {
347  Log.Error(err);
348  }
349 
350  return types;
351  }
352 
353  /// <summary>
354  /// Creates a new instance of the class in the library, safely.
355  /// </summary>
356  /// <param name="assemblyPath">Location of the DLL</param>
357  /// <param name="ramLimit">Limit of the RAM for this process</param>
358  /// <param name="algorithmInstance">Output algorithm instance</param>
359  /// <param name="errorMessage">Output error message on failure</param>
360  /// <returns>bool success</returns>
361  public bool TryCreateAlgorithmInstanceWithIsolator(string assemblyPath, int ramLimit, out IAlgorithm algorithmInstance, out string errorMessage)
362  {
363  IAlgorithm instance = null;
364  var error = string.Empty;
365 
366  var success = false;
367  var isolator = new Isolator();
368  var complete = isolator.ExecuteWithTimeLimit(_loaderTimeLimit, () =>
369  {
370  success = TryCreateAlgorithmInstance(assemblyPath, out instance, out error);
371  }, ramLimit, sleepIntervalMillis:100, workerThread:_workerThread);
372 
373  algorithmInstance = instance;
374  errorMessage = error;
375 
376  // if the isolator stopped us early add that to our error message
377  if (!complete)
378  {
379  errorMessage = "Failed to create algorithm instance within 10 seconds. Try re-building algorithm. " + error;
380  }
381 
382  return complete && success && algorithmInstance != null;
383  }
384 
385  #pragma warning disable CS1574
386  /// <summary>
387  /// Unload this factory's appDomain.
388  /// </summary>
389  /// <remarks>Not used in lean engine. Running the library in an app domain is 10x slower.</remarks>
390  /// <seealso cref="AppDomain.CreateDomain(string, Evidence, string, string, bool, AppDomainInitializer, string[])"/>
391  #pragma warning restore CS1574
392  public void Unload() {
393  if (appDomain != null)
394  {
395  AppDomain.Unload(appDomain);
396  appDomain = null;
397  }
398  }
399 
400  } // End Algorithm Factory Class
401 
402 } // End QC Namespace.