def _StopDebugAdapter(self, interactive=False, callback=None): self._splash_screen = utils.DisplaySplash( self._api_prefix, self._splash_screen, "Shutting down debug adapter...") def handler(*args): self._splash_screen = utils.HideSplash(self._api_prefix, self._splash_screen) if callback: self._logger.debug( "Setting server exit handler before disconnect") assert not self._run_on_server_exit self._run_on_server_exit = callback vim.eval('vimspector#internal#{}#StopDebugSession()'.format( self._connection_type)) arguments = {} if (interactive and self._server_capabilities.get('supportTerminateDebuggee')): if self._stackTraceView.AnyThreadsRunning(): choice = utils.AskForInput( "Terminate debuggee [Y/N/default]? ", "") if choice == "Y" or choice == "y": arguments['terminateDebuggee'] = True elif choice == "N" or choice == 'n': arguments['terminateDebuggee'] = False self._connection.DoRequest(handler, { 'command': 'disconnect', 'arguments': arguments, }, failure_handler=handler, timeout=5000)
def _StartDebugAdapter( self ): self._splash_screen = utils.DisplaySplash( self._api_prefix, self._splash_screen, "Starting debug adapter..." ) if self._connection: utils.UserMessage( 'The connection is already created. Please try again', persist = True ) return self._logger.info( 'Starting debug adapter with: %s', json.dumps( self._adapter ) ) self._init_complete = False self._on_init_complete_handlers = [] self._launch_complete = False self._run_on_server_exit = None self._connection_type = 'job' if 'port' in self._adapter: self._connection_type = 'channel' if self._adapter[ 'port' ] == 'ask': port = utils.AskForInput( 'Enter port to connect to: ' ) if port is None: self._Reset() return self._adapter[ 'port' ] = port self._connection_type = self._api_prefix + self._connection_type # TODO: Do we actually need to copy and update or does Vim do that? env = os.environ.copy() if os.name == "nt": env = {} if 'env' in self._adapter: env.update( self._adapter[ 'env' ] ) self._adapter[ 'env' ] = env if 'cwd' not in self._adapter: self._adapter[ 'cwd' ] = os.getcwd() vim.vars[ '_vimspector_adapter_spec' ] = self._adapter if not vim.eval( "vimspector#internal#{}#StartDebugSession( " " g:_vimspector_adapter_spec " ")".format( self._connection_type ) ): self._logger.error( "Unable to start debug server" ) self._splash_screen = utils.DisplaySplash( self._api_prefix, self._splash_screen, "Unable to start adapter" ) else: self._connection = debug_adapter_connection.DebugAdapterConnection( self, lambda msg: utils.Call( "vimspector#internal#{}#Send".format( self._connection_type ), msg ) ) self._logger.info( 'Debug Adapter Started' )
def _PrepareAttach(self, adapter_config, launch_config): atttach_config = adapter_config.get('attach') if not atttach_config: return if 'remote' in atttach_config: # FIXME: We almost want this to feed-back variables to be expanded later, # e.g. expand variables when we use them, not all at once. This would # remove the whole %PID% hack. remote = atttach_config['remote'] ssh = ['ssh'] if 'account' in remote: ssh.append(remote['account'] + '@' + remote['host']) else: ssh.append(remote['host']) cmd = ssh + remote['pidCommand'] self._logger.debug('Getting PID: %s', cmd) pid = subprocess.check_output(cmd).decode('utf-8').strip() self._logger.debug('Got PID: %s', pid) if not pid: # FIXME: We should raise an exception here or something utils.UserMessage('Unable to get PID', persist=True) return if 'initCompleteCommand' in remote: initcmd = ssh + remote['initCompleteCommand'][:] for index, item in enumerate(initcmd): initcmd[index] = item.replace('%PID%', pid) self._on_init_complete_handlers.append( lambda: subprocess.check_call(initcmd)) commands = self._GetCommands(remote, 'attach') for command in commands: cmd = ssh + command[:] for index, item in enumerate(cmd): cmd[index] = item.replace('%PID%', pid) self._logger.debug('Running remote app: %s', cmd) self._outputView.RunJobWithOutput('Remote', cmd) else: if atttach_config['pidSelect'] == 'ask': pid = utils.AskForInput('Enter PID to attach to: ') launch_config[atttach_config['pidProperty']] = pid return elif atttach_config['pidSelect'] == 'none': return raise ValueError('Unrecognised pidSelect {0}'.format( atttach_config['pidSelect']))
def SetVariableValue(self, new_value=None, buf=None, line_num=None): variable: Variable view: View if not self._server_capabilities.get('supportsSetVariable'): return variable, view = self._GetVariable(buf, line_num) if variable is None: return if not variable.IsContained(): return if new_value is None: new_value = utils.AskForInput('New Value: ', variable.variable.get('value', ''), completion='expr') if new_value is None: return def handler(message): # Annoyingly the response to setVariable request doesn't return a # Variable, but some part of it, so take a copy of the existing Variable # dict and update it, then call its update method with the updated copy. new_variable = dict(variable.variable) new_variable.update(message['body']) # Clear any existing known children (FIXME: Is this the right thing to do) variable.variables = None # If the variable is expanded, re-request its children if variable.IsExpanded(): self._connection.DoRequest( partial(self._ConsumeVariables, view.draw, variable), { 'command': 'variables', 'arguments': { 'variablesReference': variable.VariablesReference() }, }) variable.Update(new_variable) view.draw() def failure_handler(reason, message): utils.UserMessage(f'Cannot set value: { reason }', error=True) self._connection.DoRequest(handler, { 'command': 'setVariable', 'arguments': { 'variablesReference': variable.container.VariablesReference(), 'name': variable.variable['name'], 'value': new_value }, }, failure_handler=failure_handler)
def _SelectProcess(self, adapter_config, launch_config): atttach_config = adapter_config['attach'] if atttach_config['pidSelect'] == 'ask': pid = utils.AskForInput('Enter PID to attach to: ') launch_config[atttach_config['pidProperty']] = pid return elif atttach_config['pidSelect'] == 'none': return raise ValueError('Unrecognised pidSelect {0}'.format( atttach_config['pidSelect']))
def _SetUpExceptionBreakpoints( self, configured_breakpoints ): exception_breakpoint_filters = self._server_capabilities.get( 'exceptionBreakpointFilters', [] ) if exception_breakpoint_filters or not self._server_capabilities.get( 'supportsConfigurationDoneRequest' ): # Note the supportsConfigurationDoneRequest part: prior to there being a # configuration done request, the "exception breakpoints" request was the # indication that configuraiton was done (and its response is used to # trigger requesting threads etc.). See the note in # debug_session.py:_Initialise for more detials exception_filters = [] configured_filter_options = configured_breakpoints.get( 'exception', {} ) if exception_breakpoint_filters: for f in exception_breakpoint_filters: default_value = 'Y' if f.get( 'default' ) else 'N' if f[ 'filter' ] in configured_filter_options: result = configured_filter_options[ f[ 'filter' ] ] if isinstance( result, bool ): result = 'Y' if result else 'N' if not isinstance( result, str ) or result not in ( 'Y', 'N', '' ): raise ValueError( f"Invalid value for exception breakpoint filter '{f}': " f"'{result}'. Must be boolean, 'Y', 'N' or '' (default)" ) else: try: result = utils.AskForInput( "{}: Break on {} (Y/N/default: {})? ".format( f[ 'filter' ], f[ 'label' ], default_value ), default_value ) except KeyboardInterrupt: result = '' if result == 'Y': exception_filters.append( f[ 'filter' ] ) elif not result and f.get( 'default' ): exception_filters.append( f[ 'filter' ] ) self._exception_breakpoints = { 'filters': exception_filters } if self._server_capabilities.get( 'supportsExceptionOptions' ): # TODO: There are more elaborate exception breakpoint options here, but # we don't support them. It doesn't seem like any of the servers really # pay any attention to them anyway. self._exception_breakpoints[ 'exceptionOptions' ] = []
def _StartDebugAdapter( self ): if self._connection: utils.UserMessage( 'The connection is already created. Please try again', persist = True ) return self._logger.info( 'Starting debug adapter with: %s', json.dumps( self._adapter ) ) self._init_complete = False self._on_init_complete_handlers = [] self._launch_complete = False self._run_on_server_exit = None self._connection_type = 'job' if 'port' in self._adapter: self._connection_type = 'channel' if self._adapter[ 'port' ] == 'ask': port = utils.AskForInput( 'Enter port to connect to: ' ) self._adapter[ 'port' ] = port # TODO: Do we actually need to copy and update or does Vim do that? env = os.environ.copy() if 'env' in self._adapter: env.update( self._adapter[ 'env' ] ) self._adapter[ 'env' ] = env if 'cwd' not in self._adapter: self._adapter[ 'cwd' ] = os.getcwd() channel_send_func = vim.bindeval( "vimspector#internal#{}#StartDebugSession( {} )".format( self._connection_type, json.dumps( self._adapter ) ) ) if channel_send_func is None: self._logger.error( "Unable to start debug server" ) else: self._connection = debug_adapter_connection.DebugAdapterConnection( self, channel_send_func ) self._logger.info( 'Debug Adapter Started' )
def _SetUpExceptionBreakpoints( self ): exceptionBreakpointFilters = self._server_capabilities.get( 'exceptionBreakpointFilters', [] ) if exceptionBreakpointFilters or not self._server_capabilities.get( 'supportsConfigurationDoneRequest' ): exceptionFilters = [] if exceptionBreakpointFilters: for f in exceptionBreakpointFilters: response = utils.AskForInput( "Enable exception filter '{}'? (Y/N)".format( f[ 'label' ] ) ) if response == 'Y': exceptionFilters.append( f[ 'filter' ] ) elif not response and f.get( 'default' ): exceptionFilters.append( f[ 'filter' ] ) self._exceptionBreakpoints = { 'filters': exceptionFilters } if self._server_capabilities.get( 'supportsExceptionOptions' ): # FIXME Sigh. The python debug adapter requires this # key to exist. Even though it is optional. break_mode = utils.SelectFromList( 'When to break on exception?', [ 'never', 'always', 'unhandled', 'userHandled' ] ) if not break_mode: break_mode = 'unhandled' path = [ { 'nagate': True, 'names': [ 'DO_NOT_MATCH' ] } ] self._exceptionBreakpoints[ 'exceptionOptions' ] = [ { 'path': path, 'breakMode': break_mode } ]
def _PrepareAttach(self, adapter_config, launch_config): atttach_config = adapter_config.get('attach') if not atttach_config: return if 'remote' in atttach_config: # FIXME: We almost want this to feed-back variables to be expanded later, # e.g. expand variables when we use them, not all at once. This would # remove the whole %PID% hack. remote = atttach_config['remote'] remote_exec_cmd = self._GetRemoteExecCommand(remote) # FIXME: Why does this not use self._GetCommands ? pid_cmd = remote_exec_cmd + remote['pidCommand'] self._logger.debug('Getting PID: %s', pid_cmd) pid = subprocess.check_output(pid_cmd).decode('utf-8').strip() self._logger.debug('Got PID: %s', pid) if not pid: # FIXME: We should raise an exception here or something utils.UserMessage('Unable to get PID', persist=True) return if 'initCompleteCommand' in remote: initcmd = remote_exec_cmd + remote['initCompleteCommand'][:] for index, item in enumerate(initcmd): initcmd[index] = item.replace('%PID%', pid) self._on_init_complete_handlers.append( lambda: subprocess.check_call(initcmd)) commands = self._GetCommands(remote, 'attach') for command in commands: cmd = remote_exec_cmd + command for index, item in enumerate(cmd): cmd[index] = item.replace('%PID%', pid) self._logger.debug('Running remote app: %s', cmd) self._remote_term = terminal.LaunchTerminal( self._api_prefix, { 'args': cmd, 'cwd': os.getcwd() }, self._codeView._window, self._remote_term) else: if atttach_config['pidSelect'] == 'ask': prop = atttach_config['pidProperty'] if prop not in launch_config: pid = utils.AskForInput('Enter PID to attach to: ') if pid is None: return launch_config[prop] = pid return elif atttach_config['pidSelect'] == 'none': return raise ValueError('Unrecognised pidSelect {0}'.format( atttach_config['pidSelect']))
def Start(self, launch_variables=None): # We mutate launch_variables, so don't mutate the default argument. # https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments if launch_variables is None: launch_variables = {} self._logger.info("User requested start debug session with %s", launch_variables) self._configuration = None self._adapter = None current_file = utils.GetBufferFilepath(vim.current.buffer) filetypes = utils.GetBufferFiletypes(vim.current.buffer) configurations = {} adapters = {} glob.glob(install.GetGadgetDir(VIMSPECTOR_HOME)) for gadget_config_file in PathsToAllGadgetConfigs( VIMSPECTOR_HOME, current_file): self._logger.debug(f'Reading gadget config: {gadget_config_file}') if not gadget_config_file or not os.path.exists( gadget_config_file): continue with open(gadget_config_file, 'r') as f: a = json.loads(minify(f.read())).get('adapters') or {} adapters.update(a) for launch_config_file in PathsToAllConfigFiles( VIMSPECTOR_HOME, current_file, filetypes): self._logger.debug( f'Reading configurations from: {launch_config_file}') if not launch_config_file or not os.path.exists( launch_config_file): continue with open(launch_config_file, 'r') as f: database = json.loads(minify(f.read())) adapters.update(database.get('adapters') or {}) configurations.update(database.get('configurations' or {})) if not configurations: utils.UserMessage('Unable to find any debug configurations. ' 'You need to tell vimspector how to launch your ' 'application.') return if 'configuration' in launch_variables: configuration_name = launch_variables.pop('configuration') elif (len(configurations) == 1 and next(iter(configurations.values())).get("autoselect", True)): configuration_name = next(iter(configurations.keys())) else: # Find a single configuration with 'default' True and autoselect not False defaults = { n: c for n, c in configurations.items() if c.get('default', False) is True and c.get('autoselect', True) is not False } if len(defaults) == 1: configuration_name = next(iter(defaults.keys())) else: configuration_name = utils.SelectFromList( 'Which launch configuration?', sorted(configurations.keys())) if not configuration_name or configuration_name not in configurations: return if launch_config_file: self._workspace_root = os.path.dirname(launch_config_file) else: self._workspace_root = os.path.dirname(current_file) configuration = configurations[configuration_name] adapter = configuration.get('adapter') if isinstance(adapter, str): adapter_dict = adapters.get(adapter) if adapter_dict is None: suggested_gadgets = installer.FindGadgetForAdapter(adapter) if suggested_gadgets: response = utils.AskForInput( f"The specified adapter '{adapter}' is not " "installed. Would you like to install the following gadgets? ", ' '.join(suggested_gadgets)) if response: new_launch_variables = dict(launch_variables) new_launch_variables[ 'configuration'] = configuration_name installer.RunInstaller( self._api_prefix, False, # Don't leave open *shlex.split(response), then=lambda: self.Start(new_launch_variables)) return elif response is None: return utils.UserMessage( f"The specified adapter '{adapter}' is not " "available. Did you forget to run " "'install_gadget.py'?", persist=True, error=True) return adapter = adapter_dict # Additional vars as defined by VSCode: # # ${workspaceFolder} - the path of the folder opened in VS Code # ${workspaceFolderBasename} - the name of the folder opened in VS Code # without any slashes (/) # ${file} - the current opened file # ${relativeFile} - the current opened file relative to workspaceFolder # ${fileBasename} - the current opened file's basename # ${fileBasenameNoExtension} - the current opened file's basename with no # file extension # ${fileDirname} - the current opened file's dirname # ${fileExtname} - the current opened file's extension # ${cwd} - the task runner's current working directory on startup # ${lineNumber} - the current selected line number in the active file # ${selectedText} - the current selected text in the active file # ${execPath} - the path to the running VS Code executable def relpath(p, relative_to): if not p: return '' return os.path.relpath(p, relative_to) def splitext(p): if not p: return ['', ''] return os.path.splitext(p) variables = { 'dollar': '$', # HACK. Hote '$$' also works. 'workspaceRoot': self._workspace_root, 'workspaceFolder': self._workspace_root, 'gadgetDir': install.GetGadgetDir(VIMSPECTOR_HOME), 'file': current_file, } calculus = { 'relativeFile': lambda: relpath(current_file, self._workspace_root), 'fileBasename': lambda: os.path.basename(current_file), 'fileBasenameNoExtension': lambda: splitext(os.path.basename(current_file))[0], 'fileDirname': lambda: os.path.dirname(current_file), 'fileExtname': lambda: splitext(os.path.basename(current_file))[1], # NOTE: this is the window-local cwd for the current window, *not* Vim's # working directory. 'cwd': os.getcwd, 'unusedLocalPort': utils.GetUnusedLocalPort, } # Pretend that vars passed to the launch command were typed in by the user # (they may have been in theory) USER_CHOICES.update(launch_variables) variables.update(launch_variables) try: variables.update( utils.ParseVariables(adapter.get('variables', {}), variables, calculus, USER_CHOICES)) variables.update( utils.ParseVariables(configuration.get('variables', {}), variables, calculus, USER_CHOICES)) utils.ExpandReferencesInDict(configuration, variables, calculus, USER_CHOICES) utils.ExpandReferencesInDict(adapter, variables, calculus, USER_CHOICES) except KeyboardInterrupt: self._Reset() return if not adapter: utils.UserMessage( 'No adapter configured for {}'.format(configuration_name), persist=True) return self._StartWithConfiguration(configuration, adapter)
def _StartDebugAdapter(self): self._splash_screen = utils.DisplaySplash(self._api_prefix, self._splash_screen, "Starting debug adapter...") if self._connection: utils.UserMessage( 'The connection is already created. Please try again', persist=True) return # There is the problem with the current flow when we try to use a debugger # which is located fully on the remote server e.g container or SSH Server. # The problem is in the order: it tries to connect to debugger before it is even started # To solve that problem, I offer adding an optional boolean key "bootstrap" to a configuration. # If we have that key, we should perform launch or attach commands to first bootstrap a remote debugger. # Then we can skip that step in the _Launch() function if self._adapter.get('bootstrap'): self._BootstrapRemoteDebugger() self._logger.info('Starting debug adapter with: %s', json.dumps(self._adapter)) self._init_complete = False self._on_init_complete_handlers = [] self._launch_complete = False self._run_on_server_exit = None self._connection_type = 'job' if 'port' in self._adapter: self._connection_type = 'channel' if self._adapter['port'] == 'ask': port = utils.AskForInput('Enter port to connect to: ') if port is None: self._Reset() return self._adapter['port'] = port self._connection_type = self._api_prefix + self._connection_type # TODO: Do we actually need to copy and update or does Vim do that? env = os.environ.copy() if 'env' in self._adapter: env.update(self._adapter['env']) self._adapter['env'] = env if 'cwd' not in self._adapter: self._adapter['cwd'] = os.getcwd() vim.vars['_vimspector_adapter_spec'] = self._adapter if not vim.eval("vimspector#internal#{}#StartDebugSession( " " g:_vimspector_adapter_spec " ")".format(self._connection_type)): self._logger.error("Unable to start debug server") self._splash_screen = utils.DisplaySplash( self._api_prefix, self._splash_screen, "Unable to start adapter") else: self._connection = debug_adapter_connection.DebugAdapterConnection( self, lambda msg: utils.Call( "vimspector#internal#{}#Send".format(self._connection_type ), msg)) self._logger.info('Debug Adapter Started')
def _StartDebugAdapter( self ): self._splash_screen = utils.DisplaySplash( self._api_prefix, self._splash_screen, "Starting debug adapter..." ) if self._connection: utils.UserMessage( 'The connection is already created. Please try again', persist = True ) return self._logger.info( 'Starting debug adapter with: %s', json.dumps( self._adapter ) ) self._init_complete = False self._launch_complete = False self._run_on_server_exit = None self._connection_type = 'job' if 'port' in self._adapter: self._connection_type = 'channel' if self._adapter[ 'port' ] == 'ask': port = utils.AskForInput( 'Enter port to connect to: ' ) if port is None: self._Reset() return self._adapter[ 'port' ] = port self._connection_type = self._api_prefix + self._connection_type self._logger.debug( f"Connection Type: { self._connection_type }" ) self._adapter[ 'env' ] = self._adapter.get( 'env', {} ) if 'cwd' not in self._adapter: self._adapter[ 'cwd' ] = os.getcwd() vim.vars[ '_vimspector_adapter_spec' ] = self._adapter if not vim.eval( "vimspector#internal#{}#StartDebugSession( " " g:_vimspector_adapter_spec " ")".format( self._connection_type ) ): self._logger.error( "Unable to start debug server" ) self._splash_screen = utils.DisplaySplash( self._api_prefix, self._splash_screen, "Unable to start adapter" ) else: if 'custom_handler' in self._adapter: spec = self._adapter[ 'custom_handler' ] if isinstance( spec, dict ): module = spec[ 'module' ] cls = spec[ 'class' ] else: module, cls = spec.rsplit( '.', 1 ) CustomHandler = getattr( importlib.import_module( module ), cls ) handlers = [ CustomHandler( self ), self ] else: handlers = [ self ] self._connection = debug_adapter_connection.DebugAdapterConnection( handlers, lambda msg: utils.Call( "vimspector#internal#{}#Send".format( self._connection_type ), msg ) ) self._logger.info( 'Debug Adapter Started' )