src/eric7/Debugger/DebuggerInterfacePython.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9207
c0b4ca34de2f
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the Python3 debugger interface for the debug server.
8 """
9
10 import sys
11 import os
12 import logging
13 import shlex
14 import contextlib
15
16 from PyQt6.QtCore import (
17 QObject, QProcess, QProcessEnvironment, QTimer
18 )
19
20 from EricWidgets.EricApplication import ericApp
21 from EricWidgets import EricMessageBox
22
23 from . import DebugClientCapabilities
24
25 import Globals
26 import Preferences
27 import Utilities
28
29 from eric7config import getConfig
30
31
32 ClientDefaultCapabilities = DebugClientCapabilities.HasAll
33
34
35 class DebuggerInterfacePython(QObject):
36 """
37 Class implementing the debugger interface for the debug server for
38 Python 3.
39 """
40 def __init__(self, debugServer, passive):
41 """
42 Constructor
43
44 @param debugServer reference to the debug server
45 @type DebugServer
46 @param passive flag indicating passive connection mode
47 @type bool
48 """
49 super().__init__()
50
51 self.__isNetworked = True
52 self.__autoContinue = False
53 self.__autoContinued = []
54 self.__isStepCommand = False
55
56 self.debugServer = debugServer
57 self.passive = passive
58 self.process = None
59 self.__startedVenv = ""
60
61 self.queue = []
62 self.__master = None
63 self.__connections = {}
64 self.__pendingConnections = []
65 self.__inShutdown = False
66
67 # set default values for capabilities of clients
68 self.clientCapabilities = ClientDefaultCapabilities
69
70 # set translation function
71 self.translate = self.__identityTranslation
72
73 if passive:
74 # set translation function
75 if Preferences.getDebugger("PathTranslation"):
76 self.translateRemote = Preferences.getDebugger(
77 "PathTranslationRemote")
78 self.translateRemoteWindows = "\\" in self.translateRemote
79 self.translateLocal = Preferences.getDebugger(
80 "PathTranslationLocal")
81 self.translateLocalWindows = "\\" in self.translateLocal
82 self.translate = self.__remoteTranslation
83 else:
84 self.translate = self.__identityTranslation
85
86 # attribute to remember the name of the executed script
87 self.__scriptName = ""
88
89 def __identityTranslation(self, fn, remote2local=True):
90 """
91 Private method to perform the identity path translation.
92
93 @param fn filename to be translated
94 @type str
95 @param remote2local flag indicating the direction of translation
96 (False = local to remote, True = remote to local [default])
97 @type bool
98 @return translated filename
99 @rtype str
100 """
101 return fn
102
103 def __remoteTranslation(self, fn, remote2local=True):
104 """
105 Private method to perform the path translation.
106
107 @param fn filename to be translated
108 @type str
109 @param remote2local flag indicating the direction of translation
110 (False = local to remote, True = remote to local [default])
111 @type bool
112 @return translated filename
113 @rtype str
114 """
115 if remote2local:
116 path = fn.replace(self.translateRemote, self.translateLocal)
117 if self.translateLocalWindows:
118 path = path.replace("/", "\\")
119 else:
120 path = fn.replace(self.translateLocal, self.translateRemote)
121 if not self.translateRemoteWindows:
122 path = path.replace("\\", "/")
123
124 return path
125
126 def __startProcess(self, program, arguments, environment=None,
127 workingDir=None):
128 """
129 Private method to start the debugger client process.
130
131 @param program name of the executable to start
132 @type str
133 @param arguments arguments to be passed to the program
134 @type list of str
135 @param environment dictionary of environment settings to pass
136 @type dict of str
137 @param workingDir directory to start the debugger client in
138 @type str
139 @return the process object
140 @rtype QProcess or None
141 """
142 proc = QProcess(self)
143 if environment is not None:
144 env = QProcessEnvironment()
145 for key, value in list(environment.items()):
146 env.insert(key, value)
147 proc.setProcessEnvironment(env)
148 args = arguments[:]
149 if workingDir:
150 proc.setWorkingDirectory(workingDir)
151 proc.start(program, args)
152 if not proc.waitForStarted(10000):
153 proc = None
154
155 return proc
156
157 def startRemote(self, port, runInConsole, venvName, originalPathString,
158 workingDir=None, configOverride=None):
159 """
160 Public method to start a remote Python interpreter.
161
162 @param port port number the debug server is listening on
163 @type int
164 @param runInConsole flag indicating to start the debugger in a
165 console window
166 @type bool
167 @param venvName name of the virtual environment to be used
168 @type str
169 @param originalPathString original PATH environment variable
170 @type str
171 @param workingDir directory to start the debugger client in
172 @type str
173 @param configOverride dictionary containing the global config override
174 data
175 @type dict
176 @return client process object, a flag to indicate a network connection
177 and the name of the interpreter in case of a local execution
178 @rtype tuple of (QProcess, bool, str)
179 """
180 global origPathEnv
181
182 if not venvName:
183 venvName = Preferences.getDebugger("Python3VirtualEnv")
184 venvManager = ericApp().getObject("VirtualEnvManager")
185 interpreter = venvManager.getVirtualenvInterpreter(venvName)
186 execPath = venvManager.getVirtualenvExecPath(venvName)
187 if interpreter == "":
188 # use the interpreter used to run eric for identical variants
189 interpreter = Globals.getPythonExecutable()
190 if interpreter == "":
191 EricMessageBox.critical(
192 None,
193 self.tr("Start Debugger"),
194 self.tr(
195 """<p>No suitable Python3 environment configured.</p>""")
196 )
197 return None, False, ""
198
199 self.__inShutdown = False
200
201 debugClientType = Preferences.getDebugger("DebugClientType3")
202 if debugClientType == "standard":
203 debugClient = os.path.join(getConfig('ericDir'),
204 "DebugClients", "Python",
205 "DebugClient.py")
206 else:
207 debugClient = Preferences.getDebugger("DebugClient3")
208 if debugClient == "":
209 debugClient = os.path.join(sys.path[0],
210 "DebugClients", "Python",
211 "DebugClient.py")
212
213 redirect = (
214 str(configOverride["redirect"])
215 if configOverride and configOverride["enable"] else
216 str(Preferences.getDebugger("Python3Redirect"))
217 )
218 noencoding = (Preferences.getDebugger("Python3NoEncoding") and
219 '--no-encoding' or '')
220 multiprocessEnabled = (
221 '--multiprocess' if Preferences.getDebugger("MultiProcessEnabled")
222 else ''
223 )
224
225 if Preferences.getDebugger("RemoteDbgEnabled"):
226 ipaddr = self.debugServer.getHostAddress(False)
227 rexec = Preferences.getDebugger("RemoteExecution")
228 rhost = Preferences.getDebugger("RemoteHost")
229 if rhost == "":
230 rhost = "localhost"
231 if rexec:
232 args = Utilities.parseOptionString(rexec) + [
233 rhost, interpreter, debugClient]
234 if noencoding:
235 args.append(noencoding)
236 if multiprocessEnabled:
237 args.append(multiprocessEnabled)
238 args.extend([str(port), redirect, ipaddr])
239 if Utilities.isWindowsPlatform():
240 if not os.path.splitext(args[0])[1]:
241 for ext in [".exe", ".com", ".cmd", ".bat"]:
242 prog = Utilities.getExecutablePath(args[0] + ext)
243 if prog:
244 args[0] = prog
245 break
246 else:
247 args[0] = Utilities.getExecutablePath(args[0])
248 process = self.__startProcess(args[0], args[1:],
249 workingDir=workingDir)
250 if process is None:
251 EricMessageBox.critical(
252 None,
253 self.tr("Start Debugger"),
254 self.tr(
255 """<p>The debugger backend could not be"""
256 """ started.</p>"""))
257
258 # set translation function
259 if Preferences.getDebugger("PathTranslation"):
260 self.translateRemote = Preferences.getDebugger(
261 "PathTranslationRemote")
262 self.translateRemoteWindows = "\\" in self.translateRemote
263 self.translateLocal = Preferences.getDebugger(
264 "PathTranslationLocal")
265 self.translate = self.__remoteTranslation
266 self.translateLocalWindows = "\\" in self.translateLocal
267 else:
268 self.translate = self.__identityTranslation
269 return process, self.__isNetworked, ""
270
271 # set translation function
272 self.translate = self.__identityTranslation
273
274 # setup the environment for the debugger
275 if Preferences.getDebugger("DebugEnvironmentReplace"):
276 clientEnv = {}
277 else:
278 clientEnv = os.environ.copy()
279 if originalPathString:
280 clientEnv["PATH"] = originalPathString
281 envlist = shlex.split(
282 Preferences.getDebugger("DebugEnvironment"))
283 for el in envlist:
284 with contextlib.suppress(ValueError):
285 key, value = el.split('=', 1)
286 clientEnv[str(key)] = str(value)
287 if execPath:
288 if "PATH" in clientEnv:
289 clientEnv["PATH"] = os.pathsep.join(
290 [execPath, clientEnv["PATH"]])
291 else:
292 clientEnv["PATH"] = execPath
293
294 ipaddr = self.debugServer.getHostAddress(True)
295 if runInConsole or Preferences.getDebugger("ConsoleDbgEnabled"):
296 ccmd = Preferences.getDebugger("ConsoleDbgCommand")
297 if ccmd:
298 args = Utilities.parseOptionString(ccmd) + [
299 interpreter, os.path.abspath(debugClient)]
300 if noencoding:
301 args.append(noencoding)
302 if multiprocessEnabled:
303 args.append(multiprocessEnabled)
304 args.extend([str(port), '0', ipaddr])
305 args[0] = Utilities.getExecutablePath(args[0])
306 process = self.__startProcess(args[0], args[1:], clientEnv,
307 workingDir=workingDir)
308 if process is None:
309 EricMessageBox.critical(
310 None,
311 self.tr("Start Debugger"),
312 self.tr(
313 """<p>The debugger backend could not be"""
314 """ started.</p>"""))
315 return process, self.__isNetworked, interpreter
316
317 args = [debugClient]
318 if noencoding:
319 args.append(noencoding)
320 if multiprocessEnabled:
321 args.append(multiprocessEnabled)
322 args.extend([str(port), redirect, ipaddr])
323 process = self.__startProcess(interpreter, args, clientEnv,
324 workingDir=workingDir)
325 if process is None:
326 self.__startedVenv = ""
327 EricMessageBox.critical(
328 None,
329 self.tr("Start Debugger"),
330 self.tr(
331 """<p>The debugger backend could not be started.</p>"""))
332 else:
333 self.__startedVenv = venvName
334
335 return process, self.__isNetworked, interpreter
336
337 def startRemoteForProject(self, port, runInConsole, venvName,
338 originalPathString, workingDir=None,
339 configOverride=None):
340 """
341 Public method to start a remote Python interpreter for a project.
342
343 @param port port number the debug server is listening on
344 @type int
345 @param runInConsole flag indicating to start the debugger in a
346 console window
347 @type bool
348 @param venvName name of the virtual environment to be used
349 @type str
350 @param originalPathString original PATH environment variable
351 @type str
352 @param workingDir directory to start the debugger client in
353 @type str
354 @param configOverride dictionary containing the global config override
355 data
356 @type dict
357 @return client process object, a flag to indicate a network connection
358 and the name of the interpreter in case of a local execution
359 @rtype tuple of (QProcess, bool, str)
360 """
361 global origPathEnv
362
363 project = ericApp().getObject("Project")
364 if not project.isDebugPropertiesLoaded():
365 return None, self.__isNetworked, ""
366
367 # start debugger with project specific settings
368 debugClient = project.getDebugProperty("DEBUGCLIENT")
369
370 redirect = (
371 str(configOverride["redirect"])
372 if configOverride and configOverride["enable"] else
373 str(project.getDebugProperty("REDIRECT"))
374 )
375 noencoding = (
376 '--no-encoding' if project.getDebugProperty("NOENCODING") else ''
377 )
378 multiprocessEnabled = (
379 '--multiprocess' if Preferences.getDebugger("MultiProcessEnabled")
380 else ''
381 )
382
383 if venvName:
384 venvManager = ericApp().getObject("VirtualEnvManager")
385 interpreter = venvManager.getVirtualenvInterpreter(venvName)
386 execPath = venvManager.getVirtualenvExecPath(venvName)
387 else:
388 venvName = project.getProjectVenv()
389 execPath = project.getProjectExecPath()
390 interpreter = project.getProjectInterpreter()
391 if interpreter == "":
392 EricMessageBox.critical(
393 None,
394 self.tr("Start Debugger"),
395 self.tr(
396 """<p>No suitable Python3 environment configured.</p>""")
397 )
398 return None, self.__isNetworked, ""
399
400 self.__inShutdown = False
401
402 if project.getDebugProperty("REMOTEDEBUGGER"):
403 ipaddr = self.debugServer.getHostAddress(False)
404 rexec = project.getDebugProperty("REMOTECOMMAND")
405 rhost = project.getDebugProperty("REMOTEHOST")
406 if rhost == "":
407 rhost = "localhost"
408 if rexec:
409 args = Utilities.parseOptionString(rexec) + [
410 rhost, interpreter, debugClient]
411 if noencoding:
412 args.append(noencoding)
413 if multiprocessEnabled:
414 args.append(multiprocessEnabled)
415 args.extend([str(port), redirect, ipaddr])
416 if Utilities.isWindowsPlatform():
417 if not os.path.splitext(args[0])[1]:
418 for ext in [".exe", ".com", ".cmd", ".bat"]:
419 prog = Utilities.getExecutablePath(args[0] + ext)
420 if prog:
421 args[0] = prog
422 break
423 else:
424 args[0] = Utilities.getExecutablePath(args[0])
425 process = self.__startProcess(args[0], args[1:],
426 workingDir=workingDir)
427 if process is None:
428 EricMessageBox.critical(
429 None,
430 self.tr("Start Debugger"),
431 self.tr(
432 """<p>The debugger backend could not be"""
433 """ started.</p>"""))
434 # set translation function
435 if project.getDebugProperty("PATHTRANSLATION"):
436 self.translateRemote = project.getDebugProperty(
437 "REMOTEPATH")
438 self.translateRemoteWindows = "\\" in self.translateRemote
439 self.translateLocal = project.getDebugProperty("LOCALPATH")
440 self.translateLocalWindows = "\\" in self.translateLocal
441 self.translate = self.__remoteTranslation
442 else:
443 self.translate = self.__identityTranslation
444 return process, self.__isNetworked, ""
445 else:
446 # remote shell command is missing
447 return None, self.__isNetworked, ""
448
449 # set translation function
450 self.translate = self.__identityTranslation
451
452 # setup the environment for the debugger
453 if project.getDebugProperty("ENVIRONMENTOVERRIDE"):
454 clientEnv = {}
455 else:
456 clientEnv = os.environ.copy()
457 if originalPathString:
458 clientEnv["PATH"] = originalPathString
459 envlist = shlex.split(
460 project.getDebugProperty("ENVIRONMENTSTRING"))
461 for el in envlist:
462 with contextlib.suppress(ValueError):
463 key, value = el.split('=', 1)
464 clientEnv[str(key)] = str(value)
465 if execPath:
466 if "PATH" in clientEnv:
467 clientEnv["PATH"] = os.pathsep.join(
468 [execPath, clientEnv["PATH"]])
469 else:
470 clientEnv["PATH"] = execPath
471
472 ipaddr = self.debugServer.getHostAddress(True)
473 if runInConsole or project.getDebugProperty("CONSOLEDEBUGGER"):
474 ccmd = (project.getDebugProperty("CONSOLECOMMAND") or
475 Preferences.getDebugger("ConsoleDbgCommand"))
476 if ccmd:
477 args = Utilities.parseOptionString(ccmd) + [
478 interpreter, os.path.abspath(debugClient)]
479 if noencoding:
480 args.append(noencoding)
481 if multiprocessEnabled:
482 args.append(multiprocessEnabled)
483 args.extend([str(port), '0', ipaddr])
484 args[0] = Utilities.getExecutablePath(args[0])
485 process = self.__startProcess(args[0], args[1:], clientEnv,
486 workingDir=workingDir)
487 if process is None:
488 EricMessageBox.critical(
489 None,
490 self.tr("Start Debugger"),
491 self.tr(
492 """<p>The debugger backend could not be"""
493 """ started.</p>"""))
494 return process, self.__isNetworked, interpreter
495
496 args = [debugClient]
497 if noencoding:
498 args.append(noencoding)
499 if multiprocessEnabled:
500 args.append(multiprocessEnabled)
501 args.extend([str(port), redirect, ipaddr])
502 process = self.__startProcess(interpreter, args, clientEnv,
503 workingDir=workingDir)
504 if process is None:
505 self.__startedVenv = ""
506 EricMessageBox.critical(
507 None,
508 self.tr("Start Debugger"),
509 self.tr(
510 """<p>The debugger backend could not be started.</p>"""))
511 else:
512 self.__startedVenv = venvName
513
514 return process, self.__isNetworked, interpreter
515
516 def getClientCapabilities(self):
517 """
518 Public method to retrieve the debug clients capabilities.
519
520 @return debug client capabilities
521 @rtype int
522 """
523 return self.clientCapabilities
524
525 def newConnection(self, sock):
526 """
527 Public slot to handle a new connection.
528
529 @param sock reference to the socket object
530 @type QTcpSocket
531 @return flag indicating success
532 @rtype bool
533 """
534 self.__pendingConnections.append(sock)
535
536 sock.readyRead.connect(lambda: self.__parseClientLine(sock))
537 sock.disconnected.connect(lambda: self.__socketDisconnected(sock))
538
539 return True
540
541 def __assignDebuggerId(self, sock, debuggerId):
542 """
543 Private method to set the debugger id for a recent debugger connection
544 attempt.
545
546 @param sock reference to the socket object
547 @type QTcpSocket
548 @param debuggerId id of the connected debug client
549 @type str
550 """
551 if sock in self.__pendingConnections:
552 self.__connections[debuggerId] = sock
553 self.__pendingConnections.remove(sock)
554
555 if self.__master is None:
556 self.__master = debuggerId
557 # Get the remote clients capabilities
558 self.remoteCapabilities(debuggerId)
559
560 self.debugServer.signalClientDebuggerId(debuggerId)
561
562 if debuggerId == self.__master:
563 self.__flush()
564 self.debugServer.masterClientConnected()
565
566 self.debugServer.initializeClient(debuggerId)
567
568 # perform auto-continue except for master
569 if (
570 debuggerId != self.__master and
571 self.__autoContinue and
572 not self.__isStepCommand
573 ):
574 self.__autoContinued.append(debuggerId)
575 QTimer.singleShot(
576 0, lambda: self.remoteContinue(debuggerId))
577
578 def __socketDisconnected(self, sock):
579 """
580 Private slot handling a socket disconnecting.
581
582 @param sock reference to the disconnected socket
583 @type QTcpSocket
584 """
585 for debuggerId in self.__connections:
586 if self.__connections[debuggerId] is sock:
587 del self.__connections[debuggerId]
588 if debuggerId == self.__master:
589 self.__master = None
590 if debuggerId in self.__autoContinued:
591 self.__autoContinued.remove(debuggerId)
592 if not self.__inShutdown:
593 with contextlib.suppress(RuntimeError):
594 # can be ignored during a shutdown
595 self.debugServer.signalClientDisconnected(debuggerId)
596 break
597 else:
598 if sock in self.__pendingConnections:
599 self.__pendingConnections.remove(sock)
600
601 if not self.__connections:
602 # no active connections anymore
603 with contextlib.suppress(RuntimeError):
604 self.debugServer.signalLastClientExited()
605 # debug server object might have been deleted already
606 # ignore this
607 self.__autoContinued.clear()
608 self.debugServer.startClient()
609
610 def getDebuggerIds(self):
611 """
612 Public method to return the IDs of the connected debugger backends.
613
614 @return list of connected debugger backend IDs
615 @rtype list of str
616 """
617 return sorted(self.__connections.keys())
618
619 def __flush(self):
620 """
621 Private slot to flush the queue.
622 """
623 if self.__master:
624 # Send commands that were waiting for the connection.
625 for cmd in self.queue:
626 self.__writeJsonCommandToSocket(
627 cmd, self.__connections[self.__master])
628
629 self.queue = []
630
631 def shutdown(self):
632 """
633 Public method to cleanly shut down.
634
635 It closes our sockets and shuts down the debug clients.
636 (Needed on Win OS)
637 """
638 if not self.__master:
639 return
640
641 self.__inShutdown = True
642
643 while self.__connections:
644 debuggerId, sock = self.__connections.popitem()
645 self.__shutdownSocket(sock)
646
647 while self.__pendingConnections:
648 sock = self.__pendingConnections.pop()
649 self.__shutdownSocket(sock)
650
651 # reinitialize
652 self.queue = []
653
654 self.__master = None
655
656 def __shutdownSocket(self, sock):
657 """
658 Private slot to shut down a socket.
659
660 @param sock reference to the socket
661 @type QTcpSocket
662 """
663 # do not want any slots called during shutdown
664 sock.readyRead.disconnect()
665 sock.disconnected.disconnect()
666
667 # close down socket, and shut down client as well.
668 self.__sendJsonCommand("RequestShutdown", {}, sock=sock)
669 sock.flush()
670 sock.close()
671
672 sock.setParent(None)
673 sock.deleteLater()
674 del sock
675
676 def isConnected(self):
677 """
678 Public method to test, if a debug client has connected.
679
680 @return flag indicating the connection status
681 @rtype bool
682 """
683 return bool(self.__connections)
684
685 def remoteEnvironment(self, env):
686 """
687 Public method to set the environment for a program to debug, run, ...
688
689 @param env environment settings
690 @type dict
691 """
692 self.__sendJsonCommand("RequestEnvironment", {"environment": env},
693 self.__master)
694
695 def remoteLoad(self, fn, argv, wd, traceInterpreter=False,
696 autoContinue=True, enableMultiprocess=False):
697 """
698 Public method to load a new program to debug.
699
700 @param fn the filename to debug
701 @type str
702 @param argv the commandline arguments to pass to the program
703 @type str
704 @param wd the working directory for the program
705 @type str
706 @param traceInterpreter flag indicating if the interpreter library
707 should be traced as well
708 @type bool
709 @param autoContinue flag indicating, that the debugger should not
710 stop at the first executable line
711 @type bool
712 @param enableMultiprocess flag indicating to perform multiprocess
713 debugging
714 @type bool
715 """
716 self.__autoContinue = autoContinue
717 self.__scriptName = os.path.abspath(fn)
718 self.__isStepCommand = False
719
720 wd = self.translate(wd, False)
721 fn = self.translate(os.path.abspath(fn), False)
722 self.__sendJsonCommand("RequestLoad", {
723 "workdir": wd,
724 "filename": fn,
725 "argv": Utilities.parseOptionString(argv),
726 "traceInterpreter": traceInterpreter,
727 "multiprocess": enableMultiprocess,
728 }, self.__master)
729
730 def remoteRun(self, fn, argv, wd):
731 """
732 Public method to load a new program to run.
733
734 @param fn the filename to run
735 @type str
736 @param argv the commandline arguments to pass to the program
737 @type str
738 @param wd the working directory for the program
739 @type str
740 """
741 self.__scriptName = os.path.abspath(fn)
742
743 wd = self.translate(wd, False)
744 fn = self.translate(os.path.abspath(fn), False)
745 self.__sendJsonCommand("RequestRun", {
746 "workdir": wd,
747 "filename": fn,
748 "argv": Utilities.parseOptionString(argv),
749 }, self.__master)
750
751 def remoteCoverage(self, fn, argv, wd, erase=False):
752 """
753 Public method to load a new program to collect coverage data.
754
755 @param fn the filename to run
756 @type str
757 @param argv the commandline arguments to pass to the program
758 @type str
759 @param wd the working directory for the program
760 @type str
761 @param erase flag indicating that coverage info should be
762 cleared first
763 @type bool
764 """
765 self.__scriptName = os.path.abspath(fn)
766
767 wd = self.translate(wd, False)
768 fn = self.translate(os.path.abspath(fn), False)
769 self.__sendJsonCommand("RequestCoverage", {
770 "workdir": wd,
771 "filename": fn,
772 "argv": Utilities.parseOptionString(argv),
773 "erase": erase,
774 }, self.__master)
775
776 def remoteProfile(self, fn, argv, wd, erase=False):
777 """
778 Public method to load a new program to collect profiling data.
779
780 @param fn the filename to run
781 @type str
782 @param argv the commandline arguments to pass to the program
783 @type str
784 @param wd the working directory for the program
785 @type str
786 @param erase flag indicating that timing info should be cleared
787 first
788 @type bool
789 """
790 self.__scriptName = os.path.abspath(fn)
791
792 wd = self.translate(wd, False)
793 fn = self.translate(os.path.abspath(fn), False)
794 self.__sendJsonCommand("RequestProfile", {
795 "workdir": wd,
796 "filename": fn,
797 "argv": Utilities.parseOptionString(argv),
798 "erase": erase,
799 }, self.__master)
800
801 def remoteStatement(self, debuggerId, stmt):
802 """
803 Public method to execute a Python statement.
804
805 @param debuggerId ID of the debugger backend
806 @type str
807 @param stmt the Python statement to execute.
808 @type str
809 """
810 self.__sendJsonCommand("ExecuteStatement", {
811 "statement": stmt,
812 }, debuggerId)
813
814 def remoteStep(self, debuggerId):
815 """
816 Public method to single step the debugged program.
817
818 @param debuggerId ID of the debugger backend
819 @type str
820 """
821 self.__isStepCommand = True
822 self.__sendJsonCommand("RequestStep", {}, debuggerId)
823
824 def remoteStepOver(self, debuggerId):
825 """
826 Public method to step over the debugged program.
827
828 @param debuggerId ID of the debugger backend
829 @type str
830 """
831 self.__isStepCommand = True
832 self.__sendJsonCommand("RequestStepOver", {}, debuggerId)
833
834 def remoteStepOut(self, debuggerId):
835 """
836 Public method to step out the debugged program.
837
838 @param debuggerId ID of the debugger backend
839 @type str
840 """
841 self.__isStepCommand = True
842 self.__sendJsonCommand("RequestStepOut", {}, debuggerId)
843
844 def remoteStepQuit(self, debuggerId):
845 """
846 Public method to stop the debugged program.
847
848 @param debuggerId ID of the debugger backend
849 @type str
850 """
851 self.__isStepCommand = True
852 self.__sendJsonCommand("RequestStepQuit", {}, debuggerId)
853
854 def remoteContinue(self, debuggerId, special=False):
855 """
856 Public method to continue the debugged program.
857
858 @param debuggerId ID of the debugger backend
859 @type str
860 @param special flag indicating a special continue operation
861 @type bool
862 """
863 self.__isStepCommand = False
864 self.__sendJsonCommand("RequestContinue", {
865 "special": special,
866 }, debuggerId)
867
868 def remoteContinueUntil(self, debuggerId, line):
869 """
870 Public method to continue the debugged program to the given line
871 or until returning from the current frame.
872
873 @param debuggerId ID of the debugger backend
874 @type str
875 @param line the new line, where execution should be continued to
876 @type int
877 """
878 self.__isStepCommand = False
879 self.__sendJsonCommand("RequestContinueUntil", {
880 "newLine": line,
881 }, debuggerId)
882
883 def remoteMoveIP(self, debuggerId, line):
884 """
885 Public method to move the instruction pointer to a different line.
886
887 @param debuggerId ID of the debugger backend
888 @type str
889 @param line the new line, where execution should be continued
890 @type int
891 """
892 self.__sendJsonCommand("RequestMoveIP", {
893 "newLine": line,
894 }, debuggerId)
895
896 def remoteBreakpoint(self, debuggerId, fn, line, setBreakpoint, cond=None,
897 temp=False):
898 """
899 Public method to set or clear a breakpoint.
900
901 @param debuggerId ID of the debugger backend
902 @type str
903 @param fn filename the breakpoint belongs to
904 @type str
905 @param line linenumber of the breakpoint
906 @type int
907 @param setBreakpoint flag indicating setting or resetting a breakpoint
908 @type bool
909 @param cond condition of the breakpoint
910 @type str
911 @param temp flag indicating a temporary breakpoint
912 @type bool
913 """
914 debuggerList = ([debuggerId] if debuggerId
915 else list(self.__connections.keys()))
916 for debuggerId in debuggerList:
917 self.__sendJsonCommand("RequestBreakpoint", {
918 "filename": self.translate(fn, False),
919 "line": line,
920 "temporary": temp,
921 "setBreakpoint": setBreakpoint,
922 "condition": cond,
923 }, debuggerId)
924
925 def remoteBreakpointEnable(self, debuggerId, fn, line, enable):
926 """
927 Public method to enable or disable a breakpoint.
928
929 @param debuggerId ID of the debugger backend
930 @type str
931 @param fn filename the breakpoint belongs to
932 @type str
933 @param line linenumber of the breakpoint
934 @type int
935 @param enable flag indicating enabling or disabling a breakpoint
936 @type bool
937 """
938 debuggerList = ([debuggerId] if debuggerId
939 else list(self.__connections.keys()))
940 for debuggerId in debuggerList:
941 self.__sendJsonCommand("RequestBreakpointEnable", {
942 "filename": self.translate(fn, False),
943 "line": line,
944 "enable": enable,
945 }, debuggerId)
946
947 def remoteBreakpointIgnore(self, debuggerId, fn, line, count):
948 """
949 Public method to ignore a breakpoint the next couple of occurrences.
950
951 @param debuggerId ID of the debugger backend
952 @type str
953 @param fn filename the breakpoint belongs to
954 @type str
955 @param line linenumber of the breakpoint
956 @type int
957 @param count number of occurrences to ignore
958 @type int
959 """
960 debuggerList = ([debuggerId] if debuggerId
961 else list(self.__connections.keys()))
962 for debuggerId in debuggerList:
963 self.__sendJsonCommand("RequestBreakpointIgnore", {
964 "filename": self.translate(fn, False),
965 "line": line,
966 "count": count,
967 }, debuggerId)
968
969 def remoteWatchpoint(self, debuggerId, cond, setWatch, temp=False):
970 """
971 Public method to set or clear a watch expression.
972
973 @param debuggerId ID of the debugger backend
974 @type str
975 @param cond expression of the watch expression
976 @type str
977 @param setWatch flag indicating setting or resetting a watch expression
978 @type bool
979 @param temp flag indicating a temporary watch expression
980 @type bool
981 """
982 debuggerList = ([debuggerId] if debuggerId
983 else list(self.__connections.keys()))
984 for debuggerId in debuggerList:
985 # cond is combination of cond and special (s. watch expression
986 # viewer)
987 self.__sendJsonCommand("RequestWatch", {
988 "temporary": temp,
989 "setWatch": setWatch,
990 "condition": cond,
991 }, debuggerId)
992
993 def remoteWatchpointEnable(self, debuggerId, cond, enable):
994 """
995 Public method to enable or disable a watch expression.
996
997 @param debuggerId ID of the debugger backend
998 @type str
999 @param cond expression of the watch expression
1000 @type str
1001 @param enable flag indicating enabling or disabling a watch expression
1002 @type bool
1003 """
1004 debuggerList = ([debuggerId] if debuggerId
1005 else list(self.__connections.keys()))
1006 for debuggerId in debuggerList:
1007 # cond is combination of cond and special (s. watch expression
1008 # viewer)
1009 self.__sendJsonCommand("RequestWatchEnable", {
1010 "condition": cond,
1011 "enable": enable,
1012 }, debuggerId)
1013
1014 def remoteWatchpointIgnore(self, debuggerId, cond, count):
1015 """
1016 Public method to ignore a watch expression the next couple of
1017 occurrences.
1018
1019 @param debuggerId ID of the debugger backend
1020 @type str
1021 @param cond expression of the watch expression
1022 @type str
1023 @param count number of occurrences to ignore
1024 @type int
1025 """
1026 debuggerList = ([debuggerId] if debuggerId
1027 else list(self.__connections.keys()))
1028 for debuggerId in debuggerList:
1029 # cond is combination of cond and special (s. watch expression
1030 # viewer)
1031 self.__sendJsonCommand("RequestWatchIgnore", {
1032 "condition": cond,
1033 "count": count,
1034 }, debuggerId)
1035
1036 def remoteRawInput(self, debuggerId, inputString):
1037 """
1038 Public method to send the raw input to the debugged program.
1039
1040 @param debuggerId ID of the debugger backend
1041 @type str
1042 @param inputString the raw input
1043 @type str
1044 """
1045 self.__sendJsonCommand("RawInput", {
1046 "input": inputString,
1047 }, debuggerId)
1048
1049 def remoteThreadList(self, debuggerId):
1050 """
1051 Public method to request the list of threads from the client.
1052
1053 @param debuggerId ID of the debugger backend
1054 @type str
1055 """
1056 self.__sendJsonCommand("RequestThreadList", {}, debuggerId)
1057
1058 def remoteSetThread(self, debuggerId, tid):
1059 """
1060 Public method to request to set the given thread as current thread.
1061
1062 @param debuggerId ID of the debugger backend
1063 @type str
1064 @param tid id of the thread
1065 @type int
1066 """
1067 self.__sendJsonCommand("RequestThreadSet", {
1068 "threadID": tid,
1069 }, debuggerId)
1070
1071 def remoteClientStack(self, debuggerId):
1072 """
1073 Public method to request the stack of the main thread.
1074
1075 @param debuggerId ID of the debugger backend
1076 @type str
1077 """
1078 self.__sendJsonCommand("RequestStack", {}, debuggerId)
1079
1080 def remoteClientVariables(self, debuggerId, scope, filterList, framenr=0,
1081 maxSize=0):
1082 """
1083 Public method to request the variables of the debugged program.
1084
1085 @param debuggerId ID of the debugger backend
1086 @type str
1087 @param scope the scope of the variables (0 = local, 1 = global)
1088 @type int
1089 @param filterList list of variable types to filter out
1090 @type list of str
1091 @param framenr framenumber of the variables to retrieve
1092 @type int
1093 @param maxSize maximum size the formatted value of a variable will
1094 be shown. If it is bigger than that, a 'too big' indication will
1095 be given (@@TOO_BIG_TO_SHOW@@).
1096 @type int
1097 """
1098 self.__sendJsonCommand("RequestVariables", {
1099 "frameNumber": framenr,
1100 "scope": scope,
1101 "filters": filterList,
1102 "maxSize": maxSize,
1103 }, debuggerId)
1104
1105 def remoteClientVariable(self, debuggerId, scope, filterList, var,
1106 framenr=0, maxSize=0):
1107 """
1108 Public method to request the variables of the debugged program.
1109
1110 @param debuggerId ID of the debugger backend
1111 @type str
1112 @param scope the scope of the variables (0 = local, 1 = global)
1113 @type int
1114 @param filterList list of variable types to filter out
1115 @type list of str
1116 @param var list encoded name of variable to retrieve
1117 @type list of str
1118 @param framenr framenumber of the variables to retrieve
1119 @type int
1120 @param maxSize maximum size the formatted value of a variable will
1121 be shown. If it is bigger than that, a 'too big' indication will
1122 be given (@@TOO_BIG_TO_SHOW@@).
1123 @type int
1124 """
1125 self.__sendJsonCommand("RequestVariable", {
1126 "variable": var,
1127 "frameNumber": framenr,
1128 "scope": scope,
1129 "filters": filterList,
1130 "maxSize": maxSize,
1131 }, debuggerId)
1132
1133 def remoteClientDisassembly(self, debuggerId):
1134 """
1135 Public method to ask the client for the latest traceback disassembly.
1136
1137 @param debuggerId ID of the debugger backend
1138 @type str
1139 """
1140 self.__sendJsonCommand("RequestDisassembly", {}, debuggerId)
1141
1142 def remoteClientSetFilter(self, debuggerId, scope, filterStr):
1143 """
1144 Public method to set a variables filter list.
1145
1146 @param debuggerId ID of the debugger backend
1147 @type str
1148 @param scope the scope of the variables (0 = local, 1 = global)
1149 @type int
1150 @param filterStr regexp string for variable names to filter out
1151 @type str
1152 """
1153 self.__sendJsonCommand("RequestSetFilter", {
1154 "scope": scope,
1155 "filter": filterStr,
1156 }, debuggerId)
1157
1158 def setCallTraceEnabled(self, debuggerId, on):
1159 """
1160 Public method to set the call trace state.
1161
1162 @param debuggerId ID of the debugger backend
1163 @type str
1164 @param on flag indicating to enable the call trace function
1165 @type bool
1166 """
1167 self.__sendJsonCommand("RequestCallTrace", {
1168 "enable": on,
1169 }, debuggerId)
1170
1171 def remoteNoDebugList(self, debuggerId, noDebugList):
1172 """
1173 Public method to set a list of programs not to be debugged.
1174
1175 The programs given in the list will not be run under the control
1176 of the multi process debugger.
1177
1178 @param debuggerId ID of the debugger backend
1179 @type str
1180 @param noDebugList list of Python programs not to be debugged
1181 @type list of str
1182 """
1183 self.__sendJsonCommand("RequestSetNoDebugList", {
1184 "noDebug": noDebugList,
1185 }, debuggerId)
1186
1187 def remoteBanner(self):
1188 """
1189 Public slot to get the banner info of the remote client.
1190 """
1191 self.__sendJsonCommand("RequestBanner", {})
1192
1193 def remoteCapabilities(self, debuggerId):
1194 """
1195 Public slot to get the debug clients capabilities.
1196
1197 @param debuggerId ID of the debugger backend
1198 @type str
1199 """
1200 self.__sendJsonCommand("RequestCapabilities", {}, debuggerId)
1201
1202 def remoteCompletion(self, debuggerId, text):
1203 """
1204 Public slot to get the a list of possible commandline completions
1205 from the remote client.
1206
1207 @param debuggerId ID of the debugger backend
1208 @type str
1209 @param text the text to be completed
1210 @type str
1211 """
1212 self.__sendJsonCommand("RequestCompletion", {
1213 "text": text,
1214 }, debuggerId)
1215
1216 def __parseClientLine(self, sock):
1217 """
1218 Private method to handle data from the client.
1219
1220 @param sock reference to the socket to read data from
1221 @type QTcpSocket
1222 """
1223 while sock and sock.canReadLine():
1224 qs = sock.readLine()
1225 line = bytes(qs).decode(
1226 encoding=Preferences.getSystem("StringEncoding"))
1227
1228 logging.debug("<Debug-Server> %s", line)
1229 ## print("Server: ", line) ## debug # __IGNORE_WARNING_M891__
1230
1231 self.__handleJsonCommand(line, sock)
1232
1233 def __handleJsonCommand(self, jsonStr, sock):
1234 """
1235 Private method to handle a command or response serialized as a
1236 JSON string.
1237
1238 @param jsonStr string containing the command or response received
1239 from the debug backend
1240 @type str
1241 @param sock reference to the socket the data was received from
1242 @type QTcpSocket
1243 """
1244 import json
1245
1246 try:
1247 commandDict = json.loads(jsonStr.strip())
1248 except (TypeError, ValueError) as err:
1249 EricMessageBox.critical(
1250 None,
1251 self.tr("Debug Protocol Error"),
1252 self.tr("""<p>The response received from the debugger"""
1253 """ backend could not be decoded. Please report"""
1254 """ this issue with the received data to the"""
1255 """ eric bugs email address.</p>"""
1256 """<p>Error: {0}</p>"""
1257 """<p>Data:<br/>{1}</p>""").format(
1258 str(err), Utilities.html_encode(jsonStr.strip())),
1259 EricMessageBox.Ok)
1260 return
1261
1262 method = commandDict["method"]
1263 params = commandDict["params"]
1264
1265 if method == "DebuggerId":
1266 self.__assignDebuggerId(sock, params["debuggerId"])
1267
1268 elif method == "ClientOutput":
1269 self.debugServer.signalClientOutput(
1270 params["text"], params["debuggerId"])
1271
1272 elif method in ["ResponseLine", "ResponseStack"]:
1273 # Check if obsolete thread was clicked
1274 if params["stack"] == []:
1275 # Request updated list
1276 self.remoteThreadList(params["debuggerId"])
1277 return
1278 for s in params["stack"]:
1279 s[0] = self.translate(s[0], True)
1280 cf = params["stack"][0]
1281 if (
1282 self.__autoContinue and
1283 params["debuggerId"] not in self.__autoContinued
1284 ):
1285 self.__autoContinued.append(params["debuggerId"])
1286 QTimer.singleShot(
1287 0, lambda: self.remoteContinue(params["debuggerId"]))
1288 else:
1289 self.debugServer.signalClientLine(
1290 cf[0], int(cf[1]), params["debuggerId"],
1291 method == "ResponseStack", threadName=params["threadName"])
1292 self.debugServer.signalClientStack(
1293 params["stack"], params["debuggerId"],
1294 threadName=params["threadName"])
1295
1296 elif method == "CallTrace":
1297 isCall = params["event"].lower() == "c"
1298 fromInfo = params["from"]
1299 toInfo = params["to"]
1300 self.debugServer.signalClientCallTrace(
1301 isCall,
1302 fromInfo["filename"], str(fromInfo["linenumber"]),
1303 fromInfo["codename"],
1304 toInfo["filename"], str(toInfo["linenumber"]),
1305 toInfo["codename"],
1306 params["debuggerId"])
1307
1308 elif method == "ResponseVariables":
1309 self.debugServer.signalClientVariables(
1310 params["scope"], params["variables"], params["debuggerId"])
1311
1312 elif method == "ResponseVariable":
1313 self.debugServer.signalClientVariable(
1314 params["scope"], [params["variable"]] + params["variables"],
1315 params["debuggerId"])
1316
1317 elif method == "ResponseThreadList":
1318 self.debugServer.signalClientThreadList(
1319 params["currentID"], params["threadList"],
1320 params["debuggerId"])
1321
1322 elif method == "ResponseThreadSet":
1323 self.debugServer.signalClientThreadSet(params["debuggerId"])
1324
1325 elif method == "ResponseCapabilities":
1326 self.clientCapabilities = params["capabilities"]
1327 if params["debuggerId"] == self.__master:
1328 # signal only for the master connection
1329 self.debugServer.signalClientCapabilities(
1330 params["capabilities"],
1331 params["clientType"],
1332 self.__startedVenv,
1333 )
1334
1335 elif method == "ResponseBanner":
1336 if params["debuggerId"] == self.__master:
1337 # signal only for the master connection
1338 self.debugServer.signalClientBanner(
1339 params["version"],
1340 params["platform"],
1341 self.__startedVenv,
1342 )
1343
1344 elif method == "ResponseOK":
1345 self.debugServer.signalClientStatement(False, params["debuggerId"])
1346
1347 elif method == "ResponseContinue":
1348 self.debugServer.signalClientStatement(True, params["debuggerId"])
1349
1350 elif method == "RequestRaw":
1351 self.debugServer.signalClientRawInput(
1352 params["prompt"], params["echo"], params["debuggerId"])
1353
1354 elif method == "ResponseBPConditionError":
1355 fn = self.translate(params["filename"], True)
1356 self.debugServer.signalClientBreakConditionError(
1357 fn, params["line"], params["debuggerId"])
1358
1359 elif method == "ResponseClearBreakpoint":
1360 fn = self.translate(params["filename"], True)
1361 self.debugServer.signalClientClearBreak(
1362 fn, params["line"], params["debuggerId"])
1363
1364 elif method == "ResponseWatchConditionError":
1365 self.debugServer.signalClientWatchConditionError(
1366 params["condition"], params["debuggerId"])
1367
1368 elif method == "ResponseClearWatch":
1369 self.debugServer.signalClientClearWatch(
1370 params["condition"], params["debuggerId"])
1371
1372 elif method == "ResponseDisassembly":
1373 self.debugServer.signalClientDisassembly(
1374 params["disassembly"], params["debuggerId"])
1375
1376 elif method == "ResponseException":
1377 exctype = params["type"]
1378 excmessage = params["message"]
1379 stack = params["stack"]
1380 if stack:
1381 for stackEntry in stack:
1382 stackEntry[0] = self.translate(stackEntry[0], True)
1383 if stack[0] and stack[0][0] == "<string>":
1384 for stackEntry in stack:
1385 if stackEntry[0] == "<string>":
1386 stackEntry[0] = self.__scriptName
1387 else:
1388 break
1389
1390 self.debugServer.signalClientException(
1391 exctype, excmessage, stack, params["debuggerId"],
1392 params["threadName"])
1393
1394 elif method == "ResponseSyntax":
1395 self.debugServer.signalClientSyntaxError(
1396 params["message"], self.translate(params["filename"], True),
1397 params["linenumber"], params["characternumber"],
1398 params["debuggerId"], params["threadName"])
1399
1400 elif method == "ResponseSignal":
1401 self.debugServer.signalClientSignal(
1402 params["message"], self.translate(params["filename"], True),
1403 params["linenumber"], params["function"], params["arguments"],
1404 params["debuggerId"])
1405
1406 elif method == "ResponseExit":
1407 self.__scriptName = ""
1408 self.debugServer.signalClientExit(
1409 params["program"], params["status"], params["message"],
1410 params["debuggerId"])
1411 if params["debuggerId"] == self.__master:
1412 self.debugServer.signalMainClientExit()
1413
1414 elif method == "PassiveStartup":
1415 self.debugServer.passiveStartUp(
1416 self.translate(params["filename"], True), params["exceptions"],
1417 params["debuggerId"])
1418
1419 elif method == "ResponseCompletion":
1420 self.debugServer.signalClientCompletionList(
1421 params["completions"], params["text"], params["debuggerId"])
1422
1423 def __sendJsonCommand(self, command, params, debuggerId="", sock=None):
1424 """
1425 Private method to send a single command to the client.
1426
1427 @param command command name to be sent
1428 @type str
1429 @param params dictionary of named parameters for the command
1430 @type dict
1431 @param debuggerId id of the debug client to send the command to
1432 @type str
1433 @param sock reference to the socket object to be used (only used if
1434 debuggerId is not given)
1435 @type QTcpSocket
1436 """
1437 import json
1438
1439 commandDict = {
1440 "jsonrpc": "2.0",
1441 "method": command,
1442 "params": params,
1443 }
1444 cmd = json.dumps(commandDict) + '\n'
1445
1446 if debuggerId and debuggerId in self.__connections:
1447 sock = self.__connections[debuggerId]
1448 elif sock is None and self.__master is not None:
1449 sock = self.__connections[self.__master]
1450 if sock is not None:
1451 self.__writeJsonCommandToSocket(cmd, sock)
1452 else:
1453 self.queue.append(cmd)
1454
1455 def __writeJsonCommandToSocket(self, cmd, sock):
1456 """
1457 Private method to write a JSON command to the socket.
1458
1459 @param cmd JSON command to be sent
1460 @type str
1461 @param sock reference to the socket to write to
1462 @type QTcpSocket
1463 """
1464 data = cmd.encode('utf8', 'backslashreplace')
1465 length = "{0:09d}".format(len(data))
1466 sock.write(length.encode() + data)
1467 sock.flush()
1468
1469
1470 def createDebuggerInterfacePython3(debugServer, passive):
1471 """
1472 Module function to create a debugger interface instance.
1473
1474
1475 @param debugServer reference to the debug server
1476 @type DebugServer
1477 @param passive flag indicating passive connection mode
1478 @type bool
1479 @return instantiated debugger interface
1480 @rtype DebuggerInterfacePython
1481 """
1482 return DebuggerInterfacePython(debugServer, passive)
1483
1484
1485 def getRegistryData():
1486 """
1487 Module function to get characterizing data for the supported debugger
1488 interfaces.
1489
1490 @return list of tuples containing the client type, the client capabilities,
1491 the client file type associations and a reference to the creation
1492 function
1493 @rtype list of tuple of (str, int, list of str, function)
1494 """
1495 py3Exts = []
1496 for ext in Preferences.getDebugger("Python3Extensions").split():
1497 if ext.startswith("."):
1498 py3Exts.append(ext)
1499 else:
1500 py3Exts.append(".{0}".format(ext))
1501
1502 registryData = []
1503 if py3Exts:
1504 registryData.append(
1505 ("Python3", ClientDefaultCapabilities, py3Exts,
1506 createDebuggerInterfacePython3)
1507 )
1508
1509 return registryData

eric ide

mercurial