diff --git a/src/consts.py b/src/consts.py index 9baf3ebc..a9623a90 100644 --- a/src/consts.py +++ b/src/consts.py @@ -2,6 +2,7 @@ # binary names BINARY_NAME__POSTGRES = "postgres" +BINARY_NAME__PG_CTL = "pg_ctl" # names for dirs in base_dir DATA_DIR = "data" @@ -44,5 +45,7 @@ LOGICAL_REPL_MAX_CATCHUP_ATTEMPTS = 60 PG_CTL__STATUS__OK = 0 + +PG_CTL__STATUS__FAILED = 1 PG_CTL__STATUS__NODE_IS_STOPPED = 3 PG_CTL__STATUS__BAD_DATADIR = 4 diff --git a/src/impl/platforms/linux/internal_platform_utils.py b/src/impl/platforms/linux/internal_platform_utils.py index 8ceddf24..1021b735 100644 --- a/src/impl/platforms/linux/internal_platform_utils.py +++ b/src/impl/platforms/linux/internal_platform_utils.py @@ -291,9 +291,13 @@ def _is_file_not_found_exception(e: Exception) -> bool: if isinstance(e, FileNotFoundError): return True + if isinstance(e, ProcessLookupError): + return True + if isinstance(e, ExecUtilException): - if e.exit_code == 2: + if e.exit_code == 1: return True + return False return False diff --git a/src/utils.py b/src/utils.py index 77bf1bd3..eb63694d 100644 --- a/src/utils.py +++ b/src/utils.py @@ -8,9 +8,7 @@ from .config import testgres_config as tconf from .raise_error import RaiseError from .enums import NodeStatus -from .consts import PG_CTL__STATUS__OK -from .consts import PG_CTL__STATUS__NODE_IS_STOPPED -from .consts import PG_CTL__STATUS__BAD_DATADIR + from testgres.operations.types import T_OS_CMD from testgres.operations.types import T_OS_EXEC_ENV from testgres.operations.os_ops import OsOperations @@ -453,208 +451,241 @@ def get_pg_node_state( assert type(data_dir) is str assert utils_log_file is None or type(utils_log_file) is str - C_MAX_ATTEMPTS = 3 - C_SLEEP_TIME1 = 1 - C_SLEEP_TIME_MULT = 2 - - _params = [ - os_ops.build_path(bin_dir, "pg_ctl"), - "-D", + return PostgresNodeStateUtils.exec( + os_ops, + bin_dir, data_dir, - "status", - ] - - attempt = 0 - sleep_time = C_SLEEP_TIME1 + utils_log_file, + ) - class tagPlaformUtilsProvider: - T_PLATFORM_UTILS = internal_platform_utils_factory.InternalPlatformUtils - _platform_utils: typing.Optional[T_PLATFORM_UTILS] = None +class InternalPlaformUtilsProvider: + T_PLATFORM_UTILS = internal_platform_utils_factory.InternalPlatformUtils - def __init__(self): - self._platform_utils = None + _os_ops: OsOperations + _platform_utils: typing.Optional[T_PLATFORM_UTILS] = None - def get(self) -> T_PLATFORM_UTILS: - if self._platform_utils is None: - self._platform_utils = internal_platform_utils_factory.create_internal_platform_utils(os_ops) - assert isinstance(self._platform_utils, __class__.T_PLATFORM_UTILS) + def __init__( + self, + os_ops: OsOperations, + ): + assert isinstance(os_ops, OsOperations) + self._os_ops = os_ops + self._platform_utils = None + return + def get(self) -> T_PLATFORM_UTILS: + if self._platform_utils is None: + self._platform_utils = internal_platform_utils_factory.create_internal_platform_utils( + self._os_ops, + ) assert isinstance(self._platform_utils, __class__.T_PLATFORM_UTILS) - return self._platform_utils - platform_utils_provider = tagPlaformUtilsProvider() + assert isinstance(self._platform_utils, __class__.T_PLATFORM_UTILS) + return self._platform_utils - while True: - assert type(attempt) is int - assert attempt >= 0 - assert attempt < C_MAX_ATTEMPTS - attempt += 1 +class PostgresNodeStateUtils: + T_PLATFORM_UTILS = InternalPlaformUtilsProvider.T_PLATFORM_UTILS + + @staticmethod + def exec( + os_ops: OsOperations, + bin_dir: str, + data_dir: str, + utils_log_file: typing.Optional[str], + ) -> PostgresNodeState: + assert isinstance(os_ops, OsOperations) + assert type(bin_dir) is str + assert type(data_dir) is str + assert utils_log_file is None or type(utils_log_file) is str + + C_MAX_ATTEMPTS = 3 + C_SLEEP_TIME1 = 1 + C_SLEEP_TIME_MULT = 2 + + pg_ctl_params = [ + os_ops.build_path(bin_dir, consts.BINARY_NAME__PG_CTL), + "-D", + data_dir, + "status", + ] - if attempt > 1: - internal_utils.send_log_debug("Sleep {} second(s) before an attempt #{}".format( - sleep_time, - attempt, - )) - time.sleep(sleep_time) - sleep_time = sleep_time * C_SLEEP_TIME_MULT + attempt = 0 + sleep_time = C_SLEEP_TIME1 - exec_r = execute_utility3( + platform_utils_provider = InternalPlaformUtilsProvider( os_ops, - _params, - utils_log_file, - check=False, ) - status_code = exec_r.returncode - out = exec_r.stdout - error = exec_r.stderr + while True: + assert type(attempt) is int + assert attempt >= 0 + assert attempt < C_MAX_ATTEMPTS - assert type(status_code) is int - assert type(out) is str - assert type(error) is str + attempt += 1 - # ----------------- - if status_code == PG_CTL__STATUS__NODE_IS_STOPPED: - return PostgresNodeState(NodeStatus.Stopped, None) + if attempt > 1: + internal_utils.send_log_debug("Sleep {} second(s) before an attempt #{}".format( + sleep_time, + attempt, + )) + time.sleep(sleep_time) + sleep_time = sleep_time * C_SLEEP_TIME_MULT + + exec_r = execute_utility3( + os_ops, + pg_ctl_params, + utils_log_file, + check=False, + ) - # ----------------- - if status_code == PG_CTL__STATUS__BAD_DATADIR: - return PostgresNodeState(NodeStatus.Uninitialized, None) + status_code = exec_r.returncode + out = exec_r.stdout + error = exec_r.stderr - # ----------------- - if status_code == PG_CTL__STATUS__OK: - if out == "": - RaiseError.pg_ctl_returns_an_empty_string( - _params - ) + assert type(status_code) is int + assert type(out) is str + assert type(error) is str - C_PID_PREFIX = "(PID: " + # ----------------- + if status_code == consts.PG_CTL__STATUS__NODE_IS_STOPPED: + return PostgresNodeState(NodeStatus.Stopped, None) - i = out.find(C_PID_PREFIX) + # ----------------- + if status_code == consts.PG_CTL__STATUS__BAD_DATADIR: + return PostgresNodeState(NodeStatus.Uninitialized, None) - if i == -1: - RaiseError.pg_ctl_returns_an_unexpected_string( + # ----------------- + if status_code == consts.PG_CTL__STATUS__OK: + pid = __class__._parse_pid( out, - _params, + pg_ctl_params, ) + assert type(pid) is int + assert pid != 0 - assert i > 0 - assert i < len(out) - assert len(C_PID_PREFIX) <= len(out) - assert i <= len(out) - len(C_PID_PREFIX) + # ----------------- detect zombie + if platform_utils_provider.get().ProcessIsZombi_soft_check(os_ops, pid) is True: + internal_utils.send_log_debug("Postmaster process {} is a zombie.".format( + pid, + )) + return PostgresNodeState(NodeStatus.Zombie, pid) - i += len(C_PID_PREFIX) - start_pid_s = i + # ----------------- + return PostgresNodeState(NodeStatus.Running, pid) - while True: - if i == len(out): - RaiseError.pg_ctl_returns_an_unexpected_string( - out, - _params, - ) + assert status_code != consts.PG_CTL__STATUS__OK - ch = out[i] + errMsg = "Getting of a node status [data_dir is {0}] failed.".format( + data_dir, + ) - if ch == ")": - break + e1 = ExecUtilException( + message=errMsg, + command=pg_ctl_params, + exit_code=status_code, + out=out, + error=error, + ) - if ch.isdigit(): - i += 1 - continue + if status_code == consts.PG_CTL__STATUS__FAILED: + internal_utils.send_log_debug( + "pg_ctl fails with an error: {}".format( + exec_r.stderr, + )) - RaiseError.pg_ctl_returns_an_unexpected_string( - out, - _params, - ) - assert False + try: + find_postmaster_r = platform_utils_provider.get().FindPostmaster( + os_ops, + bin_dir, + data_dir, + ) + except Exception as e2: + raise e2 from e1 - if i == start_pid_s: - RaiseError.pg_ctl_returns_an_unexpected_string( - out, - _params, - ) + assert type(find_postmaster_r) is __class__.T_PLATFORM_UTILS.FindPostmasterResult - # TODO: Let's verify a length of pid string. + if find_postmaster_r.code == __class__.T_PLATFORM_UTILS.FindPostmasterResultCode.ok: + # Postmaster is alive. Let's wait a few seconds and check its status again. + internal_utils.send_log_debug( + "Postmaster is found and has PID {}.".format( + find_postmaster_r.pid, + )) - pid = int(out[start_pid_s:i]) + if attempt < C_MAX_ATTEMPTS: + continue - if pid == 0: - RaiseError.pg_ctl_returns_a_zero_pid( - out, - _params, - ) + raise e1 - assert pid != 0 + @staticmethod + def _parse_pid( + out: str, + pg_ctl_params, + ) -> int: + assert type(out) is str - # ----------------- detect zombie - if platform_utils_provider.get().ProcessIsZombi_soft_check(os_ops, pid) is True: - internal_utils.send_log_debug("Postmaster process {} is a zombie.".format( - pid, - )) - return PostgresNodeState(NodeStatus.Zombie, pid) + if out == "": + RaiseError.pg_ctl_returns_an_empty_string( + pg_ctl_params, + ) - # ----------------- - return PostgresNodeState(NodeStatus.Running, pid) + C_PID_PREFIX = "(PID: " - assert status_code != PG_CTL__STATUS__OK + i = out.find(C_PID_PREFIX) - errMsg = "Getting of a node status [data_dir is {0}] failed.".format( - data_dir - ) + if i == -1: + RaiseError.pg_ctl_returns_an_unexpected_string( + out, + pg_ctl_params, + ) - e1 = ExecUtilException( - message=errMsg, - command=_params, - exit_code=status_code, - out=out, - error=error, - ) + assert i > 0 + assert i < len(out) + assert len(C_PID_PREFIX) <= len(out) + assert i <= len(out) - len(C_PID_PREFIX) - pid_file = os_ops.build_path(data_dir, "postmaster.pid") + i += len(C_PID_PREFIX) + start_pid_s = i - postmaster_pid_is_empty = "pg_ctl: the PID file \"{}\" is empty\n".format( - pid_file, - ) + while True: + if i == len(out): + RaiseError.pg_ctl_returns_an_unexpected_string( + out, + pg_ctl_params, + ) - if error == postmaster_pid_is_empty: - internal_utils.send_log_debug( - "PID file [{}] is empty. A check is being carried out to ensure that the postmaster is alive [bindir: {}] ...".format( - pid_file, - bin_dir, - )) + ch = out[i] - try: - find_postmaster_r = platform_utils_provider.get().FindPostmaster( - os_ops, - bin_dir, - data_dir, - ) - except Exception as e2: - e2.__cause__ = e1 - raise e2 + if ch == ")": + break - assert type(find_postmaster_r) is internal_platform_utils_factory.InternalPlatformUtils.FindPostmasterResult + if ch.isdigit(): + i += 1 + continue - if find_postmaster_r.code == internal_platform_utils_factory.InternalPlatformUtils.FindPostmasterResultCode.ok: - # Postmaster is alive. Let's wait a few seconds and check its status again. - internal_utils.send_log_debug( - "Postmaster is found and has PID {}.".format( - find_postmaster_r.pid, - )) + RaiseError.pg_ctl_returns_an_unexpected_string( + out, + pg_ctl_params, + ) + assert False - if attempt < C_MAX_ATTEMPTS: - continue + if i == start_pid_s: + RaiseError.pg_ctl_returns_an_unexpected_string( + out, + pg_ctl_params, + ) - errMsg = "Getting of a node status [data_dir is {0}] failed.".format( - data_dir, - ) + # TODO: Let's verify a length of pid string. - raise ExecUtilException( - message=errMsg, - command=_params, - exit_code=status_code, - out=out, - error=error, - ) + pid = int(out[start_pid_s:i]) + + if pid == 0: + RaiseError.pg_ctl_returns_a_zero_pid( + out, + pg_ctl_params, + ) + + assert pid != 0 + + return pid diff --git a/tests/helpers/pg_msg_builder.py b/tests/helpers/pg_msg_builder.py new file mode 100644 index 00000000..dfd1e5d0 --- /dev/null +++ b/tests/helpers/pg_msg_builder.py @@ -0,0 +1,19 @@ +class PgMsgBuilder: + @staticmethod + def pg_ctl__pid_file_is_empty( + path: str, + ) -> str: + msg = "pg_ctl: the PID file \"{}\" is empty".format( + path, + ) + return msg + + # -------------------------------------------------------------------- + @staticmethod + def pg_ctl__invalid_data_in_pid_file( + path: str, + ) -> str: + msg = "pg_ctl: invalid data in PID file \"{}\"".format( + path, + ) + return msg diff --git a/tests/test_testgres_common.py b/tests/test_testgres_common.py index ca1c35f7..f4f60a9e 100644 --- a/tests/test_testgres_common.py +++ b/tests/test_testgres_common.py @@ -1,12 +1,15 @@ from __future__ import annotations -from .helpers.global_data import OsOpsDescrs -from .helpers.global_data import OsOpsDescr -from .helpers.global_data import PostgresNodeService -from .helpers.global_data import PostgresNodeServices -from .helpers.global_data import OsOperations -from .helpers.global_data import PortManager -from .helpers.pg_cfg_os_ops import PgCfgOsOps +from tests.helpers.global_data import OsOpsDescrs +from tests.helpers.global_data import OsOpsDescr +from tests.helpers.global_data import PostgresNodeService +from tests.helpers.global_data import PostgresNodeServices +from tests.helpers.global_data import OsOperations +from tests.helpers.global_data import PortManager +from tests.helpers.pg_cfg_os_ops import PgCfgOsOps +from tests.helpers.pg_msg_builder import PgMsgBuilder + +from tests.conftest_helpers import TestServices from src import __version__ as testgres_version from src.node import PostgresNode @@ -27,6 +30,7 @@ from src import IsolationLevel from src import NodeApp from src import enums +from src import consts as testgres_consts # New name prevents to collect test-functions in TestgresException and fixes # the problem with pytest warning. @@ -57,9 +61,12 @@ import typing import types import psutil +import threading import testgres.postgres_configuration as testgres_pgconf from packaging.version import Version +from concurrent.futures import ThreadPoolExecutor +from concurrent.futures import Future as ThreadFuture @contextmanager @@ -628,24 +635,38 @@ def test_status__empty_postmaster_pid(self, node_svc: PostgresNodeService): node.init() - postmaster_pid_file = node.os_ops.build_path(node.data_dir, "postmaster.pid") + postmaster_pid_file = node.os_ops.build_path( + node.data_dir, + testgres_consts.PG_PID_FILE, + ) node.os_ops.write( postmaster_pid_file, - "" + "", ) with pytest.raises(expected_exception=ExecUtilException) as x: node.status() - expected_msg = "pg_ctl: the PID file \"{}\" is empty\n".format( - postmaster_pid_file - ) + expected_msg = PgMsgBuilder.pg_ctl__pid_file_is_empty( + postmaster_pid_file, + ) + "\n" assert expected_msg == x.value.error return - sm_false_true = [False, True] + sm_sleep_time_after_clean_pm_pid = [ + 0, + 1, + 5, + 10, + 30, + 50, + 55, + 60, + 65, + 90, + ] @pytest.fixture( params=[ @@ -653,20 +674,22 @@ def test_status__empty_postmaster_pid(self, node_svc: PostgresNodeService): x, id="sleep_after_clean={}".format(x), ) - for x in sm_false_true + for x in sm_sleep_time_after_clean_pm_pid ] ) - def sleep_after_clean(self, request: pytest.FixtureRequest) -> bool: + def sleep_time_after_clean_pm_pid(self, request: pytest.FixtureRequest) -> int: assert isinstance(request, pytest.FixtureRequest) - assert type(request.param) is bool + assert type(request.param) is int return request.param def test_status__force_clean_postmaster_pid( self, node_svc: PostgresNodeService, - sleep_after_clean: bool, + sleep_time_after_clean_pm_pid: int, ): assert isinstance(node_svc, PostgresNodeService) + assert type(sleep_time_after_clean_pm_pid) is int + assert sleep_time_after_clean_pm_pid >= 0 assert (NodeStatus.Running) assert not (NodeStatus.Stopped) @@ -683,10 +706,13 @@ def test_status__force_clean_postmaster_pid( assert node.status() == NodeStatus.Running logging.info("Postmaster PID is {}.".format(node.pid)) - postmaster_pid_file = node.os_ops.build_path(node.data_dir, "postmaster.pid") + postmaster_pid_file = node.os_ops.build_path( + node.data_dir, + testgres_consts.PG_PID_FILE, + ) logging.info("Clean postmaster pid file [{}].".format( - postmaster_pid_file + postmaster_pid_file, )) logging.info("Clean pid file...") @@ -696,10 +722,8 @@ def test_status__force_clean_postmaster_pid( truncate=True, ) - if sleep_after_clean: - # server removes pid file and shutdown within 60 seconds. - logging.info("SLEEP 65 sec!") - time.sleep(65) + # server removes pid file and shutdown within 60 seconds. + TestServices.SleepWithPrint(sleep_time_after_clean_pm_pid) logging.info("Check node status...") node_status: typing.Optional[NodeStatus] @@ -708,13 +732,37 @@ def test_status__force_clean_postmaster_pid( except ExecUtilException as e: logging.info("Catch exception ({}): {}".format( type(e).__name__, - str(e), + TestServices.ExceptionToHumanText(e), )) - expected_msg = "pg_ctl: the PID file \"{}\" is empty\n".format( - postmaster_pid_file - ) - assert expected_msg == e.error + error_is_detected = False + if e.exit_code != 1: + error_is_detected = True + logging.error("Unexpected exit_code: {}".format( + e.exit_code, + )) + + assert type(e.error) is str + assert e.error.endswith("\n") + + expected_msgs = [ + PgMsgBuilder.pg_ctl__pid_file_is_empty( + postmaster_pid_file, + ), + PgMsgBuilder.pg_ctl__invalid_data_in_pid_file( + postmaster_pid_file, + ), + ] + + if e.error[:-1] not in expected_msgs: + error_is_detected = True + logging.error("Unexpected error msg: {}".format( + e.error, + )) + + if error_is_detected: + raise RuntimeError("Unexpected exception is catched.") from e + else: assert node_status is not None @@ -833,6 +881,186 @@ def test_kill__ok( node.cleanup(release_resources=True) return + def test_kill__ok__mt( + self, + node_svc: PostgresNodeService + ): + assert isinstance(node_svc, PostgresNodeService) + + N_WORKERS = 4 + + class tagCtx: + m_stop_guard: typing.Any + m_stop_flag: bool + + def __init__(self): + self.m_stop_guard = threading.Lock() + self.m_stop_flag = False + + @property + def is_stopped(self) -> bool: + assert self.m_stop_guard is not None + with self.m_stop_guard: + assert type(self.m_stop_flag) is bool + return self.m_stop_flag + + def set_stop(self) -> None: + with self.m_stop_guard: + assert type(self.m_stop_flag) is bool + self.m_stop_flag = False + return + + def LOCAL__worker( + ctx: tagCtx, + worker_id: int, + node: PostgresNode, + ) -> bool: + assert type(ctx) is tagCtx + assert type(worker_id) is int + assert type(node) is PostgresNode + + logging.info("Worker [{}] is started.".format( + worker_id, + )) + + node_status: typing.Optional[NodeStatus] = None + + try: + while True: + if ctx.is_stopped: + logging.info("Worker [{}] is stopped.".format( + worker_id, + )) + break + + node_status = node.status() + assert type(node_status) is NodeStatus + + if node_status == NodeStatus.Stopped: + logging.info("Worker [{}] detected that node is stopped.".format( + worker_id, + )) + break + continue + except BaseException as e: + logging.error("Worker [id: {}] catch an exception ({}): {}".format( + worker_id, + type(e).__name__, + TestServices.ExceptionToHumanText(e), + )) + raise + + return node_status == NodeStatus.Stopped + + class tadWorkerData: + future: ThreadFuture + + workCtx = tagCtx() + + node = __class__.helper__get_node(node_svc) + + try: + assert isinstance(node, PostgresNode) + assert (node.pid == 0) + assert (node.status() == NodeStatus.Uninitialized) + + node.init() + assert not node.is_started + node.slow_start() + assert node.is_started + + assert node.status() == NodeStatus.Running + + workerDatas: typing.List[tadWorkerData] = list() + + logging.info("Worker are creating ...") + threadPool = ThreadPoolExecutor( + max_workers=N_WORKERS, + thread_name_prefix="ex_creator", + ) + nErrors = 0 + + try: + for n in range(N_WORKERS): + logging.info("worker #{} is creating ...".format(n)) + + workerDatas.append(tadWorkerData()) + + workerDatas[n].future = threadPool.submit( + LOCAL__worker, + workCtx, + n, + node, + ) + + assert workerDatas[n].future is not None + continue + + logging.info("OK. All the workers were created!") + except BaseException as e: + nErrors += 1 + logging.error("A problem is detected ({}): {}".format( + type(e).__name__, + TestServices.ExceptionToHumanText(e), + )) + + TestServices.SleepWithPrint(5) + + logging.info("Kill node") + node.kill() + assert not node.is_started + + TestServices.SleepWithPrint(5) + + workCtx.set_stop() + + logging.info("Will wait for stop of all the workers...") + + nWorkers = 0 + + assert type(workerDatas) is list + + for i in range(len(workerDatas)): + worker = workerDatas[i].future + + if worker is None: + break + + nWorkers += 1 + + assert isinstance(worker, ThreadFuture) + + try: + logging.info("Wait for worker #{}".format(i)) + worker_r = worker.result() + assert type(worker_r) is bool + + if worker_r is not True: + logging.error("Worker [{}] did not detect node stop.".format( + i + )) + except BaseException as e: + nErrors += 1 + logging.error("Worker #{} finished with error ({}): {}".format( + i, + type(e).__name__, + TestServices.ExceptionToHumanText(e), + )) + continue + + assert nWorkers == N_WORKERS + + if nErrors != 0: + raise RuntimeError("Some problems were detected. Please examine the log messages.") + finally: + workCtx.set_stop() + + if node.is_started: + node.stop() + + node.cleanup(release_resources=True) + return + def test_kill_backgroud_writer__ok( self, node_svc: PostgresNodeService diff --git a/tests/units/impl/platforms/internal_platform_utils/InternalPlatformUtils/test_set000__Research.py b/tests/units/impl/platforms/internal_platform_utils/InternalPlatformUtils/test_set000__Research.py new file mode 100755 index 00000000..0371dc28 --- /dev/null +++ b/tests/units/impl/platforms/internal_platform_utils/InternalPlatformUtils/test_set000__Research.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +from tests.helpers.global_data import OsOpsDescrs +from tests.helpers.global_data import OsOperations +from tests.helpers.run_conditions import RunConditions + +from src.exceptions import ExecUtilException + +import pytest +import subprocess +import logging +import typing +import io + + +class TestSet001__Reseach: + def test_000__zombie_file_via_python__linux( + self, + ): + os_ops = OsOpsDescrs.sm_local_os_ops_descr.os_ops + assert isinstance(os_ops, OsOperations) + + RunConditions.skip_if_darwin() + RunConditions.skip_if_windows() + + with pytest.raises(expected_exception=FileNotFoundError): + open("/proc/111892/stat") + + return + + def test_001__zombie_file_via_local_os_ops__linux( + self, + ): + os_ops = OsOpsDescrs.sm_local_os_ops_descr.os_ops + assert isinstance(os_ops, OsOperations) + + RunConditions.skip_if_darwin() + RunConditions.skip_if_windows() + + with pytest.raises(expected_exception=FileNotFoundError): + os_ops.read_binary("/proc/111892/stat", 0) + + return + + def test_002__zombie_file_via_remote_os_ops__linux( + self, + ): + os_ops = OsOpsDescrs.sm_remote_os_ops_descr.os_ops + assert isinstance(os_ops, OsOperations) + + RunConditions.skip_if_darwin() + RunConditions.skip_if_windows() + + with pytest.raises(expected_exception=ExecUtilException) as x: + os_ops.read_binary("/proc/111892/stat", 0) + + assert type(x.value) is ExecUtilException + assert x.value.exit_code == 1 + return + + def test_003__process_lookup_error_race_condition__linux( + self, + ): + RunConditions.skip_if_darwin() + RunConditions.skip_if_windows() + + proc: typing.Optional[subprocess.Popen] = None + file: typing.Optional[io.IOBase] = None + + try: + # 1: Run a long-running background process + proc = subprocess.Popen( + ["sleep", "120"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + pid = proc.pid + + logging.info("Process is created. PID={}.".format( + pid, + )) + + proc_stat_file = f"/proc/{pid}/stat" + + # 2. Successfully open the descriptor file while the process is still alive + logging.info("Open file [{}].".format( + proc_stat_file, + )) + + file = open(proc_stat_file, "rb") + + logging.info("File [{}] is open.".format( + proc_stat_file, + )) + + # 3. Hard kill the process (SIGKILL) and wait for the Linux kernel to clean up its structures + logging.info("Kill process.") + proc.kill() + logging.info("Wait process.") + proc.wait() + + # 4. We try to read from an already open file. + # We expect the Linux kernel to return ESRCH (Errno 3), and Python to throw a ProcessLookupError + logging.info("Try to read stat file.") + with pytest.raises(expected_exception=ProcessLookupError) as x: + file.read() + + logging.info("OK. Exception {} is catched.".format( + type(x.value).__name__, + )) + + assert type(x.value) is ProcessLookupError + finally: + if file: + file.close() + + if proc and proc.poll() is None: + proc.terminate() + proc.wait() + return