From 583fb95ea0738a9358cb3463612f3a5d9d00aa27 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Fri, 17 Jan 2020 17:44:17 +0000 Subject: [PATCH 1/5] Read configurations from a per-filetype directory or a default location as well as local config file --- .gitignore | 1 + python3/vimspector/debug_session.py | 57 ++++++++++++++++++++--------- python3/vimspector/install.py | 10 +++++ python3/vimspector/utils.py | 5 +++ 4 files changed, 55 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index df249b4..b09d196 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ README.md.toc.* *.vimspector.log support/test/csharp/*.exe* .neomake.log +configurations/ diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 08b29cd..a0d8418 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -79,31 +79,37 @@ class DebugSession( object ): self._adapter = None current_file = utils.GetBufferFilepath( vim.current.buffer ) - - launch_config_file = utils.PathToConfigFile( - '.vimspector.json', - os.path.dirname( current_file ) ) - - if not launch_config_file: - utils.UserMessage( 'Unable to find .vimspector.json. You need to tell ' - 'vimspector how to launch your application.' ) - return - - with open( launch_config_file, 'r' ) as f: - database = json.load( f ) - - configurations = database.get( 'configurations' ) + filetypes = utils.GetBufferFiletypes( vim.current.buffer ) + configurations = {} adapters = {} glob.glob( install.GetGadgetDir( VIMSPECTOR_HOME, install.GetOS() ) ) for gadget_config_file in PathsToAllGadgetConfigs( VIMSPECTOR_HOME, current_file ): self._logger.debug( f'Reading gadget config: {gadget_config_file}' ) - if gadget_config_file and os.path.exists( gadget_config_file ): - with open( gadget_config_file, 'r' ) as f: - adapters.update( json.load( f ).get( 'adapters' ) or {} ) + if not gadget_config_file or not os.path.exists( gadget_config_file ): + continue - adapters.update( database.get( 'adapters' ) or {} ) + with open( gadget_config_file, 'r' ) as f: + adapters.update( json.load( f ).get( 'adapters' ) or {} ) + + 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.load( f ) + 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' ) @@ -936,3 +942,18 @@ def PathsToAllGadgetConfigs( vimspector_base, current_file ): yield utils.PathToConfigFile( '.gadgets.json', os.path.dirname( current_file ) ) + + +def PathsToAllConfigFiles( vimspector_base, current_file, filetypes ): + for ft in filetypes: + for p in sorted( glob.glob( + os.path.join( install.GetConfigDirForFiletype( vimspector_base, ft ), + '*.json' ) ) ): + yield p + + for ft in filetypes: + yield utils.PathToConfigFile( f'.vimspector.{ft}.json', + os.path.dirname( current_file ) ) + + yield utils.PathToConfigFile( '.vimspector.json', + os.path.dirname( current_file ) ) diff --git a/python3/vimspector/install.py b/python3/vimspector/install.py index 2adafcd..b91c90b 100644 --- a/python3/vimspector/install.py +++ b/python3/vimspector/install.py @@ -38,3 +38,13 @@ def GetGadgetConfigFile( vimspector_base ): def GetGadgetConfigDir( vimspector_base ): return os.path.join( GetGadgetDir( vimspector_base, GetOS() ), '.gadgets.d' ) + + +def GetConfigDirForFiletype( vimspector_base, filetype ): + if not filetype: + filetype = 'default' + + return os.path.join( os.path.abspath( vimspector_base ), + 'configurations', + GetOS(), + filetype ) diff --git a/python3/vimspector/utils.py b/python3/vimspector/utils.py index 4ae7f94..3ac41bf 100644 --- a/python3/vimspector/utils.py +++ b/python3/vimspector/utils.py @@ -515,3 +515,8 @@ def SetSyntax( current_syntax, syntax, *args ): vim.command( 'set syntax={}'.format( Escape( syntax ) ) ) return syntax + + +def GetBufferFiletypes( buf ): + ft = ToUnicode( vim.eval( f"getbufvar( {buf.number}, '&ft' )" ) ) + return ft.split( '.' ) From be32a0a1a82ce6420c7bc7eedb5672dac0023479 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 22 Jan 2020 22:36:55 +0000 Subject: [PATCH 2/5] Add --enable-custom to load custom gadget meta --- install_gadget.py | 155 ++++++++++++++++++---------- python3/vimspector/debug_session.py | 2 +- 2 files changed, 102 insertions(+), 55 deletions(-) diff --git a/install_gadget.py b/install_gadget.py index 433261e..08738c1 100755 --- a/install_gadget.py +++ b/install_gadget.py @@ -18,7 +18,8 @@ import sys if sys.version_info.major < 3: - exit( "Sorry, you need to be running this script with python3 or later" ) + sys.exit( "You need to run this with python 3. Your version is " + + '.'.join( map( str, sys.version_info[ :3 ] ) ) ) from urllib import request import argparse @@ -37,6 +38,8 @@ import functools import time import ssl import io +import operator +import glob # Include vimspector source, for utils sys.path.insert( 1, os.path.join( os.path.dirname( __file__ ), @@ -200,7 +203,8 @@ GADGETS = { }, 'macos': { 'file_name': 'netcoredbg-osx-master.tar.gz', - 'checksum': '', + 'checksum': + 'c1dc6ed58c3f5b0473cfb4985a96552999360ceb9795e42d9c9be64af054f821', }, 'linux': { 'file_name': 'netcoredbg-linux-master.tar.gz', @@ -345,7 +349,7 @@ GADGETS = { 'enabled': False, 'repo': { 'url': 'https://github.com/microsoft/vscode-node-debug2', - 'ref': 'v1.39.1', + 'ref': 'v1.42.0', }, 'do': lambda name, root, gadget: InstallNodeDebug( name, root, gadget ), 'adapters': { @@ -525,7 +529,7 @@ def DownloadFileTo( url, destination, file_name = None, checksum = None, - sslcheck = True ): + check_certificate = True ): if not file_name: file_name = url.split( '/' )[ -1 ] @@ -550,7 +554,7 @@ def DownloadFileTo( url, print( "Downloading {} to {}/{}".format( url, destination, file_name ) ) - if not sslcheck: + if not check_certificate: context = ssl.create_default_context() context.check_hostname = False context.verify_mode = ssl.CERT_NONE @@ -663,13 +667,75 @@ def CloneRepoTo( url, ref, destination ): '--recursive' ] ) +def InstallGagdet( name, gadget, failed, all_adapters ): + try: + v = {} + v.update( gadget.get( 'all', {} ) ) + v.update( gadget.get( OS, {} ) ) + + if 'download' in gadget: + if 'file_name' not in v: + raise RuntimeError( "Unsupported OS {} for gadget {}".format( OS, + name ) ) + + destination = os.path.join( gadget_dir, 'download', name, v[ 'version' ] ) + + url = string.Template( gadget[ 'download' ][ 'url' ] ).substitute( v ) + + file_path = DownloadFileTo( + url, + destination, + file_name = gadget[ 'download' ].get( 'target' ), + checksum = v.get( 'checksum' ), + check_certificate = not args.no_check_certificate ) + + root = os.path.join( destination, 'root' ) + ExtractZipTo( file_path, + root, + format = gadget[ 'download' ].get( 'format', 'zip' ) ) + elif 'repo' in gadget: + url = string.Template( gadget[ 'repo' ][ 'url' ] ).substitute( v ) + ref = string.Template( gadget[ 'repo' ][ 'ref' ] ).substitute( v ) + + destination = os.path.join( gadget_dir, 'download', name ) + CloneRepoTo( url, ref, destination ) + root = destination + + if 'do' in gadget: + gadget[ 'do' ]( name, root, v ) + else: + MakeExtensionSymlink( name, root ) + + all_adapters.update( gadget.get( 'adapters', {} ) ) + + print( "Done installing {}".format( name ) ) + except Exception as e: + traceback.print_exc() + failed.append( name ) + print( "FAILED installing {}: {}".format( name, e ) ) + + +vimspector_base = os.path.dirname( __file__ ) OS = install.GetOS() -gadget_dir = install.GetGadgetDir( os.path.dirname( __file__ ), OS ) +gadget_dir = install.GetGadgetDir( vimspector_base, OS ) print( 'OS = ' + OS ) print( 'gadget_dir = ' + gadget_dir ) -parser = argparse.ArgumentParser() +parser = argparse.ArgumentParser( + description = 'Install DAP Servers for use with Vimspector.', + epilog = + """ + If you're not sure, normally --all is enough to get started. + + Custom server definitions can be defined in JSON files, allowing + installation of arbitrary servers packaged in one of the ways that this + installer understands. + + The format of the file can be found on the Vimspector reference guide: + https://puremourning.github.io/vimspector + """ +) parser.add_argument( '--all', action = 'store_true', help = 'Enable all supported completers' ) @@ -682,6 +748,15 @@ parser.add_argument( '--no-gadget-config', action = 'store_true', help = "Don't write the .gagets.json, just install" ) +parser.add_argument( '--enable-custom', + dest='custom_gadget_file', + action='append', + nargs='*', + default = [], + help = 'Read custom gadget from supplied file. This ' + 'can be supplied multiple times and each time ' + 'multiple files can be passed.' ) + done_languages = set() for name, gadget in GADGETS.items(): lang = gadget[ 'language' ] @@ -722,6 +797,18 @@ args = parser.parse_args() if args.force_all and not args.all: args.all = True +CUSTOM_GADGETS = {} +custom_files = glob.glob( os.path.join( vimspector_base, + 'gadgets', + 'custom', + '*.json' ) ) +for custom_file_name in functools.reduce( operator.add, + args.custom_gadget_file, + custom_files ): + with open( custom_file_name, 'r' ) as custom_file: + CUSTOM_GADGETS.update( json.load( custom_file ) ) + + failed = [] all_adapters = {} for name, gadget in GADGETS.items(): @@ -735,53 +822,14 @@ for name, gadget in GADGETS.items(): if getattr( args, 'disable_' + gadget[ 'language' ] ): continue - try: - v = {} - v.update( gadget.get( 'all', {} ) ) - v.update( gadget.get( OS, {} ) ) - - if 'download' in gadget: - if 'file_name' not in v: - raise RuntimeError( "Unsupported OS {} for gadget {}".format( OS, - name ) ) - - destination = os.path.join( gadget_dir, 'download', name, v[ 'version' ] ) - - url = string.Template( gadget[ 'download' ][ 'url' ] ).substitute( v ) - verify_cert_off = args.no_check_certificate - - file_path = DownloadFileTo( - url, - destination, - file_name = gadget[ 'download' ].get( 'target' ), - checksum = v.get( 'checksum' ), - sslcheck = not verify_cert_off ) - root = os.path.join( destination, 'root' ) - ExtractZipTo( file_path, - root, - format = gadget[ 'download' ].get( 'format', 'zip' ) ) - elif 'repo' in gadget: - url = string.Template( gadget[ 'repo' ][ 'url' ] ).substitute( v ) - ref = string.Template( gadget[ 'repo' ][ 'ref' ] ).substitute( v ) - - destination = os.path.join( gadget_dir, 'download', name ) - CloneRepoTo( url, ref, destination ) - root = destination - - if 'do' in gadget: - gadget[ 'do' ]( name, root, v ) - else: - MakeExtensionSymlink( name, root ) - - all_adapters.update( gadget.get( 'adapters', {} ) ) + InstallGagdet( name, + gadget, + failed, + all_adapters ) - print( "Done installing {}".format( name ) ) - except Exception as e: - traceback.print_exc() - failed.append( name ) - print( "FAILED installing {}: {}".format( name, e ) ) - +for name, gadget in CUSTOM_GADGETS.items(): + InstallGagdet( name, gadget, failed, all_adapters ) adapter_config = json.dumps ( { 'adapters': all_adapters }, indent=2, @@ -792,8 +840,7 @@ if args.no_gadget_config: print( "Would write the following gadgets: " ) print( adapter_config ) else: - with open( install.GetGadgetConfigFile( os.path.dirname( __file__ ) ), - 'w' ) as f: + with open( install.GetGadgetConfigFile( vimspector_base ), 'w' ) as f: f.write( adapter_config ) if failed: diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index a0d8418..87deaf6 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -98,7 +98,7 @@ class DebugSession( object ): 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 + continue with open( launch_config_file, 'r' ) as f: database = json.load( f ) From e37ef18c2812cbe2341a9c32dce613d485e0858e Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 9 Feb 2020 16:08:55 +0000 Subject: [PATCH 3/5] Move installation routines into proper library --- install_gadget.py | 234 ++++---------------------------- python3/vimspector/installer.py | 218 +++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+), 210 deletions(-) create mode 100644 python3/vimspector/installer.py diff --git a/install_gadget.py b/install_gadget.py index 08738c1..561af21 100755 --- a/install_gadget.py +++ b/install_gadget.py @@ -21,23 +21,13 @@ if sys.version_info.major < 3: sys.exit( "You need to run this with python 3. Your version is " + '.'.join( map( str, sys.version_info[ :3 ] ) ) ) -from urllib import request import argparse -import contextlib import os import string -import zipfile -import gzip -import shutil import subprocess import traceback -import tarfile -import hashlib import json import functools -import time -import ssl -import io import operator import glob @@ -45,7 +35,7 @@ import glob sys.path.insert( 1, os.path.join( os.path.dirname( __file__ ), 'python3' ) ) -from vimspector import install +from vimspector import install, installer GADGETS = { 'vscode-cpptools': { @@ -210,7 +200,7 @@ GADGETS = { 'file_name': 'netcoredbg-linux-master.tar.gz', 'checksum': '', }, - 'do': lambda name, root, gadget: MakeSymlink( + 'do': lambda name, root, gadget: installer.MakeSymlink( gadget_dir, name, os.path.join( root, 'netcoredbg' ) ), @@ -393,29 +383,15 @@ GADGETS = { } -@contextlib.contextmanager -def CurrentWorkingDir( d ): - cur_d = os.getcwd() - try: - os.chdir( d ) - yield - finally: - os.chdir( cur_d ) - - -def MakeExecutable( file_path ): - # TODO: import stat and use them by _just_ adding the X bit. - print( 'Making executable: {}'.format( file_path ) ) - os.chmod( file_path, 0o755 ) - - def InstallCppTools( name, root, gadget ): extension = os.path.join( root, 'extension' ) # It's hilarious, but the execute bits aren't set in the vsix. So they # actually have javascript code which does this. It's just a horrible horrible # hack that really is not funny. - MakeExecutable( os.path.join( extension, 'debugAdapters', 'OpenDebugAD7' ) ) + installer.MakeExecutable( os.path.join( extension, + 'debugAdapters', + 'OpenDebugAD7' ) ) with open( os.path.join( extension, 'package.json' ) ) as f: package = json.load( f ) runtime_dependencies = package[ 'runtimeDependencies' ] @@ -423,14 +399,17 @@ def InstallCppTools( name, root, gadget ): for binary in dependency.get( 'binaries' ): file_path = os.path.abspath( os.path.join( extension, binary ) ) if os.path.exists( file_path ): - MakeExecutable( os.path.join( extension, binary ) ) + installer.MakeExecutable( os.path.join( extension, binary ) ) - MakeExtensionSymlink( name, root ) + installer.MakeExtensionSymlink( vimspector_base, name, root ) def InstallBashDebug( name, root, gadget ): - MakeExecutable( os.path.join( root, 'extension', 'bashdb_dir', 'bashdb' ) ) - MakeExtensionSymlink( name, root ) + installer.MakeExecutable( os.path.join( root, + 'extension', + 'bashdb_dir', + 'bashdb' ) ) + installer.MakeExtensionSymlink( vimspector_base, name, root ) def InstallDebugpy( name, root, gadget ): @@ -442,7 +421,7 @@ def InstallDebugpy( name, root, gadget ): finally: os.chdir( wd ) - MakeSymlink( gadget_dir, name, root ) + installer.MakeSymlink( gadget_dir, name, root ) def InstallTclProDebug( name, root, gadget ): @@ -473,11 +452,11 @@ def InstallTclProDebug( name, root, gadget ): break - with CurrentWorkingDir( os.path.join( root, 'lib', 'tclparser' ) ): + with installer.CurrentWorkingDir( os.path.join( root, 'lib', 'tclparser' ) ): subprocess.check_call( configure ) subprocess.check_call( [ 'make' ] ) - MakeSymlink( gadget_dir, name, root ) + installer.MakeSymlink( gadget_dir, name, root ) def InstallNodeDebug( name, root, gadget ): @@ -495,176 +474,10 @@ def InstallNodeDebug( name, root, gadget ): print( " $ ./install_gadget.py --enable-node ..." ) raise RuntimeError( 'Invalid node environent for node debugger' ) - with CurrentWorkingDir( root ): + with installer.CurrentWorkingDir( root ): subprocess.check_call( [ 'npm', 'install' ] ) subprocess.check_call( [ 'npm', 'run', 'build' ] ) - MakeSymlink( gadget_dir, name, root ) - - -def WithRetry( f ): - retries = 5 - timeout = 1 # seconds - - @functools.wraps( f ) - def wrapper( *args, **kwargs ): - thrown = None - for _ in range( retries ): - try: - return f( *args, **kwargs ) - except Exception as e: - thrown = e - print( "Failed - {}, will retry in {} seconds".format( e, timeout ) ) - time.sleep( timeout ) - raise thrown - - return wrapper - - -@WithRetry -def UrlOpen( *args, **kwargs ): - return request.urlopen( *args, **kwargs ) - - -def DownloadFileTo( url, - destination, - file_name = None, - checksum = None, - check_certificate = True ): - if not file_name: - file_name = url.split( '/' )[ -1 ] - - file_path = os.path.abspath( os.path.join( destination, file_name ) ) - - if not os.path.isdir( destination ): - os.makedirs( destination ) - - if os.path.exists( file_path ): - if checksum: - if ValidateCheckSumSHA256( file_path, checksum ): - print( "Checksum matches for {}, using it".format( file_path ) ) - return file_path - else: - print( "Checksum doesn't match for {}, removing it".format( - file_path ) ) - - print( "Removing existing {}".format( file_path ) ) - os.remove( file_path ) - - r = request.Request( url, headers = { 'User-Agent': 'Vimspector' } ) - - print( "Downloading {} to {}/{}".format( url, destination, file_name ) ) - - if not check_certificate: - context = ssl.create_default_context() - context.check_hostname = False - context.verify_mode = ssl.CERT_NONE - kwargs = { "context": context } - else: - kwargs = {} - - with contextlib.closing( UrlOpen( r, **kwargs ) ) as u: - with open( file_path, 'wb' ) as f: - f.write( u.read() ) - - if checksum: - if not ValidateCheckSumSHA256( file_path, checksum ): - raise RuntimeError( - 'Checksum for {} ({}) does not match expected {}'.format( - file_path, - GetChecksumSHA254( file_path ), - checksum ) ) - else: - print( "Checksum for {}: {}".format( file_path, - GetChecksumSHA254( file_path ) ) ) - - return file_path - - -def GetChecksumSHA254( file_path ): - with open( file_path, 'rb' ) as existing_file: - return hashlib.sha256( existing_file.read() ).hexdigest() - - -def ValidateCheckSumSHA256( file_path, checksum ): - existing_sha256 = GetChecksumSHA254( file_path ) - return existing_sha256 == checksum - - -def RemoveIfExists( destination ): - if os.path.exists( destination ) or os.path.islink( destination ): - if os.path.islink( destination ): - print( "Removing file {}".format( destination ) ) - os.remove( destination ) - else: - print( "Removing dir {}".format( destination ) ) - shutil.rmtree( destination ) - - -# Python's ZipFile module strips execute bits from files, for no good reason -# other than crappy code. Let's do it's job for it. -class ModePreservingZipFile( zipfile.ZipFile ): - def extract( self, member, path = None, pwd = None ): - if not isinstance( member, zipfile.ZipInfo ): - member = self.getinfo( member ) - - if path is None: - path = os.getcwd() - - ret_val = self._extract_member( member, path, pwd ) - attr = member.external_attr >> 16 - os.chmod( ret_val, attr ) - return ret_val - - -def ExtractZipTo( file_path, destination, format ): - print( "Extracting {} to {}".format( file_path, destination ) ) - RemoveIfExists( destination ) - - if format == 'zip': - with ModePreservingZipFile( file_path ) as f: - f.extractall( path = destination ) - elif format == 'zip.gz': - with gzip.open( file_path, 'rb' ) as f: - file_contents = f.read() - - with ModePreservingZipFile( io.BytesIO( file_contents ) ) as f: - f.extractall( path = destination ) - - elif format == 'tar': - try: - with tarfile.open( file_path ) as f: - f.extractall( path = destination ) - except Exception: - # There seems to a bug in python's tarfile that means it can't read some - # windows-generated tar files - os.makedirs( destination ) - with CurrentWorkingDir( destination ): - subprocess.check_call( [ 'tar', 'zxvf', file_path ] ) - - -def MakeExtensionSymlink( name, root ): - MakeSymlink( gadget_dir, name, os.path.join( root, 'extension' ) ), - - -def MakeSymlink( in_folder, link, pointing_to ): - RemoveIfExists( os.path.join( in_folder, link ) ) - - in_folder = os.path.abspath( in_folder ) - pointing_to = os.path.relpath( os.path.abspath( pointing_to ), - in_folder ) - os.symlink( pointing_to, os.path.join( in_folder, link ) ) - - -def CloneRepoTo( url, ref, destination ): - RemoveIfExists( destination ) - git_in_repo = [ 'git', '-C', destination ] - subprocess.check_call( [ 'git', 'clone', url, destination ] ) - subprocess.check_call( git_in_repo + [ 'checkout', ref ] ) - subprocess.check_call( git_in_repo + [ 'submodule', 'sync', '--recursive' ] ) - subprocess.check_call( git_in_repo + [ 'submodule', - 'update', - '--init', - '--recursive' ] ) + installer.MakeSymlink( gadget_dir, name, root ) def InstallGagdet( name, gadget, failed, all_adapters ): @@ -682,7 +495,7 @@ def InstallGagdet( name, gadget, failed, all_adapters ): url = string.Template( gadget[ 'download' ][ 'url' ] ).substitute( v ) - file_path = DownloadFileTo( + file_path = installer.DownloadFileTo( url, destination, file_name = gadget[ 'download' ].get( 'target' ), @@ -690,21 +503,22 @@ def InstallGagdet( name, gadget, failed, all_adapters ): check_certificate = not args.no_check_certificate ) root = os.path.join( destination, 'root' ) - ExtractZipTo( file_path, - root, - format = gadget[ 'download' ].get( 'format', 'zip' ) ) + installer.ExtractZipTo( + file_path, + root, + format = gadget[ 'download' ].get( 'format', 'zip' ) ) elif 'repo' in gadget: url = string.Template( gadget[ 'repo' ][ 'url' ] ).substitute( v ) ref = string.Template( gadget[ 'repo' ][ 'ref' ] ).substitute( v ) destination = os.path.join( gadget_dir, 'download', name ) - CloneRepoTo( url, ref, destination ) + installer.CloneRepoTo( url, ref, destination ) root = destination if 'do' in gadget: gadget[ 'do' ]( name, root, v ) else: - MakeExtensionSymlink( name, root ) + installer.MakeExtensionSymlink( vimspector_base, name, root ) all_adapters.update( gadget.get( 'adapters', {} ) ) diff --git a/python3/vimspector/installer.py b/python3/vimspector/installer.py new file mode 100644 index 0000000..53a0150 --- /dev/null +++ b/python3/vimspector/installer.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 + +# vimspector - A multi-language debugging system for Vim +# Copyright 2019 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. + +from urllib import request +import io +import contextlib +import zipfile +import gzip +import shutil +import tarfile +import hashlib +import time +import ssl +import subprocess +import functools +import os + +from vimspector import install + + +@contextlib.contextmanager +def CurrentWorkingDir( d ): + cur_d = os.getcwd() + try: + os.chdir( d ) + yield + finally: + os.chdir( cur_d ) + + +def MakeExecutable( file_path ): + # TODO: import stat and use them by _just_ adding the X bit. + print( 'Making executable: {}'.format( file_path ) ) + os.chmod( file_path, 0o755 ) + + + +def WithRetry( f ): + retries = 5 + timeout = 1 # seconds + + @functools.wraps( f ) + def wrapper( *args, **kwargs ): + thrown = None + for _ in range( retries ): + try: + return f( *args, **kwargs ) + except Exception as e: + thrown = e + print( "Failed - {}, will retry in {} seconds".format( e, timeout ) ) + time.sleep( timeout ) + raise thrown + + return wrapper + + +@WithRetry +def UrlOpen( *args, **kwargs ): + return request.urlopen( *args, **kwargs ) + + +def DownloadFileTo( url, + destination, + file_name = None, + checksum = None, + check_certificate = True ): + if not file_name: + file_name = url.split( '/' )[ -1 ] + + file_path = os.path.abspath( os.path.join( destination, file_name ) ) + + if not os.path.isdir( destination ): + os.makedirs( destination ) + + if os.path.exists( file_path ): + if checksum: + if ValidateCheckSumSHA256( file_path, checksum ): + print( "Checksum matches for {}, using it".format( file_path ) ) + return file_path + else: + print( "Checksum doesn't match for {}, removing it".format( + file_path ) ) + + print( "Removing existing {}".format( file_path ) ) + os.remove( file_path ) + + r = request.Request( url, headers = { 'User-Agent': 'Vimspector' } ) + + print( "Downloading {} to {}/{}".format( url, destination, file_name ) ) + + if not check_certificate: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + kwargs = { "context": context } + else: + kwargs = {} + + with contextlib.closing( UrlOpen( r, **kwargs ) ) as u: + with open( file_path, 'wb' ) as f: + f.write( u.read() ) + + if checksum: + if not ValidateCheckSumSHA256( file_path, checksum ): + raise RuntimeError( + 'Checksum for {} ({}) does not match expected {}'.format( + file_path, + GetChecksumSHA254( file_path ), + checksum ) ) + else: + print( "Checksum for {}: {}".format( file_path, + GetChecksumSHA254( file_path ) ) ) + + return file_path + + +def GetChecksumSHA254( file_path ): + with open( file_path, 'rb' ) as existing_file: + return hashlib.sha256( existing_file.read() ).hexdigest() + + +def ValidateCheckSumSHA256( file_path, checksum ): + existing_sha256 = GetChecksumSHA254( file_path ) + return existing_sha256 == checksum + + +def RemoveIfExists( destination ): + if os.path.exists( destination ) or os.path.islink( destination ): + if os.path.islink( destination ): + print( "Removing file {}".format( destination ) ) + os.remove( destination ) + else: + print( "Removing dir {}".format( destination ) ) + shutil.rmtree( destination ) + + +# Python's ZipFile module strips execute bits from files, for no good reason +# other than crappy code. Let's do it's job for it. +class ModePreservingZipFile( zipfile.ZipFile ): + def extract( self, member, path = None, pwd = None ): + if not isinstance( member, zipfile.ZipInfo ): + member = self.getinfo( member ) + + if path is None: + path = os.getcwd() + + ret_val = self._extract_member( member, path, pwd ) + attr = member.external_attr >> 16 + os.chmod( ret_val, attr ) + return ret_val + + +def ExtractZipTo( file_path, destination, format ): + print( "Extracting {} to {}".format( file_path, destination ) ) + RemoveIfExists( destination ) + + if format == 'zip': + with ModePreservingZipFile( file_path ) as f: + f.extractall( path = destination ) + elif format == 'zip.gz': + with gzip.open( file_path, 'rb' ) as f: + file_contents = f.read() + + with ModePreservingZipFile( io.BytesIO( file_contents ) ) as f: + f.extractall( path = destination ) + + elif format == 'tar': + try: + with tarfile.open( file_path ) as f: + f.extractall( path = destination ) + except Exception: + # There seems to a bug in python's tarfile that means it can't read some + # windows-generated tar files + os.makedirs( destination ) + with CurrentWorkingDir( destination ): + subprocess.check_call( [ 'tar', 'zxvf', file_path ] ) + + +def MakeExtensionSymlink( vimspector_base, name, root ): + MakeSymlink( install.GetGadgetDir( vimspector_base, + install.GetOS() ), + name, + os.path.join( root, 'extension' ) ), + + +def MakeSymlink( in_folder, link, pointing_to ): + RemoveIfExists( os.path.join( in_folder, link ) ) + + in_folder = os.path.abspath( in_folder ) + pointing_to = os.path.relpath( os.path.abspath( pointing_to ), + in_folder ) + os.symlink( pointing_to, os.path.join( in_folder, link ) ) + + +def CloneRepoTo( url, ref, destination ): + RemoveIfExists( destination ) + git_in_repo = [ 'git', '-C', destination ] + subprocess.check_call( [ 'git', 'clone', url, destination ] ) + subprocess.check_call( git_in_repo + [ 'checkout', ref ] ) + subprocess.check_call( git_in_repo + [ 'submodule', 'sync', '--recursive' ] ) + subprocess.check_call( git_in_repo + [ 'submodule', + 'update', + '--init', + '--recursive' ] ) From 267f202dad7e974b3e9c36509760efb7551f6ff1 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 16 Feb 2020 17:50:28 +0000 Subject: [PATCH 4/5] Fix LGTM error: python default arguments are mutable\! --- python3/vimspector/debug_session.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index 87deaf6..f684d6a 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -72,7 +72,12 @@ class DebugSession( object ): self._on_init_complete_handlers = [] self._server_capabilities = {} - def Start( self, launch_variables = {} ): + 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 From 68a45b5771d8301d12a73e208ae113a7c16a74a0 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Sun, 16 Feb 2020 19:59:31 +0000 Subject: [PATCH 5/5] Fix workspace root when no vimspector conf - use dir of current file --- .vimspector.json | 139 ---------------------------- python3/vimspector/debug_session.py | 5 +- support/test/python/no_conf/main.py | 29 ++++++ 3 files changed, 33 insertions(+), 140 deletions(-) delete mode 100644 .vimspector.json create mode 100755 support/test/python/no_conf/main.py diff --git a/.vimspector.json b/.vimspector.json deleted file mode 100644 index 5f7a93d..0000000 --- a/.vimspector.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "adapters": { - "lldb-mi": { - "name": "lldb-mi", - "command": [ - "node", - "$HOME/.vscode/extensions/webfreak.debug-0.22.0/out/src/lldb.js" - ] - }, - "cppdbg": { - "name": "cppdbg", - "command": [ "$HOME/.vscode/extensions/ms-vscode.cpptools-0.20.1/debugAdapters/OpenDebugAD7" ], - "attach": { - "pidProperty": "processId", - "pidSelect": "ask" - } - }, - "python": { - "name": "python", - "command": [ - "node", - "$HOME/.vscode/extensions/ms-python.python-2018.4.0/out/client/debugger/Main.js" - ] - }, - "bashdb": { - "name": "bashdb", - "command": [ - "node", - "$HOME/.vscode/extensions/rogalmic.bash-debug-0.2.0/out/bashDebug.js" - ] - }, - "lldb": { - "name": "lldb", - "command": [ - "lldb", - "-b", - "-O", - "command script import '$HOME/.vscode/extensions/vadimcn.vscode-lldb-0.8.7/adapter'", - "-O", - "script adapter.main.run_stdio_session()" - ] - } - }, - "configurations": { - "simple_c_program - lldb-mi Launch": { - "adapter": "lldb-mi", - "configuration": { - "request": "launch", - "target": "support/test/cpp/simple_c_program/test", - "args": [], - "cwd": ".", - "lldbmipath": "$HOME/.vscode/extensions/ms-vscode.cpptools-0.20.1/debugAdapters/lldb/bin/lldb-mi", - "trace": true, - "logFilePath": "$HOME/.vimspector.protocol.log" - } - }, - "simple_c_progra - ms Launch": { - "adapter": "cppdbg", - "configuration": { - "name": "ms Launch", - "type": "cppdbg", - "request": "launch", - "program": "${workspaceRoot}/support/test/cpp/simple_c_program/test", - "args": [], - "cwd": "$HOME", - "environment": [], - "externalConsole": true, - "MIMode": "lldb" - } - }, - "simple_python - launch": { - "adapter": "python", - "configuration": { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "cwd": "${workspaceRoot}/support/test/python/simple_python", - "stopOnEntry": true, - "console": "externalTerminal", - "debugOptions": [], - "program": "${workspaceRoot}/support/test/python/simple_python/main.py" - } - }, - "simple_c_program - MS Attach": { - "adapter": "cppdbg", - "configuration": { - "name": "(lldb) Attach", - "type": "cppdbg", - "request": "attach", - "program": "${workspaceRoot}/support/test/cpp/simple_c_program/test", - "MIMode": "lldb" - } - }, - "bashdb": { - "adapter": "bashdb", - "configuration": { - "type": "bashdb", - "request": "launch", - "name": "Bash-Debug (simplest configuration)", - "program": "$HOME/.vim/bundle/YouCompleteMe/install.sh", - "args": [], - "cwd": "$HOME/.vim/bundle/YouCompleteMe", - "pathBash": "bash", - "pathBashdb": "bashdb", - "pathCat": "cat", - "pathMkfifo": "mkfifo", - "pathPkill": "pkill", - "showDebugOutput": true, - "trace": true - } - }, - "lldb launch": { - "adapter": "lldb", - "configuration": { - "type": "lldb", - "request": "launch", - "name": "LLDB: Launch", - "program": "$HOME/Development/vim/src/vim", - "args": [], - "cwd": "$HOME/Development/vim" - } - }, - "racerd": { - "adapter": "lldb", - "configuration": { - "type": "lldb", - "request": "launch", - "name": "LLDB: Launch", - "program": "$HOME/.vim/bundle/YouCompleteMe/third_party/ycmd/third_party/racerd/target/debug/racerd", - "args": [ - "serve", - "--port=12345", - "--secret-file=secretfile" - ], - "cwd": "$HOME/.vim/bundle/YouCompleteMe/third_party/ycmd" - } - } - } -} diff --git a/python3/vimspector/debug_session.py b/python3/vimspector/debug_session.py index f684d6a..83c64f7 100644 --- a/python3/vimspector/debug_session.py +++ b/python3/vimspector/debug_session.py @@ -128,7 +128,10 @@ class DebugSession( object ): if not configuration_name or configuration_name not in configurations: return - self._workspace_root = os.path.dirname( launch_config_file ) + 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' ) diff --git a/support/test/python/no_conf/main.py b/support/test/python/no_conf/main.py new file mode 100755 index 0000000..6515c3f --- /dev/null +++ b/support/test/python/no_conf/main.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + + +class TestClass( object ): + def __init__( self, value ): + self._var = value + try: + self.DoSomething() + except ValueError: + pass + + def DoSomething( self ): + for i in range( 0, 100 ): + if i < self._var: + print( '{0} is less than the value'.format( i ) ) + else: + print( '{0} might be more'.format( i ) ) + + raise ValueError( 'Done' ) + + +def Main(): + t = TestClass( 18 ) + + t._var = 99 + t.DoSomething() + + +Main()