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