PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 3.3.1
WP STAGING – WordPress Backup, Restore, Migration & Clone v3.3.1
4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Framework / Queue / FileSeekableQueue.php
wp-staging / Framework / Queue Last commit date
Storage 2 years ago FileSeekableQueue.php 2 years ago FinishedQueueException.php 5 years ago Queue.php 2 years ago QueueInterface.php 4 years ago SeekableQueueInterface.php 4 years ago
FileSeekableQueue.php
315 lines
1 <?php
2
3 namespace WPStaging\Framework\Queue;
4
5 use Error;
6 use Exception;
7 use RuntimeException;
8 use WPStaging\Core\Utils\Logger;
9 use WPStaging\Framework\Adapter\Directory;
10 use WPStaging\Framework\Filesystem\FileObject;
11 use WPStaging\Framework\Filesystem\Filesystem;
12 use WPStaging\Framework\Utils\Cache\Cache;
13
14 use function WPStaging\functions\debug_log;
15
16 class FileSeekableQueue implements SeekableQueueInterface, \SeekableIterator
17 {
18 /** @var string */
19 const FILE_EXTENSION = 'cache.php';
20
21 /** @var string The string identifier of this task */
22 protected $taskName;
23
24 /** @var FileObject The file resource that persists this queue */
25 protected $handle;
26
27 /** @var \Generator */
28 protected $fileGenerator;
29
30 /** @var Directory */
31 protected $directory;
32
33 /** @var Filesystem */
34 protected $filesystem;
35
36 /** @var int */
37 protected $offsetBefore;
38
39 /** @var bool */
40 protected $needsUnlock = false;
41
42 /** @var bool Whether the Queue is in write-only mode. */
43 protected $isWriteOnly;
44
45 public function __construct(Directory $directory, Filesystem $filesystem)
46 {
47 $this->directory = $directory;
48 $this->filesystem = $filesystem;
49 }
50
51 public function __destruct()
52 {
53 $this->shutdown();
54 }
55
56 /**
57 * @param string $taskName
58 * @param string $queueMode Either opens the Queue for read and write, or optimized to write-only.
59 * @return void
60 */
61 public function setup($taskName, $queueMode = SeekableQueueInterface::MODE_READ_WRITE)
62 {
63 $this->taskName = $taskName;
64
65 $extension = self::FILE_EXTENSION;
66 $path = "{$this->directory->getCacheDirectory()}$taskName.$extension";
67
68 $this->filesystem->mkdir(dirname($path), true);
69
70 $isNewQueue = $this->createQueue($path);
71
72 // Developer exception
73 if ($queueMode !== SeekableQueueInterface::MODE_WRITE && $queueMode !== SeekableQueueInterface::MODE_READ_WRITE) {
74 throw new \BadMethodCallException();
75 }
76
77 $this->handle = new FileObject($path, $queueMode);
78 $this->handle->setFlags(FileObject::DROP_NEW_LINE);
79 $this->fileGenerator = $this->initializeGenerator();
80
81 $this->isWriteOnly = $queueMode === SeekableQueueInterface::MODE_WRITE;
82
83 if ($this->isWriteOnly) {
84 $waitedTimes = 0;
85 do {
86 $wouldBlock = false;
87
88 /*
89 * Windows does not support LOCK_NB (Advisory locking), so we read from the return of flock.
90 * Unix supports LOCK_NB, so we read from the second parameter of flock.
91 */
92 $locked = $this->handle->flock(LOCK_EX | LOCK_NB, $wouldBlock) || (bool)!$wouldBlock;
93
94 if (!$locked) {
95 usleep(250000); // 0.25s
96 $waitedTimes++;
97 if ($waitedTimes > 5) {
98 throw new \RuntimeException(sprintf(esc_html__('Could not acquire exclusive lock for writing to Queue file: %s.task', 'wp-staging'), $this->taskName));
99 }
100 }
101 } while (!$locked);
102
103 $this->needsUnlock = true;
104 }
105
106 // Add the PHP header to the queue if it is a new queue
107 if ($isNewQueue) {
108 $this->enqueue(Cache::PHP_HEADER);
109 }
110 }
111
112 /**
113 * @return \Generator
114 */
115 protected function initializeGenerator()
116 {
117 while ($this->handle->valid()) {
118 $this->offsetBefore = $this->handle->ftell();
119 yield $this->handle->readAndMoveNext();
120 }
121 }
122
123 #[\ReturnTypeWillChange]
124 public function current()
125 {
126 return $this->fileGenerator->current();
127 }
128
129 #[\ReturnTypeWillChange]
130 public function next()
131 {
132 $this->fileGenerator->next();
133 }
134
135 #[\ReturnTypeWillChange]
136 public function key()
137 {
138 return $this->fileGenerator->key();
139 }
140
141 #[\ReturnTypeWillChange]
142 public function valid()
143 {
144 return $this->fileGenerator->valid();
145 }
146
147 #[\ReturnTypeWillChange]
148 public function rewind()
149 {
150 $this->handle->fseek(0);
151 }
152
153 #[\ReturnTypeWillChange]
154 public function seek($offset)
155 {
156 $this->handle->fseek($offset);
157 }
158
159 /**
160 * @return bool
161 */
162 public function isFinished(): bool
163 {
164 return $this->handle->eof();
165 }
166
167 /**
168 * @param bool $dequeue
169 * @return string|void
170 */
171 public function retry($dequeue = true)
172 {
173 $this->seek($this->offsetBefore);
174
175 if ($dequeue) {
176 return $this->dequeue();
177 }
178 }
179
180 /**
181 * @param string $data
182 * @return false|int
183 */
184 public function enqueue($data)
185 {
186 // Early bail: Write-only optimization
187 if ($this->isWriteOnly) {
188 $this->handle->fwrite(trim($data) . PHP_EOL);
189
190 return $this->handle->ftell();
191 }
192
193 $currentOffset = $this->handle->ftell();
194
195 $this->handle->fseek(0, SEEK_END);
196 $this->handle->flock(LOCK_EX);
197 $this->handle->fwrite(trim($data) . PHP_EOL);
198 $this->handle->flock(LOCK_UN);
199
200 $offsetEndOfQueue = $this->handle->ftell();
201 $this->handle->fseek($currentOffset);
202
203 return $offsetEndOfQueue;
204 }
205
206 /**
207 * @return string|false
208 */
209 public function dequeue()
210 {
211 if ($this->isWriteOnly) {
212 throw new \BadMethodCallException('Trying to read from read-only Queue');
213 }
214
215 $first = is_null($this->offsetBefore);
216
217 if (!$first) {
218 $this->next();
219 }
220
221 $current = $this->current();
222 if ($current !== rtrim(Cache::PHP_HEADER)) {
223 return $current;
224 }
225
226 $this->next();
227 return $this->current();
228 }
229
230 /**
231 * @param array $data
232 * @return false|int
233 */
234 public function enqueueMany(array $data = [])
235 {
236 foreach ($data as $item) {
237 if (is_scalar($item)) {
238 $this->enqueue((string)$item);
239 }
240 }
241
242 return $this->handle->ftell();
243 }
244
245 /**
246 * @return void
247 */
248 public function reset()
249 {
250 $this->handle->ftruncate(0);
251 }
252
253 /**
254 * @return false|int
255 */
256 public function getOffset()
257 {
258 if (!isset($this->handle) || !$this->handle instanceof FileObject) {
259 return false;
260 }
261
262 return $this->handle->ftell();
263 }
264
265 /**
266 * @return void
267 */
268 public function shutdown()
269 {
270 if ($this->needsUnlock && $this->handle instanceof FileObject) {
271 $this->unlockObject();
272 return;
273 }
274 }
275
276 /**
277 * @return void
278 */
279 protected function unlockObject()
280 {
281 try {
282 $this->handle->flock(LOCK_UN);
283 } catch (Exception $e) {
284 $message = $e->getMessage();
285 if ($message !== 'Object not initialized') {
286 debug_log("Unable to unlock handle " . $this->taskName . '.task : ' . $message, Logger::TYPE_DEBUG);
287 }
288 } catch (Error $e) {
289 $message = $e->getMessage();
290 if ($message !== 'Object not initialized') {
291 debug_log("Unable to unlock handle " . $this->taskName . '.task : ' . $message, Logger::TYPE_DEBUG);
292 }
293 }
294 }
295
296 /**
297 * @param string $path
298 * @return bool True if the file was created, false if it already existed
299 * @throws RuntimeException when file cannot be created
300 */
301 protected function createQueue(string $path): bool
302 {
303 if (file_exists($path)) {
304 return false;
305 }
306
307 if (!touch($path)) {
308 debug_log("Check if there is enough free space and the file permissions are 644 or 755. Could not create file: $path");
309 throw new RuntimeException(sprintf(esc_html__("Check if there is enough free space and the file permissions are 644 or 755. Could not create file: %s", 'wp-staging'), $path));
310 }
311
312 return true;
313 }
314 }
315