TaskRunnerController::addTaskAction()   B
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 17
nc 4
nop 1
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
 * @author     Andreas Schempp <[email protected]>
17
 * @copyright  2015 Christian Schiffler <[email protected]>
18
 * @license    https://github.com/tenside/core-bundle/blob/master/LICENSE MIT
19
 * @link       https://github.com/tenside/core-bundle
20
 * @filesource
21
 */
22
23
namespace Tenside\CoreBundle\Controller;
24
25
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
26
use Psr\Log\LoggerInterface;
27
use Symfony\Component\HttpFoundation\JsonResponse;
28
use Symfony\Component\HttpFoundation\Request;
29
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
30
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
31
use Tenside\Core\Util\PhpProcessSpawner;
32
use Tenside\CoreBundle\Annotation\ApiDescription;
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
     *   authentication = true,
52
     *   authenticationRoles = {
53
     *     "ROLE_MANIPULATE_REQUIREMENTS"
54
     *   },
55
     * )
56
     * @ApiDescription(
57
     *   response={
58
     *     "status" = {
59
     *       "dataType" = "string",
60
     *       "description" = "OK on success"
61
     *     },
62
     *     "tasks" = {
63
     *         "id" = {
64
     *           "dataType" = "string",
65
     *           "description" = "The task id."
66
     *         },
67
     *         "status" = {
68
     *           "dataType" = "string",
69
     *           "description" = "The task status."
70
     *         },
71
     *         "type" = {
72
     *           "dataType" = "string",
73
     *           "description" = "The type of the task."
74
     *         },
75
     *         "created_at" = {
76
     *           "dataType" = "string",
77
     *           "description" = "The date the task was created in ISO 8601 format."
78
     *         },
79
     *         "user_data" = {
80
     *           "dataType" = "object",
81
     *           "description" = "The user submitted additional data."
82
     *         }
83
     *      }
84
     *   }
85
     * )
86
     */
87
    public function getTasksAction()
88
    {
89
        $tasks = [];
90
        $list  = $this->getTensideTasks();
91
        foreach ($list->getIds() as $taskId) {
92
            $tasks[$taskId] = $this->convertTaskToArray($list->getTask($taskId));
0 ignored issues
show
Bug introduced by
It seems like $list->getTask($taskId) can be null; however, convertTaskToArray() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
93
        }
94
95
        return $this->createJsonResponse($tasks, true);
96
    }
97
98
    /**
99
     * Retrieve the given task task.
100
     *
101
     * @param string  $taskId  The id of the task to retrieve.
102
     *
103
     * @param Request $request The request.
104
     *
105
     * @return JsonResponse
106
     *
107
     * @throws NotFoundHttpException When the task could not be found.
108
     *
109
     * @ApiDoc(
110
     *   section="tasks",
111
     *   statusCodes = {
112
     *     200 = "When everything worked out ok"
113
     *   },
114
     *   authentication = true,
115
     *   authenticationRoles = {
116
     *     "ROLE_MANIPULATE_REQUIREMENTS"
117
     *   },
118
     *   filters = {
119
     *     {
120
     *       "name"="offset",
121
     *       "dataType" = "int",
122
     *       "description"="If present, the output will be returned from the given byte offset."
123
     *     }
124
     *   }
125
     * )
126
     * @ApiDescription(
127
     *   response={
128
     *     "status" = {
129
     *       "dataType" = "string",
130
     *       "description" = "OK on success"
131
     *     },
132
     *     "task" = {
133
     *         "id" = {
134
     *           "dataType" = "string",
135
     *           "description" = "The task id."
136
     *         },
137
     *         "status" = {
138
     *           "dataType" = "string",
139
     *           "description" = "The task status."
140
     *         },
141
     *         "type" = {
142
     *           "dataType" = "string",
143
     *           "description" = "The type of the task."
144
     *         },
145
     *         "created_at" = {
146
     *           "dataType" = "string",
147
     *           "description" = "The date the task was created in ISO 8601 format."
148
     *         },
149
     *         "user_data" = {
150
     *           "dataType" = "object",
151
     *           "description" = "The user submitted additional data."
152
     *         },
153
     *         "output" = {
154
     *            "dataType" = "string",
155
     *            "description" = "The command line output of the task."
156
     *         }
157
     *      }
158
     *   }
159
     * )
160
     */
161
    public function getTaskAction($taskId, Request $request)
162
    {
163
        // Retrieve the status file of the task.
164
        $task   = $this->getTensideTasks()->getTask($taskId);
165
        $offset = null;
166
167
        if (!$task) {
168
            throw new NotFoundHttpException('No such task.');
169
        }
170
171
        if ($request->query->has('offset')) {
172
            $offset = (int) $request->query->get('offset');
173
        }
174
175
        return $this->createJsonResponse([$this->convertTaskToArray($task, true, $offset)]);
0 ignored issues
show
Bug introduced by
It seems like $offset defined by (int) $request->query->get('offset') on line 172 can also be of type integer; however, Tenside\CoreBundle\Contr...r::convertTaskToArray() does only seem to accept null, 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...
176
    }
177
178
    /**
179
     * Queue a task in the list.
180
     *
181
     * @param Request $request The request.
182
     *
183
     * @return JsonResponse
184
     *
185
     * @throws NotAcceptableHttpException When the payload is invalid.
186
     *
187
     * @ApiDoc(
188
     *   section="tasks",
189
     *   statusCodes = {
190
     *     201 = "When everything worked out ok",
191
     *     406 = "When the payload is invalid"
192
     *   },
193
     *   authentication = true,
194
     *   authenticationRoles = {
195
     *     "ROLE_MANIPULATE_REQUIREMENTS"
196
     *   },
197
     * )
198
     * @ApiDescription(
199
     *   response={
200
     *     "status" = {
201
     *       "dataType" = "string",
202
     *       "description" = "OK on success"
203
     *     },
204
     *     "task" = {
205
     *         "id" = {
206
     *           "dataType" = "string",
207
     *           "description" = "The task id."
208
     *         },
209
     *         "status" = {
210
     *           "dataType" = "string",
211
     *           "description" = "The task status."
212
     *         },
213
     *         "type" = {
214
     *           "dataType" = "string",
215
     *           "description" = "The type of the task."
216
     *         },
217
     *         "user_data" = {
218
     *           "dataType" = "object",
219
     *           "description" = "The user submitted additional data."
220
     *         },
221
     *         "created_at" = {
222
     *           "dataType" = "string",
223
     *           "description" = "The date the task was created in ISO 8601 format."
224
     *         }
225
     *      }
226
     *   }
227
     * )
228
     */
229
    public function addTaskAction(Request $request)
230
    {
231
        $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...
232
        $content  = $request->getContent();
233
        if (empty($content)) {
234
            throw new NotAcceptableHttpException('Invalid payload');
235
        }
236
        $metaData = new JsonArray($content);
0 ignored issues
show
Bug introduced by
It seems like $content defined by $request->getContent() on line 232 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...
237
        if (!$metaData->has('type')) {
238
            throw new NotAcceptableHttpException('Invalid payload');
239
        }
240
241
        try {
242
            $taskId = $this->getTensideTasks()->queue($metaData->get('type'), $metaData);
243
        } catch (\InvalidArgumentException $exception) {
244
            throw new NotAcceptableHttpException($exception->getMessage());
245
        }
246
247
        return $this->createJsonResponse(
248
            [$this->convertTaskToArray($this->getTensideTasks()->getTask($taskId))],
0 ignored issues
show
Bug introduced by
It seems like $this->getTensideTasks()->getTask($taskId) can be null; however, convertTaskToArray() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
249
            false,
250
            'OK',
251
            JsonResponse::HTTP_CREATED
252
        );
253
    }
254
255
    /**
256
     * Remove a task from the list.
257
     *
258
     * @param string $taskId The id of the task to remove.
259
     *
260
     * @return JsonResponse
261
     *
262
     * @throws NotFoundHttpException      When the given task could not be found.
263
     * @throws NotAcceptableHttpException When trying to delete a running task.
264
     *
265
     * @ApiDoc(
266
     *   section="tasks",
267
     *   statusCodes = {
268
     *     200 = "When everything worked out ok"
269
     *   },
270
     *   authentication = true,
271
     *   authenticationRoles = {
272
     *     "ROLE_MANIPULATE_REQUIREMENTS"
273
     *   },
274
     * )
275
     * @ApiDescription(
276
     *   response={
277
     *     "status" = {
278
     *       "dataType" = "string",
279
     *       "description" = "OK on success"
280
     *     }
281
     *   }
282
     * )
283
     */
284
    public function deleteTaskAction($taskId)
285
    {
286
        $list = $this->getTensideTasks();
287
        $task = $list->getTask($taskId);
288
289
        if (!$task) {
290
            throw new NotFoundHttpException('Task id ' . $taskId . ' not found');
291
        }
292
293
        if ($task->getStatus() === Task::STATE_RUNNING) {
294
            throw new NotAcceptableHttpException('Task id ' . $taskId . ' is running and can not be deleted');
295
        }
296
297
        $task->removeAssets();
298
        $list->remove($task->getId());
299
300
        return JsonResponse::create(
301
            [
302
                'status' => 'OK'
303
            ]
304
        );
305
    }
306
307
    /**
308
     * Starts the next pending task if any.
309
     *
310
     * @return JsonResponse
311
     *
312
     * @throws NotFoundHttpException      When no task could be found.
313
     * @throws NotAcceptableHttpException When a task is already running and holds the lock.
314
     *
315
     * @ApiDoc(
316
     *   section="tasks",
317
     *   statusCodes = {
318
     *     200 = "When everything worked out ok",
319
     *     404 = "When no pending task has been found",
320
     *     406 = "When another task is still running"
321
     *   },
322
     *   authentication = true,
323
     *   authenticationRoles = {
324
     *     "ROLE_MANIPULATE_REQUIREMENTS"
325
     *   },
326
     * )
327
     * @ApiDescription(
328
     *   response={
329
     *     "status" = {
330
     *       "dataType" = "string",
331
     *       "description" = "OK on success"
332
     *     },
333
     *     "task" = {
334
     *         "id" = {
335
     *           "dataType" = "string",
336
     *           "description" = "The task id."
337
     *         },
338
     *         "status" = {
339
     *           "dataType" = "string",
340
     *           "description" = "The task status."
341
     *         },
342
     *         "type" = {
343
     *           "dataType" = "string",
344
     *           "description" = "The type of the task."
345
     *         },
346
     *         "created_at" = {
347
     *           "dataType" = "string",
348
     *           "description" = "The date the task was created in ISO 8601 format."
349
     *         },
350
     *         "user_data" = {
351
     *           "dataType" = "object",
352
     *           "description" = "The user submitted additional data."
353
     *         }
354
     *      }
355
     *   }
356
     * )
357
     */
358
    public function runAction()
359
    {
360
        $lock = $this->container->get('tenside.taskrun_lock');
361
362
        if ($this->getTensideConfig()->isForkingAvailable() && !$lock->lock()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getTensideConfig()->isForkingAvailable() of type string|null is loosely compared to true; 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...
363
            throw new NotAcceptableHttpException('Task already running');
364
        }
365
366
        // Fetch the next queued task.
367
        $task = $this->getTensideTasks()->getNext();
368
369
        if (!$task) {
370
            throw new NotFoundHttpException('Task not found');
371
        }
372
373
        if ($task::STATE_PENDING !== $task->getStatus()) {
374
            return $this->createJsonResponse([$this->convertTaskToArray($task)]);
375
        }
376
377
        // Now spawn a runner.
378
        try {
379
            $this->spawn($task);
380
        } finally {
381
            if ($this->getTensideConfig()->isForkingAvailable()) {
382
                $lock->release();
383
            }
384
        }
385
386
        return $this->createJsonResponse([$this->convertTaskToArray($task)]);
387
    }
388
389
    /**
390
     * Spawn a detached process for a task.
391
     *
392
     * @param Task $task The task to spawn a process for.
393
     *
394
     * @return void
395
     *
396
     * @throws \RuntimeException When the task could not be started.
397
     */
398
    private function spawn(Task $task)
399
    {
400
        $config      = $this->getTensideConfig();
401
        $home        = $this->getTensideHome();
402
        $commandline = PhpProcessSpawner::create($config, $home)->spawn(
403
            [
404
                $this->get('tenside.cli_script')->cliExecutable(),
405
                'tenside:runtask',
406
                $task->getId(),
407
                '-v',
408
                '--no-interaction'
409
            ]
410
        );
411
412
        $this->get('logger')->debug('SPAWN CLI: ' . $commandline->getCommandLine());
413
        $commandline->setTimeout(0);
414
        $commandline->start();
415
        if (!$commandline->isRunning()) {
416
            // We might end up here when the process has been forked.
417
            // If exit code is neither 0 nor null, we have a problem here.
418
            if ($exitCode = $commandline->getExitCode()) {
419
                /** @var LoggerInterface $logger */
420
                $logger = $this->get('logger');
421
                $logger->error('Failed to execute "' . $commandline->getCommandLine() . '"');
422
                $logger->error('Exit code: ' . $commandline->getExitCode());
423
                $logger->error('Output: ' . $commandline->getOutput());
424
                $logger->error('Error output: ' . $commandline->getErrorOutput());
425
                throw new \RuntimeException(
426
                    sprintf(
427
                        'Spawning process task %s resulted in exit code %s',
428
                        $task->getId(),
429
                        $exitCode
430
                    )
431
                );
432
            }
433
        }
434
435
        $commandline->wait();
436
    }
437
438
    /**
439
     * Convert a task to an array.
440
     *
441
     * @param Task $task         The task to convert.
442
     *
443
     * @param bool $addOutput    Flag determining if the output shall get added or not.
444
     *
445
     * @param null $outputOffset The output offset to use.
446
     *
447
     * @return array
448
     */
449
    private function convertTaskToArray(Task $task, $addOutput = false, $outputOffset = null)
450
    {
451
        $data = [
452
            'id'         => $task->getId(),
453
            'status'     => $task->getStatus(),
454
            'type'       => $task->getType(),
455
            'created_at' => $task->getCreatedAt()->format(\DateTime::ISO8601),
456
            'user_data'  => $task->getUserData()
457
        ];
458
459
        if (true === $addOutput) {
460
            $data['output'] = $task->getOutput($outputOffset);
461
        }
462
463
        return $data;
464
    }
465
466
    /**
467
     * Create a JsonResponse based on an array of tasks.
468
     *
469
     * @param array[] $tasks        The task data.
470
     *
471
     * @param bool    $isCollection Flag if the data is a task collection.
472
     *
473
     * @param string  $status       Status code string.
474
     *
475
     * @param int     $httpStatus   HTTP Status to send.
476
     *
477
     * @return JsonResponse
478
     */
479
    private function createJsonResponse(
480
        array $tasks,
481
        $isCollection = false,
482
        $status = 'OK',
483
        $httpStatus = JsonResponse::HTTP_OK
484
    ) {
485
        $data       = ['status' => $status];
486
        $key        = $isCollection ? 'tasks' : 'task';
487
        $data[$key] = $isCollection ? $tasks : $tasks[0];
488
489
        return JsonResponse::create($data, $httpStatus)
490
            ->setEncodingOptions((JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_FORCE_OBJECT));
491
    }
492
}
493