# -*- coding: UTF-8 -*- # This file is part of the jetson_stats package (https://github.com/rbonghi/jetson_stats or http://rnext.it). # Copyright (c) 2019-2023 Raffaello Bonghi. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import os import sys import logging import threading # Launch command import subprocess as sp # Load queue library for python 2 and python 3 try: import queue except ImportError: import Queue as queue # pyright: ignore[reportMissingImports] # Create logger logger = logging.getLogger(__name__) EXTRA_TIMEOUT = 1.0 # Reference: # https://eli.thegreenplace.net/2017/interacting-with-a-long-running-child-process-in-python/ # https://stackoverflow.com/questions/37942022/returncode-of-popen-object-is-none-after-the-process-is-terminated/42376107 # https://stackoverflow.com/questions/375427/non-blocking-read-on-a-subprocess-pipe-in-python # https://docs.python.org/3/tutorial/errors.html # https://stackoverflow.com/questions/10756383/timeout-on-subprocess-readline-in-python # https://stackoverflow.com/questions/3733270/python-subprocess-timeout class Command(object): class CommandException(Exception): def __init__(self, message, errno): self.message = message self.errno = errno def __str__(self): return "[errno:{errno}] {message}".format(message=self.message, errno=self.errno) class TimeoutException(CommandException): def __init__(self): super(Command.TimeoutException, self).__init__("Process does not replied in time", -1) @staticmethod def run_command(command, repeat=5, timeout=2, input=None): cmd = Command(command, input=input) for idx in range(repeat): try: return cmd(timeout=timeout) except Command.TimeoutException as error: logger.error("[{idx}] {error}".format(idx=idx, error=error)) raise Command.CommandException("Error to start {command}".format(command=command), -2) def __init__(self, command, input=None): self.process = None self.command = command self.input = input def __call__(self, timeout=None): def target(out_queue, err_queue): # Run process try: # https://stackoverflow.com/questions/33277452/prevent-unexpected-stdin-reads-and-lock-in-subprocess if self.input is not None: self.process = sp.Popen(self.command, stdout=sp.PIPE, stderr=sp.PIPE, stdin=sp.PIPE, preexec_fn=os.setsid) # Write input stdout, _ = self.process.communicate(bytes(self.input, 'utf-8')) self.process.stdin.close() self.process.wait() # Read lines output for line in stdout.splitlines(): try: line = line.decode('utf-8') line = str(line.strip()) except UnicodeEncodeError: line = line.encode('ascii', 'ignore').decode('ascii') out_queue.put(line) else: self.process = sp.Popen(self.command, stdout=sp.PIPE, stderr=sp.PIPE, stdin=open(os.devnull), preexec_fn=os.setsid) # Read lines output for line in iter(self.process.stdout.readline, b''): try: line = line.decode('utf-8') line = str(line.strip()) except UnicodeEncodeError: line = line.encode('ascii', 'ignore').decode('ascii') out_queue.put(line) # Close and terminate self.process.stdout.close() self.process.wait() except Exception: # Store error message err_queue.put(sys.exc_info()) # Initialize lists is_timeout = False out_queue = queue.Queue() err_queue = queue.Queue() thread = threading.Thread(target=target, args=(out_queue, err_queue, )) thread.start() # Wait timeout process thread.join(timeout) if thread.is_alive(): logger.error('Terminating process: {command}'.format(command=self.command)) if self.process is not None: self.process.terminate() thread.join(timeout=EXTRA_TIMEOUT) logger.warning('Process terminated: {command}'.format(command=self.command)) is_timeout = True # Read the output # Extract exception and raise if not err_queue.empty(): ex_type, ex_value, tb_str = err_queue.get() ex_value.__traceback__ = tb_str raise ex_value if is_timeout: raise Command.TimeoutException() if self.process.returncode != 0: raise Command.CommandException('Error process:', self.process.returncode) return list(out_queue.queue) def communicate(self, timeout=None): self.__call__(timeout=timeout) # EOF