Kaydet (Commit) a00050f2 authored tarafından Kurt B. Kaiser's avatar Kurt B. Kaiser

1. Implement processing of user code in subprocess MainThread. Pass loop

   is now interruptable on Windows.
2. Tweak signal.signal() wait parameters as called by various methods
   to improve I/O response, especially on Windows.
3. Debugger is disabled at this check-in pending further development.

M NEWS.txt
M PyShell.py
M rpc.py
M run.py
üst c4607dad
......@@ -5,7 +5,13 @@ IDLEfork NEWS
What's New in IDLEfork 0.9b1?
===================================
*Release date: 25-Apr-2003*
*Release date: XX-XXX-2003*
- Improved I/O response by tweaking the wait parameter in various
calls to signal.signal().
- Implemented a threaded subprocess which allows interrupting a pass
loop in user code using the 'interrupt' extension.
- Implemented the 'interrupt' extension module, which allows a subthread
to raise a KeyboardInterrupt in the main thread.
......@@ -36,11 +42,10 @@ What's New in IDLEfork 0.9b1?
- Known issues:
+ Can't kill/restart a tight loop in the Windows version: add
I/O to the loop or use the Task Manager to kill the subprocess.
+ Typing two Control-C in close succession when the subprocess is busy can
cause IDLE to lose communication with the subprocess. Please type one
only and wait for the exception to complete.
only and wait for the exception to complete. If you do manage to
interrupt the interrupt, simply restart the shell.
+ Printing under some versions of Linux may be problematic.
......
......@@ -35,6 +35,11 @@ import RemoteDebugger
IDENTCHARS = string.ascii_letters + string.digits + "_"
try:
from signal import SIGTERM
except ImportError:
SIGTERM = 15
# Change warnings module to write to sys.__stderr__
try:
import warnings
......@@ -367,13 +372,8 @@ class ModifiedInterpreter(InteractiveInterpreter):
except:
pass
# Kill subprocess, spawn a new one, accept connection.
try:
self.interrupt_subprocess()
self.shutdown_subprocess()
self.rpcclt.close()
os.wait()
except:
pass
self.rpcclt.close()
self.unix_terminate()
self.tkconsole.executing = False
self.spawn_subprocess()
self.rpcclt.accept()
......@@ -391,42 +391,31 @@ class ModifiedInterpreter(InteractiveInterpreter):
# reload remote debugger breakpoints for all PyShellEditWindows
debug.load_breakpoints()
def __signal_interrupt(self):
try:
from signal import SIGINT
except ImportError:
SIGINT = 2
try:
os.kill(self.rpcpid, SIGINT)
except OSError: # subprocess may have already exited
pass
def __request_interrupt(self):
try:
self.rpcclt.asynccall("exec", "interrupt_the_server", (), {})
except:
pass
self.rpcclt.remotecall("exec", "interrupt_the_server", (), {})
def interrupt_subprocess(self):
# XXX KBK 22Mar03 Use interrupt message on all platforms for now.
# XXX if hasattr(os, "kill"):
if False:
self.__signal_interrupt()
else:
# Windows has no os.kill(), use an RPC message.
# This is async, must be done in a thread.
threading.Thread(target=self.__request_interrupt).start()
threading.Thread(target=self.__request_interrupt).start()
def __request_shutdown(self):
try:
self.rpcclt.asynccall("exec", "shutdown_the_server", (), {})
except:
pass
def kill_subprocess(self):
self.rpcclt.close()
self.unix_terminate()
self.tkconsole.executing = False
self.rpcclt = None
def shutdown_subprocess(self):
t = threading.Thread(target=self.__request_shutdown)
t.start()
t.join()
def unix_terminate(self):
"UNIX: make sure subprocess is terminated and collect status"
if hasattr(os, 'kill'):
try:
os.kill(self.rpcpid, SIGTERM)
except OSError:
# process already terminated:
return
else:
try:
os.waitpid(self.rpcpid, 0)
except OSError:
return
def transfer_path(self):
self.runcommand("""if 1:
......@@ -445,21 +434,15 @@ class ModifiedInterpreter(InteractiveInterpreter):
if clt is None:
return
try:
response = clt.pollresponse(self.active_seq)
except (EOFError, IOError):
# lost connection: subprocess terminated itself, restart
response = clt.pollresponse(self.active_seq, wait=0.05)
except (EOFError, IOError, KeyboardInterrupt):
# lost connection or subprocess terminated itself, restart
# [the KBI is from rpc.SocketIO.handle_EOF()]
if self.tkconsole.closing:
return
response = None
try:
# stake any zombie before restarting
os.wait()
except (AttributeError, OSError):
pass
self.restart_subprocess()
self.tkconsole.endexecuting()
# Reschedule myself in 50 ms
self.tkconsole.text.after(50, self.poll_subprocess)
if response:
self.tkconsole.resetoutput()
self.active_seq = None
......@@ -477,13 +460,8 @@ class ModifiedInterpreter(InteractiveInterpreter):
print >>console, errmsg, what
# we received a response to the currently active seq number:
self.tkconsole.endexecuting()
def kill_subprocess(self):
clt = self.rpcclt
if clt is not None:
self.shutdown_subprocess()
clt.close()
self.rpcclt = None
# Reschedule myself in 50 ms
self.tkconsole.text.after(50, self.poll_subprocess)
debugger = None
......@@ -495,7 +473,7 @@ class ModifiedInterpreter(InteractiveInterpreter):
def remote_stack_viewer(self):
import RemoteObjectBrowser
oid = self.rpcclt.remotecall("exec", "stackviewer", ("flist",), {})
oid = self.rpcclt.remotequeue("exec", "stackviewer", ("flist",), {})
if oid is None:
self.tkconsole.root.bell()
return
......@@ -628,7 +606,7 @@ class ModifiedInterpreter(InteractiveInterpreter):
self.display_executing_dialog()
return 0
if self.rpcclt:
self.rpcclt.remotecall("exec", "runcode", (code,), {})
self.rpcclt.remotequeue("exec", "runcode", (code,), {})
else:
exec code in self.locals
return 1
......@@ -645,7 +623,7 @@ class ModifiedInterpreter(InteractiveInterpreter):
self.tkconsole.beginexecuting()
try:
if not debugger and self.rpcclt is not None:
self.active_seq = self.rpcclt.asynccall("exec", "runcode",
self.active_seq = self.rpcclt.asyncqueue("exec", "runcode",
(code,), {})
elif debugger:
debugger.run(code, self.locals)
......@@ -712,7 +690,7 @@ class PyShell(OutputWindow):
text.bind("<<beginning-of-line>>", self.home_callback)
text.bind("<<end-of-file>>", self.eof_callback)
text.bind("<<open-stack-viewer>>", self.open_stack_viewer)
text.bind("<<toggle-debugger>>", self.toggle_debugger)
##text.bind("<<toggle-debugger>>", self.toggle_debugger)
text.bind("<<open-python-shell>>", self.flist.open_shell)
text.bind("<<toggle-jit-stack-viewer>>", self.toggle_jit_stack_viewer)
text.bind("<<view-restart>>", self.view_restart_mark)
......@@ -799,13 +777,9 @@ class PyShell(OutputWindow):
"Helper for ModifiedInterpreter"
self.resetoutput()
self.executing = 1
##self._cancel_check = self.cancel_check
##sys.settrace(self._cancel_check)
def endexecuting(self):
"Helper for ModifiedInterpreter"
##sys.settrace(None)
##self._cancel_check = None
self.executing = 0
self.canceled = 0
self.showprompt()
......@@ -822,7 +796,6 @@ class PyShell(OutputWindow):
return "cancel"
# interrupt the subprocess
self.closing = True
self.cancel_callback()
self.endexecuting()
return EditorWindow.close(self)
......@@ -1017,23 +990,6 @@ class PyShell(OutputWindow):
line = line[:i]
more = self.interp.runsource(line)
def cancel_check(self, frame, what, args,
dooneevent=tkinter.dooneevent,
dontwait=tkinter.DONT_WAIT):
# Hack -- use the debugger hooks to be able to handle events
# and interrupt execution at any time.
# This slows execution down quite a bit, so you may want to
# disable this (by not calling settrace() in beginexecuting() and
# endexecuting() for full-bore (uninterruptable) speed.)
# XXX This should become a user option.
if self.canceled:
return
dooneevent(dontwait)
if self.canceled:
self.canceled = 0
raise KeyboardInterrupt
return self._cancel_check
def open_stack_viewer(self, event=None):
if self.interp.rpcclt:
return self.interp.remote_stack_viewer()
......
......@@ -28,17 +28,21 @@ accomplished in Idle.
"""
import sys
import os
import socket
import select
import SocketServer
import struct
import cPickle as pickle
import threading
import Queue
import traceback
import copy_reg
import types
import marshal
import interrupt
def unpickle_code(ms):
co = marshal.loads(ms)
assert isinstance(co, types.CodeType)
......@@ -98,8 +102,6 @@ class RPCServer(SocketServer.TCPServer):
raise
except SystemExit:
raise
except EOFError:
pass
except:
erf = sys.__stderr__
print>>erf, '\n' + '-'*40
......@@ -110,28 +112,29 @@ class RPCServer(SocketServer.TCPServer):
traceback.print_exc(file=erf)
print>>erf, '\n*** Unrecoverable, server exiting!'
print>>erf, '-'*40
import os
os._exit(0)
#----------------- end class RPCServer --------------------
objecttable = {}
request_queue = Queue.Queue(0)
response_queue = Queue.Queue(0)
class SocketIO:
nextseq = 0
def __init__(self, sock, objtable=None, debugging=None):
self.mainthread = threading.currentThread()
self.sockthread = threading.currentThread()
if debugging is not None:
self.debugging = debugging
self.sock = sock
if objtable is None:
objtable = objecttable
self.objtable = objtable
self.cvar = threading.Condition()
self.responses = {}
self.cvars = {}
self.interrupted = False
def close(self):
sock = self.sock
......@@ -139,6 +142,10 @@ class SocketIO:
if sock is not None:
sock.close()
def exithook(self):
"override for specific exit action"
os._exit()
def debug(self, *args):
if not self.debugging:
return
......@@ -156,13 +163,12 @@ class SocketIO:
except KeyError:
pass
def localcall(self, request):
def localcall(self, seq, request):
self.debug("localcall:", request)
try:
how, (oid, methodname, args, kwargs) = request
except TypeError:
return ("ERROR", "Bad request format")
assert how == "call"
if not self.objtable.has_key(oid):
return ("ERROR", "Unknown object id: %s" % `oid`)
obj = self.objtable[oid]
......@@ -178,14 +184,20 @@ class SocketIO:
return ("ERROR", "Unsupported method name: %s" % `methodname`)
method = getattr(obj, methodname)
try:
ret = method(*args, **kwargs)
if isinstance(ret, RemoteObject):
ret = remoteref(ret)
return ("OK", ret)
if how == 'CALL':
ret = method(*args, **kwargs)
if isinstance(ret, RemoteObject):
ret = remoteref(ret)
return ("OK", ret)
elif how == 'QUEUE':
request_queue.put((seq, (method, args, kwargs)))
return("QUEUED", None)
else:
return ("ERROR", "Unsupported message type: %s" % how)
except SystemExit:
raise
except socket.error:
pass
raise
except:
self.debug("localcall:EXCEPTION")
traceback.print_exc(file=sys.__stderr__)
......@@ -193,24 +205,37 @@ class SocketIO:
def remotecall(self, oid, methodname, args, kwargs):
self.debug("remotecall:asynccall: ", oid, methodname)
# XXX KBK 06Feb03 self.interrupted logic may not be necessary if
# subprocess is threaded.
if self.interrupted:
self.interrupted = False
raise KeyboardInterrupt
seq = self.asynccall(oid, methodname, args, kwargs)
return self.asyncreturn(seq)
def remotequeue(self, oid, methodname, args, kwargs):
self.debug("remotequeue:asyncqueue: ", oid, methodname)
seq = self.asyncqueue(oid, methodname, args, kwargs)
return self.asyncreturn(seq)
def asynccall(self, oid, methodname, args, kwargs):
request = ("call", (oid, methodname, args, kwargs))
request = ("CALL", (oid, methodname, args, kwargs))
seq = self.newseq()
if threading.currentThread() != self.sockthread:
cvar = threading.Condition()
self.cvars[seq] = cvar
self.debug(("asynccall:%d:" % seq), oid, methodname, args, kwargs)
self.putmessage((seq, request))
return seq
def asyncqueue(self, oid, methodname, args, kwargs):
request = ("QUEUE", (oid, methodname, args, kwargs))
seq = self.newseq()
if threading.currentThread() != self.sockthread:
cvar = threading.Condition()
self.cvars[seq] = cvar
self.debug(("asyncqueue:%d:" % seq), oid, methodname, args, kwargs)
self.putmessage((seq, request))
return seq
def asyncreturn(self, seq):
self.debug("asyncreturn:%d:call getresponse(): " % seq)
response = self.getresponse(seq, wait=None)
response = self.getresponse(seq, wait=0.05)
self.debug(("asyncreturn:%d:response: " % seq), response)
return self.decoderesponse(response)
......@@ -218,25 +243,36 @@ class SocketIO:
how, what = response
if how == "OK":
return what
if how == "QUEUED":
return None
if how == "EXCEPTION":
self.debug("decoderesponse: EXCEPTION")
return None
if how == "EOF":
self.debug("decoderesponse: EOF")
self.decode_interrupthook()
return None
if how == "ERROR":
self.debug("decoderesponse: Internal ERROR:", what)
raise RuntimeError, what
raise SystemError, (how, what)
def decode_interrupthook(self):
""
raise EOFError
def mainloop(self):
"""Listen on socket until I/O not ready or EOF
Main thread pollresponse() will loop looking for seq number None, which
pollresponse() will loop looking for seq number None, which
never comes, and exit on EOFError.
"""
try:
self.getresponse(myseq=None, wait=None)
self.getresponse(myseq=None, wait=0.05)
except EOFError:
pass
self.debug("mainloop:return")
return
def getresponse(self, myseq, wait):
response = self._getresponse(myseq, wait)
......@@ -256,23 +292,24 @@ class SocketIO:
def _getresponse(self, myseq, wait):
self.debug("_getresponse:myseq:", myseq)
if threading.currentThread() is self.mainthread:
# Main thread: does all reading of requests or responses
# Loop here, blocking each time until socket is ready.
if threading.currentThread() is self.sockthread:
# this thread does all reading of requests or responses
while 1:
response = self.pollresponse(myseq, wait)
if response is not None:
return response
else:
# Auxiliary thread: wait for notification from main thread
self.cvar.acquire()
self.cvars[myseq] = self.cvar
# wait for notification from socket handling thread
cvar = self.cvars[myseq]
cvar.acquire()
while not self.responses.has_key(myseq):
self.cvar.wait()
cvar.wait()
response = self.responses[myseq]
self.debug("_getresponse:%s: thread woke up: response: %s" %
(myseq, response))
del self.responses[myseq]
del self.cvars[myseq]
self.cvar.release()
cvar.release()
return response
def newseq(self):
......@@ -283,7 +320,7 @@ class SocketIO:
self.debug("putmessage:%d:" % message[0])
try:
s = pickle.dumps(message)
except:
except pickle.UnpicklingError:
print >>sys.__stderr__, "Cannot pickle:", `message`
raise
s = struct.pack("<i", len(s)) + s
......@@ -293,10 +330,13 @@ class SocketIO:
except AttributeError:
# socket was closed
raise IOError
except socket.error:
self.debug("putmessage:socketerror:pid:%s" % os.getpid())
os._exit(0)
else:
s = s[n:]
def ioready(self, wait=0.0):
def ioready(self, wait):
r, w, x = select.select([self.sock.fileno()], [], [], wait)
return len(r)
......@@ -304,7 +344,7 @@ class SocketIO:
bufneed = 4
bufstate = 0 # meaning: 0 => reading count; 1 => reading data
def pollpacket(self, wait=0.0):
def pollpacket(self, wait):
self._stage0()
if len(self.buffer) < self.bufneed:
if not self.ioready(wait):
......@@ -334,7 +374,7 @@ class SocketIO:
self.bufstate = 0
return packet
def pollmessage(self, wait=0.0):
def pollmessage(self, wait):
packet = self.pollpacket(wait)
if packet is None:
return None
......@@ -348,45 +388,97 @@ class SocketIO:
raise
return message
def pollresponse(self, myseq, wait=0.0):
def pollresponse(self, myseq, wait):
"""Handle messages received on the socket.
Some messages received may be asynchronous 'call' commands, and
some may be responses intended for other threads.
Some messages received may be asynchronous 'call' or 'queue' requests,
and some may be responses for other threads.
'call' requests are passed to self.localcall() with the expectation of
immediate execution, during which time the socket is not serviced.
Loop until message with myseq sequence number is received. Save others
in self.responses and notify the owning thread, except that 'call'
commands are handed off to localcall() and the response sent back
across the link with the appropriate sequence number.
'queue' requests are used for tasks (which may block or hang) to be
processed in a different thread. These requests are fed into
request_queue by self.localcall(). Responses to queued requests are
taken from response_queue and sent across the link with the associated
sequence numbers. Messages in the queues are (sequence_number,
request/response) tuples and code using this module removing messages
from the request_queue is responsible for returning the correct
sequence number in the response_queue.
pollresponse() will loop until a response message with the myseq
sequence number is received, and will save other responses in
self.responses and notify the owning thread.
"""
while 1:
message = self.pollmessage(wait)
if message is None: # socket not ready
# send queued response if there is one available
try:
qmsg = response_queue.get(0)
except Queue.Empty:
pass
else:
seq, response = qmsg
message = (seq, ('OK', response))
self.putmessage(message)
# poll for message on link
try:
message = self.pollmessage(wait)
if message is None: # socket not ready
return None
except EOFError:
self.handle_EOF()
return None
except AttributeError:
return None
#wait = 0.0 # poll on subsequent passes instead of blocking
seq, resq = message
how = resq[0]
self.debug("pollresponse:%d:myseq:%s" % (seq, myseq))
if resq[0] == "call":
# process or queue a request
if how in ("CALL", "QUEUE"):
self.debug("pollresponse:%d:localcall:call:" % seq)
response = self.localcall(resq)
response = self.localcall(seq, resq)
self.debug("pollresponse:%d:localcall:response:%s"
% (seq, response))
self.putmessage((seq, response))
if how == "CALL":
self.putmessage((seq, response))
elif how == "QUEUE":
# don't acknowledge the 'queue' request!
pass
continue
# return if completed message transaction
elif seq == myseq:
return resq
# must be a response for a different thread:
else:
self.cvar.acquire()
cv = self.cvars.get(seq)
cv = self.cvars.get(seq, None)
# response involving unknown sequence number is discarded,
# probably intended for prior incarnation
# probably intended for prior incarnation of server
if cv is not None:
cv.acquire()
self.responses[seq] = resq
cv.notify()
self.cvar.release()
cv.release()
continue
def handle_EOF(self):
"action taken upon link being closed by peer"
self.EOFhook()
self.debug("handle_EOF")
for key in self.cvars:
cv = self.cvars[key]
cv.acquire()
self.responses[key] = ('EOF', None)
cv.notify()
cv.release()
interrupt.interrupt_main()
# call our (possibly overridden) exit function
self.exithook()
def EOFhook(self):
"Classes using rpc client/server can override to augment EOF action"
pass
#----------------- end class SocketIO --------------------
class RemoteObject:
......@@ -465,7 +557,8 @@ class RPCProxy:
self.__getattributes()
if not self.__attributes.has_key(name):
raise AttributeError, name
__getattr__.DebuggerStepThrough=1
__getattr__.DebuggerStepThrough = 1
def __getattributes(self):
self.__attributes = self.sockio.remotecall(self.oid,
......
import sys
import os
import time
import socket
import traceback
......@@ -20,12 +21,8 @@ import __main__
# the socket) and the main thread (which runs user code), plus global
# completion and exit flags:
server = None # RPCServer instance
queue = Queue.Queue(0)
execution_finished = False
exit_requested = False
def main():
"""Start the Python execution server in a subprocess
......@@ -44,8 +41,6 @@ def main():
register and unregister themselves.
"""
global queue, execution_finished, exit_requested
port = 8833
if sys.argv[1:]:
port = int(sys.argv[1])
......@@ -58,21 +53,23 @@ def main():
while 1:
try:
if exit_requested:
sys.exit()
# XXX KBK 22Mar03 eventually check queue here!
pass
time.sleep(0.05)
os._exit(0)
try:
seq, request = rpc.request_queue.get(0)
except Queue.Empty:
time.sleep(0.05)
continue
method, args, kwargs = request
ret = method(*args, **kwargs)
rpc.response_queue.put((seq, ret))
except KeyboardInterrupt:
##execution_finished = True
continue
def manage_socket(address):
global server, exit_requested
for i in range(6):
time.sleep(i)
try:
server = rpc.RPCServer(address, MyHandler)
server = MyRPCServer(address, MyHandler)
break
except socket.error, err:
if i < 3:
......@@ -82,10 +79,41 @@ def manage_socket(address):
+ err[1] + ", retrying...."
else:
print>>sys.__stderr__, "\nConnection to Idle failed, exiting."
global exit_requested
exit_requested = True
return
server.handle_request() # A single request only
class MyRPCServer(rpc.RPCServer):
def handle_error(self, request, client_address):
"""Override RPCServer method for IDLE
Interrupt the MainThread and exit server if link is dropped.
"""
try:
raise
except SystemExit:
raise
except EOFError:
global exit_requested
exit_requested = True
interrupt.interrupt_main()
except:
erf = sys.__stderr__
print>>erf, '\n' + '-'*40
print>>erf, 'Unhandled server exception!'
print>>erf, 'Thread: %s' % threading.currentThread().getName()
print>>erf, 'Client Address: ', client_address
print>>erf, 'Request: ', repr(request)
traceback.print_exc(file=erf)
print>>erf, '\n*** Unrecoverable, server exiting!'
print>>erf, '-'*40
os._exit(0)
class MyHandler(rpc.RPCHandler):
def handle(self):
......@@ -95,7 +123,20 @@ class MyHandler(rpc.RPCHandler):
sys.stdin = self.get_remote_proxy("stdin")
sys.stdout = self.get_remote_proxy("stdout")
sys.stderr = self.get_remote_proxy("stderr")
rpc.RPCHandler.getresponse(self, myseq=None, wait=0.5)
rpc.RPCHandler.getresponse(self, myseq=None, wait=0.05)
def exithook(self):
"override SocketIO method - wait for MainThread to shut us down"
while 1: pass
def EOFhook(self):
"Override SocketIO method - terminate wait on callback and exit thread"
global exit_requested
exit_requested = True
def decode_interrupthook(self):
"interrupt awakened thread"
interrupt.interrupt_main()
class Executive:
......@@ -106,44 +147,30 @@ class Executive:
self.calltip = CallTips.CallTips()
def runcode(self, code):
global queue, execution_finished
execution_finished = False
queue.put(code)
# dequeue and run in subthread
self.runcode_from_queue()
while not execution_finished:
time.sleep(0.05)
def runcode_from_queue(self):
global queue, execution_finished
# poll until queue has code object, using threads, just block?
while True:
try:
code = queue.get(0)
break
except Queue.Empty:
time.sleep(0.05)
try:
exec code in self.locals
except:
self.flush_stdout()
efile = sys.stderr
typ, val, tb = info = sys.exc_info()
sys.last_type, sys.last_value, sys.last_traceback = info
tbe = traceback.extract_tb(tb)
print >>efile, 'Traceback (most recent call last):'
exclude = ("run.py", "rpc.py", "RemoteDebugger.py", "bdb.py")
self.cleanup_traceback(tbe, exclude)
traceback.print_list(tbe, file=efile)
lines = traceback.format_exception_only(typ, val)
for line in lines:
print>>efile, line,
execution_finished = True
try:
if exit_requested:
os._exit(0)
self.flush_stdout()
efile = sys.stderr
typ, val, tb = info = sys.exc_info()
sys.last_type, sys.last_value, sys.last_traceback = info
tbe = traceback.extract_tb(tb)
print >>efile, 'Traceback (most recent call last):'
exclude = ("run.py", "rpc.py", "threading.py",
"RemoteDebugger.py", "bdb.py")
self.cleanup_traceback(tbe, exclude)
traceback.print_list(tbe, file=efile)
lines = traceback.format_exception_only(typ, val)
for line in lines:
print>>efile, line,
except:
sys.stderr = sys.__stderr__
raise
else:
self.flush_stdout()
execution_finished = True
def flush_stdout(self):
try:
......@@ -184,15 +211,8 @@ class Executive:
tb[i] = fn, ln, nm, line
def interrupt_the_server(self):
self.rpchandler.interrupted = True
##print>>sys.__stderr__, "** Interrupt main!"
interrupt.interrupt_main()
def shutdown_the_server(self):
global exit_requested
exit_requested = True
def start_the_debugger(self, gui_adap_oid):
return RemoteDebugger.start_debugger(self.rpchandler, gui_adap_oid)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment