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