예제 #1
0
    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)
예제 #2
0
  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' )
예제 #3
0
    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']))
예제 #4
0
    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)
예제 #5
0
    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']))
예제 #6
0
  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' ] = []
예제 #7
0
  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' )
예제 #8
0
  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
        } ]
예제 #9
0
    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']))
예제 #10
0
    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)
예제 #11
0
    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')
예제 #12
0
  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' )