From 77e4a55baaab2d0e0e2e95ae5d376958318b0c0f 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 33baf91ce31..49e3174d988 100755 --- a/src/DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py +++ b/src/DIRAC/WorkloadManagementSystem/JobWrapper/JobWrapper.py @@ -17,6 +17,7 @@ import os import re import shutil +import signal import stat import sys import tarfile @@ -1645,6 +1646,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 e556fd40987..07d338618f3 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") @@ -223,6 +226,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()