|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2014 - 2016 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing an import hook patching thread modules to get debugged too. |
|
8 """ |
|
9 |
|
10 import os.path |
|
11 import sys |
|
12 |
|
13 if sys.version_info[0] == 2: |
|
14 import thread as _thread |
|
15 else: |
|
16 import _thread |
|
17 |
|
18 import threading |
|
19 |
|
20 from DebugBase import DebugBase |
|
21 |
|
22 |
|
23 class ThreadExtension(object): |
|
24 """ |
|
25 Class implementing the thread support for the debugger. |
|
26 |
|
27 Provides methods for intercepting thread creation, retriving the running |
|
28 threads and their name and state. |
|
29 """ |
|
30 def __init__(self): |
|
31 """ |
|
32 Constructor |
|
33 """ |
|
34 self.threadNumber = 1 |
|
35 self._original_start_new_thread = None |
|
36 |
|
37 self.clientLock = threading.RLock() |
|
38 |
|
39 # dictionary of all threads running {id: DebugBase} |
|
40 self.threads = {} |
|
41 |
|
42 # the "current" thread, basically for variables view |
|
43 self.currentThread = self |
|
44 |
|
45 # special objects representing the main scripts thread and frame |
|
46 self.mainThread = self |
|
47 |
|
48 if sys.version_info[0] == 2: |
|
49 self.threadModName = 'thread' |
|
50 else: |
|
51 self.threadModName = '_thread' |
|
52 |
|
53 # reset already imported thread module to apply hooks at next import |
|
54 del sys.modules[self.threadModName] |
|
55 del sys.modules['threading'] |
|
56 |
|
57 # provide a hook to perform a hard breakpoint |
|
58 # Use it like this: |
|
59 # if hasattr(sys, 'breakpoint): sys.breakpoint() |
|
60 sys.breakpoint = self.set_trace |
|
61 |
|
62 def attachThread(self, target=None, args=None, kwargs=None, |
|
63 mainThread=False): |
|
64 """ |
|
65 Public method to setup a thread for DebugClient to debug. |
|
66 |
|
67 If mainThread is True, then we are attaching to the already |
|
68 started mainthread of the app and the rest of the args are ignored. |
|
69 |
|
70 @param target the start function of the target thread (i.e. the |
|
71 user code) |
|
72 @param args arguments to pass to target |
|
73 @param kwargs keyword arguments to pass to target |
|
74 @param mainThread True, if we are attaching to the already |
|
75 started mainthread of the app |
|
76 @return identifier of the created thread |
|
77 """ |
|
78 return |
|
79 try: |
|
80 self.lockClient() |
|
81 newThread = DebugThread(self, target, args, kwargs, mainThread) |
|
82 ident = -1 |
|
83 if mainThread: |
|
84 ident = _thread.get_ident() |
|
85 self.mainThread = newThread |
|
86 if self.debugging: |
|
87 sys.setprofile(newThread.profile) |
|
88 else: |
|
89 ident = _original_start_thread(newThread.bootstrap, ()) |
|
90 if self.mainThread is not None: |
|
91 self.tracePython = self.mainThread.tracePython |
|
92 newThread.set_ident(ident) |
|
93 self.threads[newThread.get_ident()] = newThread |
|
94 finally: |
|
95 self.unlockClient() |
|
96 return ident |
|
97 |
|
98 def threadTerminated(self, dbgThread): |
|
99 """ |
|
100 Public method called when a DebugThread has exited. |
|
101 |
|
102 @param dbgThread the DebugThread that has exited |
|
103 """ |
|
104 self.lockClient() |
|
105 try: |
|
106 del self.threads[dbgThread.get_ident()] |
|
107 except KeyError: |
|
108 pass |
|
109 finally: |
|
110 self.unlockClient() |
|
111 |
|
112 def lockClient(self, blocking=True): |
|
113 """ |
|
114 Public method to acquire the lock for this client. |
|
115 |
|
116 @param blocking flag to indicating a blocking lock |
|
117 @type bool |
|
118 @return flag indicating successful locking |
|
119 @rtype bool |
|
120 """ |
|
121 if blocking: |
|
122 self.clientLock.acquire() |
|
123 else: |
|
124 return self.clientLock.acquire(blocking) |
|
125 |
|
126 def unlockClient(self): |
|
127 """ |
|
128 Public method to release the lock for this client. |
|
129 """ |
|
130 try: |
|
131 self.clientLock.release() |
|
132 except AssertionError: |
|
133 pass |
|
134 |
|
135 def setCurrentThread(self, id): |
|
136 """ |
|
137 Public method to set the current thread. |
|
138 |
|
139 @param id the id the current thread should be set to. |
|
140 """ |
|
141 try: |
|
142 self.lockClient() |
|
143 if id is None or id not in self.threads: |
|
144 self.currentThread = None |
|
145 else: |
|
146 self.currentThread = self.threads[id] |
|
147 finally: |
|
148 self.unlockClient() |
|
149 |
|
150 def dumpThreadList(self): |
|
151 """ |
|
152 Public method to send the list of threads. |
|
153 """ |
|
154 threadList = [] |
|
155 if self.threads and self.currentThread: |
|
156 # indication for the threaded debugger |
|
157 currentId = self.currentThread.get_ident() |
|
158 for t in self.threads.values(): |
|
159 d = {} |
|
160 d["id"] = t.get_ident() |
|
161 d["name"] = t.get_name() |
|
162 d["broken"] = t.isBroken |
|
163 threadList.append(d) |
|
164 else: |
|
165 currentId = -1 |
|
166 d = {} |
|
167 d["id"] = -1 |
|
168 d["name"] = "MainThread" |
|
169 if hasattr(self, "isBroken"): |
|
170 d["broken"] = self.isBroken |
|
171 else: |
|
172 d["broken"] = False |
|
173 threadList.append(d) |
|
174 |
|
175 self.sendJsonCommand("ResponseThreadList", { |
|
176 "currentID": currentId, |
|
177 "threadList": threadList, |
|
178 }) |
|
179 |
|
180 |
|
181 # |
|
182 # eflag: noqa = M702 |