Completed
Pull Request — master (#723)
by Sean
04:32
created

CapistranoDeploymentBackend   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 559
Duplicated Lines 17.35 %

Coupling/Cohesion

Components 1
Dependencies 17

Importance

Changes 0
Metric Value
wmc 60
lcom 1
cbo 17
dl 97
loc 559
rs 3.5483
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getPackageGenerator() 0 3 1
A setPackageGenerator() 0 3 1
A planDeploy() 0 14 3
A enableMaintenance() 12 12 2
A disableMaintenance() 12 12 2
A ping() 0 7 1
A rebuild() 12 12 2
D deploy() 0 107 13
A getDeployOptions() 0 6 1
B dataTransfer() 0 36 4
B getCommand() 0 47 6
C dataTransferBackup() 24 78 7
B dataTransferRestore() 37 51 5
C smokeTest() 0 85 12

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like CapistranoDeploymentBackend often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CapistranoDeploymentBackend, and based on these observations, apply Extract Interface, too.

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
		// Deployment cleanup. We assume it is always safe to run this at the end, regardless of the outcome.
90
		$self = $this;
91
		$cleanupFn = function() use($self, $environment, $args, $log, $sha, $project) {
92
			$command = $self->getCommand('deploy:cleanup', 'web', $environment, $args, $log);
93
			$command->run(function($type, $buffer) use($log) {
94
				$log->write($buffer);
95
			});
96
97
			if(!$command->isSuccessful()) {
98
				$self->extend('cleanupFailure', $environment, $sha, $log, $project);
99
				$log->write('Warning: Cleanup failed, but fine to continue. Needs manual cleanup sometime.');
100
			}
101
		};
102
103
		// Once the deployment has run it's necessary to update the maintenance page status
104
		// as deploying removes .htaccess
105
		$this->enableMaintenance($environment, $log, $project);
106
107
		$rolledBack = null;
108
		if(!$command->isSuccessful() || !$this->smokeTest($environment, $log)) {
109
			$cleanupFn();
110
			$this->extend('deployFailure', $environment, $sha, $log, $project);
111
112
			$currentBuild = $environment->CurrentBuild();
113
			if (empty($currentBuild) || (!empty($options['no_rollback']) && $options['no_rollback'] !== 'false')) {
114
				throw new RuntimeException($command->getErrorOutput());
115
			}
116
117
			// re-run deploy with the current build sha to rollback
118
			$log->write('Deploy failed. Rolling back');
119
			$rollbackArgs = array_merge($args, ['branch' => $currentBuild->SHA]);
120
			$command = $this->getCommand('deploy', 'web', $environment, $rollbackArgs, $log);
121
			$command->run(function($type, $buffer) use($log) {
122
				$log->write($buffer);
123
			});
124
125
			// Once the deployment has run it's necessary to update the maintenance page status
126
			// as deploying removes .htaccess
127
			$this->enableMaintenance($environment, $log, $project);
128
129
			if (!$command->isSuccessful() || !$this->smokeTest($environment, $log)) {
130
				$this->extend('deployRollbackFailure', $environment, $currentBuild->SHA, $log, $project);
131
				$log->write('Rollback failed');
132
				throw new RuntimeException($command->getErrorOutput());
133
			}
134
135
			// By getting here, it means we have successfully rolled back without any errors
136
			$rolledBack = true;
137
		}
138
139
		$this->disableMaintenance($environment, $log, $project);
140
141
		$cleanupFn();
142
143
		// Rolling back means the rollback succeeded, but ultimately the deployment
144
		// has failed. Throw an exception so the job is marked as failed accordingly.
145
		if ($rolledBack === true) {
146
			throw new RuntimeException('Rollback successful');
147
		}
148
149
		$log->write(sprintf('Deploy of "%s" to "%s" finished', $sha, $name));
150
151
		$this->extend('deployEnd', $environment, $sha, $log, $project);
152
	}
153
154
	/**
155
	 * @param DNEnvironment $environment
156
	 * @return ArrayList
157
	 */
158
	public function getDeployOptions(DNEnvironment $environment) {
159
		return new ArrayList(
160
			new PredeployBackupOption($environment->Usage === DNEnvironment::PRODUCTION),
0 ignored issues
show
Documentation introduced by
new \PredeployBackupOpti...nvironment::PRODUCTION) is of type object<PredeployBackupOption>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
161
			new NoRollbackDeployOption()
0 ignored issues
show
Unused Code introduced by
The call to ArrayList::__construct() has too many arguments starting with new \NoRollbackDeployOption().

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
162
		);
163
	}
164
165
	/**
166
	 * Enable a maintenance page for the given environment using the maintenance:enable Capistrano task.
167
	 */
168 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...
169
		$name = $environment->getFullName();
170
		$command = $this->getCommand('maintenance:enable', 'web', $environment, null, $log);
171
		$command->run(function($type, $buffer) use($log) {
172
			$log->write($buffer);
173
		});
174
		if(!$command->isSuccessful()) {
175
			$this->extend('maintenanceEnableFailure', $environment, $log);
176
			throw new RuntimeException($command->getErrorOutput());
177
		}
178
		$log->write(sprintf('Maintenance page enabled on "%s"', $name));
179
	}
180
181
	/**
182
	 * Disable the maintenance page for the given environment using the maintenance:disable Capistrano task.
183
	 */
184 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...
185
		$name = $environment->getFullName();
186
		$command = $this->getCommand('maintenance:disable', 'web', $environment, null, $log);
187
		$command->run(function($type, $buffer) use($log) {
188
			$log->write($buffer);
189
		});
190
		if(!$command->isSuccessful()) {
191
			$this->extend('maintenanceDisableFailure', $environment, $log);
192
			throw new RuntimeException($command->getErrorOutput());
193
		}
194
		$log->write(sprintf('Maintenance page disabled on "%s"', $name));
195
	}
196
197
	/**
198
	 * Check the status using the deploy:check capistrano method
199
	 */
200
	public function ping(DNEnvironment $environment, DeploynautLogFile $log, DNProject $project) {
201
		$command = $this->getCommand('deploy:check', 'web', $environment, null, $log);
202
		$command->run(function($type, $buffer) use($log) {
203
			$log->write($buffer);
204
			echo $buffer;
205
		});
206
	}
207
208
	/**
209
	 * @inheritdoc
210
	 */
211
	public function dataTransfer(DNDataTransfer $dataTransfer, DeploynautLogFile $log) {
212
		if($dataTransfer->Direction == 'get') {
213
			$this->dataTransferBackup($dataTransfer, $log);
214
		} else {
215
			$environment = $dataTransfer->Environment();
216
			$project = $environment->Project();
217
			$workingDir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID;
218
			$archive = $dataTransfer->DataArchive();
219
220
			// extract the sspak contents, we'll need these so capistrano can restore that content
221
			try {
222
				$archive->extractArchive($workingDir);
223
			} catch(Exception $e) {
224
				$log->write($e->getMessage());
225
				throw new RuntimeException($e->getMessage());
226
			}
227
228
			// validate the contents match the requested transfer mode
229
			$result = $archive->validateArchiveContents($dataTransfer->Mode);
230
			if(!$result->valid()) {
231
				// do some cleaning, get rid of the extracted archive lying around
232
				$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
233
				$process->setTimeout(120);
234
				$process->run();
235
236
				// log the reason why we can't restore the snapshot and halt the process
237
				$log->write($result->message());
238
				throw new RuntimeException($result->message());
239
			}
240
241
			// Put up a maintenance page during a restore of db or assets.
242
			$this->enableMaintenance($environment, $log, $project);
243
			$this->dataTransferRestore($workingDir, $dataTransfer, $log);
244
			$this->disableMaintenance($environment, $log, $project);
245
		}
246
	}
247
248
	/**
249
	 * @param string $action Capistrano action to be executed
250
	 * @param string $roles Defining a server role is required to target only the required servers.
251
	 * @param DNEnvironment $environment
252
	 * @param array<string>|null $args Additional arguments for process
253
	 * @param DeploynautLogFile $log
254
	 * @return \Symfony\Component\Process\Process
255
	 */
256
	public function getCommand($action, $roles, DNEnvironment $environment, $args = null, DeploynautLogFile $log) {
257
		$name = $environment->getFullName();
258
		$env = $environment->Project()->getProcessEnv();
259
260
		if(!$args) {
261
			$args = array();
262
		}
263
		$args['history_path'] = realpath(DEPLOYNAUT_LOG_PATH . '/');
264
		$args['environment_id'] = $environment->ID;
265
266
		// Inject env string directly into the command.
267
		// Capistrano doesn't like the $process->setEnv($env) we'd normally do below.
268
		$envString = '';
269
		if(!empty($env)) {
270
			$envString .= 'env ';
271
			foreach($env as $key => $value) {
272
				$envString .= "$key=\"$value\" ";
273
			}
274
		}
275
276
		$data = DNData::inst();
277
		// Generate a capfile from a template
278
		$capTemplate = file_get_contents(BASE_PATH . '/deploynaut/Capfile.template');
279
		$cap = str_replace(
280
			array('<config root>', '<ssh key>', '<base path>'),
281
			array($data->getEnvironmentDir(), DEPLOYNAUT_SSH_KEY, BASE_PATH),
282
			$capTemplate
283
		);
284
285
		if(defined('DEPLOYNAUT_CAPFILE')) {
286
			$capFile = DEPLOYNAUT_CAPFILE;
287
		} else {
288
			$capFile = ASSETS_PATH . '/Capfile';
289
		}
290
		file_put_contents($capFile, $cap);
291
292
		$command = "{$envString}cap -f " . escapeshellarg($capFile) . " -vv $name $action ROLES=$roles";
293
		foreach($args as $argName => $argVal) {
294
			$command .= ' -s ' . escapeshellarg($argName) . '=' . escapeshellarg($argVal);
295
		}
296
297
		$log->write(sprintf('Running command: %s', $command));
298
299
		$process = new AbortableProcess($command);
300
		$process->setTimeout(3600);
301
		return $process;
302
	}
303
304
	/**
305
	 * Backs up database and/or assets to a designated folder,
306
	 * and packs up the files into a single sspak.
307
	 *
308
	 * @param DNDataTransfer    $dataTransfer
309
	 * @param DeploynautLogFile $log
310
	 */
311
	protected function dataTransferBackup(DNDataTransfer $dataTransfer, DeploynautLogFile $log) {
312
		$environment = $dataTransfer->Environment();
313
		$name = $environment->getFullName();
314
315
		// Associate a new archive with the transfer.
316
		// Doesn't retrieve a filepath just yet, need to generate the files first.
317
		$dataArchive = DNDataArchive::create();
318
		$dataArchive->Mode = $dataTransfer->Mode;
319
		$dataArchive->AuthorID = $dataTransfer->AuthorID;
320
		$dataArchive->OriginalEnvironmentID = $environment->ID;
321
		$dataArchive->EnvironmentID = $environment->ID;
322
		$dataArchive->IsBackup = $dataTransfer->IsBackupDataTransfer();
323
324
		// Generate directory structure with strict permissions (contains very sensitive data)
325
		$filepathBase = $dataArchive->generateFilepath($dataTransfer);
326
		mkdir($filepathBase, 0700, true);
327
328
		$databasePath = $filepathBase . DIRECTORY_SEPARATOR . 'database.sql.gz';
329
330
		// Backup database
331 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...
332
			$log->write(sprintf('Backup of database from "%s" started', $name));
333
			$command = $this->getCommand('data:getdb', 'db', $environment, array('data_path' => $databasePath), $log);
334
			$command->run(function($type, $buffer) use($log) {
335
				$log->write($buffer);
336
			});
337
			if(!$command->isSuccessful()) {
338
				$this->extend('dataTransferFailure', $environment, $log);
339
				throw new RuntimeException($command->getErrorOutput());
340
			}
341
			$log->write(sprintf('Backup of database from "%s" done', $name));
342
		}
343
344
		// Backup assets
345 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...
346
			$log->write(sprintf('Backup of assets from "%s" started', $name));
347
			$command = $this->getCommand('data:getassets', 'web', $environment, array('data_path' => $filepathBase), $log);
348
			$command->run(function($type, $buffer) use($log) {
349
				$log->write($buffer);
350
			});
351
			if(!$command->isSuccessful()) {
352
				$this->extend('dataTransferFailure', $environment, $log);
353
				throw new RuntimeException($command->getErrorOutput());
354
			}
355
			$log->write(sprintf('Backup of assets from "%s" done', $name));
356
		}
357
358
		// ensure the database connection is re-initialised, which is needed if the transfer
359
		// above took a really long time because the handle to the db may have become invalid.
360
		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...
361
		DB::connect($databaseConfig);
362
363
		$log->write('Creating sspak...');
364
365
		$sspakFilename = sprintf('%s.sspak', $dataArchive->generateFilename($dataTransfer));
366
		$sspakFilepath = $filepathBase . DIRECTORY_SEPARATOR . $sspakFilename;
367
368
		try {
369
			$dataArchive->attachFile($sspakFilepath, $dataTransfer);
370
			$dataArchive->setArchiveFromFiles($filepathBase);
371
		} catch(Exception $e) {
372
			$log->write($e->getMessage());
373
			throw new RuntimeException($e->getMessage());
374
		}
375
376
		// Remove any assets and db files lying around, they're not longer needed as they're now part
377
		// of the sspak file we just generated. Use --force to avoid errors when files don't exist,
378
		// e.g. when just an assets backup has been requested and no database.sql exists.
379
		$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...
380
		$process->setTimeout(120);
381
		$process->run();
382
		if(!$process->isSuccessful()) {
383
			$log->write('Could not delete temporary files');
384
			throw new RuntimeException($process->getErrorOutput());
385
		}
386
387
		$log->write(sprintf('Creating sspak file done: %s', $dataArchive->ArchiveFile()->getAbsoluteURL()));
388
	}
389
390
	/**
391
	 * Utility function for triggering the db rebuild and flush.
392
	 * Also cleans up and generates new error pages.
393
	 * @param DeploynautLogFile $log
394
	 */
395 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...
396
		$name = $environment->getFullName();
397
		$command = $this->getCommand('deploy:migrate', 'web', $environment, null, $log);
398
		$command->run(function($type, $buffer) use($log) {
399
			$log->write($buffer);
400
		});
401
		if(!$command->isSuccessful()) {
402
			$log->write(sprintf('Rebuild of "%s" failed: %s', $name, $command->getErrorOutput()));
403
			throw new RuntimeException($command->getErrorOutput());
404
		}
405
		$log->write(sprintf('Rebuild of "%s" done', $name));
406
	}
407
408
	/**
409
	 * Extracts a *.sspak file referenced through the passed in $dataTransfer
410
	 * and pushes it to the environment referenced in $dataTransfer.
411
	 *
412
	 * @param string $workingDir Directory for the unpacked files.
413
	 * @param DNDataTransfer $dataTransfer
414
	 * @param DeploynautLogFile $log
415
	 */
416
	protected function dataTransferRestore($workingDir, DNDataTransfer $dataTransfer, DeploynautLogFile $log) {
417
		$environment = $dataTransfer->Environment();
418
		$name = $environment->getFullName();
419
420
		// Rollback cleanup.
421
		$self = $this;
422 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...
423
			// Rebuild makes sense even if failed - maybe we can at least partly recover.
424
			$self->rebuild($environment, $log);
425
			$process = new AbortableProcess(sprintf('rm -rf %s', escapeshellarg($workingDir)));
426
			$process->setTimeout(120);
427
			$process->run();
428
		};
429
430
		// Restore database into target environment
431 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...
432
			$log->write(sprintf('Restore of database to "%s" started', $name));
433
			$args = array('data_path' => $workingDir . DIRECTORY_SEPARATOR . 'database.sql');
434
			$command = $this->getCommand('data:pushdb', 'db', $environment, $args, $log);
435
			$command->run(function($type, $buffer) use($log) {
436
				$log->write($buffer);
437
			});
438
			if(!$command->isSuccessful()) {
439
				$cleanupFn();
440
				$log->write(sprintf('Restore of database to "%s" failed: %s', $name, $command->getErrorOutput()));
441
				$this->extend('dataTransferFailure', $environment, $log);
442
				throw new RuntimeException($command->getErrorOutput());
443
			}
444
			$log->write(sprintf('Restore of database to "%s" done', $name));
445
		}
446
447
		// Restore assets into target environment
448 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...
449
			$log->write(sprintf('Restore of assets to "%s" started', $name));
450
			$args = array('data_path' => $workingDir . DIRECTORY_SEPARATOR . 'assets');
451
			$command = $this->getCommand('data:pushassets', 'web', $environment, $args, $log);
452
			$command->run(function($type, $buffer) use($log) {
453
				$log->write($buffer);
454
			});
455
			if(!$command->isSuccessful()) {
456
				$cleanupFn();
457
				$log->write(sprintf('Restore of assets to "%s" failed: %s', $name, $command->getErrorOutput()));
458
				$this->extend('dataTransferFailure', $environment, $log);
459
				throw new RuntimeException($command->getErrorOutput());
460
			}
461
			$log->write(sprintf('Restore of assets to "%s" done', $name));
462
		}
463
464
		$log->write('Rebuilding and cleaning up');
465
		$cleanupFn();
466
	}
467
468
	/**
469
	 * This is mostly copy-pasted from Anthill/Smoketest.
470
	 *
471
	 * @param DNEnvironment $environment
472
	 * @param DeploynautLogFile $log
473
	 * @return bool
474
	 */
475
	protected function smokeTest(DNEnvironment $environment, DeploynautLogFile $log) {
476
		$url = $environment->getBareURL();
477
		$timeout = 600;
478
		$tick = 60;
479
480
		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...
481
			$log->write('Skipping site accessible check: no URL found.');
482
			return true;
483
		}
484
485
		$start = time();
486
		$infoTick = time() + $tick;
487
488
		$log->write(sprintf(
489
			'Waiting for "%s" to become accessible... (timeout: %smin)',
490
			$url,
491
			$timeout / 60
492
		));
493
494
		// configure curl so that curl_exec doesn't wait a long time for a response
495
		$ch = curl_init();
496
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
497
		curl_setopt($ch, CURLOPT_TIMEOUT, 5);
498
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
499
		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
500
		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...
501
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
502
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
503
		curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
504
		curl_setopt($ch, CURLOPT_URL, $url);
505
		curl_setopt($ch, CURLOPT_USERAGENT, 'Rainforest');
506
		$success = false;
507
508
		// query the site every second. Note that if the URL doesn't respond,
509
		// curl_exec will take 5 seconds to timeout (see CURLOPT_CONNECTTIMEOUT and CURLOPT_TIMEOUT above)
510
		do {
511
			if(time() > $start + $timeout) {
512
				$log->write(sprintf(' * Failed: check for %s timed out after %smin', $url, $timeout / 60));
513
				return false;
514
			}
515
516
			$response = curl_exec($ch);
517
518
			// check the HTTP response code for HTTP protocols
519
			$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
520
			if($status && !in_array($status, [500, 501, 502, 503, 504])) {
521
				$success = true;
522
			}
523
524
			// check for any curl errors, mostly for checking the response state of non-HTTP protocols,
525
			// but applies to checks of any protocol
526
			if($response && !curl_errno($ch)) {
527
				$success = true;
528
			}
529
530
			// Produce an informational ticker roughly every $tick
531
			if (time() > $infoTick) {
532
				$message = [];
533
534
				// Collect status information from different sources.
535
				if ($status) {
536
					$message[] = sprintf('HTTP status code is %s', $status);
537
				}
538
				if (!$response) {
539
					$message[] = 'response is empty';
540
				}
541
				if ($error = curl_error($ch)) {
542
					$message[] = sprintf('request error: %s', $error);
543
				}
544
545
				$log->write(sprintf(
546
					' * Still waiting: %s...',
547
					implode(', ', $message)
548
				));
549
550
				$infoTick = time() + $tick;
551
			}
552
553
			sleep(1);
554
		} while(!$success);
555
556
		curl_close($ch);
557
		$log->write(' * Success: site is accessible!');
558
		return true;
559
	}
560
561
}
562