1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* this file is part of pipelines */ |
4
|
|
|
|
5
|
|
|
namespace Ktomk\Pipelines\Runner; |
6
|
|
|
|
7
|
|
|
use Ktomk\Pipelines\Cli\Docker; |
8
|
|
|
use Ktomk\Pipelines\Cli\Exec; |
9
|
|
|
use Ktomk\Pipelines\Cli\Streams; |
10
|
|
|
use Ktomk\Pipelines\DestructibleString; |
11
|
|
|
use Ktomk\Pipelines\File\Image; |
12
|
|
|
use Ktomk\Pipelines\File\Step; |
13
|
|
|
use Ktomk\Pipelines\Lib; |
14
|
|
|
use Ktomk\Pipelines\LibFs; |
15
|
|
|
use Ktomk\Pipelines\LibTmp; |
16
|
|
|
use Ktomk\Pipelines\Runner\Docker\ArtifactSource; |
17
|
|
|
use Ktomk\Pipelines\Runner\Docker\Binary\Repository; |
18
|
|
|
use Ktomk\Pipelines\Runner\Docker\ImageLogin; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* Runner for a single step of a pipeline |
22
|
|
|
*/ |
23
|
|
|
class StepRunner |
24
|
|
|
{ |
25
|
|
|
/** |
26
|
|
|
* @var RunOpts |
27
|
|
|
*/ |
28
|
|
|
private $runOpts; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var Directories |
32
|
|
|
*/ |
33
|
|
|
private $directories; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var Exec |
37
|
|
|
*/ |
38
|
|
|
private $exec; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var Flags |
42
|
|
|
*/ |
43
|
|
|
private $flags; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @var Env |
47
|
|
|
*/ |
48
|
|
|
private $env; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @var Streams |
52
|
|
|
*/ |
53
|
|
|
private $streams; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* list of temporary directory destructible markers |
57
|
|
|
* |
58
|
|
|
* @var array |
59
|
|
|
*/ |
60
|
|
|
private $temporaryDirectories = array(); |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* DockerSession constructor. |
64
|
|
|
* |
65
|
|
|
* @param RunOpts $runOpts |
66
|
|
|
* @param Directories $directories source repository root directory based directories object |
67
|
|
|
* @param Exec $exec |
68
|
|
|
* @param Flags $flags |
69
|
|
|
* @param Env $env |
70
|
|
|
* @param Streams $streams |
71
|
|
|
*/ |
72
|
23 |
|
public function __construct( |
73
|
|
|
RunOpts $runOpts, |
74
|
|
|
Directories $directories, |
75
|
|
|
Exec $exec, |
76
|
|
|
Flags $flags, |
77
|
|
|
Env $env, |
78
|
|
|
Streams $streams |
79
|
|
|
) |
80
|
|
|
{ |
81
|
23 |
|
$this->runOpts = $runOpts; |
82
|
23 |
|
$this->directories = $directories; |
83
|
23 |
|
$this->exec = $exec; |
84
|
23 |
|
$this->flags = $flags; |
85
|
23 |
|
$this->env = $env; |
86
|
23 |
|
$this->streams = $streams; |
87
|
23 |
|
} |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* @param Step $step |
91
|
|
|
* |
92
|
|
|
* @return null|int exist status of step script or null if the run operation failed |
93
|
|
|
*/ |
94
|
22 |
|
public function runStep(Step $step) |
95
|
|
|
{ |
96
|
22 |
|
$dir = $this->directories->getProjectDirectory(); |
97
|
22 |
|
$env = $this->env; |
98
|
22 |
|
$exec = $this->exec; |
99
|
22 |
|
$streams = $this->streams; |
100
|
|
|
|
101
|
22 |
|
$env->setPipelinesProjectPath($dir); |
102
|
|
|
|
103
|
22 |
|
$container = StepContainer::create($step, $exec); |
104
|
|
|
|
105
|
22 |
|
$name = $container->generateName( |
106
|
22 |
|
$this->runOpts->getPrefix(), |
107
|
22 |
|
$this->env->getValue('BITBUCKET_REPO_SLUG') ?: $this->directories->getName() |
108
|
|
|
); |
109
|
22 |
|
$env->setContainerName($name); |
110
|
|
|
|
111
|
22 |
|
$image = $step->getImage(); |
112
|
|
|
|
113
|
|
|
# launch container |
114
|
22 |
|
$streams->out(sprintf( |
115
|
22 |
|
"\x1D+++ step #%d\n\n name...........: %s\n effective-image: %s\n container......: %s\n", |
116
|
22 |
|
$step->getIndex() + 1, |
117
|
22 |
|
$step->getName() ? '"' . $step->getName() . '"' : '(unnamed)', |
118
|
22 |
|
$image->getName(), |
119
|
22 |
|
$name |
120
|
|
|
)); |
121
|
|
|
|
122
|
22 |
|
$id = $container->keepOrKill($this->flags->reuseContainer()); |
123
|
|
|
|
124
|
22 |
|
$deployCopy = $this->flags->deployCopy(); |
125
|
|
|
|
126
|
22 |
|
if (null === $id) { |
127
|
20 |
|
list($id, $status) = $this->runNewContainer($container, $dir, $deployCopy, $step); |
128
|
20 |
|
if (null === $id) { |
129
|
3 |
|
return $status; |
130
|
|
|
} |
131
|
|
|
} |
132
|
|
|
|
133
|
19 |
|
$streams->out(sprintf(" container-id...: %s\n\n", substr($id, 0, 12))); |
134
|
|
|
|
135
|
|
|
# TODO: different deployments, mount (default), mount-ro, copy |
136
|
19 |
|
if (null !== $result = $this->deployCopy($deployCopy, $id, $dir)) { |
137
|
2 |
|
return $result; |
138
|
|
|
} |
139
|
|
|
|
140
|
17 |
|
list($status, $message) = $this->deployDockerClient($step, $id); |
141
|
16 |
|
if (0 !== $status) { |
142
|
1 |
|
$this->streams->err(rtrim($message, "\n") . "\n"); |
143
|
|
|
|
144
|
1 |
|
return $status; |
145
|
|
|
} |
146
|
|
|
|
147
|
15 |
|
$status = $this->runStepScript($step, $streams, $exec, $name); |
148
|
|
|
|
149
|
15 |
|
$this->captureStepArtifacts($step, $deployCopy && 0 === $status, $id, $dir); |
150
|
|
|
|
151
|
15 |
|
$this->shutdownStepContainer($container, $status); |
152
|
|
|
|
153
|
15 |
|
return $status; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* method to wrap new to have a test-point |
158
|
|
|
* |
159
|
|
|
* @return Repository |
160
|
|
|
*/ |
161
|
2 |
|
public function getDockerBinaryRepository() |
162
|
|
|
{ |
163
|
2 |
|
$repo = Repository::create($this->exec, $this->directories); |
164
|
2 |
|
$repo->resolve($this->runOpts->getBinaryPackage()); |
165
|
|
|
|
166
|
1 |
|
return $repo; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* @param Step $step |
171
|
|
|
* @param bool $copy |
172
|
|
|
* @param string $id container id |
173
|
|
|
* @param string $dir to put artifacts in (project directory) |
174
|
|
|
* |
175
|
|
|
* @throws \RuntimeException |
176
|
|
|
*/ |
177
|
15 |
|
private function captureStepArtifacts(Step $step, $copy, $id, $dir) |
178
|
|
|
{ |
179
|
|
|
# capturing artifacts is only supported for deploy copy |
180
|
15 |
|
if (!$copy) { |
181
|
10 |
|
return; |
182
|
|
|
} |
183
|
|
|
|
184
|
5 |
|
$artifacts = $step->getArtifacts(); |
185
|
|
|
|
186
|
5 |
|
if (null === $artifacts) { |
187
|
2 |
|
return; |
188
|
|
|
} |
189
|
|
|
|
190
|
3 |
|
$exec = $this->exec; |
191
|
3 |
|
$streams = $this->streams; |
192
|
|
|
|
193
|
3 |
|
$streams->out("\x1D+++ copying artifacts from container...\n"); |
194
|
|
|
|
195
|
3 |
|
$source = new ArtifactSource($exec, $id, $dir); |
196
|
|
|
|
197
|
3 |
|
$patterns = $artifacts->getPatterns(); |
198
|
3 |
|
foreach ($patterns as $pattern) { |
199
|
3 |
|
$this->captureArtifactPattern($source, $pattern, $dir); |
200
|
|
|
} |
201
|
|
|
|
202
|
3 |
|
$streams(''); |
203
|
3 |
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* @param ArtifactSource $source |
207
|
|
|
* @param string $pattern |
208
|
|
|
* @param string $dir |
209
|
|
|
* |
210
|
|
|
* @throws \RuntimeException |
211
|
|
|
* @see Runner::captureStepArtifacts() |
212
|
|
|
* |
213
|
|
|
*/ |
214
|
3 |
|
private function captureArtifactPattern(ArtifactSource $source, $pattern, $dir) |
215
|
|
|
{ |
216
|
3 |
|
$exec = $this->exec; |
217
|
3 |
|
$streams = $this->streams; |
218
|
|
|
|
219
|
3 |
|
$id = $source->getId(); |
220
|
3 |
|
$paths = $source->findByPattern($pattern); |
221
|
3 |
|
if (empty($paths)) { |
222
|
1 |
|
return; |
223
|
|
|
} |
224
|
|
|
|
225
|
2 |
|
$chunks = Lib::arrayChunkByStringLength($paths, 131072, 4); |
226
|
|
|
|
227
|
2 |
|
foreach ($chunks as $paths) { |
228
|
2 |
|
$docker = Lib::cmd('docker', array('exec', '-w', '/app', $id)); |
229
|
2 |
|
$tar = Lib::cmd('tar', array('c', '-f', '-', $paths)); |
230
|
2 |
|
$unTar = Lib::cmd('tar', array('x', '-f', '-', '-C', $dir)); |
231
|
|
|
|
232
|
2 |
|
$command = $docker . ' ' . $tar . ' | ' . $unTar; |
233
|
2 |
|
$status = $exec->pass($command, array()); |
234
|
|
|
|
235
|
2 |
|
if (0 !== $status) { |
236
|
1 |
|
$streams->err(sprintf( |
237
|
1 |
|
"pipelines: Artifact failure: '%s' (%d, %d paths, %d bytes)\n", |
238
|
1 |
|
$pattern, |
239
|
1 |
|
$status, |
240
|
1 |
|
count($paths), |
241
|
1 |
|
strlen($command) |
242
|
|
|
)); |
243
|
|
|
} |
244
|
|
|
} |
245
|
2 |
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* @param bool $copy |
249
|
|
|
* @param string $id container id |
250
|
|
|
* @param string $dir directory to copy contents into container |
251
|
|
|
* |
252
|
|
|
* @throws \RuntimeException |
253
|
|
|
* @return null|int null if all clear, integer for exit status |
254
|
|
|
*/ |
255
|
19 |
|
private function deployCopy($copy, $id, $dir) |
256
|
|
|
{ |
257
|
19 |
|
if (!$copy) { |
258
|
12 |
|
return null; |
259
|
|
|
} |
260
|
|
|
|
261
|
7 |
|
$streams = $this->streams; |
262
|
7 |
|
$exec = $this->exec; |
263
|
|
|
|
264
|
7 |
|
$streams->out("\x1D+++ copying files into container...\n"); |
265
|
|
|
|
266
|
7 |
|
$tmpDir = LibTmp::tmpDir('pipelines-cp.'); |
267
|
7 |
|
$this->temporaryDirectories[] = DestructibleString::rmDir($tmpDir); |
268
|
7 |
|
LibFs::symlink($dir, $tmpDir . '/app'); |
269
|
7 |
|
$cd = Lib::cmd('cd', array($tmpDir . '/.')); |
270
|
7 |
|
$tar = Lib::cmd('tar', array('c', '-h', '-f', '-', '--no-recursion', 'app')); |
271
|
7 |
|
$dockerCp = Lib::cmd('docker ', array('cp', '-', $id . ':/.')); |
272
|
|
|
$status = $exec->pass("${cd} && echo 'app' | ${tar} | ${dockerCp}", array()); |
273
|
|
|
LibFs::unlink($tmpDir . '/app'); |
274
|
7 |
|
if (0 !== $status) { |
275
|
1 |
|
$streams->err('pipelines: deploy copy failure\n'); |
276
|
|
|
|
277
|
1 |
|
return $status; |
278
|
|
|
} |
279
|
|
|
|
280
|
6 |
|
$cd = Lib::cmd('cd', array($dir . '/.')); |
281
|
6 |
|
$tar = Lib::cmd('tar', array('c', '-f', '-', '.')); |
282
|
6 |
|
$dockerCp = Lib::cmd('docker ', array('cp', '-', $id . ':/app')); |
283
|
6 |
|
$status = $exec->pass("${cd} && ${tar} | ${dockerCp}", array()); |
284
|
6 |
|
if (0 !== $status) { |
285
|
1 |
|
$streams->err('pipelines: deploy copy failure\n'); |
286
|
|
|
|
287
|
1 |
|
return $status; |
288
|
|
|
} |
289
|
|
|
|
290
|
5 |
|
$streams(''); |
291
|
|
|
|
292
|
5 |
|
return null; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* if there is the docker service in the step, deploy the |
297
|
|
|
* docker client |
298
|
|
|
* |
299
|
|
|
* @param Step $step |
300
|
|
|
* @param string $id |
301
|
|
|
* |
302
|
|
|
* @throws |
303
|
|
|
* @return array array(int $status, string $message) |
304
|
|
|
*/ |
305
|
|
|
private function deployDockerClient(Step $step, $id) |
306
|
|
|
{ |
307
|
17 |
|
if (!$step->getServices()->has('docker')) { |
308
|
14 |
|
return array(0, ''); |
309
|
|
|
} |
310
|
|
|
|
311
|
3 |
|
$this->streams->out(' +++ docker client install...: '); |
312
|
|
|
|
313
|
|
|
try { |
314
|
3 |
|
list($status, $message) = $this->getDockerBinaryRepository()->inject($id); |
315
|
1 |
|
} catch (\Exception $e) { |
316
|
1 |
|
$this->streams->out("pipelines internal failure.\n"); |
317
|
|
|
|
318
|
1 |
|
throw new \InvalidArgumentException('inject docker client failed: ' . $e->getMessage(), 1, $e); |
319
|
|
|
} |
320
|
|
|
|
321
|
2 |
|
$this->streams->out("${message}\n"); |
322
|
|
|
|
323
|
2 |
|
return array($status, $message); |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* @param Image $image |
328
|
|
|
* |
329
|
|
|
* @throws \RuntimeException |
330
|
|
|
* @throws \InvalidArgumentException |
331
|
|
|
*/ |
332
|
|
|
private function imageLogin(Image $image) |
333
|
|
|
{ |
334
|
20 |
|
$login = new ImageLogin($this->exec, $this->env->getResolver()); |
335
|
20 |
|
$login->byImage($image); |
336
|
20 |
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* @param StepContainer $container |
340
|
|
|
* @param string $dir |
341
|
|
|
* @param bool $copy |
342
|
|
|
* @param Step $step |
343
|
|
|
* |
344
|
|
|
* @return array array(string|null $id, int $status) |
345
|
|
|
*/ |
346
|
|
|
private function runNewContainer(StepContainer $container, $dir, $copy, Step $step) |
347
|
|
|
{ |
348
|
20 |
|
$env = $this->env; |
349
|
20 |
|
$streams = $this->streams; |
350
|
|
|
|
351
|
20 |
|
$image = $step->getImage(); |
352
|
|
|
|
353
|
|
|
# process docker login if image demands so, but continue on failure |
354
|
20 |
|
$this->imageLogin($image); |
355
|
|
|
|
356
|
20 |
|
$mountDockerSock = $this->obtainDockerSocketMount(); |
357
|
|
|
|
358
|
20 |
|
$mountWorkingDirectory = $this->obtainWorkingDirMount($copy, $dir, $mountDockerSock); |
359
|
20 |
|
if ($mountWorkingDirectory && is_int($mountWorkingDirectory[1])) { |
360
|
2 |
|
return $mountWorkingDirectory; |
361
|
|
|
} |
362
|
|
|
|
363
|
18 |
|
list($status, $out, $err) = $container->run( |
364
|
|
|
array( |
365
|
18 |
|
'-i', '--name', $container->getName(), |
366
|
18 |
|
$env->getArgs('-e'), |
367
|
18 |
|
$mountWorkingDirectory, '-e', 'BITBUCKET_CLONE_DIR=/app', |
368
|
18 |
|
$mountDockerSock, |
369
|
18 |
|
'--workdir', '/app', '--detach', '--entrypoint=/bin/sh', $image->getName(), |
370
|
|
|
) |
371
|
|
|
); |
372
|
18 |
|
if (0 !== $status) { |
373
|
1 |
|
$streams->out(" container-id...: *failure*\n\n"); |
374
|
1 |
|
$streams->err("pipelines: setting up the container failed\n"); |
375
|
1 |
|
$streams->err("${err}\n"); |
376
|
1 |
|
$streams->out("${out}\n"); |
377
|
1 |
|
$streams->out(sprintf("exit status: %d\n", $status)); |
378
|
|
|
|
379
|
1 |
|
return array(null, $status); |
380
|
|
|
} |
381
|
17 |
|
$id = $container->getDisplayId(); |
382
|
|
|
|
383
|
17 |
|
return array($id, $status); |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
/** |
387
|
|
|
* enable docker client inside docker by mounting docker socket |
388
|
|
|
* |
389
|
|
|
* @return array docker socket volume args for docker run, empty if not mounting |
390
|
|
|
*/ |
391
|
|
|
private function obtainDockerSocketMount() |
392
|
|
|
{ |
393
|
20 |
|
$args = array(); |
394
|
|
|
|
395
|
|
|
// FIXME give more controlling options, this is serious /!\ |
396
|
20 |
|
if (!$this->flags->useDockerSocket()) { |
397
|
1 |
|
return $args; |
398
|
|
|
} |
399
|
|
|
|
400
|
19 |
|
$defaultSocketPath = $this->runOpts->getOption('docker.socket.path'); |
401
|
19 |
|
$hostPathDockerSocket = $defaultSocketPath; |
402
|
|
|
|
403
|
|
|
// pipelines inside pipelines |
404
|
19 |
|
$hostPath = $this->pipHostConfigBind($defaultSocketPath); |
405
|
19 |
|
if (null !== $hostPath) { |
406
|
|
|
return array( |
407
|
1 |
|
'-v', sprintf('%s:%s', $hostPath, $defaultSocketPath), |
408
|
|
|
); |
409
|
|
|
} |
410
|
|
|
|
411
|
18 |
|
$dockerHost = $this->env->getInheritValue('DOCKER_HOST'); |
412
|
18 |
|
if (null !== $dockerHost && 0 === strpos($dockerHost, 'unix://')) { |
413
|
1 |
|
$hostPathDockerSocket = LibFs::normalizePath(substr($dockerHost, 7)); |
414
|
|
|
} |
415
|
|
|
|
416
|
18 |
|
$pathDockerSock = $defaultSocketPath; |
417
|
|
|
|
418
|
18 |
|
if (file_exists($hostPathDockerSocket)) { |
419
|
|
|
$args = array( |
420
|
14 |
|
'-v', sprintf('%s:%s', $hostPathDockerSocket, $pathDockerSock), |
421
|
|
|
); |
422
|
|
|
} |
423
|
|
|
|
424
|
18 |
|
return $args; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
/** |
428
|
|
|
* @param bool $copy |
429
|
|
|
* @param string $dir |
430
|
|
|
* @param array $mountDockerSock |
431
|
|
|
* |
432
|
|
|
* @return array mount options or array(null, int $status) for error handling |
433
|
|
|
*/ |
434
|
|
|
private function obtainWorkingDirMount($copy, $dir, array $mountDockerSock) |
435
|
|
|
{ |
436
|
20 |
|
if ($copy) { |
437
|
7 |
|
return array(); |
438
|
|
|
} |
439
|
|
|
|
440
|
13 |
|
$parentName = $this->env->getValue('PIPELINES_PARENT_CONTAINER_NAME'); |
441
|
13 |
|
$hostDeviceDir = $this->pipHostConfigBind($dir); |
442
|
13 |
|
$checkMount = $mountDockerSock && null !== $parentName; |
|
|
|
|
443
|
13 |
|
$deviceDir = $hostDeviceDir ?: $dir; |
444
|
13 |
|
if ($checkMount && '/app' === $dir && null === $hostDeviceDir) { // FIXME(tk): hard encoded /app |
445
|
2 |
|
$deviceDir = $this->env->getPipelinesProjectPath($deviceDir); |
446
|
2 |
|
if ($deviceDir === $dir || null === $deviceDir) { |
447
|
2 |
|
$this->streams->err("pipelines: fatal: can not detect ${dir} mount point. preventing new container.\n"); |
448
|
|
|
|
449
|
2 |
|
return array(null, 1); |
450
|
|
|
} |
451
|
|
|
} |
452
|
|
|
|
453
|
|
|
// FIXME(tk): Never mount anything not matching /home/[a-zA-Z][a-zA-Z0-9]*/[^.].*/... |
454
|
|
|
// + do realpath checking |
455
|
|
|
// + prevent dot path injections (logical fix first) |
456
|
11 |
|
return array('-v', "${deviceDir}:/app"); // FIXME(tk): hard encoded /app |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
/** |
460
|
|
|
* get host path from mount point if in pip level 2+ |
461
|
|
|
* |
462
|
|
|
* @param mixed $mountPoint |
463
|
|
|
* @return null|string |
464
|
|
|
*/ |
465
|
|
|
private function pipHostConfigBind($mountPoint) |
466
|
|
|
{ |
467
|
|
|
// if there is a parent name, this is level 2+ |
468
|
20 |
|
if (null === $this->env->getValue('PIPELINES_PARENT_CONTAINER_NAME')) { |
469
|
16 |
|
return null; |
470
|
|
|
} |
471
|
|
|
|
472
|
4 |
|
if (null === $pipName = $this->env->getValue('PIPELINES_PIP_CONTAINER_NAME')) { |
473
|
2 |
|
return null; |
474
|
|
|
} |
475
|
|
|
|
476
|
2 |
|
return Docker::create($this->exec)->hostConfigBind($pipName, $mountPoint); |
477
|
|
|
} |
478
|
|
|
|
479
|
|
|
/** |
480
|
|
|
* @param Step $step |
481
|
|
|
* @param Streams $streams |
482
|
|
|
* @param Exec $exec |
483
|
|
|
* @param string $name container name |
484
|
|
|
* |
485
|
|
|
* @return null|int should never be null, status, non-zero if a command failed |
486
|
|
|
*/ |
487
|
|
|
private function runStepScript(Step $step, Streams $streams, Exec $exec, $name) |
488
|
|
|
{ |
489
|
15 |
|
$script = $step->getScript(); |
490
|
|
|
|
491
|
15 |
|
$buffer = Lib::cmd("<<'SCRIPT' docker", array( |
492
|
15 |
|
'exec', '-i', $name, '/bin/sh', |
493
|
|
|
)); |
494
|
15 |
|
$buffer .= "\n# this /bin/sh script is generated from a pipelines pipeline:\n"; |
495
|
15 |
|
$buffer .= "set -e\n"; |
496
|
15 |
|
foreach ($script as $line => $command) { |
497
|
15 |
|
$line && $buffer .= 'printf \'\\n\'' . "\n"; |
498
|
15 |
|
$buffer .= 'printf \'\\035+ %s\\n\' ' . Lib::quoteArg($command) . "\n"; |
499
|
15 |
|
$buffer .= $command . "\n"; |
500
|
|
|
} |
501
|
15 |
|
$buffer .= "SCRIPT\n"; |
502
|
|
|
|
503
|
15 |
|
$status = $exec->pass($buffer, array()); |
504
|
|
|
|
505
|
15 |
|
if (0 !== $status) { |
506
|
2 |
|
$streams->err(sprintf("script non-zero exit status: %d\n", $status)); |
507
|
|
|
} |
508
|
|
|
|
509
|
15 |
|
return $status; |
510
|
|
|
} |
511
|
|
|
|
512
|
|
|
/** |
513
|
|
|
* @param StepContainer $container |
514
|
|
|
* @param int $status |
515
|
|
|
*/ |
516
|
|
|
private function shutdownStepContainer(StepContainer $container, $status) |
517
|
|
|
{ |
518
|
15 |
|
$flags = $this->flags; |
519
|
15 |
|
$id = $container->getDisplayId(); |
520
|
|
|
|
521
|
|
|
# keep container on error |
522
|
15 |
|
if (0 !== $status && $flags->keepOnError()) { |
523
|
2 |
|
$this->streams->err(sprintf( |
524
|
2 |
|
"error, keeping container id %s\n", |
525
|
2 |
|
substr($id, 0, 12) |
526
|
|
|
)); |
527
|
|
|
|
528
|
2 |
|
return; |
529
|
|
|
} |
530
|
|
|
|
531
|
|
|
# keep or kill/remove container |
532
|
13 |
|
$container->killAndRemove($flags->killContainer(), $flags->removeContainer()); |
533
|
|
|
|
534
|
13 |
|
if ($flags->keep()) { |
535
|
1 |
|
$this->streams->out(sprintf( |
536
|
1 |
|
"keeping container id %s\n", |
537
|
1 |
|
substr($id, 0, 12) |
538
|
|
|
)); |
539
|
|
|
} |
540
|
13 |
|
} |
541
|
|
|
} |
542
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.