From 4d89c6be155cc20fa24e961e9168aee42c09cca6 Mon Sep 17 00:00:00 2001 From: Chris Burr Date: Mon, 19 Oct 2026 21:58:09 +0200 Subject: [PATCH] fix (WMS): clear the signal mask before starting the payload Creating a gfal2 context leaves most signals (SIGTERM, SIGINT, SIGQUIT, SIGALRM, ...) blocked in the calling thread. The payload inherits the signal mask of the thread that starts it, so it couldn't be stopped with SIGTERM, and Gaudi's EventWatchdog could not abort stuck jobs with SIGQUIT (gaudi/Gaudi#428). --- .../JobWrapper/JobWrapper.py | 4 +++ .../JobWrapper/test/Test_JobWrapper.py | 26 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py b/src/DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py index 30648ba3101..b82f8f134b8 100755 --- a/src/DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py +++ b/src/DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py @@ -18,6 +18,7 @@ import os import re import shutil +import signal import stat import sys import tarfile @@ -1616,6 +1617,9 @@ def run(self): """ log = gLogger.getSubLogger(self.__class__.__name__) + # Unblock signals left blocked by libraries such as gfal2, which the payload would inherit + signal.pthread_sigmask(signal.SIG_SETMASK, set()) + start = time.time() initialStat = os.times() log.verbose("Cmd called", self.cmd) diff --git a/src/DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapper.py b/src/DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapper.py index b30db4a6ccc..e0e180e4df3 100644 --- a/src/DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapper.py +++ b/src/DIRAC/WorkloadManagementSystem/JobWrapper/test/Test_JobWrapper.py @@ -2,6 +2,8 @@ import os import shutil +import signal +import sys import tempfile import time from pathlib import Path @@ -17,7 +19,8 @@ from DIRAC.Resources.Catalog.test.mock_FC import fc_mock from DIRAC.WorkloadManagementSystem.Client import JobMinorStatus, JobStatus from DIRAC.WorkloadManagementSystem.JobWrapper.JobExecutionCoordinator import JobExecutionCoordinator -from DIRAC.WorkloadManagementSystem.JobWrapper.JobWrapper import JobWrapper +from DIRAC.Core.Utilities.Subprocess import Subprocess +from DIRAC.WorkloadManagementSystem.JobWrapper.JobWrapper import ExecutionThread, JobWrapper gLogger.setLevel("DEBUG") @@ -221,6 +224,27 @@ def test_preProcess_dirac_jobexec_with_args(setup_job_wrapper): @pytest.mark.slow +@pytest.mark.skipif(not sys.platform.startswith("linux"), reason="Reads /proc/self/status") +def test_executionThread_clears_signal_mask(tmp_path): + """The payload must not inherit signals blocked in the JobWrapper (e.g. by gfal2).""" + stdout = tmp_path / "std.out" + executionResults = {} + blocked = {signal.SIGQUIT, signal.SIGTERM} + exeThread = ExecutionThread( + Subprocess(), "grep SigBlk /proc/self/status", 100, str(stdout), None, os.environ.copy(), executionResults + ) + oldMask = signal.pthread_sigmask(signal.SIG_BLOCK, blocked) + try: + # The thread inherits the mask of this one when it starts, as it would in the JobWrapper + exeThread.start() + finally: + signal.pthread_sigmask(signal.SIG_SETMASK, oldMask) + exeThread.join() + + assert executionResults["Thread"]["OK"], executionResults["Thread"] + assert stdout.read_text().split() == ["SigBlk:", "0000000000000000"] + + def test_processSuccessfulCommand(mocker): """Test the process method of the JobWrapper class: most common scenario.""" jw = JobWrapper()