diff --git a/doc/Vdebug.txt b/doc/Vdebug.txt index 3c17fcc0..495d8830 100644 --- a/doc/Vdebug.txt +++ b/doc/Vdebug.txt @@ -194,7 +194,7 @@ able to apply it to your circumstance. The most popular DBGP debugger for PHP is Xdebug. I find that the most effective way to install Xdebug is to use PECL, instead -of a OS-level package manager such as aptitude or yum. You can also compile it +of an OS-level package manager such as aptitude or yum. You can also compile it from source if you're feeling hardy. To install it via PECL, run this from a command line (requires root/admin privileges): > @@ -206,7 +206,7 @@ it as a zend extension. This can be done in the PHP INI file, but in Ubuntu I can add a new file to /etc/php5/conf.d/ that contains all the configuration options and gets loaded automatically by PHP. -Add these options to the INI file: > +For Xdebug v2, add these options to the INI file: > zend_extension=/path/to/xdebug.so xdebug.remote_enable=on @@ -214,6 +214,16 @@ Add these options to the INI file: > xdebug.remote_host=localhost xdebug.remote_port=9000 < + +For Xdebug v3, add these options to the INI file: > + + zend_extension=/path/to/xdebug.so + xdebug.mode=debug + xdebug.client_host=localhost + xdebug.client_port=9000 +< +Please refer to https://xdebug.org/docs/upgrade_guide for instructions on how to upgrade from v2 to v3. + If using Apache, restart it to enable the new library. The command line interface should be ready to go - type "php -v" and you should see the line > with Xdebug v2.2.0, Copyright (c) 2002-2012, by Derick Rethans @@ -232,13 +242,13 @@ then use this instead of php when debugging. For instance, instead of "php myscript.php", run "php-xdebug myscript.php" to start the debugger session. You are now officially ready to start debugging with PHP. However, if you've -become unstuck at any point then there are plenty of Google tutorials on -setting up Xdebug, or you can email me and I'll do my best to help. +become stuck at any point then there are plenty of Google tutorials on setting +up Xdebug, or you can email me and I'll do my best to help. ------------------------------------------------------------------------------ 3.2 Python set up *VdebugSetUpPython* -Python has an standalone debugging tool that you can use from the command line, +Python has a standalone debugging tool that you can use from the command line, but to use Vdebug in conjunction with your Python scripts you will have to grab the "pydbgp" tool, created by ActiveState (who make the Komodo Edit/IDE software). @@ -283,7 +293,7 @@ Try installing and using it like this: > ------------------------------------------------------------------------------ 3.3 Ruby set up *VdebugSetUpRuby* -Like Python, Ruby has an standalone debugging tool that you can use from the +Like Python, Ruby has a standalone debugging tool that you can use from the command line, but to use Vdebug in conjunction with your Ruby scripts you will have to get the "rdbgp.rb" script that comes bundled with Komodo Edit/IDE by Activestate. @@ -325,7 +335,7 @@ If you're still having trouble, drop me an email. Perl is one of the trickier languages to set up, unless you have very specific instructions on what to do. Fortunately, that's what I'm going to give you! -Like Python and Ruby, Activestate provide a standalone module that you can use +Like Python and Ruby, Activestate provides a standalone module that you can use to remotely debug Perl applications. What makes this more difficult than Python and Ruby is that the debugging script changes and, as far as I can see, breaks after a particular version. That means you have to get the right version from @@ -383,7 +393,7 @@ is the port. ------------------------------------------------------------------------------ 3.6 TCL/Wish set up *VdebugSetUpTcl* -Like Python and Ruby, Tcl and Wish have an standalone debugging tool that you +Like Python and Ruby, Tcl and Wish have a standalone debugging tool that you can use from the command line, which has again been made available by ActiveState. @@ -1167,6 +1177,15 @@ The remote log of the debugger (or the local, see |VdebugOptions-debug_window_le or |VdebugOptions-debug_file_level|) may be helpful to inspect the paths that are present in requests and responses. +If you happen to be using the same layout for all your remote servers in +different projects, you may map the same remote directory to the root of your +project. For instance, if the root of your project is always /app, and you +always run vim from the root of your projects, you may want to use the +following configuration: > + + let g:vdebug_options.path_maps = {"/app": getcwd() } +< + ------------------------------------------------------------------------------ 9.2 Connecting the two machines *VdebugRemoteConnection* @@ -1220,7 +1239,7 @@ the question is good I might even add it to this list. the language you're using and that you're doing what's necessary to activate the engine when running the script (e.g. setting environment variables/URL variables). If the problem persists, check that the - debugger engine is connecting the same port and address that Vdebug is + debugger engine is connecting to the same port and address that Vdebug is binding to (the 'port' and 'server' options). The server option is blank by default, which means that it will connect to all available interfaces. Unless you've changed this option, it's unlikely to be the issue. Also @@ -1280,7 +1299,7 @@ the question is good I might even add it to this list. repository and try to implement it yourself! I do accept merges. Q. Why doesn't Ross, the largest friend, simply eat the other ones? - A. Think of the indigestion that would result. + A. Think of the indigestion that would result in. Q. My command line php script seems to stop at the first line, but the source code is not loaded in the debugger window, how can that be? diff --git a/plugin/vdebug.vim b/plugin/vdebug.vim index 7c605d5a..216fe681 100644 --- a/plugin/vdebug.vim +++ b/plugin/vdebug.vim @@ -79,6 +79,8 @@ let g:vdebug_options_defaults = { \ 'port' : 9000, \ 'timeout' : 20, \ 'server' : '', +\ "proxy_host" : '', +\ "proxy_port" : 9001, \ 'on_close' : 'stop', \ 'break_on_open' : 1, \ 'ide_key' : '', diff --git a/python3/vdebug/connection.py b/python3/vdebug/connection.py index a73aa111..f7494e23 100644 --- a/python3/vdebug/connection.py +++ b/python3/vdebug/connection.py @@ -4,6 +4,8 @@ import sys import threading import time +import asyncio +import xml.etree.ElementTree as ET from . import log @@ -80,7 +82,7 @@ def send_msg(self, cmd): cmd -- command to send """ - #self.sock.send(cmd + '\0') + # self.sock.send(cmd + '\0') MSGLEN = len(cmd) totalsent = 0 while totalsent < MSGLEN: @@ -102,13 +104,17 @@ def __init__(self, input_stream=None): """ self.__sock = None self.input_stream = input_stream + self.proxy_success = False - def start(self, host='', port=9000, timeout=30): + def start(self, host='', proxy_host = '', proxy_port = 9001, idekey = None, port=9000, timeout=30): """Listen for a connection from the debugger. Listening for the actual connection is handled by self.listen() host -- host name where debugger is running (default '') port -- port number which debugger is listening on (default 9000) + proxy_host -- If using a DBGp Proxy, host name where the proxy is running (default None to disable) + proxy_port -- If using a DBGp Proxy, port where the proxy is listening for debugger connections (default 9001) + idekey -- The idekey that our Api() wrapper is expecting. Only required if using a proxy timeout -- time in seconds to wait for a debugger connection before giving up (default 30) """ print('Waiting for a connection (Ctrl-C to cancel, this message will ' @@ -119,13 +125,17 @@ def start(self, host='', port=9000, timeout=30): serv.setblocking(1) serv.bind((host, port)) serv.listen(5) - self.__sock = self.listen(serv, timeout) + if proxy_host and proxy_port: + # Register ourselves with the proxy server + self.proxyinit(proxy_host, proxy_port, port, idekey) + self.__sock = self.accept(serv, timeout) except socket.timeout: raise TimeoutError("Timeout waiting for connection") finally: + self.proxystop(proxy_host, proxy_port, idekey) serv.close() - def listen(self, serv, timeout): + def accept(self, serv, timeout): """Non-blocking listener. Provides support for keyboard interrupts from the user. Although it's non-blocking, the user interface will still block until the timeout is reached. @@ -154,14 +164,52 @@ def socket(self): def has_socket(self): return self.__sock is not None + def proxyinit(self, proxy_host, proxy_port, port, idekey): + """Register ourselves with the proxy.""" + if not proxy_host or not proxy_port: + return + + self.log("Connecting to DBGp proxy [%s:%d]" % (proxy_host, proxy_port)) + proxy_conn = socket.create_connection((proxy_host, proxy_port), 30) + + self.log("Sending proxyinit command") + msg = 'proxyinit -p %d -k %s -m 0' % (port, idekey) + proxy_conn.send(msg.encode()) + proxy_conn.shutdown(socket.SHUT_WR) + + # Parse proxy response + response = proxy_conn.recv(8192) + proxy_conn.close() + response = ET.fromstring(response) + self.proxy_success = bool(response.get("success")) + + def proxystop(self, proxy_host, proxy_port, idekey): + """De-register ourselves from the proxy.""" + if not self.proxy_success: + return + + proxy_conn = socket.create_connection((proxy_host, proxy_port), 30) + + self.log("Sending proxystop command") + msg = 'proxystop -k %s' % str(idekey) + proxy_conn.send(msg.encode()) + proxy_conn.close() + self.proxy_success = False + + class BackgroundSocketCreator(threading.Thread): - def __init__(self, host, port, message_q, output_q): - self.__message_q = message_q + def __init__(self, host, port, proxy_host, proxy_port, idekey, output_q): self.__output_q = output_q self.__host = host self.__port = port + self.__proxy_host = proxy_host + self.__proxy_port = proxy_port + self.__idekey = idekey + self.proxy_success = False + self.__socket_task = None + self.__loop = None threading.Thread.__init__(self) @staticmethod @@ -169,23 +217,36 @@ def log(message): log.Log(message, log.Logger.DEBUG) def run(self): + # needed for python 3.5 + self.__loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.__loop) + self.__loop.run_until_complete(self.run_async()) + + async def run_async(self): self.log("Started") self.log("Listening on port %s" % self.__port) try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.setblocking(1) + s.setblocking(False) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((self.__host, self.__port)) - s.settimeout(5) # timeout after 5 seconds so we can check messages s.listen(5) while 1: try: - self.__peek_for_exit() - client, address = s.accept() + # using ensure_future here since before 3.7, this is not a coroutine, but returns a future + self.__socket_task = asyncio.ensure_future(self.__loop.sock_accept(s)) + if self.__proxy_host and self.__proxy_port: + # Register ourselves with the proxy server + await self.proxyinit() + client, address = await self.__socket_task + # set resulting socket to blocking + client.setblocking(True) + self.log("Found client, %s" % str(address)) self.__output_q.put((client, address)) break except socket.error: + await self.proxystop() # No connection pass except socket.error as socket_error: @@ -195,41 +256,75 @@ def run(self): if socket_error.errno == errno.EADDRINUSE: self.log("Address already in use") print("Socket is already in use") - except Exception: + except asyncio.CancelledError as e: + self.log("Stopping server") + self.__socket_task = None + except Exception as e: print("Exception caught") self.log("Error: %s" % str(sys.exc_info())) self.log("Stopping server") finally: + await self.proxystop() self.log("Finishing socket server") s.close() - def __peek_for_exit(self): - try: - # self.log("Checking for exit") - self.__check_exit(self.__message_q.get_nowait()) - except queue.Empty: - pass + async def proxyinit(self): + """Register ourselves with the proxy.""" + if not self.__proxy_host or not self.__proxy_port: + return - @staticmethod - def __check_exit(message): - if message == "exit": - raise Exception("Exiting") + self.log("Connecting to DBGp proxy [%s:%d]" % (self.__proxy_host, self.__proxy_port)) + proxy_conn = socket.create_connection((self.__proxy_host, self.__proxy_port), 30) + + self.log("Sending proxyinit command") + msg = 'proxyinit -p %d -k %s -m 0' % (self.__port, self.__idekey) + proxy_conn.send(msg.encode()) + proxy_conn.shutdown(socket.SHUT_WR) + + # Parse proxy response + response = proxy_conn.recv(8192) + proxy_conn.close() + response = ET.fromstring(response) + self.proxy_success = bool(response.get("success")) + + async def proxystop(self): + """De-register ourselves from the proxy.""" + if not self.proxy_success: + return + + proxy_conn = socket.create_connection((self.__proxy_host, self.__proxy_port), 30) + + self.log("Sending proxystop command") + msg = 'proxystop -k %s' % str(self.__idekey) + proxy_conn.send(msg.encode()) + proxy_conn.close() + self.proxy_success = False + + + + def _exit(self): + if self.__socket_task: + # this will raise asyncio.CancelledError + self.__socket_task.cancel() + + # called from outside of the thread + def exit(self): + self.__loop.call_soon_threadsafe(self._exit) class SocketServer: def __init__(self): - self.__message_q = queue.Queue(0) self.__socket_q = queue.Queue(1) self.__thread = None def __del__(self): self.stop() - def start(self, host, port): + def start(self, host, port, proxy_host, proxy_port, ide_key): if not self.is_alive(): self.__thread = BackgroundSocketCreator( - host, port, self.__message_q, self.__socket_q) + host, port, proxy_host, proxy_port, ide_key, self.__socket_q) self.__thread.start() def is_alive(self): @@ -243,7 +338,7 @@ def socket(self): def stop(self): if self.is_alive(): - self.__message_q.put_nowait("exit") + self.__thread.exit() self.__thread.join(3000) if self.has_socket(): self.socket()[0].close() diff --git a/python3/vdebug/dbgp.py b/python3/vdebug/dbgp.py index b8f1a108..053f0186 100644 --- a/python3/vdebug/dbgp.py +++ b/python3/vdebug/dbgp.py @@ -181,7 +181,7 @@ def is_supported(self): def __str__(self): if self.is_supported(): xml = self.as_xml() - return xml.text + return xml.text if xml.text else "" return "* Feature not supported *" diff --git a/python3/vdebug/event.py b/python3/vdebug/event.py index 7e4acb3a..87842e4c 100644 --- a/python3/vdebug/event.py +++ b/python3/vdebug/event.py @@ -44,16 +44,16 @@ class CursorEvalEvent(Event): """Evaluate the variable currently under the cursor. """ char_regex = { - "default": "a-zA-Z0-9_.\[\]'\"", - "ruby": "$@a-zA-Z0-9_.\[\]'\"", + "default": "a-zA-Z0-9_.\\[\\]'\"", + "ruby": "$@a-zA-Z0-9_.\\[\\]'\"", "perl": "$a-zA-Z0-9_{}'\"", - "php": "$@%a-zA-Z0-9_\[\]'\"\->" + "php": "$@%a-zA-Z0-9_\\[\\]'\">-" } var_regex = { "default": "^[a-zA-Z_]", "ruby": "^[$@a-zA-Z_]", - "php": "^[\$A-Z]", + "php": r"^[\$A-Z]", "perl": "^[$@%]" } @@ -559,7 +559,7 @@ def run(self): line = self.ui.windows.breakpoints().line_at(lineno - 1) # Match on ID - id = re.findall('^[\s][0-9]*[\s]', line) + id = re.findall('^[\\s][0-9]*[\\s]', line) if not id: return False @@ -877,9 +877,9 @@ def _get_window_name(): @staticmethod def _get_breakpoint_id_breakpoint_window(line): # Match on ID - id = re.findall('^[\s][0-9]*[\s]', line) + id = re.findall(r'^[\s][0-9]*[\s]', line) if not id: - log.Log("No breakpoint founr at current cursor position", + log.Log("No breakpoint found at current cursor position", log.Logger.DEBUG) return False diff --git a/python3/vdebug/listener.py b/python3/vdebug/listener.py index 15775904..0c39ad2d 100644 --- a/python3/vdebug/listener.py +++ b/python3/vdebug/listener.py @@ -22,6 +22,9 @@ def __init__(self): def start(self): self.__server.start(opts.Options.get('server'), opts.Options.get('port', int), + opts.Options.get('proxy_host'), + opts.Options.get('proxy_port', int), + opts.Options.get('ide_key'), opts.Options.get('timeout', int)) def stop(self): @@ -51,7 +54,10 @@ def start(self): if opts.Options.get("auto_start", int): vim.command('autocmd Vdebug CursorHold,CursorHoldI,CursorMoved,CursorMovedI,FocusGained,FocusLost * python3 debugger.start_if_ready()') self.__server.start(opts.Options.get('server'), - opts.Options.get('port', int)) + opts.Options.get('port', int), + opts.Options.get('proxy_host'), + opts.Options.get('proxy_port', int), + opts.Options.get('ide_key')) def stop(self): if opts.Options.get("auto_start", bool): diff --git a/python3/vdebug/session.py b/python3/vdebug/session.py index a23d6eac..a6792bbc 100644 --- a/python3/vdebug/session.py +++ b/python3/vdebug/session.py @@ -37,13 +37,14 @@ def listen(self): print("Waiting for a connection: none found so far") elif self.listener and self.listener.is_ready(): print("Found connection, starting debugger") + log.Log("Got connection, starting", log.Logger.DEBUG) self.__new_session() else: self.start_listener() def start_listener(self): self.listener = listener.Listener.create() - print("Vdebug will wait for a connection in the background") + print("Vdebug will wait for a connection") util.Environment.reload() if self.is_open(): self.ui().set_status("listening") @@ -103,6 +104,7 @@ def start_if_ready(self): try: if self.listener.is_ready(): print("Found connection, starting debugger") + log.Log("Got connection, starting", log.Logger.DEBUG) self.__new_session() return True return False @@ -206,7 +208,9 @@ def start(self, connection): self.__ui.set_conn_details(addr[0], addr[1]) self.__collect_context_names() - self.__set_features() + self.__check_features() # only for debugging at the moment + self.__set_default_features() # features we try by default + self.__set_features() # user defined features self.__initialize_breakpoints() if opts.Options.get('break_on_open', int) == 1: @@ -229,6 +233,64 @@ def detach(self): self.close_connection(False) + def __check_features(self): + must_features = [ + 'language_supports_threads', + 'language_name', + 'language_version', + 'encoding', # has set + 'protocol_version', + 'supports_async', + 'data_encoding', + 'breakpoint_languages', + 'breakpoint_types', + 'resolved_breakpoints', + 'multiple_sessions', # has set + 'max_children', # has set + 'max_data', # has set + 'max_depth', # has set + 'extended_properties', # has set + ] + maybe_features = [ + 'supported_encodings', + 'supports_postmortem', + 'show_hidden', # has set + 'notify_ok', # has set + ] + for feature in must_features: + try: + feature_value = self.__api.feature_get(feature) + log.Log( + "Must Feature: %s = %s" % (feature, str(feature_value)), + log.Logger.DEBUG + ) + except dbgp.DBGPError: + error_str = "Failed to get feature %s" % feature + log.Log(error_str, log.Logger.DEBUG) + + for feature in maybe_features: + try: + feature_value = self.__api.feature_get(feature) + log.Log( + "Maybe Feature: %s = %s" % (feature, str(feature_value)), + log.Logger.DEBUG + ) + except dbgp.DBGPError: + error_str = "Failed to get feature %s" % feature + log.Log(error_str, log.Logger.DEBUG) + + def __set_default_features(self): + features = { + 'multiple_sessions': 0, # explicitly disable multiple sessions atm + 'extended_properties': 1, + } + for name, value in features.items(): + try: + self.__api.feature_set(name, value) + except dbgp.DBGPError as e: + error_str = "Failed to set feature %s: %s" % (name, e.args[0]) + log.Log(error_str, log.Logger.DEBUG) + def __set_features(self): """Evaluate vim dictionary of features and pass to debugger. diff --git a/python3/vdebug/ui/vimui.py b/python3/vdebug/ui/vimui.py index 38cdf119..5f46ba66 100644 --- a/python3/vdebug/ui/vimui.py +++ b/python3/vdebug/ui/vimui.py @@ -158,8 +158,8 @@ def __init__(self): 'DebuggerStatus': 'vertical leftabove new' }, 'window_size': { - 'DebuggerWatch': { 'height' : 15 }, - 'DebuggerStatus': { 'height' : 1 } + 'DebuggerWatch': {'height': 15}, + 'DebuggerStatus': {'height': 1} }, 'window_arrangement': [ 'DebuggerWatch', @@ -352,7 +352,7 @@ class SourceWindow(interface.Window): pointer_sign_id = '6145' breakpoint_sign_id = '6146' has_sign_priority = vim.vvars['version'] > 801 \ - or vim.funcs.has('nvim-0-4-0') + or (hasattr(vim, 'funcs') and vim.funcs.has('nvim-0-4-0')) def focus(self): vim.command("1wincmd w") @@ -586,7 +586,8 @@ def create(self, open_cmd): self.creation_count += 1 if self.creation_count == 1: - cmd = 'autocmd Vdebug BufWinLeave %s' % self.name + cmd = 'autocmd Vdebug BufWinLeave %s silent! bdelete %s' \ + % (self.name, self.name) cmd += ' python3 debugger.mark_window_as_closed("%s")' % self.name vim.command(cmd) @@ -704,7 +705,7 @@ class StackWindow(Window): pointer_sign_id = '6147' has_sign_priority = vim.vvars['version'] > 801 \ - or vim.funcs.has('nvim-0-4-0') + or (hasattr(vim, 'funcs') and vim.funcs.has('nvim-0-4-0')) def on_create(self): self.command('inoremap ' diff --git a/tests/test_dbgp_api.py b/tests/test_dbgp_api.py index 26382d57..4e998059 100644 --- a/tests/test_dbgp_api.py +++ b/tests/test_dbgp_api.py @@ -127,10 +127,10 @@ def test_stop_retval(self): assert str(status_res) == "stopping" def test_detatch_retval(self): - """Test that the detatch command receives a message from the api.""" + """Test that the detach command receives a message from the api.""" self.p.conn.recv_msg.return_value = """\n -