From c5e053ee2c91d9ed9c73bd678caab1bbb5d93d66 Mon Sep 17 00:00:00 2001 From: Yihuang Yu Date: Thu, 1 Aug 2024 14:57:22 +0800 Subject: [PATCH 1/2] fix: Raise error when aexpect_helper doesn't work properly When aexpect_helper does not work properly, the process will hang without any log information. This fix adds a timeout and checks the process status and raises an error if the process terminates prematurely. Signed-off-by: Yihuang Yu --- aexpect/client.py | 68 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/aexpect/client.py b/aexpect/client.py index d686f59..8c89a38 100644 --- a/aexpect/client.py +++ b/aexpect/client.py @@ -178,24 +178,8 @@ def __init__(self, command=None, a_id=None, auto_close=False, echo=False, # Start the server (which runs the command) if command: helper_cmd = utils_path.find_command('aexpect_helper') - self._aexpect_helper = subprocess.Popen([helper_cmd], # pylint: disable=R1732 - shell=True, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - pass_fds=pass_fds) - sub = self._aexpect_helper - # Send parameters to the server - sub.stdin.write(f"{self.a_id}\n".encode(self.encoding)) - sub.stdin.write(f"{echo}\n".encode(self.encoding)) - readers = ",".join(self.readers) - sub.stdin.write(f"{readers}\n".encode(self.encoding)) - sub.stdin.write(f"{command}\n".encode(self.encoding)) - sub.stdin.flush() - # Wait for the server to complete its initialization - while (f"Server {self.a_id} ready" not in - sub.stdout.readline().decode(self.encoding, "ignore")): - pass + self._aexpect_helper = self._get_aexpect_helper( + helper_cmd, pass_fds, echo, command) # Open the reading pipes if is_file_locked(self.lock_server_running_filename): @@ -232,6 +216,54 @@ def __del__(self): if self.auto_close: self.close() + def _get_aexpect_helper(self, helper_cmd, pass_fds, echo, command): + """ + Start the aexpect_helper server, send the command and wait for the server to + complete its initialization. + + :param helper_cmd: The aexpect_helper command location. + :param pass_fds: Optional sequence of file descriptors to keep open + between the parent and child. + :param echo: Boolean indicating whether echo should be initially + enabled for the pseudo terminal running the subprocess. This + parameter has an effect only when starting a new server. + :param command: Command to run, or None if accessing an already running + server. + :returns: The aexpect_helper process object + :raise ExpectTimeoutError: Raised if timeout expires + :raise ExpectProcessTerminatedError: Raised if the child process + terminates while waiting for output + + """ + sub = subprocess.Popen([helper_cmd], # pylint: disable=R1732 + shell=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + pass_fds=pass_fds) + # Send parameters to the server + sub.stdin.write(f"{self.a_id}\n".encode(self.encoding)) + sub.stdin.write(f"{echo}\n".encode(self.encoding)) + readers = ",".join(self.readers) + sub.stdin.write(f"{readers}\n".encode(self.encoding)) + sub.stdin.write(f"{command}\n".encode(self.encoding)) + sub.stdin.flush() + # Wait for the server to complete its initialization + full_output = "" + pattern = f"Server {self.a_id} ready" + end_time = time.time() + 60 + while time.time() < end_time: + output = sub.stdout.readline().decode(self.encoding, "ignore") + if pattern in output: + break + full_output += output + sub_status = sub.poll() + if sub_status is not None: + raise ExpectProcessTerminatedError(pattern, sub_status, full_output) + else: + raise ExpectTimeoutError(pattern, full_output) + return sub + def _add_reader(self, reader): """ Add a reader whose file descriptor can be obtained with _get_fd(). From 4699c57aeaec6d65cc6b9e445e3f3f6d4189f7a6 Mon Sep 17 00:00:00 2001 From: Yihuang Yu Date: Thu, 1 Aug 2024 16:42:00 +0800 Subject: [PATCH 2/2] fix: Ensure self.tail_thread to be set before add_close_hook The self.tail_thread attrbuite must to be set before adding the Tail._join_thread to prevent unable to use it on cleanup, otherwise sometimes will raise AttributeError. Signed-off-by: Yihuang Yu --- aexpect/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aexpect/client.py b/aexpect/client.py index 8c89a38..522ece1 100644 --- a/aexpect/client.py +++ b/aexpect/client.py @@ -554,6 +554,7 @@ def __init__(self, command=None, a_id=None, auto_close=False, echo=False, :param encoding: Override text encoding (by default: autodetect by locale.getpreferredencoding()) """ + self.tail_thread = None # Add a reader and a close hook self._add_reader("tail") self._add_close_hook(Tail._join_thread) @@ -574,7 +575,6 @@ def __init__(self, command=None, a_id=None, auto_close=False, echo=False, self.output_prefix = output_prefix # Start the thread in the background - self.tail_thread = None if self.is_alive(): if termination_func or output_func: self._start_thread()