From 84373974913d1a54fc171addbcb56fbe7853da85 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 20 May 2018 15:11:58 +0100 Subject: [PATCH] Actually connect to a real debug server and add some basic debugging UI is totally placeholder Step over/step in are the only supported commands Hardcoded launch config using a specific debug adapter that happened to work Adds a trivial log file hack and fixes the protocol handler for bytes --- autoload/vimspector.vim | 70 ++-- python3/vimspector/channel.py | 93 ----- .../vimspector/debug_adapter_connection.py | 343 ++++++++++++++++++ support/test/cpp/simple_c_program/test_c.cpp | 38 ++ 4 files changed, 428 insertions(+), 116 deletions(-) delete mode 100644 python3/vimspector/channel.py create mode 100644 python3/vimspector/debug_adapter_connection.py create mode 100644 support/test/cpp/simple_c_program/test_c.cpp diff --git a/autoload/vimspector.vim b/autoload/vimspector.vim index ab0d92d..375c47e 100644 --- a/autoload/vimspector.vim +++ b/autoload/vimspector.vim @@ -20,27 +20,39 @@ set cpo&vim " }}} let s:plugin_base = expand( ':p:h' ) . '/../' +let s:command = [ + \ 'node', + \ '/Users/ben/.vscode/extensions/webfreak.debug-0.22.0/out/src/lldb.js' + \ ] -function! s:_OnServerData( channel, data ) - echom 'Got data: ' . a:data +" let s:command = [ +" \ '/Users/ben/.vscode/extensions/ms-vscode.cpptools-0.17.1/' +" \ 'debugAdapters/OpenDebugAD7' +" \ ] +" +" \ 'node', +" \ '/Users/ben/Development/debugger/vscode-mock-debug/out/debugAdapter.js' +" \ ] + +function! s:_OnServerData( channel, data ) abort py3 << EOF -_PyChannel.OnData( vim.eval( 'a:data' ) ) +_session.OnChannelData( vim.eval( 'a:data' ) ) EOF endfunction -function! s:_OnServerError( channel, data ) +function! s:_OnServerError( channel, data ) abort echom "Channel received error: " . a:data endfunction -function! s:_OnExit( channel, status ) +function! s:_OnExit( channel, status ) abort echom "Channel exit with status " . a:status endfunction -function! s:_OnClose( channel ) +function! s:_OnClose( channel ) abort echom "Channel closed" endfunction -function! vimspector#StartDebugSession() +function! vimspector#StartDebugSession() abort " TODO: " - Work out the debug configuration (e.g. using RemoteDebug) " - Start a job running the server in raw mode @@ -57,14 +69,17 @@ function! vimspector#StartDebugSession() return endif - let s:job = job_start( s:plugin_base . 'support/bin/testecho', { - \ 'in_mode': 'raw', - \ 'out_mode': 'raw', - \ 'err_mode': 'raw', - \ 'exit_cb': function( 's:_OnExit' ), - \ 'close_cb': function( 's:_OnClose' ), - \ 'out_cb': function( 's:_OnServerData' ), - \ 'err_cb': function( 's:_OnServerError' ) } ) + let s:job = job_start( s:command, + \ { + \ 'in_mode': 'raw', + \ 'out_mode': 'raw', + \ 'err_mode': 'raw', + \ 'exit_cb': function( 's:_OnExit' ), + \ 'close_cb': function( 's:_OnClose' ), + \ 'out_cb': function( 's:_OnServerData' ), + \ 'err_cb': function( 's:_OnServerError' ) + \ } + \ ) if job_status( s:job ) != 'run' echom 'Fail whale. Job is ' . job_status( s:job ) @@ -72,7 +87,7 @@ function! vimspector#StartDebugSession() endif endfunction -function! s:_Send( msg ) +function! s:_Send( msg ) abort if job_status( s:job ) != 'run' echom "Server isnt running" return @@ -84,12 +99,14 @@ function! s:_Send( msg ) return endif - call ch_sendraw( ch, a:msg . "\n" ) + call ch_sendraw( ch, a:msg ) endfunction -function! vimspector#StopDebugSession() +function! vimspector#StopDebugSession() abort + py3 _session.Stop() + if job_status( s:job ) == 'run' job_stop( s:job, 'term' ) endif @@ -97,20 +114,27 @@ function! vimspector#StopDebugSession() unlet s:job endfunction -function! vimspector#Test() +function! vimspector#Launch() abort call vimspector#StartDebugSession() " call vimspector#WriteMessageToServer( 'test' ) let ch = job_getchannel( s:job ) py3 << EOF -from vimspector import channel -_PyChannel = channel.Channel( vim.Function( 's:_Send' ) ) -_PyChannel.Write( 'Another Test' ) +from vimspector import debug_adapter_connection +_session = debug_adapter_connection.DebugSession( + vim.Function( 's:_Send' ) ) +_session.Start() EOF - endfunction +function! vimspector#StepOver() abort + py3 _session.StepOver() +endfunction + +function! vimspector#StepInto() abort + py3 _session.StepInto() +endfunction " Boilerplate {{{ let &cpo=s:save_cpo diff --git a/python3/vimspector/channel.py b/python3/vimspector/channel.py deleted file mode 100644 index b82c21d..0000000 --- a/python3/vimspector/channel.py +++ /dev/null @@ -1,93 +0,0 @@ -# vimspector - A multi-language debugging system for Vim -# Copyright 2018 Ben Jackson -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import vim -import json - -_logger = logging.getLogger( __name__ ) - - -class Channel( object ): - def __init__( self, send_func ): - self._Write = send_func - self._SetState( 'AWAIT_HEADER' ) - self._buffer = '' - - def _SetState( self, state ): - self._state = state - if state == 'AWAIT_HEADER': - self._headers = {} - - def Write( self, data ): - self._Write( data ) - - def OnData( self, data ): - self._buffer = self._buffer + data - - while True: - if self._state == 'AWAIT_HEADER': - vim.command( 'echom "reading headers"' ) - data = self._ReadHeaders() - - if self._state == 'READ_BODY': - vim.command( 'echom "got headers"' ) - self._ReadBody() - else: - break - - if self._state != 'AWAIT_HEADER': - break - - - def _ReadHeaders( self ): - vim.command( "echom 'Reading from: {0}'".format( json.dumps( str( - self._buffer ) ) ) ) - headers = self._buffer.split( '\n\n', 1 ) - - vim.command( "echom 'Headers: {0}'".format( json.dumps( headers ) ) ) - - if len( headers ) > 1: - for header_line in headers[ 0 ].split( '\n' ): - if header_line.strip(): - key, value = header_line.split( ':', 1 ) - self._headers[ key ] = value - - # Chomp - self._buffer = self._buffer[ len( headers[ 0 ] ) + 2 : ] - self._SetState( 'READ_BODY' ) - return - - # otherwise waiting for more data - - def _ReadBody( self ): - content_length = int( self._headers[ 'Content-Length' ] ) - - vim.command( "echom 'Reading body of len {0} from: {1}'".format( - content_length, - json.dumps( str( self._buffer ) ) ) ) - - if len( self._buffer ) < content_length: - # Need more data - return - - payload = self._buffer[ : content_length ] - self._buffer = self._buffer[ content_length : ] # Off by one? - - vim.command( 'echom {0}'.format( json.dumps( str( payload ) ) ) ) - - # TODO Handle message - - self._SetState( 'AWAIT_HEADER' ) diff --git a/python3/vimspector/debug_adapter_connection.py b/python3/vimspector/debug_adapter_connection.py new file mode 100644 index 0000000..ca25580 --- /dev/null +++ b/python3/vimspector/debug_adapter_connection.py @@ -0,0 +1,343 @@ +# vimspector - A multi-language debugging system for Vim +# Copyright 2018 Ben Jackson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import vim +import json +import os +import contextlib + +_logger = logging.getLogger( __name__ ) + + +def SetUpLogging(): + handler = logging.FileHandler( os.path.expanduser( '~/.vimspector.log' ) ) + _logger.setLevel( logging.DEBUG ) + handler.setFormatter( + logging.Formatter( '%(asctime)s - %(levelname)s - %(message)s' ) ) + _logger.addHandler( handler ) + + +def SetUpScratchBuffer( buf ): + buf.options[ 'buftype' ] = 'nofile' + buf.options[ 'swapfile' ] = False + buf.options[ 'modifiable' ] = False + buf.options[ 'modified' ] = False + buf.options[ 'readonly' ] = True + buf.options[ 'buflisted' ] = False + buf.options[ 'bufhidden' ] = 'wipe' + + +@contextlib.contextmanager +def ModifiableScratchBuffer( buf ): + buf.options[ 'modifiable' ] = True + buf.options[ 'readonly' ] = False + try: + yield + finally: + buf.options[ 'modifiable' ] = False + buf.options[ 'readonly' ] = True + + + +class DebugSession( object ): + def __init__( self, channel_send_func ): + SetUpLogging() + + self._connection = DebugAdapterConnection( self, + channel_send_func ) + self._next_message_id = 0 + self._outstanding_requests = dict() + + self._uiTab = None + self._codeWindow = None + self._callStackBuffer = None + self._threadsBuffer = None + self._consoleBuffer = None + + # TODO: How to hold/model this data + self._currentThread = None + self._currentFrame = None + + self._SetUpUI() + + def _SetUpUI( self ): + vim.command( 'tabnew' ) + self._uiTab = vim.current.tabpage + self._codeWindow = vim.current.window + + vim.command( 'nnoremenu WinBar.Next :call vimspector#StepOver()' ) + vim.command( 'nnoremenu WinBar.Step :call vimspector#StepInto()' ) + + vim.command( 'vspl' ) + vim.command( 'enew' ) + + self._threadsBuffer = vim.current.buffer + vim.command( 'spl' ) + vim.command( 'enew' ) + self._callStackBuffer = vim.current.buffer + vim.command( 'spl' ) + vim.command( 'enew' ) + self._consoleBuffer = vim.current.buffer + + SetUpScratchBuffer( self._threadsBuffer ) + SetUpScratchBuffer( self._callStackBuffer ) + SetUpScratchBuffer( self._consoleBuffer ) + + def _LoadFrame( self, frame ): + with ModifiableScratchBuffer( self._consoleBuffer ): + self._consoleBuffer.append( json.dumps( frame, indent=2 ).splitlines() ) + + vim.current.window = self._codeWindow + buffer_number = vim.eval( 'bufnr( "{0}", 1 )'.format( + frame[ 'source' ][ 'path' ] ) ) + + try: + vim.command( 'bu {0}'.format( buffer_number ) ) + except vim.error as e: + if 'E325' not in str( e ): + raise + + self._codeWindow.cursor = ( frame[ 'line' ], frame[ 'column' ] ) + + def OnChannelData( self, data ): + self._connection.OnData( data ) + + def Start( self ): + self._Initialise() + + def Stop( self ): + self._DoRequest( None, { + 'command': 'disconnect', + 'arguments': { + 'terminateDebugee': True + }, + } ) + + def StepOver( self ): + self._DoRequest( None, { + 'command': 'next', + 'arguments': { + 'threadId': self._currentThread + }, + } ) + + def StepInto( self ): + self._DoRequest( None, { + 'command': 'stepIn', + 'arguments': { + 'threadId': self._currentThread + }, + } ) + + + def _DoRequest( self, handler, msg ): + this_id = self._next_message_id + self._next_message_id += 1 + + msg[ 'seq' ] = this_id + msg[ 'type' ] = 'request' + + self._outstanding_requests[ this_id ] = handler + self._connection.SendMessage( msg ) + + def _Initialise( self ): + def handler( message ) : + self._DoRequest( None, { + 'command': 'launch', + + 'arguments': { + "target": "/Users/Ben/.vim/bundle/vimspector/support/test/cpp/" + "simple_c_program/test", + "args": [], + "cwd": "/Users/ben", + "stopOnEntry": True, + 'lldbmipath': + '/Users/ben/.vscode/extensions/ms-vscode.cpptools-0.17.1/' + 'debugAdapters/lldb/bin/lldb-mi', + } + } ) + + + self._DoRequest( handler, { + 'command': 'initialize', + 'arguments': { + 'adapterID': 'cppdbg', + 'linesStartAt1': True, + 'columnsStartAt1': True, + 'pathFormat': 'path', + }, + } ) + + def _OnEvent_initialized( self, message ): + vim.command( 'echom "Debug adapter ready, sending breakpoints..."' ) + self._DoRequest( None, { + 'command': 'setFunctionBreakpoints', + 'arguments': { + 'breakpoints': [ + { 'name': 'main' } + ] + }, + } ) + + self._DoRequest( None, { + 'command': 'configurationDone', + } ) + + + def _OnEvent_output( self, message ): + vim.command( 'echom "{0}"'.format( message[ 'body' ][ 'output' ] ) ) + + + def _OnEvent_stopped( self, message ): + self._currentThread = message[ 'body' ][ 'threadId' ] + + def threads_printer( message ): + with ModifiableScratchBuffer( self._threadsBuffer ): + self._threadsBuffer[:] = None + self._threadsBuffer.append( 'Threads: ' ) + + for thread in message[ 'body' ][ 'threads' ]: + self._threadsBuffer.append( + 'Thread {0}: {1}'.format( thread[ 'id' ], thread[ 'name' ] ) ) + + self._DoRequest( threads_printer, { + 'command': 'threads', + } ) + + def stacktrace_printer( message ): + with ModifiableScratchBuffer( self._callStackBuffer ): + self._callStackBuffer.options[ 'modifiable' ] = True + self._callStackBuffer.options[ 'readonly' ] = False + + self._callStackBuffer[:] = None + self._callStackBuffer.append( 'Backtrace: ' ) + + stackFrames = message[ 'body' ][ 'stackFrames' ] + + if stackFrames: + self._currentFrame = stackFrames[ 0 ] + else: + self._currentFrame = None + + for frame in stackFrames: + self._callStackBuffer.append( + '{0}: {1}@{2}:{3}'.format( frame[ 'id' ], + frame[ 'name' ], + frame[ 'source' ][ 'name' ], + frame[ 'line' ] ) ) + + self._LoadFrame( self._currentFrame ) + + self._DoRequest( stacktrace_printer, { + 'command': 'stackTrace', + 'arguments': { + 'threadId': self._currentThread, + } + } ) + + def OnMessageReceived( self, message ): + if message[ 'type' ] == 'response': + handler = self._outstanding_requests.pop( message[ 'request_seq' ] ) + + if message[ 'success' ]: + if handler: + handler( message ) + else: + raise RuntimeError( 'Request failed: {0}'.format( + message[ 'message' ] ) ) + + elif message[ 'type' ] == 'event': + method = '_OnEvent_' + message[ 'event' ] + if method in dir( self ) and getattr( self, method ): + getattr( self, method )( message ) + + +class DebugAdapterConnection( object ): + def __init__( self, handler, send_func ): + self._Write = send_func + self._SetState( 'READ_HEADER' ) + self._buffer = bytes() + self._handler = handler + + def _SetState( self, state ): + self._state = state + if state == 'READ_HEADER': + self._headers = {} + + def SendMessage( self, msg ): + msg = json.dumps( msg ) + data = 'Content-Length: {0}\r\n\r\n{1}'.format( len( msg ), msg ) + + _logger.debug( 'Sending: {0}'.format( data ) ) + self._Write( data ) + + def OnData( self, data ): + data = bytes( data, 'utf-8' ) + _logger.debug( 'Received ({0}/{1}): {2},'.format( type( data ), + len( data ), + data ) ) + + self._buffer += data + + while True: + if self._state == 'READ_HEADER': + data = self._ReadHeaders() + + if self._state == 'READ_BODY': + self._ReadBody() + else: + break + + if self._state != 'READ_HEADER': + # We ran out of data whilst reading the body. Await more data. + break + + + def _ReadHeaders( self ): + headers = self._buffer.split( bytes( '\r\n\r\n', 'utf-8' ), 1 ) + + if len( headers ) > 1: + for header_line in headers[ 0 ].split( bytes( '\r\n', 'utf-8' ) ): + if header_line.strip(): + key, value = str( header_line, 'utf-8' ).split( ':', 1 ) + self._headers[ key ] = value + + # Chomp (+4 for the 2 newlines which were the separator) + # self._buffer = self._buffer[ len( headers[ 0 ] ) + 4 : ] + self._buffer = headers[ 1 ] + self._SetState( 'READ_BODY' ) + return + + # otherwise waiting for more data + + def _ReadBody( self ): + content_length = int( self._headers[ 'Content-Length' ] ) + + if len( self._buffer ) < content_length: + # Need more data + assert self._state == 'READ_BODY' + return + + payload = str( self._buffer[ : content_length ], 'utf-8' ) + self._buffer = self._buffer[ content_length : ] + + message = json.loads( payload ) + + _logger.debug( 'Message received: {0}'.format( message ) ) + + self._handler.OnMessageReceived( message ) + + self._SetState( 'READ_HEADER' ) diff --git a/support/test/cpp/simple_c_program/test_c.cpp b/support/test/cpp/simple_c_program/test_c.cpp new file mode 100644 index 0000000..708e9b2 --- /dev/null +++ b/support/test/cpp/simple_c_program/test_c.cpp @@ -0,0 +1,38 @@ +#include + +namespace Test +{ + struct TestStruct + { + bool isInt; + + union { + int somethingInt; + char somethingChar; + } something; + }; + + TestStruct _t; + + void bar( TestStruct b ) + { + std::string s; + s += b.isInt ? b.something.somethingInt : b.something.somethingChar; + std::cout << s << '\n'; + } + + void foo( TestStruct m ) + { + TestStruct t{ true, 11 }; + bar( t ); + } +} + + +int main ( int argc, char ** argv ) +{ + int x{ 10 }; + + Test::TestStruct t{ true, 99 }; + foo( t ); +}