class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): """IPC JSON message processor for VSC debugger protocol. This translates between the VSC debugger protocol and the pydevd protocol. """ def __init__(self, socket, pydevd, logfile=None, killonclose=True): super(VSCodeMessageProcessor, self).__init__(socket=socket, own_socket=False, logfile=logfile) self.socket = socket self.pydevd = pydevd self.killonclose = killonclose self.is_process_created = False self.is_process_created_lock = threading.Lock() self.stack_traces = {} self.stack_traces_lock = threading.Lock() self.active_exceptions = {} self.active_exceptions_lock = threading.Lock() self.thread_map = IDMap() self.frame_map = IDMap() self.var_map = IDMap() self.bp_map = IDMap() self.next_var_ref = 0 self.loop = futures.EventLoop() self.exceptions_mgr = ExceptionsManager(self) self.disconnect_request = None self.launch_arguments = None self.disconnect_request_event = threading.Event() pydevd._vscprocessor = self self._closed = False self.path_casing = PathUnNormcase() self.event_loop_thread = threading.Thread(target=self.loop.run_forever, name='ptvsd.EventLoop') self.event_loop_thread.daemon = True self.event_loop_thread.start() def close(self): """Stop the message processor and release its resources.""" if self._closed: return self._closed = True pydevd = self.pydevd self.pydevd = None pydevd.shutdown(socket.SHUT_RDWR) pydevd.close() global ptvsd_sys_exit_code self.send_event('exited', exitCode=ptvsd_sys_exit_code) self.send_event('terminated') self.disconnect_request_event.wait(WAIT_FOR_DISCONNECT_REQUEST_TIMEOUT) if self.disconnect_request is not None: self.send_response(self.disconnect_request) self.disconnect_request = None self.set_exit() self.loop.stop() self.event_loop_thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT) if self.socket: self.socket.shutdown(socket.SHUT_RDWR) self.socket.close() def pydevd_notify(self, cmd_id, args): # TODO: docstring try: return self.pydevd.pydevd_notify(cmd_id, args) except BaseException: traceback.print_exc(file=sys.__stderr__) raise def pydevd_request(self, cmd_id, args): # TODO: docstring return self.pydevd.pydevd_request(self.loop, cmd_id, args) # Instances of this class provide decorators to mark methods as # handlers for various # pydevd messages - a decorated method is # added to the map with the corresponding message ID, and is # looked up there by pydevd event handler below. class EventHandlers(dict): def handler(self, cmd_id): def decorate(f): self[cmd_id] = f return f return decorate pydevd_events = EventHandlers() def on_pydevd_event(self, cmd_id, seq, args): # TODO: docstring try: f = self.pydevd_events[cmd_id] except KeyError: raise UnsupportedPyDevdCommandError(cmd_id) return f(self, seq, args) def async_handler(m): # TODO: docstring m = futures.wrap_async(m) def f(self, *args, **kwargs): fut = m(self, self.loop, *args, **kwargs) def done(fut): try: fut.result() except BaseException: traceback.print_exc(file=sys.__stderr__) fut.add_done_callback(done) return f @async_handler def on_initialize(self, request, args): # TODO: docstring cmd = pydevd_comm.CMD_VERSION os_id = 'WINDOWS' if platform.system() == 'Windows' else 'UNIX' msg = '1.1\t{}\tID'.format(os_id) yield self.pydevd_request(cmd, msg) self.send_response( request, supportsExceptionInfoRequest=True, supportsConfigurationDoneRequest=True, supportsConditionalBreakpoints=True, supportsSetVariable=True, supportsExceptionOptions=True, supportsEvaluateForHovers=True, supportsValueFormattingOptions=True, exceptionBreakpointFilters=[ { 'filter': 'raised', 'label': 'Raised Exceptions', 'default': 'false' }, { 'filter': 'uncaught', 'label': 'Uncaught Exceptions', 'default': 'true' }, ], ) self.send_event('initialized') @async_handler def on_configurationDone(self, request, args): # TODO: docstring self.send_response(request) self.process_launch_arguments() self.pydevd_request(pydevd_comm.CMD_RUN, '') def process_launch_arguments(self): """ Process the launch arguments to configure the debugger. Further information can be found here https://code.visualstudio.com/docs/editor/debugging#_launchjson-attributes { type:'python', request:'launch'|'attach', name:'friendly name for debug config', // Custom attributes supported by PTVSD. redirectOutput:true|false, } """ # noqa if self.launch_arguments is None: return if self.launch_arguments.get('fixFilePathCase', False): self.path_casing.enable() if self.launch_arguments.get('redirectOutput', False): redirect_output = 'STDOUT\tSTDERR' else: redirect_output = '' self.pydevd_request(pydevd_comm.CMD_REDIRECT_OUTPUT, redirect_output) def on_disconnect(self, request, args): # TODO: docstring if self.start_reason == 'launch': self.disconnect_request = request self.disconnect_request_event.set() killProcess = not self._closed self.close() if killProcess and self.killonclose: os.kill(os.getpid(), signal.SIGTERM) else: self.send_response(request) @async_handler def on_attach(self, request, args): # TODO: docstring self.start_reason = 'attach' self.send_response(request) @async_handler def on_launch(self, request, args): # TODO: docstring self.start_reason = 'launch' self.launch_arguments = request.get('arguments', None) self.send_response(request) def send_process_event(self, start_method): # TODO: docstring evt = { 'name': sys.argv[0], 'systemProcessId': os.getpid(), 'isLocalProcess': True, 'startMethod': start_method, } self.send_event('process', **evt) def is_debugger_internal_thread(self, thread_name): if thread_name: if thread_name.startswith('pydevd.'): return True elif thread_name.startswith('ptvsd.'): return True return False @async_handler def on_threads(self, request, args): # TODO: docstring cmd = pydevd_comm.CMD_LIST_THREADS _, _, resp_args = yield self.pydevd_request(cmd, '') xml = untangle.parse(resp_args).xml try: xthreads = xml.thread except AttributeError: xthreads = [] threads = [] for xthread in xthreads: try: name = unquote(xthread['name']) except KeyError: name = None if not self.is_debugger_internal_thread(name): pyd_tid = xthread['id'] try: vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=False) except KeyError: # This is a previously unseen thread vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=True) self.send_event('thread', reason='started', threadId=vsc_tid) threads.append({'id': vsc_tid, 'name': name}) self.send_response(request, threads=threads) @async_handler def on_stackTrace(self, request, args): # TODO: docstring vsc_tid = int(args['threadId']) startFrame = int(args.get('startFrame', 0)) levels = int(args.get('levels', 0)) pyd_tid = self.thread_map.to_pydevd(vsc_tid) with self.stack_traces_lock: try: xframes = self.stack_traces[pyd_tid] except KeyError: # This means the stack was requested before the # thread was suspended xframes = [] totalFrames = len(xframes) if levels == 0: levels = totalFrames stackFrames = [] for xframe in xframes: if startFrame > 0: startFrame -= 1 continue if levels <= 0: break levels -= 1 key = (pyd_tid, int(xframe['id'])) fid = self.frame_map.to_vscode(key, autogen=True) name = unquote(xframe['name']) file = self.path_casing.un_normcase(unquote(xframe['file'])) line = int(xframe['line']) stackFrames.append({ 'id': fid, 'name': name, 'source': { 'path': file }, 'line': line, 'column': 1, }) self.send_response(request, stackFrames=stackFrames, totalFrames=totalFrames) @async_handler def on_scopes(self, request, args): # TODO: docstring vsc_fid = int(args['frameId']) pyd_tid, pyd_fid = self.frame_map.to_pydevd(vsc_fid) pyd_var = (pyd_tid, pyd_fid, 'FRAME') vsc_var = self.var_map.to_vscode(pyd_var, autogen=True) scope = { 'name': 'Locals', 'expensive': False, 'variablesReference': vsc_var, } self.send_response(request, scopes=[scope]) @async_handler def on_variables(self, request, args): # TODO: docstring vsc_var = int(args['variablesReference']) pyd_var = self.var_map.to_pydevd(vsc_var) safe_repr_provider.convert_to_hex = args.get('format', {}).get('hex', False) if len(pyd_var) == 3: cmd = pydevd_comm.CMD_GET_FRAME else: cmd = pydevd_comm.CMD_GET_VARIABLE cmdargs = (str(s) for s in pyd_var) msg = '\t'.join(cmdargs) _, _, resp_args = yield self.pydevd_request(cmd, msg) xml = untangle.parse(resp_args).xml try: xvars = xml.var except AttributeError: xvars = [] variables = VariablesSorter() for xvar in xvars: var_name = unquote(xvar['name']) var_type = unquote(xvar['type']) var_value = unquote(xvar['value']) var = { 'name': var_name, 'type': var_type, 'value': var_value, } if bool(xvar['isContainer']): pyd_child = pyd_var + (var_name, ) var['variablesReference'] = self.var_map.to_vscode( pyd_child, autogen=True) eval_name = self.__get_variable_evaluate_name(pyd_var, var_name) if eval_name: var['evaluateName'] = eval_name variables.append(var) # Reset hex format since this is per request. safe_repr_provider.convert_to_hex = False self.send_response(request, variables=variables.get_sorted_variables()) def __get_variable_evaluate_name(self, pyd_var_parent, var_name): # TODO: docstring eval_name = None if len(pyd_var_parent) > 3: # This means the current variable has a parent i.e, it is not a # FRAME variable. These require evaluateName to work in VS # watch window var = pyd_var_parent + (var_name, ) eval_name = var[3] for s in var[4:]: try: # Check and get the dictionary key or list index. # Note: this is best effort, keys that are object references # will not work i = self.__get_index_or_key(s) eval_name += '[{}]'.format(i) except: eval_name += '.' + s return eval_name def __get_index_or_key(self, text): # Dictionary resolver in pydevd provides key in '<repr> (<hash>)' format result = re.match(r"(.*)\ \(([0-9]*)\)", text, re.IGNORECASE | re.UNICODE) if result and len(result.groups()) == 2: try: # check if group 2 is a hash int(result.group(2)) return result.group(1) except: pass # In the result XML from pydevd list indexes appear # as names. If the name is a number then it is a index. return int(text) @async_handler def on_setVariable(self, request, args): vsc_var = int(args['variablesReference']) pyd_var = self.var_map.to_pydevd(vsc_var) var_name = args['name'] var_value = args['value'] lhs_expr = self.__get_variable_evaluate_name(pyd_var, var_name) if not lhs_expr: lhs_expr = var_name expr = '%s = %s' % (lhs_expr, var_value) # pydevd message format doesn't permit tabs in expressions expr = expr.replace('\t', ' ') pyd_tid = str(pyd_var[0]) pyd_fid = str(pyd_var[1]) safe_repr_provider.convert_to_hex = args.get('format', {}).get('hex', False) # VSC gives us variablesReference to the parent of the variable # being set, and variable name; but pydevd wants the ID # (or rather path) of the variable itself. pyd_var += (var_name, ) vsc_var = self.var_map.to_vscode(pyd_var, autogen=True) cmd_args = [pyd_tid, pyd_fid, 'LOCAL', expr, '1'] yield self.pydevd_request( pydevd_comm.CMD_EXEC_EXPRESSION, '\t'.join(cmd_args), ) cmd_args = [pyd_tid, pyd_fid, 'LOCAL', lhs_expr, '1'] _, _, resp_args = yield self.pydevd_request( pydevd_comm.CMD_EVALUATE_EXPRESSION, '\t'.join(cmd_args), ) xml = untangle.parse(resp_args).xml xvar = xml.var response = { 'type': unquote(xvar['type']), 'value': unquote(xvar['value']), } if bool(xvar['isContainer']): response['variablesReference'] = vsc_var # Reset hex format since this is per request. safe_repr_provider.convert_to_hex = False self.send_response(request, **response) @async_handler def on_evaluate(self, request, args): # pydevd message format doesn't permit tabs in expressions expr = args['expression'].replace('\t', ' ') vsc_fid = int(args['frameId']) pyd_tid, pyd_fid = self.frame_map.to_pydevd(vsc_fid) safe_repr_provider.convert_to_hex = args.get('format', {}).get('hex', False) cmd_args = (pyd_tid, pyd_fid, 'LOCAL', expr, '1') _, _, resp_args = yield self.pydevd_request( pydevd_comm.CMD_EVALUATE_EXPRESSION, '\t'.join(str(s) for s in cmd_args)) xml = untangle.parse(resp_args).xml xvar = xml.var context = args.get('context', '') is_eval_error = xvar['isErrorOnEval'] if context == 'hover' and is_eval_error == 'True': self.send_response(request, result=None, variablesReference=0) return pyd_var = (pyd_tid, pyd_fid, 'EXPRESSION', expr) vsc_var = self.var_map.to_vscode(pyd_var, autogen=True) response = { 'type': unquote(xvar['type']), 'result': unquote(xvar['value']), } if bool(xvar['isContainer']): response['variablesReference'] = vsc_var # Reset hex format since this is per request. safe_repr_provider.convert_to_hex = False self.send_response(request, **response) @async_handler def on_pause(self, request, args): # TODO: docstring # Pause requests cannot be serviced until pydevd is fully initialized. with self.is_process_created_lock: if not self.is_process_created: self.send_response( request, success=False, message='Cannot pause while debugger is initializing', ) return vsc_tid = int(args['threadId']) if vsc_tid == 0: # VS does this to mean "stop all threads": for pyd_tid in self.thread_map.pydevd_ids(): self.pydevd_notify(pydevd_comm.CMD_THREAD_SUSPEND, pyd_tid) else: pyd_tid = self.thread_map.to_pydevd(vsc_tid) self.pydevd_notify(pydevd_comm.CMD_THREAD_SUSPEND, pyd_tid) self.send_response(request) @async_handler def on_continue(self, request, args): # TODO: docstring tid = self.thread_map.to_pydevd(int(args['threadId'])) self.pydevd_notify(pydevd_comm.CMD_THREAD_RUN, tid) self.send_response(request) @async_handler def on_next(self, request, args): # TODO: docstring tid = self.thread_map.to_pydevd(int(args['threadId'])) self.pydevd_notify(pydevd_comm.CMD_STEP_OVER, tid) self.send_response(request) @async_handler def on_stepIn(self, request, args): # TODO: docstring tid = self.thread_map.to_pydevd(int(args['threadId'])) self.pydevd_notify(pydevd_comm.CMD_STEP_INTO, tid) self.send_response(request) @async_handler def on_stepOut(self, request, args): # TODO: docstring tid = self.thread_map.to_pydevd(int(args['threadId'])) self.pydevd_notify(pydevd_comm.CMD_STEP_RETURN, tid) self.send_response(request) @async_handler def on_setBreakpoints(self, request, args): # TODO: docstring bps = [] path = args['source']['path'] self.path_casing.track_file_path_case(path) src_bps = args.get('breakpoints', []) # First, we must delete all existing breakpoints in that source. cmd = pydevd_comm.CMD_REMOVE_BREAK for pyd_bpid, vsc_bpid in self.bp_map.pairs(): if pyd_bpid[0] == path: msg = 'python-line\t{}\t{}'.format(path, vsc_bpid) self.pydevd_notify(cmd, msg) self.bp_map.remove(pyd_bpid, vsc_bpid) cmd = pydevd_comm.CMD_SET_BREAK msgfmt = '{}\tpython-line\t{}\t{}\tNone\t{}\tNone' for src_bp in src_bps: line = src_bp['line'] vsc_bpid = self.bp_map.add(lambda vsc_bpid: (path, vsc_bpid)) self.path_casing.track_file_path_case(path) msg = msgfmt.format(vsc_bpid, path, line, src_bp.get('condition', None)) self.pydevd_notify(cmd, msg) bps.append({ 'id': vsc_bpid, 'verified': True, 'line': line, }) self.send_response(request, breakpoints=bps) @async_handler def on_setExceptionBreakpoints(self, request, args): # TODO: docstring filters = args['filters'] exception_options = args.get('exceptionOptions', []) if exception_options: self.exceptions_mgr.apply_exception_options(exception_options) else: self.exceptions_mgr.remove_all_exception_breaks() break_raised = 'raised' in filters break_uncaught = 'uncaught' in filters if break_raised or break_uncaught: self.exceptions_mgr.add_exception_break( 'BaseException', break_raised, break_uncaught) self.send_response(request) @async_handler def on_exceptionInfo(self, request, args): # TODO: docstring pyd_tid = self.thread_map.to_pydevd(args['threadId']) with self.active_exceptions_lock: try: exc = self.active_exceptions[pyd_tid] except KeyError: exc = ExceptionInfo('BaseException', 'exception: no description', None, None) self.send_response( request, exceptionId=exc.name, description=exc.description, breakMode=self.exceptions_mgr.get_break_mode(exc.name), details={ 'typeName': exc.name, 'message': exc.description, 'stackTrace': exc.stack, 'source': exc.source }, ) @pydevd_events.handler(pydevd_comm.CMD_THREAD_CREATE) def on_pydevd_thread_create(self, seq, args): # If this is the first thread reported, report process creation # as well. with self.is_process_created_lock: if not self.is_process_created: self.is_process_created = True self.send_process_event(self.start_reason) # TODO: docstring xml = untangle.parse(args).xml try: name = unquote(xml.thread['name']) except KeyError: name = None if not self.is_debugger_internal_thread(name): # Any internal pydevd or ptvsd threads will be ignored everywhere tid = self.thread_map.to_vscode(xml.thread['id'], autogen=True) self.send_event('thread', reason='started', threadId=tid) @pydevd_events.handler(pydevd_comm.CMD_THREAD_KILL) def on_pydevd_thread_kill(self, seq, args): # TODO: docstring pyd_tid = args.strip() try: vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=False) except KeyError: pass else: self.thread_map.remove(pyd_tid, vsc_tid) self.send_event('thread', reason='exited', threadId=vsc_tid) @pydevd_events.handler(pydevd_comm.CMD_THREAD_SUSPEND) @async_handler def on_pydevd_thread_suspend(self, seq, args): # TODO: docstring xml = untangle.parse(args).xml pyd_tid = xml.thread['id'] reason = int(xml.thread['stop_reason']) STEP_REASONS = { pydevd_comm.CMD_STEP_INTO, pydevd_comm.CMD_STEP_OVER, pydevd_comm.CMD_STEP_RETURN, } EXCEPTION_REASONS = { pydevd_comm.CMD_STEP_CAUGHT_EXCEPTION, pydevd_comm.CMD_ADD_EXCEPTION_BREAK } try: vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=False) except KeyError: return with self.stack_traces_lock: self.stack_traces[pyd_tid] = xml.thread.frame description = None text = None if reason in STEP_REASONS: reason = 'step' elif reason in EXCEPTION_REASONS: reason = 'exception' elif reason == pydevd_comm.CMD_SET_BREAK: reason = 'breakpoint' else: reason = 'pause' # For exception cases both raise and uncaught, pydevd adds a # __exception__ object to the top most frame. Extracting the # exception name and description from that frame gives accurate # exception information. if reason == 'exception': # Get exception info from frame try: xframes = list(xml.thread.frame) xframe = xframes[0] pyd_fid = xframe['id'] cmdargs = '{}\t{}\tFRAME\t__exception__'.format( pyd_tid, pyd_fid) cmdid = pydevd_comm.CMD_GET_VARIABLE _, _, resp_args = yield self.pydevd_request(cmdid, cmdargs) xml = untangle.parse(resp_args).xml text = unquote(xml.var[1]['type']) description = unquote(xml.var[1]['value']) frame_data = ((unquote(f['file']), int(f['line']), unquote(f['name']), None) for f in xframes) stack = ''.join(traceback.format_list(frame_data)) source = unquote(xframe['file']) except Exception: text = 'BaseException' description = 'exception: no description' stack = None source = None with self.active_exceptions_lock: self.active_exceptions[pyd_tid] = ExceptionInfo( text, description, stack, source) self.send_event('stopped', reason=reason, threadId=vsc_tid, text=text, description=description) @pydevd_events.handler(pydevd_comm.CMD_THREAD_RUN) def on_pydevd_thread_run(self, seq, args): # TODO: docstring pyd_tid, reason = args.split('\t') pyd_tid = pyd_tid.strip() # Stack trace, active exception, all frames, and variables for # this thread are now invalid; clear their IDs. with self.stack_traces_lock: try: del self.stack_traces[pyd_tid] except KeyError: pass with self.active_exceptions_lock: try: del self.active_exceptions[pyd_tid] except KeyError: pass for pyd_fid, vsc_fid in self.frame_map.pairs(): if pyd_fid[0] == pyd_tid: self.frame_map.remove(pyd_fid, vsc_fid) for pyd_var, vsc_var in self.var_map.pairs(): if pyd_var[0] == pyd_tid: self.var_map.remove(pyd_var, vsc_var) try: vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=False) except KeyError: pass else: self.send_event('continued', threadId=vsc_tid) @pydevd_events.handler(pydevd_comm.CMD_SEND_CURR_EXCEPTION_TRACE) def on_pydevd_send_curr_exception_trace(self, seq, args): # TODO: docstring pass @pydevd_events.handler(pydevd_comm.CMD_SEND_CURR_EXCEPTION_TRACE_PROCEEDED) def on_pydevd_send_curr_exception_trace_proceeded(self, seq, args): # TODO: docstring pyd_tid = args.strip() with self.active_exceptions_lock: try: del self.active_exceptions[pyd_tid] except KeyError: pass @pydevd_events.handler(pydevd_comm.CMD_WRITE_TO_CONSOLE) def on_pydevd_cmd_write_to_console2(self, seq, args): """Handle console output""" xml = untangle.parse(args).xml ctx = xml.io['ctx'] category = 'stdout' if ctx == '1' else 'stderr' content = unquote(xml.io['s']) self.send_event('output', category=category, output=content)
def test_path_names_lowercase_enabled(self): tool = PathUnNormcase() tool.enable() file_path = __file__ self.assertEqual(file_path, tool.un_normcase(file_path.lower()))
def test_path_names_lowercase_enabled(self): tool = PathUnNormcase() tool.enable() result = tool.un_normcase(FILENAME.lower()) self.assertEqual(result, ACTUAL)
def test_path_names_normcased(self): tool = PathUnNormcase() tool.enable() file_path = __file__ self.assertEqual(file_path, tool.un_normcase(os.path.normcase(file_path)))
def test_path_names_normcased(self): tool = PathUnNormcase() tool.enable() result = tool.un_normcase(os.path.normcase(FILENAME)) self.assertEqual(result, ACTUAL)