From b02d35e78d2010c4842e06f48bd8e25d43c68cdf Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 27 May 2018 23:40:37 +0100 Subject: [PATCH] Very hacky: Support for attach This introduced more hacks than it should. In particular, the sequence about stack trace requesting became very messy. When we attach, we don't instantly get a stopped event. This required making the Pause command actually work (sort of). In this case we often won't have a proper current thread. Instead we sort of request all threads whenever we get a thread event, thought his is horribly hacky and we should really just use the thread event as-is. We then attempt to pause ALL threads on pause and continue requests when we don't know the current thread. Another issue is that when pausing it's likely we don't have the source location info for the pause location (something like select() or whatever), so we only set the current frame to the lowest one we have source for. This sort of roughly matches what you want. This whole thing makes it clear that threads and stack trace in separate panes makes no sense. We need to replicate the hierarchy in the variables view for threads and stack traces. --- .vimspector.json | 17 +++ python3/vimspector/code.py | 7 +- .../vimspector/debug_adapter_connection.py | 7 ++ python3/vimspector/debug_session.py | 105 +++++++++++++++--- python3/vimspector/stack_trace.py | 10 +- python3/vimspector/utils.py | 36 ++++-- 6 files changed, 151 insertions(+), 31 deletions(-) diff --git a/.vimspector.json b/.vimspector.json index 278ea9f..ef8a3e0 100644 --- a/.vimspector.json +++ b/.vimspector.json @@ -52,5 +52,22 @@ "debugOptions": [], "program": "/Users/ben/.vim/bundle/vimspector/support/test/python/simple_python/main.py" } + }, + "simple_c_program - MS Attach": { + "adapter": { + "name": "cppdbg", + "command": [ "/Users/ben/.vscode/extensions/ms-vscode.cpptools-0.17.3/debugAdapters/OpenDebugAD7" ], + "attach": { + "pidProperty": "processId", + "pidSelect": "ask" + } + }, + "configuration": { + "name": "(lldb) Attach", + "type": "cppdbg", + "request": "attach", + "program": "/Users/ben/.vim/bundle/vimspector/support/test/cpp/simple_c_program/test", + "MIMode": "lldb" + } } } diff --git a/python3/vimspector/code.py b/python3/vimspector/code.py index 03f5420..123d092 100644 --- a/python3/vimspector/code.py +++ b/python3/vimspector/code.py @@ -52,12 +52,15 @@ class CodeView( object ): def SetCurrentFrame( self, frame ): - vim.current.window = self._window - if self._signs[ 'vimspectorPC' ]: vim.command( 'sign unplace {0}'.format( self._signs[ 'vimspectorPC' ] ) ) self._signs[ 'vimspectorPC' ] = None + if not frame or not frame[ 'source' ]: + return + + vim.current.window = self._window + buffer_number = vim.eval( 'bufnr( "{0}", 1 )'.format( frame[ 'source' ][ 'path' ] ) ) diff --git a/python3/vimspector/debug_adapter_connection.py b/python3/vimspector/debug_adapter_connection.py index 75793d2..e6615d1 100644 --- a/python3/vimspector/debug_adapter_connection.py +++ b/python3/vimspector/debug_adapter_connection.py @@ -41,6 +41,10 @@ class DebugAdapterConnection( object ): self._outstanding_requests[ this_id ] = handler self._SendMessage( msg ) + def Reset( self ): + self._Write = None + self._handler = None + def OnData( self, data ): data = bytes( data, 'utf-8' ) self._logger.debug( 'Received ({0}/{1}): {2},'.format( type( data ), @@ -111,6 +115,9 @@ class DebugAdapterConnection( object ): self._SetState( 'READ_HEADER' ) def _OnMessageReceived( self, message ): + if not self._handler: + return + if message[ 'type' ] == 'response': handler = self._outstanding_requests.pop( message[ 'request_seq' ] ) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 39edeb4..b78b785 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -38,12 +38,13 @@ class DebugSession( object ): self._connection = None self._uiTab = None - self._threadsBuffer = None + self._threadsBuffer = None # TODO: Move to stack trace self._outputBuffer = None self._stackTraceView = None self._variablesView = None self._currentThread = None + self._threads = [] self._currentFrame = None self._next_sign_id = SIGN_ID_OFFSET @@ -158,6 +159,9 @@ class DebugSession( object ): vim.eval( 'vimspector#internal#state#Reset()' ) def StepOver( self ): + if self._currentThread is None: + return + self._connection.DoRequest( None, { 'command': 'next', 'arguments': { @@ -166,6 +170,9 @@ class DebugSession( object ): } ) def StepInto( self ): + if self._currentThread is None: + return + self._connection.DoRequest( None, { 'command': 'stepIn', 'arguments': { @@ -174,6 +181,9 @@ class DebugSession( object ): } ) def StepOut( self ): + if self._currentThread is None: + return + self._connection.DoRequest( None, { 'command': 'stepOut', 'arguments': { @@ -182,6 +192,15 @@ class DebugSession( object ): } ) def Continue( self ): + if self._currentThread is None: + for thread in self._threads: + self._connection.DoRequest( None, { + 'command': 'continue', + 'arguments': { + 'threadId': thread[ 'id' ] + }, + } ) + self._connection.DoRequest( None, { 'command': 'continue', 'arguments': { @@ -189,7 +208,18 @@ class DebugSession( object ): }, } ) + self.ClearCurrentFrame() + def Pause( self ): + if self._currentThread is None: + for thread in self._threads: + self._connection.DoRequest( None, { + 'command': 'pause', + 'arguments': { + 'threadId': thread[ 'id' ], + }, + } ) + self._connection.DoRequest( None, { 'command': 'pause', 'arguments': { @@ -207,6 +237,9 @@ class DebugSession( object ): self._variablesView.DeleteWatch() def ShowBalloon( self, winnr, expression ): + if self._currentFrame is None: + return + if winnr == int( self._codeView._window.number ): self._variablesView.ShowBalloon( self._currentFrame, expression ) else: @@ -251,11 +284,19 @@ class DebugSession( object ): self._variablesView = variables.VariablesView( self._connection, vim.current.buffer ) + def ClearCurrentFrame( self ): + self.SetCurrentFrame( None ) + def SetCurrentFrame( self, frame ): self._currentFrame = frame self._codeView.SetCurrentFrame( frame ) - self._variablesView.LoadScopes( frame ) - self._variablesView.EvaluateWatches() + + if frame: + self._variablesView.LoadScopes( frame ) + self._variablesView.EvaluateWatches() + else: + self._stackTraceView.Clear() + self._variablesView.Clear() def _StartDebugAdapter( self ): self._logger.info( 'Starting debug adapter with: {0}'.format( json.dumps( @@ -274,6 +315,7 @@ class DebugSession( object ): def _StopDebugAdapter( self, callback = None ): def handler( message ): vim.eval( 'vimspector#internal#job#StopDebugSession()' ) + self._connection.Reset() self._connection = None self._stackTraceView.ConnectionClosed() self._variablesView.ConnectionClosed() @@ -288,24 +330,42 @@ class DebugSession( object ): } ) + 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 + + raise ValueError( 'Unrecognised pidSelect {0}'.format( + atttach_config[ 'pidSelect' ] ) ) + + def _Initialise( self ): + adapter_config = self._configuration[ 'adapter' ] + launch_config = self._configuration[ 'configuration' ] + + if 'attach' in adapter_config: + self._SelectProcess( adapter_config, launch_config ) + self._connection.DoRequest( None, { 'command': 'initialize', 'arguments': { - 'adapterID': self._configuration[ 'adapter' ].get( 'name', 'adapter' ), + 'adapterID': adapter_config.get( 'name', 'adapter' ), 'linesStartAt1': True, 'columnsStartAt1': True, 'pathFormat': 'path', }, } ) + # FIXME: name is mandatory. Forcefully add it (we should really use the # _actual_ name, but that isn't actually remembered at this point) - if 'name' not in self._configuration[ 'configuration' ]: - self._configuration[ 'configuration' ][ 'name' ] = 'test' + if 'name' not in launch_config: + launch_config[ 'name' ] = 'test' self._connection.DoRequest( None, { - 'command': self._configuration[ 'configuration' ][ 'request' ], - 'arguments': self._configuration[ 'configuration' ] + 'command': launch_config[ 'request' ], + 'arguments': launch_config } ) def _UpdateBreakpoints( self, source, message ): @@ -314,13 +374,10 @@ class DebugSession( object ): def OnEvent_initialized( self, message ): self._codeView.ClearBreakpoints() - self._SendBreakpoints() def OnEvent_thread( self, message ): - # TODO: set self_currentThread ? Not really that useful I guess as the - # stopped event basically gives us this. - pass + self._GetThreads() def OnEvent_breakpoint( self, message ): reason = message[ 'body' ][ 'reason' ] @@ -338,6 +395,7 @@ class DebugSession( object ): self._codeView.Clear() self._stackTraceView.Clear() self._variablesView.Clear() + self._threads.clear() with utils.ModifiableScratchBuffer( self._threadsBuffer ): self._threadsBuffer[:] = None @@ -415,19 +473,36 @@ class DebugSession( object ): self._outputBuffer.append( t, 0 ) def OnEvent_stopped( self, message ): - self._currentThread = message[ 'body' ][ 'threadId' ] + event = message[ 'body' ] + utils.UserMessage( 'Paused in thread {0} due to {1}'.format( + event.get( 'threadId', '' ), + event.get( 'description', event[ 'reason' ] ) ) ) + if 'threadId' in event: + self._currentThread = event[ 'threadId' ] + elif event.get( 'allThreadsStopped', False ) and self._threads: + self._currentThread = self._threads[ 0 ][ 'id' ] + + self._GetThreads() + self._stackTraceView.LoadStackTrace( self._currentThread ) + + def _GetThreads( self ): + # TODO: We need an expandable thing like variables for threads, and allow + # the user to select a thread def threads_printer( message ): + self._threads.clear() with utils.ModifiableScratchBuffer( self._threadsBuffer ): self._threadsBuffer[:] = None self._threadsBuffer.append( 'Threads: ' ) for thread in message[ 'body' ][ 'threads' ]: + if self._currentThread is None: + self._currentThread = thread[ 'id' ] + + self._threads.append( thread ) self._threadsBuffer.append( 'Thread {0}: {1}'.format( thread[ 'id' ], thread[ 'name' ] ) ) self._connection.DoRequest( threads_printer, { 'command': 'threads', } ) - - self._stackTraceView.LoadStackTrace( self._currentThread ) diff --git a/python3/vimspector/stack_trace.py b/python3/vimspector/stack_trace.py index f67b929..c3abbfc 100644 --- a/python3/vimspector/stack_trace.py +++ b/python3/vimspector/stack_trace.py @@ -67,8 +67,14 @@ class StackTraceView( object ): stackFrames = message[ 'body' ][ 'stackFrames' ] + current_frame = None for frame in stackFrames: - source = frame[ 'source' ] or { 'name': '' } + if frame[ 'source' ]: + current_frame = current_frame or frame + source = frame[ 'source' ] + else: + source = { 'name': '' } + self._buf.append( '{0}: {1}@{2}:{3}'.format( frame[ 'id' ], frame[ 'name' ], @@ -76,4 +82,4 @@ class StackTraceView( object ): frame[ 'line' ] ) ) self._line_to_frame[ len( self._buf ) ] = frame - self._session.SetCurrentFrame( stackFrames[ 0 ] ) + self._session.SetCurrentFrame( current_frame ) diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index b409d80..88ba9f5 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -96,18 +96,30 @@ def UserMessage( msg, persist=False ): vim.command( "{0} '{1}'".format( cmd, Escape( line ) ) ) -def SelectFromList( prompt, options ): +@contextlib.contextmanager +def InputSave(): vim.eval( 'inputsave()' ) - display_options = [ prompt ] - display_options.extend( [ '{0}: {1}'.format( i + 1, v ) - for i, v in enumerate( options ) ] ) try: - selection = int( vim.eval( - 'inputlist( ' + json.dumps( display_options ) + ' )' ) ) - 1 - if selection < 0 or selection >= len( options ): - return None - return options[ selection ] - except KeyboardInterrupt: - return None - finally: + yield + except: vim.eval( 'inputrestore()' ) + + +def SelectFromList( prompt, options ): + with InputSave(): + display_options = [ prompt ] + display_options.extend( [ '{0}: {1}'.format( i + 1, v ) + for i, v in enumerate( options ) ] ) + try: + selection = int( vim.eval( + 'inputlist( ' + json.dumps( display_options ) + ' )' ) ) - 1 + if selection < 0 or selection >= len( options ): + return None + return options[ selection ] + except KeyboardInterrupt: + return None + + +def AskForInput( prompt ): + with InputSave(): + return vim.eval( "input( '{0}' )".format( Escape( prompt ) ) )