diff --git a/CHANGELOG.md b/CHANGELOG.md index dc85ed6..3d10c77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## <0.11.0.1>.dev0 - *Replace this line with new entries* +- Added a flag `return_on_result`, which causes an immediate return when a response of type 'result' is received, +instead of waiting until the timeout passes. ## 0.11.0.0 diff --git a/MANIFEST.in b/MANIFEST.in index 049f853..afe5714 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,13 +7,13 @@ exclude example.py exclude .flake8 exclude noxfile.py exclude mkdocs.yml -exclude __pycache__ +global-exclude __pycache__ exclude mypy.ini exclude *.in exclude *.txt -exclude *.pyc -exclude *.sw[po] -exclude *~ +global-exclude *.pyc +global-exclude *.sw[po] +global-exclude *~ prune tests prune docs diff --git a/pygdbmi/IoManager.py b/pygdbmi/IoManager.py index dbd7276..63850dd 100644 --- a/pygdbmi/IoManager.py +++ b/pygdbmi/IoManager.py @@ -74,6 +74,7 @@ def get_gdb_response( self, timeout_sec: float = DEFAULT_GDB_TIMEOUT_SEC, raise_error_on_timeout: bool = True, + return_on_result: bool = False, ) -> List[Dict]: """Get response from GDB, and block while doing so. If GDB does not have any response ready to be read by timeout_sec, an exception is raised. @@ -81,6 +82,9 @@ def get_gdb_response( Args: timeout_sec: Maximum time to wait for reponse. Must be >= 0. Will return after raise_error_on_timeout: Whether an exception should be raised if no response was found after timeout_sec + return_on_result: Whether to return when a response of type 'result' was received. + If return_on_result and raise_error_on_timeout, will raise an exception if no response of + type 'result' was received. Returns: List of parsed GDB responses, returned from gdbmiparser.parse_response, with the @@ -96,19 +100,30 @@ def get_gdb_response( timeout_sec = 0 if USING_WINDOWS: - retval = self._get_responses_windows(timeout_sec) + retval = self._get_responses_windows(timeout_sec, return_on_result) else: - retval = self._get_responses_unix(timeout_sec) + retval = self._get_responses_unix(timeout_sec, return_on_result) if not retval and raise_error_on_timeout: raise GdbTimeoutError( "Did not get response from gdb after %s seconds" % timeout_sec ) - else: - return retval + if ( + return_on_result + and raise_error_on_timeout + and not any(response["type"] == "result" for response in retval) + ): + raise GdbTimeoutError( + "Did not get a 'result' response from gdb after %s seconds" + % timeout_sec + ) + + return retval - def _get_responses_windows(self, timeout_sec: float) -> List[Dict]: + def _get_responses_windows( + self, timeout_sec: float, return_on_result: bool + ) -> List[Dict]: """Get responses on windows. Assume no support for select and use a while loop.""" timeout_time_sec = time.time() + timeout_sec responses = [] @@ -130,19 +145,29 @@ def _get_responses_windows(self, timeout_sec: float) -> List[Dict]: pass responses += responses_list + + if return_on_result and any( + response["type"] == "result" for response in (responses_list or []) + ): + break + if timeout_sec == 0: break + elif responses_list and self._allow_overwrite_timeout_times: timeout_time_sec = min( time.time() + self.time_to_check_for_additional_output_sec, timeout_time_sec, ) + elif time.time() > timeout_time_sec: break return responses - def _get_responses_unix(self, timeout_sec: float) -> List[Dict]: + def _get_responses_unix( + self, timeout_sec: float, return_on_result: bool + ) -> List[Dict]: """Get responses on unix-like system. Use select to wait for output.""" timeout_time_sec = time.time() + timeout_sec responses = [] @@ -172,10 +197,19 @@ def _get_responses_unix(self, timeout_sec: float) -> List[Dict]: responses_list = self._get_responses_list(raw_output, stream) responses += responses_list + if return_on_result and any( + response["type"] == "result" for response in (responses_list or []) + ): + break + if timeout_sec == 0: # just exit immediately break - elif responses_list and self._allow_overwrite_timeout_times: + elif ( + responses_list + and self._allow_overwrite_timeout_times + and not return_on_result + ): # update timeout time to potentially be closer to now to avoid lengthy wait times when nothing is being output by gdb timeout_time_sec = min( time.time() + self.time_to_check_for_additional_output_sec, @@ -197,7 +231,10 @@ def _get_responses_list( """ responses: List[Dict[Any, Any]] = [] - (_new_output, self._incomplete_output[stream],) = _buffer_incomplete_responses( + ( + _new_output, + self._incomplete_output[stream], + ) = _buffer_incomplete_responses( raw_output, self._incomplete_output.get(stream) ) @@ -227,6 +264,7 @@ def write( mi_cmd_to_write: Union[str, List[str]], timeout_sec: float = DEFAULT_GDB_TIMEOUT_SEC, raise_error_on_timeout: bool = True, + return_on_result: bool = False, read_response: bool = True, ) -> List[Dict]: """Write to gdb process. Block while parsing responses from gdb for a maximum of timeout_sec. @@ -235,6 +273,9 @@ def write( mi_cmd_to_write: String to write to gdb. If list, it is joined by newlines. timeout_sec: Maximum number of seconds to wait for response before exiting. Must be >= 0. raise_error_on_timeout: If read_response is True, raise error if no response is received + return_on_result: Whether to return when a response of type 'result' was received. + If return_on_result and raise_error_on_timeout, will raise an exception if no response of + type 'result' was received. read_response: Block and read response. If there is a separate thread running, this can be false, and the reading thread read the output. Returns: List of parsed gdb responses if read_response is True, otherwise [] @@ -282,7 +323,9 @@ def write( if read_response is True: return self.get_gdb_response( - timeout_sec=timeout_sec, raise_error_on_timeout=raise_error_on_timeout + timeout_sec=timeout_sec, + raise_error_on_timeout=raise_error_on_timeout, + return_on_result=return_on_result, ) else: diff --git a/pygdbmi/gdbcontroller.py b/pygdbmi/gdbcontroller.py index d4fa59a..5cf1ca6 100644 --- a/pygdbmi/gdbcontroller.py +++ b/pygdbmi/gdbcontroller.py @@ -110,20 +110,28 @@ def get_gdb_response( self, timeout_sec: float = DEFAULT_GDB_TIMEOUT_SEC, raise_error_on_timeout: bool = True, + return_on_result: bool = False, ) -> List[Dict]: """Get gdb response. See IoManager.get_gdb_response() for details""" - return self.io_manager.get_gdb_response(timeout_sec, raise_error_on_timeout) + return self.io_manager.get_gdb_response( + timeout_sec, raise_error_on_timeout, return_on_result + ) def write( self, mi_cmd_to_write: Union[str, List[str]], timeout_sec: float = DEFAULT_GDB_TIMEOUT_SEC, raise_error_on_timeout: bool = True, + return_on_result: bool = False, read_response: bool = True, ) -> List[Dict]: """Write command to gdb. See IoManager.write() for details""" return self.io_manager.write( - mi_cmd_to_write, timeout_sec, raise_error_on_timeout, read_response + mi_cmd_to_write, + timeout_sec, + raise_error_on_timeout, + return_on_result, + read_response, ) def exit(self) -> None: diff --git a/tests/test_gdbcontroller.py b/tests/test_gdbcontroller.py index c567ac5..2e5ec54 100755 --- a/tests/test_gdbcontroller.py +++ b/tests/test_gdbcontroller.py @@ -9,6 +9,7 @@ import random import shutil import subprocess +import time import pytest @@ -68,6 +69,22 @@ def test_controller() -> None: assert response["stream"] == "stdout" assert response["token"] is None + # Verify exits quickly if return_on_result is True + t0 = time.monotonic() + responses = gdbmi.write(["-rubbish"], return_on_result=True) + t1 = time.monotonic() + duration = t1 - t0 + assert len(responses) != 0 + assert duration < 0.01 + + # Verify waits for timeout if return_on_result is not set + t0 = time.monotonic() + responses = gdbmi.write(["-rubbish"]) + t1 = time.monotonic() + duration = t1 - t0 + assert len(responses) != 0 + assert duration >= 0.2 + responses = gdbmi.write(["-file-list-exec-source-files", "-break-insert main"]) assert len(responses) != 0