xref: /plugin/combo/ComboStrap/Lock.php (revision 19a514cf2687640679beff1c5cd9b7f266669f2f)
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 */
14class Lock
15{
16    private string $lockName;
17    private string $lockFile;
18    /**
19     * @var mixed|null
20     */
21    private $perm;
22    private int $timeOut = 5;
23    /**
24     * @var false|resource
25     */
26    private $filePointer = null;
27
28
29    /**
30     * @param string $name
31     */
32    public function __construct(string $name)
33    {
34        $this->lockName = $name;
35        global $conf;
36        $this->lockFile = $conf['lockdir'] . "/_{$this->lockName}.lock";
37        $this->perm = $conf['dperm'] ?? null;
38    }
39
40    public static function create(string $name): Lock
41    {
42        return new Lock($name);
43    }
44
45    /**
46     * @throws ExceptionTimeOut - with the timeout
47     */
48    function acquire(): Lock
49    {
50        $run = 0;
51        /**
52         * The flock function follows the semantics of the Unix system call bearing the same name.
53         * Flock utilizes ADVISORY locking only; that is:
54         * * other processes may ignore the lock completely it only affects those that call the flock call.
55         *
56         * * LOCK_SH means SHARED LOCK. Any number of processes MAY HAVE A SHARED LOCK simultaneously. It is commonly called a reader lock.
57         * * LOCK_EX means EXCLUSIVE LOCK. Only a single process may possess an exclusive lock to a given file at a time.
58         *
59         * ie if the file has been LOCKED with LOCK_SH in another process,
60         * * flock with LOCK_SH will SUCCEED.
61         * * flock with LOCK_EX will BLOCK UNTIL ALL READER LOCKS HAVE BEEN RELEASED.
62         *
63         * When the file is closed, the lock is released by the system anyway.
64         */
65        // LOCK_NB to not block the process
66        while (!$this->getLock()) {
67            usleep(1000);
68            /**
69             * Old lock ? More than 10 minutes run
70             */
71            if (is_file($this->lockFile) && (time() - @filemtime($this->lockFile)) > 60 * 10) {
72                if (!@unlink($this->lockFile)) {
73                    throw new ExceptionRuntimeInternal("Removing the lock failed ($this->lockFile)");
74                }
75            }
76            $run++;
77            if ($run >= $this->timeOut) {
78                throw new ExceptionTimeOut("Unable to get the lock ($this->lockFile) for ($this->timeOut) seconds");
79            }
80        }
81        if ($this->perm) {
82            chmod($this->lockFile, $this->perm);
83        }
84        return $this;
85
86    }
87
88    /**
89     * Release the lock
90     * and the resources
91     * (Need to be called in all cases)
92     */
93    function release()
94    {
95        if ($this->filePointer !== null) {
96            fclose($this->filePointer);
97            $this->filePointer = null;
98        }
99        if (file_exists($this->lockFile)) {
100            unlink($this->lockFile);
101        }
102    }
103
104    public function isReleased(): bool
105    {
106        return !file_exists($this->lockFile);
107    }
108
109    public function isLocked(): bool
110    {
111        return file_exists($this->lockFile);
112    }
113
114    public function setTimeout(int $int): Lock
115    {
116        $this->timeOut = $int;
117        return $this;
118    }
119
120    private function getLock(): bool
121    {
122        /**
123         * We test also on the file because
124         * on some operating systems, flock() is implemented at the process level.
125         *
126         * ie when using a multithreaded server API you may not be able to rely on flock()
127         * to protect files against other PHP scripts running in parallel threads of the same server instance
128         */
129        if (file_exists($this->lockFile)) {
130            return false;
131        }
132
133        if ($this->filePointer === null) {
134            $mode = "c"; // as specified in the doc
135            $this->filePointer = fopen($this->lockFile, $mode);
136        }
137        /**
138         * LOCK_EX: exclusive lock
139         * LOCK_NB: to not wait
140         */
141        return flock($this->filePointer, LOCK_EX | LOCK_NB);
142    }
143
144}
145