|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2012 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the Call Trace viewer widget. |
|
8 """ |
|
9 |
|
10 import re |
|
11 |
|
12 from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QFileInfo |
|
13 from PyQt5.QtWidgets import QWidget, QTreeWidgetItem |
|
14 |
|
15 from E5Gui.E5Application import e5App |
|
16 from E5Gui import E5FileDialog, E5MessageBox |
|
17 |
|
18 from .Ui_CallTraceViewer import Ui_CallTraceViewer |
|
19 |
|
20 import UI.PixmapCache |
|
21 import Preferences |
|
22 import Utilities |
|
23 |
|
24 |
|
25 class CallTraceViewer(QWidget, Ui_CallTraceViewer): |
|
26 """ |
|
27 Class implementing the Call Trace viewer widget. |
|
28 |
|
29 @signal sourceFile(str, int) emitted to show the source of a call/return |
|
30 point |
|
31 """ |
|
32 sourceFile = pyqtSignal(str, int) |
|
33 |
|
34 def __init__(self, debugServer, debugViewer, parent=None): |
|
35 """ |
|
36 Constructor |
|
37 |
|
38 @param debugServer reference to the debug server object |
|
39 @type DebugServer |
|
40 @param debugViewer reference to the debug viewer object |
|
41 @type DebugViewer |
|
42 @param parent reference to the parent widget |
|
43 @type QWidget |
|
44 """ |
|
45 super().__init__(parent) |
|
46 self.setupUi(self) |
|
47 |
|
48 self.__dbs = debugServer |
|
49 self.__debugViewer = debugViewer |
|
50 |
|
51 self.startTraceButton.setIcon( |
|
52 UI.PixmapCache.getIcon("callTraceStart")) |
|
53 self.stopTraceButton.setIcon( |
|
54 UI.PixmapCache.getIcon("callTraceStop")) |
|
55 self.resizeButton.setIcon(UI.PixmapCache.getIcon("resizeColumns")) |
|
56 self.clearButton.setIcon(UI.PixmapCache.getIcon("editDelete")) |
|
57 self.saveButton.setIcon(UI.PixmapCache.getIcon("fileSave")) |
|
58 |
|
59 self.__headerItem = QTreeWidgetItem( |
|
60 ["", self.tr("From"), self.tr("To")]) |
|
61 self.__headerItem.setIcon(0, UI.PixmapCache.getIcon("callReturn")) |
|
62 self.callTrace.setHeaderItem(self.__headerItem) |
|
63 |
|
64 self.__callStack = [] |
|
65 |
|
66 self.__entryFormat = "{0}:{1} ({2})" |
|
67 self.__entryRe = re.compile(r"""(.+):(\d+)\s\((.*)\)""") |
|
68 |
|
69 self.__projectMode = False |
|
70 self.__project = None |
|
71 self.__tracedDebuggerId = "" |
|
72 |
|
73 stopOnExit = Preferences.toBool( |
|
74 Preferences.Prefs.settings.value("CallTrace/StopOnExit", True)) |
|
75 self.stopCheckBox.setChecked(stopOnExit) |
|
76 |
|
77 self.__callTraceEnabled = (Preferences.toBool( |
|
78 Preferences.Prefs.settings.value("CallTrace/Enabled", False)) and |
|
79 not stopOnExit) |
|
80 |
|
81 if self.__callTraceEnabled: |
|
82 self.startTraceButton.setEnabled(False) |
|
83 else: |
|
84 self.stopTraceButton.setEnabled(False) |
|
85 |
|
86 self.__dbs.callTraceInfo.connect(self.__addCallTraceInfo) |
|
87 self.__dbs.clientExit.connect(self.__clientExit) |
|
88 |
|
89 def __setCallTraceEnabled(self, enabled): |
|
90 """ |
|
91 Private slot to set the call trace enabled status. |
|
92 |
|
93 @param enabled flag indicating the new state |
|
94 @type bool |
|
95 """ |
|
96 if enabled: |
|
97 self.__tracedDebuggerId = ( |
|
98 self.__debugViewer.getSelectedDebuggerId() |
|
99 ) |
|
100 self.__dbs.setCallTraceEnabled(self.__tracedDebuggerId, enabled) |
|
101 self.stopTraceButton.setEnabled(enabled) |
|
102 self.startTraceButton.setEnabled(not enabled) |
|
103 self.__callTraceEnabled = enabled |
|
104 Preferences.Prefs.settings.setValue("CallTrace/Enabled", enabled) |
|
105 |
|
106 if not enabled: |
|
107 for column in range(self.callTrace.columnCount()): |
|
108 self.callTrace.resizeColumnToContents(column) |
|
109 |
|
110 @pyqtSlot(bool) |
|
111 def on_stopCheckBox_clicked(self, checked): |
|
112 """ |
|
113 Private slot to handle a click on the stop check box. |
|
114 |
|
115 @param checked state of the check box |
|
116 @type bool |
|
117 """ |
|
118 Preferences.Prefs.settings.setValue("CallTrace/StopOnExit", checked) |
|
119 |
|
120 @pyqtSlot() |
|
121 def on_startTraceButton_clicked(self): |
|
122 """ |
|
123 Private slot to start call tracing. |
|
124 """ |
|
125 self.__setCallTraceEnabled(True) |
|
126 |
|
127 @pyqtSlot() |
|
128 def on_stopTraceButton_clicked(self): |
|
129 """ |
|
130 Private slot to start call tracing. |
|
131 """ |
|
132 self.__setCallTraceEnabled(False) |
|
133 |
|
134 @pyqtSlot() |
|
135 def on_resizeButton_clicked(self): |
|
136 """ |
|
137 Private slot to resize the columns of the call trace to their contents. |
|
138 """ |
|
139 for column in range(self.callTrace.columnCount()): |
|
140 self.callTrace.resizeColumnToContents(column) |
|
141 |
|
142 @pyqtSlot() |
|
143 def on_clearButton_clicked(self): |
|
144 """ |
|
145 Private slot to clear the call trace. |
|
146 """ |
|
147 self.clear() |
|
148 |
|
149 @pyqtSlot() |
|
150 def on_saveButton_clicked(self): |
|
151 """ |
|
152 Private slot to save the call trace info to a file. |
|
153 """ |
|
154 if self.callTrace.topLevelItemCount() > 0: |
|
155 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( |
|
156 self, |
|
157 self.tr("Save Call Trace Info"), |
|
158 "", |
|
159 self.tr("Text Files (*.txt);;All Files (*)"), |
|
160 None, |
|
161 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) |
|
162 if fname: |
|
163 ext = QFileInfo(fname).suffix() |
|
164 if not ext: |
|
165 ex = selectedFilter.split("(*")[1].split(")")[0] |
|
166 if ex: |
|
167 fname += ex |
|
168 if QFileInfo(fname).exists(): |
|
169 res = E5MessageBox.yesNo( |
|
170 self, |
|
171 self.tr("Save Call Trace Info"), |
|
172 self.tr("<p>The file <b>{0}</b> already exists." |
|
173 " Overwrite it?</p>").format(fname), |
|
174 icon=E5MessageBox.Warning) |
|
175 if not res: |
|
176 return |
|
177 fname = Utilities.toNativeSeparators(fname) |
|
178 |
|
179 try: |
|
180 title = self.tr("Call Trace Info of '{0}'").format( |
|
181 self.__tracedDebuggerId) |
|
182 with open(fname, "w", encoding="utf-8") as f: |
|
183 f.write("{0}\n".format(title)) |
|
184 f.write("{0}\n\n".format(len(title) * "=")) |
|
185 itm = self.callTrace.topLevelItem(0) |
|
186 while itm is not None: |
|
187 isCall = itm.data(0, Qt.ItemDataRole.UserRole) |
|
188 call = "->" if isCall else "<-" |
|
189 f.write("{0} {1} || {2}\n".format( |
|
190 call, |
|
191 itm.text(1), itm.text(2))) |
|
192 itm = self.callTrace.itemBelow(itm) |
|
193 except OSError as err: |
|
194 E5MessageBox.critical( |
|
195 self, |
|
196 self.tr("Error saving Call Trace Info"), |
|
197 self.tr("""<p>The call trace info could not""" |
|
198 """ be written to <b>{0}</b></p>""" |
|
199 """<p>Reason: {1}</p>""") |
|
200 .format(fname, str(err))) |
|
201 |
|
202 @pyqtSlot(QTreeWidgetItem, int) |
|
203 def on_callTrace_itemDoubleClicked(self, item, column): |
|
204 """ |
|
205 Private slot to open the double clicked file in an editor. |
|
206 |
|
207 @param item reference to the double clicked item |
|
208 @type QTreeWidgetItem |
|
209 @param column column that was double clicked |
|
210 @type int |
|
211 """ |
|
212 if item is not None and column > 0: |
|
213 columnStr = item.text(column) |
|
214 match = self.__entryRe.fullmatch(columnStr.strip()) |
|
215 if match: |
|
216 filename, lineno, func = match.groups() |
|
217 try: |
|
218 lineno = int(lineno) |
|
219 except ValueError: |
|
220 # do nothing, if the line info is not an integer |
|
221 return |
|
222 if self.__projectMode: |
|
223 filename = self.__project.getAbsolutePath(filename) |
|
224 self.sourceFile.emit(filename, lineno) |
|
225 |
|
226 def clear(self): |
|
227 """ |
|
228 Public slot to clear the call trace info. |
|
229 """ |
|
230 self.callTrace.clear() |
|
231 self.__callStack = [] |
|
232 |
|
233 def setProjectMode(self, enabled): |
|
234 """ |
|
235 Public slot to set the call trace viewer to project mode. |
|
236 |
|
237 In project mode the call trace info is shown with project relative |
|
238 path names. |
|
239 |
|
240 @param enabled flag indicating to enable the project mode |
|
241 @type bool |
|
242 """ |
|
243 self.__projectMode = enabled |
|
244 if enabled and self.__project is None: |
|
245 self.__project = e5App().getObject("Project") |
|
246 |
|
247 def __addCallTraceInfo(self, isCall, fromFile, fromLine, fromFunction, |
|
248 toFile, toLine, toFunction, debuggerId): |
|
249 """ |
|
250 Private method to add an entry to the call trace viewer. |
|
251 |
|
252 @param isCall flag indicating a 'call' |
|
253 @type bool |
|
254 @param fromFile name of the originating file |
|
255 @type str |
|
256 @param fromLine line number in the originating file |
|
257 @type str |
|
258 @param fromFunction name of the originating function |
|
259 @type str |
|
260 @param toFile name of the target file |
|
261 @type str |
|
262 @param toLine line number in the target file |
|
263 @type str |
|
264 @param toFunction name of the target function |
|
265 @type str |
|
266 @param debuggerId ID of the debugger backend |
|
267 @type str |
|
268 """ |
|
269 if debuggerId == self.__tracedDebuggerId: |
|
270 if isCall: |
|
271 icon = UI.PixmapCache.getIcon("forward") |
|
272 else: |
|
273 icon = UI.PixmapCache.getIcon("back") |
|
274 parentItem = ( |
|
275 self.__callStack[-1] if self.__callStack else self.callTrace) |
|
276 |
|
277 if self.__projectMode: |
|
278 fromFile = self.__project.getRelativePath(fromFile) |
|
279 toFile = self.__project.getRelativePath(toFile) |
|
280 |
|
281 itm = QTreeWidgetItem( |
|
282 parentItem, |
|
283 ["", |
|
284 self.__entryFormat.format(fromFile, fromLine, fromFunction), |
|
285 self.__entryFormat.format(toFile, toLine, toFunction)]) |
|
286 itm.setIcon(0, icon) |
|
287 itm.setData(0, Qt.ItemDataRole.UserRole, isCall) |
|
288 itm.setExpanded(True) |
|
289 |
|
290 if isCall: |
|
291 self.__callStack.append(itm) |
|
292 else: |
|
293 if self.__callStack: |
|
294 self.__callStack.pop(-1) |
|
295 |
|
296 def isCallTraceEnabled(self): |
|
297 """ |
|
298 Public method to get the state of the call trace function. |
|
299 |
|
300 @return flag indicating the state of the call trace function |
|
301 @rtype bool |
|
302 """ |
|
303 return self.__callTraceEnabled |
|
304 |
|
305 @pyqtSlot(str, int, str, bool, str) |
|
306 def __clientExit(self, program, status, message, quiet, debuggerId): |
|
307 """ |
|
308 Private slot to handle a debug client terminating. |
|
309 |
|
310 @param program name of the exited program |
|
311 @type str |
|
312 @param status exit code of the debugged program |
|
313 @type int |
|
314 @param message exit message of the debugged program |
|
315 @type str |
|
316 @param quiet flag indicating to suppress exit info display |
|
317 @type bool |
|
318 @param debuggerId ID of the debugger backend |
|
319 @type str |
|
320 """ |
|
321 if debuggerId == self.__tracedDebuggerId: |
|
322 if self.stopCheckBox.isChecked(): |
|
323 self.__setCallTraceEnabled(False) |
|
324 self.__tracedDebuggerId = "" |