Lean  $LEAN_TAG$
DebuggerHelper.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 Python.Runtime;
18 using System.Threading;
19 using System.Diagnostics;
20 using QuantConnect.Python;
21 using QuantConnect.Logging;
23 using System.Collections.Concurrent;
24 
26 {
27  /// <summary>
28  /// Helper class used to start a new debugging session
29  /// </summary>
30  public static class DebuggerHelper
31  {
32  private static readonly ConcurrentQueue<Py.GILState> _threadsState = new();
33 
34  /// <summary>
35  /// The different implemented debugging methods
36  /// </summary>
37  public enum DebuggingMethod
38  {
39  /// <summary>
40  /// Local debugging through cmdline.
41  /// <see cref="Language.Python"/> will use built in 'pdb'
42  /// </summary>
43  LocalCmdline,
44 
45  /// <summary>
46  /// Visual studio local debugging.
47  /// <see cref="Language.Python"/> will use 'Python Tools for Visual Studio',
48  /// attach manually selecting `Python` code type.
49  /// </summary>
50  VisualStudio,
51 
52  /// <summary>
53  /// Python Tool for Visual Studio Debugger for remote python debugging.
54  /// <see cref="Language.Python"/>. Deprecated, routes to DebugPy which
55  /// is it's replacement. Used in the same way.
56  /// </summary>
57  PTVSD,
58 
59  /// <summary>
60  /// DebugPy - a debugger for Python.
61  /// <see cref="Language.Python"/> can use `Python Extension` in VS Code
62  /// or attach to Python in Visual Studio
63  /// </summary>
64  DebugPy,
65 
66  /// <summary>
67  /// PyCharm PyDev Debugger for remote python debugging.
68  /// <see cref="Language.Python"/> will use 'Python Debug Server' in PyCharm
69  /// </summary>
70  PyCharm
71  }
72 
73  /// <summary>
74  /// Will start a new debugging session
75  /// </summary>
76  /// <param name="language">The algorithms programming language</param>
77  /// <param name="workersInitializationCallback">Optionally, the debugging method will set an action which the data stack workers should execute
78  /// so we can debug code executed by them, this is specially important for python.</param>
79  public static void Initialize(Language language, out Action workersInitializationCallback)
80  {
81  workersInitializationCallback = null;
82  if (language == Language.Python)
83  {
84  DebuggingMethod debuggingType;
85  Enum.TryParse(Config.Get("debugging-method", DebuggingMethod.LocalCmdline.ToString()), true, out debuggingType);
86 
87  Log.Trace("DebuggerHelper.Initialize(): initializing python...");
89  Log.Trace("DebuggerHelper.Initialize(): python initialization done");
90 
91  using (Py.GIL())
92  {
93  Log.Trace("DebuggerHelper.Initialize(): starting...");
94  switch (debuggingType)
95  {
96  case DebuggingMethod.LocalCmdline:
97  PythonEngine.RunSimpleString("import pdb; pdb.set_trace()");
98  break;
99 
100  case DebuggingMethod.VisualStudio:
101  Log.Trace("DebuggerHelper.Initialize(): waiting for debugger to attach...");
102  PythonEngine.RunSimpleString(@"import sys; import time;
103 while not sys.gettrace():
104  time.sleep(0.25)");
105  break;
106 
107  case DebuggingMethod.PTVSD:
108  Log.Trace("DebuggerHelper.Initialize(): waiting for PTVSD debugger to attach at localhost:5678...");
109  PythonEngine.RunSimpleString("import ptvsd; ptvsd.enable_attach(); ptvsd.wait_for_attach()");
110  break;
111 
112  case DebuggingMethod.DebugPy:
113  PythonEngine.RunSimpleString(@"import debugpy
114 from AlgorithmImports import *
115 from QuantConnect.Logging import *
116 
117 Log.Trace(""DebuggerHelper.Initialize(): debugpy waiting for attach at port 5678..."");
118 
119 debugpy.listen(('0.0.0.0', 5678))
120 debugpy.wait_for_client()");
121  workersInitializationCallback = DebugpyThreadInitialization;
122  break;
123 
124  case DebuggingMethod.PyCharm:
125  Log.Trace("DebuggerHelper.Initialize(): Attempting to connect to Pycharm PyDev debugger server...");
126  PythonEngine.RunSimpleString(@"import pydevd_pycharm; import time;
127 count = 1
128 while count <= 10:
129  try:
130  pydevd_pycharm.settrace('localhost', port=6000, stdoutToServer=True, stderrToServer=True, suspend=False)
131  print('SUCCESS: Connected to local program')
132  break
133  except ConnectionRefusedError:
134  pass
135 
136  try:
137  pydevd_pycharm.settrace('host.docker.internal', port=6000, stdoutToServer=True, stderrToServer=True, suspend=False)
138  print('SUCCESS: Connected to docker container')
139  break
140  except ConnectionRefusedError:
141  pass
142 
143  print('\n')
144  print('Failed: Ensure your PyCharm Debugger is actively waiting for a connection at port 6000!')
145  print('Try ' + count.__str__() + ' out of 10')
146  print('\n')
147  count += 1
148  time.sleep(3)");
149  break;
150  }
151  Log.Trace("DebuggerHelper.Initialize(): started");
152  }
153  }
154  else if(language == Language.CSharp)
155  {
156  if (Debugger.IsAttached)
157  {
158  Log.Trace("DebuggerHelper.Initialize(): debugger is already attached, triggering initial break.");
159  }
160  else
161  {
162  Log.Trace("DebuggerHelper.Initialize(): waiting for debugger to attach...");
163  while (!Debugger.IsAttached)
164  {
165  Thread.Sleep(250);
166  }
167  Log.Trace("DebuggerHelper.Initialize(): debugger attached");
168  }
169  }
170  else
171  {
172  throw new NotImplementedException($"DebuggerHelper.Initialize(): not implemented for {language}");
173  }
174  }
175 
176  /// <summary>
177  /// For each thread we need to create it's python state, we do this by taking the GIL and we later release it by calling 'BeginAllowThreads'
178  /// but we do not dispose of it. If we did, the state of the debugpy calls we do here are lost. So we keep a reference of the GIL we've
179  /// created so it's not picked up the C# garbage collector and disposed off, which would clear the py thread state.
180  /// </summary>
181  private static void DebugpyThreadInitialization()
182  {
183  _threadsState.Enqueue(Py.GIL());
184  PythonEngine.BeginAllowThreads();
185 
186  Log.Debug($"DebuggerHelper.Initialize({Thread.CurrentThread.Name}): initializing debugpy for thread...");
187  using (Py.GIL())
188  {
189  PythonEngine.RunSimpleString("import debugpy;debugpy.debug_this_thread();debugpy.trace_this_thread(True)");
190  }
191  }
192  }
193 }