1<?php
2
3namespace ComboStrap;
4
5
6use dokuwiki\Search\Indexer;
7
8/**
9 * Adapted from the {@link Indexer::lock()}
10 * because the TaskRunner does not run serially
11 * Only the indexer does
12 * https://forum.dokuwiki.org/d/21044-taskrunner-running-multiple-times-eating-the-memory-lock
13 *
14 * Example with debug
15 * ```
16 * ComboLockTaskRunner(): Trying to get a lock
17 * ComboLockTaskRunner(): Locked
18 * runIndexer(): started
19 * Indexer: index for web:browser:selection up to date
20 * runSitemapper(): started
21 * runSitemapper(): finished
22 * sendDigest(): started
23 * sendDigest(): disabled
24 * runTrimRecentChanges(): started
25 * runTrimRecentChanges(): finished
26 * runTrimRecentChanges(1): started
27 * runTrimRecentChanges(1): finished
28 * ComboDispatchEvent(): Trying to get a lock
29 * ComboDispatchEvent(): Locked
30 * ComboDispatchEvent(): Lock Released
31 * ComboLockTaskRunner(): Lock Released
32 *
33 */
34class Lock
35{
36    private string $lockName;
37    private string $lockFile;
38    /**
39     * @var mixed|null
40     */
41    private $perm;
42    private int $timeOut = 5;
43    /**
44     * @var false|resource
45     */
46    private $filePointer = null;
47
48
49    /**
50     * @param string $name
51     */
52    public function __construct(string $name)
53    {
54        $this->lockName = $name;
55        global $conf;
56        $this->lockFile = $conf['lockdir'] . "/_{$this->lockName}.lock";
57        $this->perm = $conf['dperm'] ?? null;
58    }
59
60    public static function create(string $name): Lock
61    {
62        return new Lock($name);
63    }
64
65    /**
66     * @throws ExceptionTimeOut - with the timeout
67     */
68    function acquire(): Lock
69    {
70        $run = 0;
71        /**
72         * The flock function follows the semantics of the Unix system call bearing the same name.
73         * Flock utilizes ADVISORY locking only; that is:
74         * * other processes may ignore the lock completely it only affects those that call the flock call.
75         *
76         * * LOCK_SH means SHARED LOCK. Any number of processes MAY HAVE A SHARED LOCK simultaneously. It is commonly called a reader lock.
77         * * LOCK_EX means EXCLUSIVE LOCK. Only a single process may possess an exclusive lock to a given file at a time.
78         *
79         * ie if the file has been LOCKED with LOCK_SH in another process,
80         * * flock with LOCK_SH will SUCCEED.
81         * * flock with LOCK_EX will BLOCK UNTIL ALL READER LOCKS HAVE BEEN RELEASED.
82         *
83         * When the file is closed, the lock is released by the system anyway.
84         */
85        // LOCK_NB to not block the process
86        while (!$this->getLock()) {
87            sleep(1);
88            /**
89             * Old lock ? More than 10 minutes run
90             */
91            if (is_file($this->lockFile) && (time() - @filemtime($this->lockFile)) > 60 * 10) {
92                if (!@unlink($this->lockFile)) {
93                    throw new ExceptionRuntimeInternal("Removing the lock failed ($this->lockFile)");
94                }
95            }
96            $run++;
97            if ($run >= $this->timeOut) {
98                throw new ExceptionTimeOut("Unable to get the lock ($this->lockFile) for ($this->timeOut) seconds");
99            }
100        }
101        if ($this->perm) {
102            chmod($this->lockFile, $this->perm);
103        }
104        register_shutdown_function([Lock::class, 'shutdownHandling'], $this->lockName);
105        return $this;
106
107    }
108
109    /**
110     *
111     * A function that is called when the process shutdown
112     * due to time exceed for instance that cleans the lock created.
113     *
114     * https://www.php.net/manual/en/function.register-shutdown-function.php
115     *
116     * Why ?
117     * The lock are created in the `before` of the the task runner event
118     * and deleted in the `after` of the task runner event
119     * If their is an error somewhere such as as a timeout, the lock
120     * is not deleted and there is no task runner anymore for 5 minutes.
121     *
122     * @param $name - the lock name
123     * @return void
124     */
125    public static function shutdownHandling($name)
126    {
127        print "Lock::shutdownHandling(): Deleting the lock $name" . NL;
128        Lock::create($name)->release();
129    }
130
131    /**
132     * Release the lock
133     * and the resources
134     * (Need to be called in all cases)
135     */
136    function release()
137    {
138        if ($this->filePointer !== null) {
139            fclose($this->filePointer);
140            $this->filePointer = null;
141        }
142        if (file_exists($this->lockFile)) {
143            unlink($this->lockFile);
144        }
145    }
146
147    public function isReleased(): bool
148    {
149        return !file_exists($this->lockFile);
150    }
151
152    public function isLocked(): bool
153    {
154        return file_exists($this->lockFile);
155    }
156
157    public function setTimeout(int $int): Lock
158    {
159        $this->timeOut = $int;
160        return $this;
161    }
162
163    private function getLock(): bool
164    {
165        /**
166         * We test also on the file because
167         * on some operating systems, flock() is implemented at the process level.
168         *
169         * ie when using a multithreaded server API you may not be able to rely on flock()
170         * to protect files against other PHP scripts running in parallel threads of the same server instance
171         */
172        if (file_exists($this->lockFile)) {
173            return false;
174        }
175
176        if ($this->filePointer === null) {
177            $mode = "c"; // as specified in the doc
178            $this->filePointer = fopen($this->lockFile, $mode);
179        }
180        /**
181         * LOCK_EX: exclusive lock
182         * LOCK_NB: to not wait
183         */
184        return flock($this->filePointer, LOCK_EX | LOCK_NB);
185    }
186
187}
188