|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2012 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the Call Trace viewer widget. |
|
8 """ |
|
9 |
|
10 import pathlib |
|
11 import re |
|
12 |
|
13 from PyQt6.QtCore import pyqtSlot, pyqtSignal, Qt |
|
14 from PyQt6.QtWidgets import QWidget, QTreeWidgetItem |
|
15 |
|
16 from EricWidgets.EricApplication import ericApp |
|
17 from EricWidgets import EricFileDialog, EricMessageBox |
|
18 |
|
19 from .Ui_CallTraceViewer import Ui_CallTraceViewer |
|
20 |
|
21 import UI.PixmapCache |
|
22 import Preferences |
|
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.getSettings().value("CallTrace/StopOnExit", True)) |
|
75 self.stopCheckBox.setChecked(stopOnExit) |
|
76 |
|
77 self.__callTraceEnabled = (Preferences.toBool( |
|
78 Preferences.getSettings().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.getSettings().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.getSettings().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 = EricFileDialog.getSaveFileNameAndFilter( |
|
156 self, |
|
157 self.tr("Save Call Trace Info"), |
|
158 "", |
|
159 self.tr("Text Files (*.txt);;All Files (*)"), |
|
160 None, |
|
161 EricFileDialog.DontConfirmOverwrite) |
|
162 if fname: |
|
163 fpath = pathlib.Path(fname) |
|
164 if not fpath.suffix: |
|
165 ex = selectedFilter.split("(*")[1].split(")")[0] |
|
166 if ex: |
|
167 fpath = fpath.with_suffix(ex) |
|
168 if fpath.exists(): |
|
169 res = EricMessageBox.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(fpath), |
|
174 icon=EricMessageBox.Warning) |
|
175 if not res: |
|
176 return |
|
177 |
|
178 try: |
|
179 title = self.tr("Call Trace Info of '{0}'").format( |
|
180 self.__tracedDebuggerId) |
|
181 with fpath.open("w", encoding="utf-8") as f: |
|
182 f.write("{0}\n".format(title)) |
|
183 f.write("{0}\n\n".format(len(title) * "=")) |
|
184 itm = self.callTrace.topLevelItem(0) |
|
185 while itm is not None: |
|
186 isCall = itm.data(0, Qt.ItemDataRole.UserRole) |
|
187 call = "->" if isCall else "<-" |
|
188 f.write("{0} {1} || {2}\n".format( |
|
189 call, |
|
190 itm.text(1), itm.text(2))) |
|
191 itm = self.callTrace.itemBelow(itm) |
|
192 except OSError as err: |
|
193 EricMessageBox.critical( |
|
194 self, |
|
195 self.tr("Error saving Call Trace Info"), |
|
196 self.tr("""<p>The call trace info could not""" |
|
197 """ be written to <b>{0}</b></p>""" |
|
198 """<p>Reason: {1}</p>""") |
|
199 .format(fpath, str(err))) |
|
200 |
|
201 @pyqtSlot(QTreeWidgetItem, int) |
|
202 def on_callTrace_itemDoubleClicked(self, item, column): |
|
203 """ |
|
204 Private slot to open the double clicked file in an editor. |
|
205 |
|
206 @param item reference to the double clicked item |
|
207 @type QTreeWidgetItem |
|
208 @param column column that was double clicked |
|
209 @type int |
|
210 """ |
|
211 if item is not None and column > 0: |
|
212 columnStr = item.text(column) |
|
213 match = self.__entryRe.fullmatch(columnStr.strip()) |
|
214 if match: |
|
215 filename, lineno, func = match.groups() |
|
216 try: |
|
217 lineno = int(lineno) |
|
218 except ValueError: |
|
219 # do nothing, if the line info is not an integer |
|
220 return |
|
221 if self.__projectMode: |
|
222 filename = self.__project.getAbsolutePath(filename) |
|
223 self.sourceFile.emit(filename, lineno) |
|
224 |
|
225 def clear(self): |
|
226 """ |
|
227 Public slot to clear the call trace info. |
|
228 """ |
|
229 self.callTrace.clear() |
|
230 self.__callStack = [] |
|
231 |
|
232 def setProjectMode(self, enabled): |
|
233 """ |
|
234 Public slot to set the call trace viewer to project mode. |
|
235 |
|
236 In project mode the call trace info is shown with project relative |
|
237 path names. |
|
238 |
|
239 @param enabled flag indicating to enable the project mode |
|
240 @type bool |
|
241 """ |
|
242 self.__projectMode = enabled |
|
243 if enabled and self.__project is None: |
|
244 self.__project = ericApp().getObject("Project") |
|
245 |
|
246 def __addCallTraceInfo(self, isCall, fromFile, fromLine, fromFunction, |
|
247 toFile, toLine, toFunction, debuggerId): |
|
248 """ |
|
249 Private method to add an entry to the call trace viewer. |
|
250 |
|
251 @param isCall flag indicating a 'call' |
|
252 @type bool |
|
253 @param fromFile name of the originating file |
|
254 @type str |
|
255 @param fromLine line number in the originating file |
|
256 @type str |
|
257 @param fromFunction name of the originating function |
|
258 @type str |
|
259 @param toFile name of the target file |
|
260 @type str |
|
261 @param toLine line number in the target file |
|
262 @type str |
|
263 @param toFunction name of the target function |
|
264 @type str |
|
265 @param debuggerId ID of the debugger backend |
|
266 @type str |
|
267 """ |
|
268 if debuggerId == self.__tracedDebuggerId: |
|
269 if isCall: |
|
270 icon = UI.PixmapCache.getIcon("forward") |
|
271 else: |
|
272 icon = UI.PixmapCache.getIcon("back") |
|
273 parentItem = ( |
|
274 self.__callStack[-1] if self.__callStack else self.callTrace) |
|
275 |
|
276 if self.__projectMode: |
|
277 fromFile = self.__project.getRelativePath(fromFile) |
|
278 toFile = self.__project.getRelativePath(toFile) |
|
279 |
|
280 itm = QTreeWidgetItem( |
|
281 parentItem, |
|
282 ["", |
|
283 self.__entryFormat.format(fromFile, fromLine, fromFunction), |
|
284 self.__entryFormat.format(toFile, toLine, toFunction)]) |
|
285 itm.setIcon(0, icon) |
|
286 itm.setData(0, Qt.ItemDataRole.UserRole, isCall) |
|
287 itm.setExpanded(True) |
|
288 |
|
289 if isCall: |
|
290 self.__callStack.append(itm) |
|
291 else: |
|
292 if self.__callStack: |
|
293 self.__callStack.pop(-1) |
|
294 |
|
295 def isCallTraceEnabled(self): |
|
296 """ |
|
297 Public method to get the state of the call trace function. |
|
298 |
|
299 @return flag indicating the state of the call trace function |
|
300 @rtype bool |
|
301 """ |
|
302 return self.__callTraceEnabled |
|
303 |
|
304 @pyqtSlot(str, int, str, bool, str) |
|
305 def __clientExit(self, program, status, message, quiet, debuggerId): |
|
306 """ |
|
307 Private slot to handle a debug client terminating. |
|
308 |
|
309 @param program name of the exited program |
|
310 @type str |
|
311 @param status exit code of the debugged program |
|
312 @type int |
|
313 @param message exit message of the debugged program |
|
314 @type str |
|
315 @param quiet flag indicating to suppress exit info display |
|
316 @type bool |
|
317 @param debuggerId ID of the debugger backend |
|
318 @type str |
|
319 """ |
|
320 if debuggerId == self.__tracedDebuggerId: |
|
321 if self.stopCheckBox.isChecked(): |
|
322 self.__setCallTraceEnabled(False) |
|
323 self.__tracedDebuggerId = "" |