File: PcntlTimeout.php

package info (click to toggle)
php-malkusch-lock 2.2.1%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 412 kB
  • sloc: php: 2,193; makefile: 19
file content (113 lines) | stat: -rw-r--r-- 3,351 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
<?php

declare(strict_types=1);

namespace malkusch\lock\util;

use InvalidArgumentException;
use malkusch\lock\exception\DeadlineException;
use malkusch\lock\exception\LockAcquireException;
use RuntimeException;

/**
 * Timeout based on a scheduled alarm.
 *
 * This class requires the pcntl module and supports the cli sapi only.
 *
 * @author Markus Malkusch <markus@malkusch.de>
 * @link bitcoin:1335STSwu9hST4vcMRppEPgENMHD2r1REK Donations
 * @license WTFPL
 * @internal
 */
final class PcntlTimeout
{
    /**
     * @var int Timeout in seconds
     */
    private $timeout;

    /**
     * Builds the timeout.
     *
     * @param int $timeout Timeout in seconds.
     * @throws \RuntimeException When the PCNTL module is not enabled.
     * @throws \InvalidArgumentException When the timeout is zero or negative.
     */
    public function __construct(int $timeout)
    {
        if (!self::isSupported()) {
            throw new RuntimeException('PCNTL module not enabled');
        }

        if ($timeout <= 0) {
            throw new InvalidArgumentException(
                'Timeout must be positive and non zero'
            );
        }

        $this->timeout = $timeout;
    }

    /**
     * Runs the code and would eventually time out.
     *
     * This method has the side effect, that any signal handler for SIGALRM will
     * be reset to the default hanlder (SIG_DFL). It also expects that there is
     * no previously scheduled alarm. If your application uses alarms
     * ({@link pcntl_alarm()}) or a signal handler for SIGALRM, don't use this
     * method. It will interfer with your application and lead to unexpected
     * behaviour.
     *
     * @param  callable $code Executed code block
     * @throws \malkusch\lock\exception\DeadlineException Running the code hit
     * the deadline.
     * @throws \malkusch\lock\exception\LockAcquireException Installing the
     * timeout failed.
     * @return mixed Return value of the executed block
     */
    public function timeBoxed(callable $code)
    {
        $existingHandler = pcntl_signal_get_handler(SIGALRM);

        $signal = pcntl_signal(SIGALRM, function (): void {
            throw new DeadlineException(sprintf(
                'Timebox hit deadline of %d seconds',
                $this->timeout
            ));
        });
        if (!$signal) {
            throw new LockAcquireException('Could not install signal');
        }

        $oldAlarm = pcntl_alarm($this->timeout);
        if ($oldAlarm != 0) {
            throw new LockAcquireException('Existing alarm was not expected');
        }

        try {
            return $code();
        } finally {
            pcntl_alarm(0);
            pcntl_signal_dispatch();
            pcntl_signal(SIGALRM, $existingHandler);
        }
    }

    /**
     * Returns if this class is supported by the PHP runtime.
     *
     * This class requires the pcntl module. This method checks if
     * it is available.
     *
     * @return bool TRUE if this class is supported by the PHP runtime.
     */
    public static function isSupported(): bool
    {
        return
            PHP_SAPI === 'cli' &&
            extension_loaded('pcntl') &&
            function_exists('pcntl_alarm') &&
            function_exists('pcntl_signal') &&
            function_exists('pcntl_signal_dispatch');
    }
}