diff --git a/cli_test.go b/cli_test.go index f9ee03fea2..964bb49907 100644 --- a/cli_test.go +++ b/cli_test.go @@ -5,6 +5,7 @@ import ( "log" "os" "os/exec" + "runtime" "testing" "github.com/dunglas/frankenphp" @@ -45,6 +46,27 @@ func TestExecuteCLICode(t *testing.T) { assert.Equal(t, stdoutStderrStr, `Hello World`) } +// Regression test for https://github.com/php/frankenphp/issues/1902. A +// long-running CLI script that installs pcntl_signal handlers must +// receive its own signals reliably +func TestExecuteScriptCLISignals(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("pcntl is not available on Windows") + } + if _, err := os.Stat("internal/testcli/testcli"); err != nil { + t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`") + } + + cmd := exec.Command("internal/testcli/testcli", "testdata/command-pcntl.php") + stdoutStderr, err := cmd.CombinedOutput() + var exitError *exec.ExitError + if errors.As(err, &exitError) && exitError.ExitCode() == 2 { + t.Skipf("pcntl/posix not available: %s", stdoutStderr) + } + assert.NoError(t, err, "output: %s", stdoutStderr) + assert.Contains(t, string(stdoutStderr), "ok") +} + func ExampleExecuteScriptCLI() { if len(os.Args) <= 1 { log.Println("Usage: my-program script.php") diff --git a/frankenphp.c b/frankenphp.c index 5fa9e6ce5b..382c43dd40 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -115,6 +115,46 @@ static void frankenphp_fork_child(void) { is_forked_child = true; } static void frankenphp_register_atfork(void) { pthread_atfork(NULL, NULL, frankenphp_fork_child); } + +/* pcntl signals delivered to a Go M segfault on PCNTL_G (no TSRM there) + * Block these in a constructor so Go's schedinit captures + * the mask and every M inherits it; execute_script_cli unblocks on its own + * pthread. Caddy's signal.Notify keeps working via runtime.ensureSigM. */ +static void frankenphp_fill_cli_signal_set(sigset_t *s) { + sigemptyset(s); +#ifdef SIGHUP + sigaddset(s, SIGHUP); +#endif +#ifdef SIGINT + sigaddset(s, SIGINT); +#endif +#ifdef SIGQUIT + sigaddset(s, SIGQUIT); +#endif +#ifdef SIGTERM + sigaddset(s, SIGTERM); +#endif +#ifdef SIGUSR1 + sigaddset(s, SIGUSR1); +#endif +#ifdef SIGUSR2 + sigaddset(s, SIGUSR2); +#endif +#ifdef SIGALRM + sigaddset(s, SIGALRM); +#endif +#ifdef SIGCHLD + sigaddset(s, SIGCHLD); +#endif +} + +__attribute__((constructor)) static void frankenphp_libpreinit(void) { + sigset_t set; + frankenphp_fill_cli_signal_set(&set); + /* Single-threaded at this point (constructors run before Go's runtime), + * so sigprocmask is sufficient and portable. */ + sigprocmask(SIG_BLOCK, &set, NULL); +} #endif /* Best-effort force-kill for stuck PHP threads. @@ -1599,6 +1639,12 @@ static void *execute_script_cli(void *arg) { void *exit_status; bool eval = (bool)arg; +#ifndef PHP_WIN32 + sigset_t cli_signals; + frankenphp_fill_cli_signal_set(&cli_signals); + pthread_sigmask(SIG_UNBLOCK, &cli_signals, NULL); +#endif + /* * The SAPI name "cli" is hardcoded into too many programs... let's usurp it. */ diff --git a/testdata/command-pcntl.php b/testdata/command-pcntl.php new file mode 100644 index 0000000000..966e1e2668 --- /dev/null +++ b/testdata/command-pcntl.php @@ -0,0 +1,47 @@ + 0, 'SIGUSR2' => 0, 'SIGALRM' => 0]; + +pcntl_signal(SIGUSR1, function () use (&$received) { $received['SIGUSR1']++; }); +pcntl_signal(SIGUSR2, function () use (&$received) { $received['SIGUSR2']++; }); +pcntl_signal(SIGALRM, function () use (&$received) { $received['SIGALRM']++; }); + +$pid = getmypid(); +$attempts = 0; +$deadline = microtime(true) + 1.5; +while (microtime(true) < $deadline) { + posix_kill($pid, SIGUSR1); + posix_kill($pid, SIGUSR2); + $attempts += 2; + usleep(500); +} + +pcntl_alarm(1); +$alarmDeadline = microtime(true) + 1.5; +while (microtime(true) < $alarmDeadline && $received['SIGALRM'] === 0) { + usleep(1000); +} + +if ($received['SIGUSR1'] === 0 || $received['SIGUSR2'] === 0) { + fwrite(STDERR, "missed user signals: " . json_encode($received) . " of $attempts attempts\n"); + exit(1); +} +if ($received['SIGALRM'] === 0) { + fwrite(STDERR, "missed SIGALRM from pcntl_alarm\n"); + exit(1); +} + +echo "ok\n"; +exit(0);