1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Copyright (c) 2011-present Mediasift Ltd |
5
|
|
|
* All rights reserved. |
6
|
|
|
* |
7
|
|
|
* Redistribution and use in source and binary forms, with or without |
8
|
|
|
* modification, are permitted provided that the following conditions |
9
|
|
|
* are met: |
10
|
|
|
* |
11
|
|
|
* * Redistributions of source code must retain the above copyright |
12
|
|
|
* notice, this list of conditions and the following disclaimer. |
13
|
|
|
* |
14
|
|
|
* * Redistributions in binary form must reproduce the above copyright |
15
|
|
|
* notice, this list of conditions and the following disclaimer in |
16
|
|
|
* the documentation and/or other materials provided with the |
17
|
|
|
* distribution. |
18
|
|
|
* |
19
|
|
|
* * Neither the names of the copyright holders nor the names of his |
20
|
|
|
* contributors may be used to endorse or promote products derived |
21
|
|
|
* from this software without specific prior written permission. |
22
|
|
|
* |
23
|
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
24
|
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
25
|
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
26
|
|
|
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
27
|
|
|
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
28
|
|
|
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
29
|
|
|
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
30
|
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
31
|
|
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
32
|
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
33
|
|
|
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
34
|
|
|
* POSSIBILITY OF SUCH DAMAGE. |
35
|
|
|
* |
36
|
|
|
* @category Libraries |
37
|
|
|
* @package Storyplayer/Cli |
38
|
|
|
* @author Stuart Herbert <[email protected]> |
39
|
|
|
* @copyright 2011-present Mediasift Ltd www.datasift.com |
40
|
|
|
* @license http://www.opensource.org/licenses/bsd-license.php BSD License |
41
|
|
|
* @link http://datasift.github.io/storyplayer |
42
|
|
|
*/ |
43
|
|
|
|
44
|
|
|
namespace DataSift\Storyplayer\Cli; |
45
|
|
|
|
46
|
|
|
use Exception; |
47
|
|
|
use stdClass; |
48
|
|
|
use Phix_Project\CliEngine; |
49
|
|
|
use Phix_Project\CliEngine\CliCommand; |
50
|
|
|
use Phix_Project\ExceptionsLib1\Legacy_ErrorHandler; |
51
|
|
|
use Phix_Project\ExceptionsLib1\Legacy_ErrorException; |
52
|
|
|
use DataSift\Stone\ConfigLib\E5xx_ConfigFileNotFound; |
53
|
|
|
use DataSift\Stone\ConfigLib\E5xx_InvalidConfigFile; |
54
|
|
|
use DataSift\Storyplayer\PlayerLib\E4xx_NoSuchReport; |
55
|
|
|
use DataSift\Storyplayer\PlayerLib\PhaseGroup_Player; |
56
|
|
|
use DataSift\Storyplayer\PlayerLib\StoryTeller; |
57
|
|
|
use DataSift\Storyplayer\PlayerLib\Script_Player; |
58
|
|
|
use DataSift\Storyplayer\Console\ScriptConsole; |
59
|
|
|
use DataSift\Storyplayer\Injectables; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* A command to play a script |
63
|
|
|
* |
64
|
|
|
* @category Libraries |
65
|
|
|
* @package Storyplayer/Cli |
66
|
|
|
* @author Stuart Herbert <[email protected]> |
67
|
|
|
* @copyright 2011-present Mediasift Ltd www.datasift.com |
68
|
|
|
* @license http://www.opensource.org/licenses/bsd-license.php BSD License |
69
|
|
|
* @link http://datasift.github.io/storyplayer |
70
|
|
|
*/ |
71
|
|
|
class Script_Command extends BaseCommand implements CliSignalHandler |
72
|
|
|
{ |
73
|
|
|
/** |
74
|
|
|
* should we let background processes survive when we shutdown? |
75
|
|
|
* @var boolean |
76
|
|
|
*/ |
77
|
|
|
protected $persistProcesses = false; |
78
|
|
|
|
79
|
|
|
// we need to track this for handling CTRL-C |
80
|
|
|
protected $st; |
81
|
|
|
|
82
|
|
|
// we track this for convenience |
83
|
|
|
protected $output; |
84
|
|
|
|
85
|
|
|
// our list of scripts to execute |
86
|
|
|
protected $scriptList; |
87
|
|
|
|
88
|
|
|
// our injected data / services |
89
|
|
|
// needed for when user presses CTRL+C |
90
|
|
|
protected $injectables; |
91
|
|
|
|
92
|
|
View Code Duplication |
public function __construct($injectables) |
93
|
|
|
{ |
94
|
|
|
parent::__construct($injectables); |
95
|
|
|
|
96
|
|
|
// define the command |
97
|
|
|
$this->setName('script'); |
98
|
|
|
$this->setShortDescription('run an automation script'); |
99
|
|
|
$this->setLongDescription( |
100
|
|
|
"Use this command to play an automation script." |
101
|
|
|
.PHP_EOL |
102
|
|
|
); |
103
|
|
|
$this->setArgsList(array( |
104
|
|
|
"[<script.php>]" => "run a script" |
105
|
|
|
)); |
106
|
|
|
|
107
|
|
|
// the switches that this command supports |
108
|
|
|
$this->addFeature(new Feature_ConsoleSupport); |
109
|
|
|
$this->addFeature(new Feature_DeviceSupport); |
110
|
|
|
$this->addFeature(new Feature_TestEnvironmentConfigSupport); |
111
|
|
|
$this->addFeature(new Feature_SystemUnderTestConfigSupport); |
112
|
|
|
$this->addFeature(new Feature_LocalhostSupport); |
113
|
|
|
$this->addFeature(new Feature_ActiveConfigSupport); |
114
|
|
|
$this->addFeature(new Feature_DefinesSupport); |
115
|
|
|
$this->addFeature(new Feature_PhaseLoaderSupport); |
116
|
|
|
$this->addFeature(new Feature_ProseLoaderSupport); |
117
|
|
|
$this->addFeature(new Feature_TestUsersSupport); |
118
|
|
|
|
119
|
|
|
// now setup all of the switches that we support |
120
|
|
|
$this->addFeatureSwitches(); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* |
125
|
|
|
* @param CliEngine $engine |
126
|
|
|
* @param array $params |
127
|
|
|
* @param Injectables|null $injectables |
128
|
|
|
* @return integer |
129
|
|
|
*/ |
130
|
|
|
public function processCommand(CliEngine $engine, $params = array(), $injectables = null) |
131
|
|
|
{ |
132
|
|
|
// we need to wrap our code to catch old-style PHP errors |
133
|
|
|
$legacyHandler = new Legacy_ErrorHandler(); |
134
|
|
|
try { |
135
|
|
|
$returnCode = $legacyHandler->run([$this, 'processInsideLegacyHandler'], [$engine, $params, $injectables]); |
136
|
|
|
return $returnCode; |
137
|
|
|
} |
138
|
|
|
catch (Exception $e) { |
139
|
|
|
$injectables->output->logCliError($e->getMessage()); |
140
|
|
|
$engine->options->dev = true; |
141
|
|
|
if (isset($engine->options->dev) && $engine->options->dev) { |
142
|
|
|
$injectables->output->logCliError("Stack trace is:\n\n" . $e->getTraceAsString()); |
143
|
|
|
} |
144
|
|
|
exit(1); |
|
|
|
|
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
public function processInsideLegacyHandler(CliEngine $engine, $params = array(), $injectables = null) |
149
|
|
|
{ |
150
|
|
|
// make sure we're using the ScriptConsole by default |
151
|
|
|
$injectables->output->usePluginInSlot(new ScriptConsole, 'console'); |
152
|
|
|
|
153
|
|
|
// process the common functionality |
154
|
|
|
$this->initFeaturesBeforeModulesAvailable($engine); |
155
|
|
|
|
156
|
|
|
// now it is safe to create our shorthand |
157
|
|
|
$runtimeConfig = $injectables->getRuntimeConfig(); |
158
|
|
|
$runtimeConfigManager = $injectables->getRuntimeConfigManager(); |
159
|
|
|
$output = $injectables->output; |
160
|
|
|
|
161
|
|
|
// save the output for use in other methods |
162
|
|
|
$this->output = $output; |
163
|
|
|
|
164
|
|
|
// at this point, all of the services / data held in $injectables |
165
|
|
|
// has been initialised and is ready for use |
166
|
|
|
|
167
|
|
|
// create a new StoryTeller object |
168
|
|
|
$st = new StoryTeller($injectables); |
169
|
|
|
|
170
|
|
|
// remember our $st object, as we'll need it for our |
171
|
|
|
// shutdown function |
172
|
|
|
$this->st = $st; |
173
|
|
|
|
174
|
|
|
// now that we have $st, we can initialise any feature that |
175
|
|
|
// wants to use our modules |
176
|
|
|
$this->initFeaturesAfterModulesAvailable($st, $engine, $injectables); |
177
|
|
|
|
178
|
|
|
// install signal handling, now that $this->st is defined |
179
|
|
|
// |
180
|
|
|
// we wouldn't want signal handling called out of order :) |
181
|
|
|
$this->initSignalHandling($injectables); |
182
|
|
|
|
183
|
|
|
// build our list of stories to run |
184
|
|
|
$this->initScriptList($engine, $injectables, $params); |
185
|
|
|
|
186
|
|
|
// and we're ready to tell the world that we're here |
187
|
|
|
$output->startStoryplayer( |
188
|
|
|
$engine->getAppVersion(), |
189
|
|
|
$engine->getAppUrl(), |
190
|
|
|
$engine->getAppCopyright(), |
191
|
|
|
$engine->getAppLicense() |
192
|
|
|
); |
193
|
|
|
|
194
|
|
|
// $this->scriptList contains one or more things to run |
195
|
|
|
// |
196
|
|
|
// let's play each of them in order |
197
|
|
|
foreach ($this->scriptList as $player) |
198
|
|
|
{ |
199
|
|
|
// play the story(ies) |
200
|
|
|
$player->play($st, $injectables); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
// write out any changed runtime config to disk |
204
|
|
|
$runtimeConfigManager->saveRuntimeConfig($runtimeConfig, $output); |
205
|
|
|
|
206
|
|
|
// tell the output plugins that we're all done |
207
|
|
|
$output->endStoryplayer(0); |
208
|
|
|
|
209
|
|
|
// all done |
210
|
|
|
return 0; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
// ================================================================== |
214
|
|
|
// |
215
|
|
|
// the individual initX() methods |
216
|
|
|
// |
217
|
|
|
// these are processed *after* the objects defined in the |
218
|
|
|
// CommonFunctionalitySupport trait have been initialised |
219
|
|
|
// |
220
|
|
|
// ------------------------------------------------------------------ |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* |
224
|
|
|
* @param Injectables $injectables |
225
|
|
|
* @return void |
226
|
|
|
*/ |
227
|
|
View Code Duplication |
protected function initSignalHandling(Injectables $injectables) |
228
|
|
|
{ |
229
|
|
|
// we need to remember the injectables, for when we handle CTRL+C |
230
|
|
|
$this->injectables = $injectables; |
231
|
|
|
|
232
|
|
|
// setup signal handling |
233
|
|
|
pcntl_signal(SIGTERM, array($this, 'sigtermHandler')); |
234
|
|
|
pcntl_signal(SIGINT , array($this, 'sigtermHandler')); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* |
239
|
|
|
* @param CliEngine $cliEngine |
240
|
|
|
* @param Injectables $injectables |
241
|
|
|
* @param array $cliParams |
242
|
|
|
* @return void |
243
|
|
|
*/ |
244
|
|
|
protected function initScriptList(CliEngine $cliEngine, Injectables $injectables, $cliParams) |
245
|
|
|
{ |
246
|
|
|
// our list of stories to play |
247
|
|
|
$this->scriptList = []; |
248
|
|
|
|
249
|
|
|
foreach ($cliParams as $cliParam) { |
250
|
|
|
// figure out what to do? |
251
|
|
|
if (is_dir($cliParam)) { |
252
|
|
|
$this->scriptList = $this->scriptList + $this->addScriptsFromFolder($cliEngine, $injectables, $cliParam); |
253
|
|
|
} |
254
|
|
|
else if (is_file($cliParam)) { |
255
|
|
|
// are we loading a story, or a list of stories? |
256
|
|
|
$paramParts = explode('.', $cliParams[0]); |
257
|
|
|
$paramSuffix = end($paramParts); |
258
|
|
|
|
259
|
|
|
switch ($paramSuffix) { |
260
|
|
|
case 'php': |
261
|
|
|
$this->scriptList = $this->scriptList + $this->addScriptFromFile($cliEngine, $injectables, $cliParam); |
262
|
|
|
break; |
263
|
|
|
|
264
|
|
|
case 'json': |
265
|
|
|
$this->scriptList = $this->scriptList + $this->addScriptsFromFile($cliEngine, $injectables, $cliParam); |
266
|
|
|
break; |
267
|
|
|
|
268
|
|
|
default: |
269
|
|
|
$this->output->logCliError("unsupported script file '{$cliParam}'"); |
270
|
|
|
exit(1); |
|
|
|
|
271
|
|
|
} |
272
|
|
|
} |
273
|
|
|
else { |
274
|
|
|
// if we get here, we've no idea what to do |
275
|
|
|
$this->output->logCliError("no such file: '{$cliParam}'"); |
276
|
|
|
exit(1); |
|
|
|
|
277
|
|
|
} |
278
|
|
|
} |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
// ================================================================== |
282
|
|
|
// |
283
|
|
|
// Story loading |
284
|
|
|
// |
285
|
|
|
// ------------------------------------------------------------------ |
286
|
|
|
|
287
|
|
|
protected function addScriptFromFile(CliEngine $engine, Injectables $injectables, $storyFile) |
|
|
|
|
288
|
|
|
{ |
289
|
|
|
// these are the players we want to execute for the story |
290
|
|
|
$return = [ |
291
|
|
|
new Script_Player($storyFile, $injectables), |
292
|
|
|
]; |
293
|
|
|
|
294
|
|
|
// all done |
295
|
|
|
return $return; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
protected function addScriptsFromFolder(CliEngine $engine, Injectables $injectables, $folder) |
|
|
|
|
299
|
|
|
{ |
300
|
|
|
// tbd |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
protected function addScriptsFromFile(CliEngine $engine, Injectables $injectables, $file) |
|
|
|
|
304
|
|
|
{ |
305
|
|
|
// tbd |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
// ================================================================== |
309
|
|
|
// |
310
|
|
|
// SIGNAL handling |
311
|
|
|
// |
312
|
|
|
// ------------------------------------------------------------------ |
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* |
316
|
|
|
* @param integer $signo |
317
|
|
|
* @return void |
318
|
|
|
*/ |
319
|
|
|
public function sigtermHandler($signo) |
320
|
|
|
{ |
321
|
|
|
// tell the user what is happening |
322
|
|
|
echo "\n"; |
323
|
|
|
echo "============================================================\n"; |
324
|
|
|
echo "USER ABORT!!\n"; |
325
|
|
|
echo "============================================================\n"; |
326
|
|
|
echo "\n"; |
327
|
|
|
|
328
|
|
|
// cleanup |
329
|
|
|
$phasesPlayer = new PhaseGroup_Player(); |
330
|
|
|
$phasesPlayer->playPhases( |
331
|
|
|
"user abort", |
332
|
|
|
$this->st, |
333
|
|
|
$this->injectables, |
334
|
|
|
$this->injectables->activeConfig->getData('storyplayer.phases.userAbort'), |
335
|
|
|
null |
336
|
|
|
); |
337
|
|
|
|
338
|
|
|
// force a clean shutdown |
339
|
|
|
exit(1); |
|
|
|
|
340
|
|
|
} |
341
|
|
|
} |
An exit expression should only be used in rare cases. For example, if you write a short command line script.
In most cases however, using an
exit
expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.