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