Completed
Push — master ( d445f7...bb1795 )
by Christian
03:00
created

TaskRunnerController   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 394
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 5
Bugs 1 Features 0
Metric Value
wmc 30
c 5
b 1
f 0
lcom 1
cbo 13
dl 0
loc 394
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getTasksAction() 0 14 2
A getTaskAction() 0 21 3
B addTaskAction() 0 26 4
A deleteTaskAction() 0 22 3
B runAction() 0 40 4
B spawn() 0 35 3
A getInterpreter() 0 9 2
A getArguments() 0 13 3
A getEnvironment() 0 14 3
A getDefinedEnvironmentVariables() 0 11 3
1
<?php
2
3
/**
4
 * This file is part of tenside/core-bundle.
5
 *
6
 * (c) Christian Schiffler <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 *
11
 * This project is provided in good faith and hope to be usable by anyone.
12
 *
13
 * @package    tenside/core-bundle
14
 * @author     Christian Schiffler <[email protected]>
15
 * @author     Yanick Witschi <[email protected]>
16
 * @copyright  2015 Christian Schiffler <[email protected]>
17
 * @license    https://github.com/tenside/core-bundle/blob/master/LICENSE MIT
18
 * @link       https://github.com/tenside/core-bundle
19
 * @filesource
20
 */
21
22
namespace Tenside\CoreBundle\Controller;
23
24
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
25
use Psr\Log\LoggerInterface;
26
use Symfony\Component\HttpFoundation\JsonResponse;
27
use Symfony\Component\HttpFoundation\Request;
28
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
29
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
30
use Symfony\Component\Process\Process;
31
use Tenside\CoreBundle\Annotation\ApiDescription;
32
use Tenside\Core\Config\TensideJsonConfig;
33
use Tenside\Core\Task\Task;
34
use Tenside\Core\Util\JsonArray;
35
36
/**
37
 * Lists and executes queued tasks.
38
 */
39
class TaskRunnerController extends AbstractController
40
{
41
    /**
42
     * Retrieve the task list.
43
     *
44
     * @return JsonResponse
45
     *
46
     * @ApiDoc(
47
     *   section="tasks",
48
     *   statusCodes = {
49
     *     200 = "When everything worked out ok"
50
     *   },
51
     * )
52
     * @ApiDescription(
53
     *   response={
54
     *     "<task-id>[]" = {
55
     *       "children" = {
56
     *         "id" = {
57
     *           "dataType" = "string",
58
     *           "description" = "The task id."
59
     *         },
60
     *         "type" = {
61
     *           "dataType" = "string",
62
     *           "description" = "The type of the task."
63
     *         }
64
     *       }
65
     *     }
66
     *   }
67
     * )
68
     */
69
    public function getTasksAction()
70
    {
71
        $result = [];
72
        $list   = $this->getTensideTasks();
73
        foreach ($list->getIds() as $taskId) {
74
            $result[$taskId] = [
75
                'id'   => $taskId,
76
                'type' => $list->getTask($taskId)->getType()
77
            ];
78
        }
79
80
        return JsonResponse::create($result)
81
            ->setEncodingOptions((JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_FORCE_OBJECT));
82
    }
83
84
    /**
85
     * Retrieve the given task task.
86
     *
87
     * @param string  $taskId  The id of the task to retrieve.
88
     *
89
     * @param Request $request The request.
90
     *
91
     * @return JsonResponse
92
     *
93
     * @throws NotFoundHttpException When the task could not be found.
94
     *
95
     * @ApiDoc(
96
     *   section="tasks",
97
     *   statusCodes = {
98
     *     200 = "When everything worked out ok"
99
     *   },
100
     * )
101
     * @ApiDescription(
102
     *   response={
103
     *     "status" = {
104
     *       "dataType" = "string",
105
     *       "description" = "The task status."
106
     *     },
107
     *     "output" = {
108
     *       "dataType" = "string",
109
     *       "description" = "The command line output of the task."
110
     *     }
111
     *   }
112
     * )
113
     */
114
    public function getTaskAction($taskId, Request $request)
115
    {
116
        // Retrieve the status file of the task.
117
        $task   = $this->getTensideTasks()->getTask($taskId);
118
        $offset = null;
119
120
        if (!$task) {
121
            throw new NotFoundHttpException('No such task.');
122
        }
123
124
        if ($request->query->has('offset')) {
125
            $offset = (int) $request->query->get('offset');
126
        }
127
128
        return JsonResponse::create(
129
            [
130
                'status' => $task->getStatus(),
131
                'output' => $task->getOutput($offset)
132
            ]
133
        );
134
    }
135
136
    /**
137
     * Queue a task in the list.
138
     *
139
     * @param Request $request The request.
140
     *
141
     * @return JsonResponse
142
     *
143
     * @throws NotAcceptableHttpException When the payload is invalid.
144
     *
145
     * @ApiDoc(
146
     *   section="tasks",
147
     *   statusCodes = {
148
     *     201 = "When everything worked out ok"
149
     *   },
150
     * )
151
     * @ApiDescription(
152
     *   response={
153
     *     "status" = {
154
     *       "dataType" = "string",
155
     *       "description" = "OK on success"
156
     *     },
157
     *     "task" = {
158
     *       "dataType" = "string",
159
     *       "description" = "The id of the created task."
160
     *     }
161
     *   }
162
     * )
163
     */
164
    public function addTaskAction(Request $request)
165
    {
166
        $metaData = null;
0 ignored issues
show
Unused Code introduced by
$metaData is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
167
        $content  = $request->getContent();
168
        if (empty($content)) {
169
            throw new NotAcceptableHttpException('Invalid payload');
170
        }
171
        $metaData = new JsonArray($content);
0 ignored issues
show
Bug introduced by
It seems like $content defined by $request->getContent() on line 167 can also be of type resource; however, Tenside\Core\Util\JsonArray::__construct() does only seem to accept string|array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
172
        if (!$metaData->has('type')) {
173
            throw new NotAcceptableHttpException('Invalid payload');
174
        }
175
176
        try {
177
            $taskId = $this->getTensideTasks()->queue($metaData->get('type'), $metaData);
178
        } catch (\InvalidArgumentException $exception) {
179
            throw new NotAcceptableHttpException($exception->getMessage());
180
        }
181
182
        return JsonResponse::create(
183
            [
184
                'status' => 'OK',
185
                'task'   => $taskId
186
            ],
187
            JsonResponse::HTTP_CREATED
188
        );
189
    }
190
191
    /**
192
     * Remove a task from the list.
193
     *
194
     * @param string $taskId The id of the task to remove.
195
     *
196
     * @return JsonResponse
197
     *
198
     * @throws NotFoundHttpException      When the given task could not be found.
199
     * @throws NotAcceptableHttpException When trying to delete a running task.
200
     *
201
     * @ApiDoc(
202
     *   section="tasks",
203
     *   statusCodes = {
204
     *     200 = "When everything worked out ok"
205
     *   },
206
     * )
207
     * @ApiDescription(
208
     *   response={
209
     *     "status" = {
210
     *       "dataType" = "string",
211
     *       "description" = "OK on success"
212
     *     }
213
     *   }
214
     * )
215
     */
216
    public function deleteTaskAction($taskId)
217
    {
218
        $list = $this->getTensideTasks();
219
        $task = $list->getTask($taskId);
220
221
        if (!$task) {
222
            throw new NotFoundHttpException('Task id ' . $taskId . ' not found');
223
        }
224
225
        if ($task->getStatus() === Task::STATE_RUNNING) {
226
            throw new NotAcceptableHttpException('Task id ' . $taskId . ' is running and can not be deleted');
227
        }
228
229
        $task->removeAssets();
230
        $list->remove($task->getId());
231
232
        return JsonResponse::create(
233
            [
234
                'status' => 'OK'
235
            ]
236
        );
237
    }
238
239
    /**
240
     * Starts the next pending task if any.
241
     *
242
     * @return JsonResponse
243
     *
244
     * @throws NotFoundHttpException      When no task could be found.
245
     * @throws NotAcceptableHttpException When a task is already running and holds the lock.
246
     *
247
     * @ApiDoc(
248
     *   section="tasks",
249
     *   statusCodes = {
250
     *     200 = "When everything worked out ok",
251
     *     404 = "When no pending task has been found",
252
     *     406 = "When another task is still running"
253
     *   },
254
     * )
255
     * @ApiDescription(
256
     *   response={
257
     *     "status" = {
258
     *       "dataType" = "string",
259
     *       "description" = "OK on success"
260
     *     },
261
     *     "task" = {
262
     *       "dataType" = "string",
263
     *       "description" = "The id of the started task."
264
     *     }
265
     *   }
266
     * )
267
     */
268
    public function runAction()
269
    {
270
        $lock = $this->container->get('tenside.taskrun_lock');
271
272
        if (!$lock->lock()) {
273
            throw new NotAcceptableHttpException('Task already running');
274
        }
275
276
        // Fetch the next queued task.
277
        $task = $this->getTensideTasks()->getNext();
278
279
        if (!$task) {
280
            throw new NotFoundHttpException('Task not found');
281
        }
282
283
        if ($task::STATE_PENDING !== $task->getStatus()) {
284
            return JsonResponse::create(
285
                [
286
                    'status' => $task->getStatus(),
287
                    'task'   => $task->getId()
288
                ],
289
                JsonResponse::HTTP_OK
290
            );
291
        }
292
293
        // Now spawn a runner.
294
        try {
295
            $this->spawn($task);
296
        } finally {
297
            $lock->release();
298
        }
299
300
        return JsonResponse::create(
301
            [
302
                'status' => 'OK',
303
                'task'   => $task->getId()
304
            ],
305
            JsonResponse::HTTP_OK
306
        );
307
    }
308
309
    /**
310
     * Spawn a detached process for a task.
311
     *
312
     * @param Task $task The task to spawn a process for.
313
     *
314
     * @return void
315
     *
316
     * @throws \RuntimeException When the task could not be started.
317
     */
318
    private function spawn(Task $task)
319
    {
320
        $config = $this->getTensideConfig();
321
        $home   = $this->get('tenside.home')->homeDir();
322
        $cmd    = sprintf(
323
            '%s %s %s tenside:runtask %s -v',
324
            escapeshellcmd($this->getInterpreter($config)),
325
            $this->getArguments($config),
326
            escapeshellarg($this->get('tenside.cli_script')->cliExecutable()),
327
            escapeshellarg($task->getId())
328
        );
329
330
        $commandline = new Process($cmd, $home, $this->getEnvironment($config), null, null);
331
332
        $commandline->start();
333
        if (!$commandline->isRunning()) {
334
            // We might end up here when the process has been forked.
335
            // If exit code is neither 0 nor null, we have a problem here.
336
            if ($exitCode = $commandline->getExitCode()) {
337
                /** @var LoggerInterface $logger */
338
                $logger = $this->get('logger');
339
                $logger->error('Failed to execute "' . $cmd . '"');
340
                $logger->error('Exit code: ' . $commandline->getExitCode());
341
                $logger->error('Output: ' . $commandline->getOutput());
342
                $logger->error('Error output: ' . $commandline->getErrorOutput());
343
                throw new \RuntimeException(
344
                    sprintf(
345
                        'Spawning process task %s resulted in exit code %s',
346
                        $task->getId(),
347
                        $exitCode
348
                    )
349
                );
350
            }
351
        }
352
    }
353
354
    /**
355
     * Get the interpreter to use.
356
     *
357
     * @param TensideJsonConfig $config The config.
358
     *
359
     * @return string
360
     */
361
    private function getInterpreter(TensideJsonConfig $config)
362
    {
363
        // If defined, override the php-cli interpreter.
364
        if ($config->has('php_cli')) {
365
            return (string) $config->get('php_cli');
366
        }
367
368
        return 'php';
369
    }
370
371
    /**
372
     * Retrieve the command line arguments to use.
373
     *
374
     * @param TensideJsonConfig $config The config to obtain the arguments from.
375
     *
376
     * @return string
377
     */
378
    private function getArguments(TensideJsonConfig $config)
379
    {
380
        if (!$config->has('php_cli_arguments')) {
381
            return '';
382
        }
383
384
        $arguments = [];
385
        foreach ($config->get('php_cli_arguments') as $argument) {
0 ignored issues
show
Bug introduced by
The expression $config->get('php_cli_arguments') of type array|string|integer|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
386
            $arguments[] = $argument;
387
        }
388
389
        return implode(' ', array_map('escapeshellarg', $arguments));
390
    }
391
392
    /**
393
     * Retrieve the command line environment variables to use.
394
     *
395
     * @param TensideJsonConfig $config The config to obtain the arguments from.
396
     *
397
     * @return array
398
     */
399
    private function getEnvironment(TensideJsonConfig $config)
400
    {
401
        $variables = $this->getDefinedEnvironmentVariables(['SYMFONY_ENV', 'SYMFONY_DEBUG', 'COMPOSER']);
402
403
        if (!$config->has('php_cli_environment')) {
404
            return $variables;
405
        }
406
407
        foreach ($config->get('php_cli_environment') as $name => $value) {
0 ignored issues
show
Bug introduced by
The expression $config->get('php_cli_environment') of type array|string|integer|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
408
            $variables[$name] = escapeshellarg($value);
409
        }
410
411
        return $variables;
412
    }
413
414
    /**
415
     * Retrieve the passed environment variables from the current session and return them.
416
     *
417
     * @param array $names The names of the environment variables to inherit.
418
     *
419
     * @return array
420
     */
421
    private function getDefinedEnvironmentVariables($names)
422
    {
423
        $variables = [];
424
        foreach ($names as $name) {
425
            if (false !== ($composerEnv = getenv($name))) {
426
                $variables[$name] = escapeshellarg($composerEnv);
427
            }
428
        }
429
430
        return $variables;
431
    }
432
}
433