Completed
Push — master ( 540fce...b26f23 )
by Sean
04:04
created

CapistranoDeploymentBackend::deployRollback()   B

Complexity

Conditions 4
Paths 2

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 17
nc 2
nop 5
1
<?php
2
3
class CapistranoDeploymentBackend extends Object implements DeploymentBackend {
4
5
	protected $packageGenerator;
6
7
	public function getPackageGenerator() {
8
		return $this->packageGenerator;
9
	}
10
11
	public function setPackageGenerator(PackageGenerator $packageGenerator) {
12
		$this->packageGenerator = $packageGenerator;
13
	}
14
15
	/**
16
	 * Create a deployment strategy.
17
	 *
18
	 * @param \DNEnvironment $environment
19
	 * @param array $options
20
	 *
21
	 * @return DeploymentStrategy
22
	 */
23
	public function planDeploy(\DNEnvironment $environment, $options) {
24
		$strategy = new DeploymentStrategy($environment, $options);
25
26
		$currentBuild = $environment->CurrentBuild();
27
		$currentSha = $currentBuild ? $currentBuild->SHA : '-';
28
		if($currentSha !== $options['sha']) {
29
			$strategy->setChange('Code version', $currentSha, $options['sha']);
30
		}
31
		$strategy->setActionTitle('Confirm deployment');
32
		$strategy->setActionCode('fast');
33
		$strategy->setEstimatedTime('2');
34
35
		return $strategy;
36
	}
37
38
	/**
39
	 * Deploy the given build to the given environment.
40
	 *
41
	 * @param \DNEnvironment $environment
42
	 * @param \DeploynautLogFile $log
43
	 * @param \DNProject $project
44
	 * @param array $options
45
	 */
46
	public function deploy(
47
		\DNEnvironment $environment,
48
		\DeploynautLogFile $log,
49
		\DNProject $project,
50
		$options
51
	) {
52
		$name = $environment->getFullName();
53
		$repository = $project->getLocalCVSPath();
54
		$sha = $options['sha'];
55
56
		$args = array(
57
			'branch' => $sha,
58
			'repository' => $repository,
59
		);
60
61
		$this->extend('deployStart', $environment, $sha, $log, $project);
62
63
		$log->write(sprintf('Deploying "%s" to "%s"', $sha, $name));
64
65
		$this->enableMaintenance($environment, $log, $project);
66
67
		// Use a package generator if specified, otherwise run a direct deploy, which is the default behaviour
68
		// if build_filename isn't specified
69
		if($this->packageGenerator) {
70
			$log->write(sprintf('Using package generator "%s"', get_class($this->packageGenerator)));
71
72
			try {
73
				$args['build_filename'] = $this->packageGenerator->getPackageFilename($project->Name, $sha, $repository, $log);
74
			} catch (Exception $e) {
75
				$log->write($e->getMessage());
76
				throw $e;
77
			}
78
79
			if(empty($args['build_filename'])) {
80
				throw new RuntimeException('Failed to generate package.');
81
			}
82
		}
83
84
		$command = $this->getCommand('deploy', 'web', $environment, $args, $log);
85
		$command->run(function($type, $buffer) use($log) {
86
			$log->write($buffer);
87
		});
88
89
		$error = null;
90
91
		$deploySuccessful = $command->isSuccessful();
92
		if ($deploySuccessful) {
93
			// Deployment automatically removes .htaccess, i.e. disables maintenance. Fine to smoketest.
94
			$deploySuccessful = $this->smokeTest($environment, $log);
95
		}
96
97
		if (!$deploySuccessful) {
98
			$this->enableMaintenance($environment, $log, $project);
99
100
			$rollbackSuccessful = $this->deployRollback($environment, $log, $project, $options, $args);
101
			if ($rollbackSuccessful) {
102
				// Again, .htaccess removed, maintenance off.
103
				$rollbackSuccessful = $this->smokeTest($environment, $log);
104
			}
105
106
			if (!$rollbackSuccessful) {
107
				$this->enableMaintenance($environment, $log, $project);
108
				$currentBuild = $environment->CurrentBuild();
109
				$this->extend('deployRollbackFailure', $environment, $currentBuild, $log, $project);
110
				$log->write('Rollback failed');
111
112
				$error = $command->getErrorOutput();
113
			} else {
114
				$error = 'Rollback successful';
115
			}
116
		}
117
118
		// Regardless of outcome, try to clean up.
119
		$cleanup = $this->getCommand('deploy:cleanup', 'web', $environment, $args, $log);
120
		$cleanup->run(function($type, $buffer) use($log) {
121
			$log->write($buffer);
122
		});
123
		if(!$cleanup->isSuccessful()) {
124
			$this->extend('cleanupFailure', $environment, $sha, $log, $project);
125
			$log->write('Warning: Cleanup failed, but fine to continue. Needs manual cleanup sometime.');
126
		}
127
128
		$this->extend('deployEnd', $environment, $sha, $log, $project);
129
130
		if ($error!==null) {
131
			throw new RuntimeException($error);
132
		}
133
134
		$log->write(sprintf('Deploy of "%s" to "%s" finished', $sha, $name));
135
	}
136
137
	private function deployRollback(
138
		\DNEnvironment $environment,
139
		\DeploynautLogFile $log,
140
		\DNProject $project,
141
		$options,
142
		$args
143
	) {
144
		$sha = $options['sha'];
145
146
		$this->extend('deployFailure', $environment, $sha, $log, $project);
147
148
		$currentBuild = $environment->CurrentBuild();
149
		if (empty($currentBuild) || (!empty($options['no_rollback']) && $options['no_rollback'] !== 'false')) {
150
			throw new RuntimeException($command->getErrorOutput());
151
		}
152
153
		// re-run deploy with the current build sha to rollback
154
		$log->write('Deploy failed. Rolling back');
155
		$rollbackArgs = array_merge($args, ['branch' => $currentBuild->SHA]);
156
		$command = $this->getCommand('deploy', 'web', $environment, $rollbackArgs, $log);
157
		$command->run(function($type, $buffer) use($log) {
158
			$log->write($buffer);
159
		});
160
161
		return $command->isSuccessful();
162
	}
163
164
	/**
165
	 * @param \DNEnvironment $environment
166
	 * @return ArrayList
167
	 */
168
	public function getDeployOptions(\DNEnvironment $environment) {
169
		return new ArrayList([
170
			new PredeployBackupOption($environment->Usage === DNEnvironment::PRODUCTION),
171
			new NoRollbackDeployOption()
172
		]);
173
	}
174
175
	/**
176
	 * Enable a maintenance page for the given environment using the maintenance:enable Capistrano task.
177
	 */
178 View Code Duplication
	public function enableMaintenance(\DNEnvironment $environment, \DeploynautLogFile $log, \DNProject $project) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
179
		$name = $environment->getFullName();
180
		$command = $this->getCommand('maintenance:enable', 'web', $environment, null, $log);
181
		$command->run(function($type, $buffer) use($log) {
182
			$log->write($buffer);
183
		});
184
		if(!$command->isSuccessful()) {
185
			$this->extend('maintenanceEnableFailure', $environment, $log);
186
			throw new RuntimeException($command->getErrorOutput());
187
		}
188
		$log->write(sprintf('Maintenance page enabled on "%s"', $name));
189
	}
190
191
	/**
192
	 * Disable the maintenance page for the given environment using the maintenance:disable Capistrano task.
193
	 */
194 View Code Duplication
	public function disableMaintenance(\DNEnvironment $environment, \DeploynautLogFile $log, \DNProject $project) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
195
		$name = $environment->getFullName();
196
		$command = $this->getCommand('maintenance:disable', 'web', $environment, null, $log);
197
		$command->run(function($type, $buffer) use($log) {
198
			$log->write($buffer);
199
		});
200
		if(!$command->isSuccessful()) {
201
			$this->extend('maintenanceDisableFailure', $environment, $log);
202
			throw new RuntimeException($command->getErrorOutput());
203
		}
204
		$log->write(sprintf('Maintenance page disabled on "%s"', $name));
205
	}
206
207
	/**
208
	 * Check the status using the deploy:check capistrano method
209
	 */
210
	public function ping(\DNEnvironment $environment, \DeploynautLogFile $log, \DNProject $project) {
211
		$command = $this->getCommand('deploy:check', 'web', $environment, null, $log);
212
		$command->run(function($type, $buffer) use($log) {
213
			$log->write($buffer);
214
			echo $buffer;
215
		});
216
	}
217
218
	/**
219
	 * @inheritdoc
220
	 */
221
	public function dataTransfer(\DNDataTransfer $dataTransfer, \DeploynautLogFile $log) {
222
		if($dataTransfer->Direction == 'get') {
223
			$this->dataTransferBackup($dataTransfer, $log);
224
		} else {
225
			$environment = $dataTransfer->Environment();
226
			$project = $environment->Project();
227
			$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
228
			$archive = $dataTransfer->DataArchive();
229
230
			// extract the sspak contents, we'll need these so capistrano can restore that content
231
			try {
232
				$archive->extractArchive($workingDir);
233
			} catch(Exception $e) {
234
				$log->write($e->getMessage());
235
				throw new RuntimeException($e->getMessage());
236
			}
237
238
			// validate the contents match the requested transfer mode
239
			$result = $archive->validateArchiveContents($dataTransfer->Mode);
240
			if(!$result->valid()) {
241
				// do some cleaning, get rid of the extracted archive lying around
242
				$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
243
				$process->setTimeout(120);
244
				$process->run();
245
246
				// log the reason why we can't restore the snapshot and halt the process
247
				$log->write($result->message());
248
				throw new RuntimeException($result->message());
249
			}
250
251
			// Put up a maintenance page during a restore of db or assets.
252
			$this->enableMaintenance($environment, $log, $project);
253
			$this->dataTransferRestore($workingDir, $dataTransfer, $log);
254
			$this->disableMaintenance($environment, $log, $project);
255
		}
256
	}
257
258
	/**
259
	 * @param string $action Capistrano action to be executed
260
	 * @param string $roles Defining a server role is required to target only the required servers.
261
	 * @param \DNEnvironment $environment
262
	 * @param array<string>|null $args Additional arguments for process
263
	 * @param \DeploynautLogFile $log
264
	 * @return \Symfony\Component\Process\Process
265
	 */
266
	public function getCommand($action, $roles, \DNEnvironment $environment, $args = null, \DeploynautLogFile $log) {
267
		$name = $environment->getFullName();
268
		$env = $environment->Project()->getProcessEnv();
269
270
		if(!$args) {
271
			$args = array();
272
		}
273
		$args['history_path'] = realpath(DEPLOYNAUT_LOG_PATH . '/');
274
		$args['environment_id'] = $environment->ID;
275
276
		// Inject env string directly into the command.
277
		// Capistrano doesn't like the $process->setEnv($env) we'd normally do below.
278
		$envString = '';
279
		if(!empty($env)) {
280
			$envString .= 'env ';
281
			foreach($env as $key => $value) {
282
				$envString .= "$key=\"$value\" ";
283
			}
284
		}
285
286
		$data = DNData::inst();
287
		// Generate a capfile from a template
288
		$capTemplate = file_get_contents(BASE_PATH . '/deploynaut/Capfile.template');
289
		$cap = str_replace(
290
			array('<config root>', '<ssh key>', '<base path>'),
291
			array($data->getEnvironmentDir(), DEPLOYNAUT_SSH_KEY, BASE_PATH),
292
			$capTemplate
293
		);
294
295
		if(defined('DEPLOYNAUT_CAPFILE')) {
296
			$capFile = DEPLOYNAUT_CAPFILE;
297
		} else {
298
			$capFile = ASSETS_PATH . '/Capfile';
299
		}
300
		file_put_contents($capFile, $cap);
301
302
		$command = "{$envString}cap -f " . escapeshellarg($capFile) . " -vv $name $action ROLES=$roles";
303
		foreach($args as $argName => $argVal) {
304
			$command .= ' -s ' . escapeshellarg($argName) . '=' . escapeshellarg($argVal);
305
		}
306
307
		$log->write(sprintf('Running command: %s', $command));
308
309
		$process = new AbortableProcess($command);
310
		$process->setTimeout(3600);
311
		return $process;
312
	}
313
314
	/**
315
	 * Backs up database and/or assets to a designated folder,
316
	 * and packs up the files into a single sspak.
317
	 *
318
	 * @param \DNDataTransfer    $dataTransfer
319
	 * @param DeploynautLogFile $log
320
	 */
321
	protected function dataTransferBackup(\DNDataTransfer $dataTransfer, \DeploynautLogFile $log) {
322
		$environment = $dataTransfer->Environment();
323
		$name = $environment->getFullName();
324
325
		// Associate a new archive with the transfer.
326
		// Doesn't retrieve a filepath just yet, need to generate the files first.
327
		$dataArchive = DNDataArchive::create();
328
		$dataArchive->Mode = $dataTransfer->Mode;
329
		$dataArchive->AuthorID = $dataTransfer->AuthorID;
330
		$dataArchive->OriginalEnvironmentID = $environment->ID;
331
		$dataArchive->EnvironmentID = $environment->ID;
332
		$dataArchive->IsBackup = $dataTransfer->IsBackupDataTransfer();
333
334
		// Generate directory structure with strict permissions (contains very sensitive data)
335
		$filepathBase = $dataArchive->generateFilepath($dataTransfer);
336
		mkdir($filepathBase, 0700, true);
337
338
		$databasePath = $filepathBase . DIRECTORY_SEPARATOR . 'database.sql.gz';
339
340
		// Backup database
341 View Code Duplication
		if(in_array($dataTransfer->Mode, array('all', 'db'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
342
			$log->write(sprintf('Backup of database from "%s" started', $name));
343
			$command = $this->getCommand('data:getdb', 'db', $environment, array('data_path' => $databasePath), $log);
344
			$command->run(function($type, $buffer) use($log) {
345
				$log->write($buffer);
346
			});
347
			if(!$command->isSuccessful()) {
348
				$this->extend('dataTransferFailure', $environment, $log);
349
				throw new RuntimeException($command->getErrorOutput());
350
			}
351
			$log->write(sprintf('Backup of database from "%s" done', $name));
352
		}
353
354
		// Backup assets
355 View Code Duplication
		if(in_array($dataTransfer->Mode, array('all', 'assets'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
356
			$log->write(sprintf('Backup of assets from "%s" started', $name));
357
			$command = $this->getCommand('data:getassets', 'web', $environment, array('data_path' => $filepathBase), $log);
358
			$command->run(function($type, $buffer) use($log) {
359
				$log->write($buffer);
360
			});
361
			if(!$command->isSuccessful()) {
362
				$this->extend('dataTransferFailure', $environment, $log);
363
				throw new RuntimeException($command->getErrorOutput());
364
			}
365
			$log->write(sprintf('Backup of assets from "%s" done', $name));
366
		}
367
368
		// ensure the database connection is re-initialised, which is needed if the transfer
369
		// above took a really long time because the handle to the db may have become invalid.
370
		global $databaseConfig;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
371
		DB::connect($databaseConfig);
372
373
		$log->write('Creating sspak...');
374
375
		$sspakFilename = sprintf('%s.sspak', $dataArchive->generateFilename($dataTransfer));
376
		$sspakFilepath = $filepathBase . DIRECTORY_SEPARATOR . $sspakFilename;
377
378
		try {
379
			$dataArchive->attachFile($sspakFilepath, $dataTransfer);
380
			$dataArchive->setArchiveFromFiles($filepathBase);
381
		} catch(Exception $e) {
382
			$log->write($e->getMessage());
383
			throw new RuntimeException($e->getMessage());
384
		}
385
386
		// Remove any assets and db files lying around, they're not longer needed as they're now part
387
		// of the sspak file we just generated. Use --force to avoid errors when files don't exist,
388
		// e.g. when just an assets backup has been requested and no database.sql exists.
389
		$process = new AbortableProcess(sprintf('rm -rf %s/assets && rm -f %s', escapeshellarg($filepathBase), escapeshellarg($databasePath)));
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 137 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
390
		$process->setTimeout(120);
391
		$process->run();
392
		if(!$process->isSuccessful()) {
393
			$log->write('Could not delete temporary files');
394
			throw new RuntimeException($process->getErrorOutput());
395
		}
396
397
		$log->write(sprintf('Creating sspak file done: %s', $dataArchive->ArchiveFile()->getAbsoluteURL()));
398
	}
399
400
	/**
401
	 * Utility function for triggering the db rebuild and flush.
402
	 * Also cleans up and generates new error pages.
403
	 * @param DeploynautLogFile $log
404
	 */
405 View Code Duplication
	public function rebuild(\DNEnvironment $environment, $log) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
406
		$name = $environment->getFullName();
407
		$command = $this->getCommand('deploy:migrate', 'web', $environment, null, $log);
408
		$command->run(function($type, $buffer) use($log) {
409
			$log->write($buffer);
410
		});
411
		if(!$command->isSuccessful()) {
412
			$log->write(sprintf('Rebuild of "%s" failed: %s', $name, $command->getErrorOutput()));
413
			throw new RuntimeException($command->getErrorOutput());
414
		}
415
		$log->write(sprintf('Rebuild of "%s" done', $name));
416
	}
417
418
	/**
419
	 * Extracts a *.sspak file referenced through the passed in $dataTransfer
420
	 * and pushes it to the environment referenced in $dataTransfer.
421
	 *
422
	 * @param string $workingDir Directory for the unpacked files.
423
	 * @param DNDataTransfer $dataTransfer
424
	 * @param DeploynautLogFile $log
425
	 */
426
	protected function dataTransferRestore($workingDir, \DNDataTransfer $dataTransfer, \DeploynautLogFile $log) {
427
		$environment = $dataTransfer->Environment();
428
		$name = $environment->getFullName();
429
430
		// Rollback cleanup.
431
		$self = $this;
432 View Code Duplication
		$cleanupFn = function() use($self, $workingDir, $environment, $log) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
433
			// Rebuild makes sense even if failed - maybe we can at least partly recover.
434
			$self->rebuild($environment, $log);
435
			$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
436
			$process->setTimeout(120);
437
			$process->run();
438
		};
439
440
		// Restore database into target environment
441 View Code Duplication
		if(in_array($dataTransfer->Mode, array('all', 'db'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
442
			$log->write(sprintf('Restore of database to "%s" started', $name));
443
			$args = array('data_path' => $workingDir . DIRECTORY_SEPARATOR . 'database.sql');
444
			$command = $this->getCommand('data:pushdb', 'db', $environment, $args, $log);
445
			$command->run(function($type, $buffer) use($log) {
446
				$log->write($buffer);
447
			});
448
			if(!$command->isSuccessful()) {
449
				$cleanupFn();
450
				$log->write(sprintf('Restore of database to "%s" failed: %s', $name, $command->getErrorOutput()));
451
				$this->extend('dataTransferFailure', $environment, $log);
452
				throw new RuntimeException($command->getErrorOutput());
453
			}
454
			$log->write(sprintf('Restore of database to "%s" done', $name));
455
		}
456
457
		// Restore assets into target environment
458 View Code Duplication
		if(in_array($dataTransfer->Mode, array('all', 'assets'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
459
			$log->write(sprintf('Restore of assets to "%s" started', $name));
460
			$args = array('data_path' => $workingDir . DIRECTORY_SEPARATOR . 'assets');
461
			$command = $this->getCommand('data:pushassets', 'web', $environment, $args, $log);
462
			$command->run(function($type, $buffer) use($log) {
463
				$log->write($buffer);
464
			});
465
			if(!$command->isSuccessful()) {
466
				$cleanupFn();
467
				$log->write(sprintf('Restore of assets to "%s" failed: %s', $name, $command->getErrorOutput()));
468
				$this->extend('dataTransferFailure', $environment, $log);
469
				throw new RuntimeException($command->getErrorOutput());
470
			}
471
			$log->write(sprintf('Restore of assets to "%s" done', $name));
472
		}
473
474
		$log->write('Rebuilding and cleaning up');
475
		$cleanupFn();
476
	}
477
478
	/**
479
	 * This is mostly copy-pasted from Anthill/Smoketest.
480
	 *
481
	 * @param \DNEnvironment $environment
482
	 * @param \DeploynautLogFile $log
483
	 * @return bool
484
	 */
485
	protected function smokeTest(\DNEnvironment $environment, \DeploynautLogFile $log) {
486
		$url = $environment->getBareURL();
487
		$timeout = 600;
488
		$tick = 60;
489
490
		if(!$url) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $url of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
491
			$log->write('Skipping site accessible check: no URL found.');
492
			return true;
493
		}
494
495
		$start = time();
496
		$infoTick = time() + $tick;
497
498
		$log->write(sprintf(
499
			'Waiting for "%s" to become accessible... (timeout: %smin)',
500
			$url,
501
			$timeout / 60
502
		));
503
504
		// configure curl so that curl_exec doesn't wait a long time for a response
505
		$ch = curl_init();
506
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
507
		curl_setopt($ch, CURLOPT_TIMEOUT, 5);
508
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
509
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
510
		curl_setopt($ch, CURLOPT_MAXREDIRS, 10); // set a high number of max redirects (but not infinite amount) to avoid a potential infinite loop
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 141 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
511
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
512
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
513
		curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
514
		curl_setopt($ch, CURLOPT_URL, $url);
515
		curl_setopt($ch, CURLOPT_USERAGENT, 'Rainforest');
516
		$success = false;
517
518
		// query the site every second. Note that if the URL doesn't respond,
519
		// curl_exec will take 5 seconds to timeout (see CURLOPT_CONNECTTIMEOUT and CURLOPT_TIMEOUT above)
520
		do {
521
			if(time() > $start + $timeout) {
522
				$log->write(sprintf(' * Failed: check for %s timed out after %smin', $url, $timeout / 60));
523
				return false;
524
			}
525
526
			$response = curl_exec($ch);
527
528
			// check the HTTP response code for HTTP protocols
529
			$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
530
			if($status && !in_array($status, [500, 501, 502, 503, 504])) {
531
				$success = true;
532
			}
533
534
			// check for any curl errors, mostly for checking the response state of non-HTTP protocols,
535
			// but applies to checks of any protocol
536
			if($response && !curl_errno($ch)) {
537
				$success = true;
538
			}
539
540
			// Produce an informational ticker roughly every $tick
541
			if (time() > $infoTick) {
542
				$message = [];
543
544
				// Collect status information from different sources.
545
				if ($status) {
546
					$message[] = sprintf('HTTP status code is %s', $status);
547
				}
548
				if (!$response) {
549
					$message[] = 'response is empty';
550
				}
551
				if ($error = curl_error($ch)) {
552
					$message[] = sprintf('request error: %s', $error);
553
				}
554
555
				$log->write(sprintf(
556
					' * Still waiting: %s...',
557
					implode(', ', $message)
558
				));
559
560
				$infoTick = time() + $tick;
561
			}
562
563
			sleep(1);
564
		} while(!$success);
565
566
		curl_close($ch);
567
		$log->write(' * Success: site is accessible!');
568
		return true;
569
	}
570
571
}
572