Completed
Push — master ( 86267b...90a194 )
by Yanick
03:04
created

TaskRunnerController   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 425
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 13
Bugs 1 Features 3
Metric Value
wmc 24
c 13
b 1
f 3
lcom 1
cbo 11
dl 0
loc 425
rs 10

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getTasksAction() 0 10 2
A getTaskAction() 0 16 3
B addTaskAction() 0 24 4
A deleteTaskAction() 0 22 3
B runAction() 0 30 6
B spawn() 0 35 3
A convertTaskToArray() 0 10 1
A createJsonResponse() 0 11 2
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 Tenside\Core\Util\PhpProcessSpawner;
31
use Tenside\CoreBundle\Annotation\ApiDescription;
32
use Tenside\Core\Task\Task;
33
use Tenside\Core\Util\JsonArray;
34
35
/**
36
 * Lists and executes queued tasks.
37
 */
38
class TaskRunnerController extends AbstractController
39
{
40
    /**
41
     * Retrieve the task list.
42
     *
43
     * @return JsonResponse
44
     *
45
     * @ApiDoc(
46
     *   section="tasks",
47
     *   statusCodes = {
48
     *     200 = "When everything worked out ok"
49
     *   },
50
     *   authentication = true,
51
     *   authenticationRoles = {
52
     *     "ROLE_MANIPULATE_REQUIREMENTS"
53
     *   },
54
     * )
55
     * @ApiDescription(
56
     *   response={
57
     *     "status" = {
58
     *       "dataType" = "string",
59
     *       "description" = "OK on success"
60
     *     },
61
     *     "tasks" = {
62
     *         "id" = {
63
     *           "dataType" = "string",
64
     *           "description" = "The task id."
65
     *         },
66
     *         "status" = {
67
     *           "dataType" = "string",
68
     *           "description" = "The task status."
69
     *         },
70
     *         "type" = {
71
     *           "dataType" = "string",
72
     *           "description" = "The type of the task."
73
     *         },
74
     *         "created_at" = {
75
     *           "dataType" = "string",
76
     *           "description" = "The date the task was created in ISO 8601 format."
77
     *         },
78
     *         "output" = {
79
     *            "dataType" = "string",
80
     *            "description" = "The command line output of the task."
81
     *         }
82
     *   }
83
     * )
84
     */
85
    public function getTasksAction()
86
    {
87
        $tasks = [];
88
        $list   = $this->getTensideTasks();
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 2 spaces but found 3 spaces

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
89
        foreach ($list->getIds() as $taskId) {
90
            $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...
91
        }
92
93
        return $this->createJsonResponse($tasks);
94
    }
95
96
    /**
97
     * Retrieve the given task task.
98
     *
99
     * @param string  $taskId  The id of the task to retrieve.
100
     *
101
     * @param Request $request The request.
102
     *
103
     * @return JsonResponse
104
     *
105
     * @throws NotFoundHttpException When the task could not be found.
106
     *
107
     * @ApiDoc(
108
     *   section="tasks",
109
     *   statusCodes = {
110
     *     200 = "When everything worked out ok"
111
     *   },
112
     *   authentication = true,
113
     *   authenticationRoles = {
114
     *     "ROLE_MANIPULATE_REQUIREMENTS"
115
     *   },
116
     *   filters = {
117
     *     {
118
     *       "name"="offset",
119
     *       "dataType" = "int",
120
     *       "description"="If present, the output will be returned from the given byte offset."
121
     *     }
122
     *   }
123
     * )
124
     * @ApiDescription(
125
     *   response={
126
     *     "status" = {
127
     *       "dataType" = "string",
128
     *       "description" = "OK on success"
129
     *     },
130
     *     "task" = {
131
     *         "id" = {
132
     *           "dataType" = "string",
133
     *           "description" = "The task id."
134
     *         },
135
     *         "status" = {
136
     *           "dataType" = "string",
137
     *           "description" = "The task status."
138
     *         },
139
     *         "type" = {
140
     *           "dataType" = "string",
141
     *           "description" = "The type of the task."
142
     *         },
143
     *         "created_at" = {
144
     *           "dataType" = "string",
145
     *           "description" = "The date the task was created in ISO 8601 format."
146
     *         },
147
     *         "output" = {
148
     *            "dataType" = "string",
149
     *            "description" = "The command line output of the task."
150
     *         }
151
     *   }
152
     * )
153
     */
154
    public function getTaskAction($taskId, Request $request)
155
    {
156
        // Retrieve the status file of the task.
157
        $task   = $this->getTensideTasks()->getTask($taskId);
158
        $offset = null;
159
160
        if (!$task) {
161
            throw new NotFoundHttpException('No such task.');
162
        }
163
164
        if ($request->query->has('offset')) {
165
            $offset = (int) $request->query->get('offset');
166
        }
167
168
        return $this->createJsonResponse([$this->convertTaskToArray($task, $offset)]);
0 ignored issues
show
Bug introduced by
It seems like $offset defined by (int) $request->query->get('offset') on line 165 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...
Documentation introduced by
array($this->convertTaskToArray($task, $offset)) is of type array<integer,array,{"0":"array"}>, but the function expects a array<integer,object<Tenside\Core\Task\Task>>.

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...
169
    }
170
171
    /**
172
     * Queue a task in the list.
173
     *
174
     * @param Request $request The request.
175
     *
176
     * @return JsonResponse
177
     *
178
     * @throws NotAcceptableHttpException When the payload is invalid.
179
     *
180
     * @ApiDoc(
181
     *   section="tasks",
182
     *   statusCodes = {
183
     *     201 = "When everything worked out ok"
184
     *   },
185
     *   authentication = true,
186
     *   authenticationRoles = {
187
     *     "ROLE_MANIPULATE_REQUIREMENTS"
188
     *   },
189
     * )
190
     * @ApiDescription(
191
     *   response={
192
     *     "status" = {
193
     *       "dataType" = "string",
194
     *       "description" = "OK on success"
195
     *     },
196
     *     "task" = {
197
     *         "id" = {
198
     *           "dataType" = "string",
199
     *           "description" = "The task id."
200
     *         },
201
     *         "status" = {
202
     *           "dataType" = "string",
203
     *           "description" = "The task status."
204
     *         },
205
     *         "type" = {
206
     *           "dataType" = "string",
207
     *           "description" = "The type of the task."
208
     *         },
209
     *         "created_at" = {
210
     *           "dataType" = "string",
211
     *           "description" = "The date the task was created in ISO 8601 format."
212
     *         },
213
     *         "output" = {
214
     *            "dataType" = "string",
215
     *            "description" = "The command line output of the task."
216
     *         }
217
     *   }
218
     * )
219
     */
220
    public function addTaskAction(Request $request)
221
    {
222
        $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...
223
        $content  = $request->getContent();
224
        if (empty($content)) {
225
            throw new NotAcceptableHttpException('Invalid payload');
226
        }
227
        $metaData = new JsonArray($content);
0 ignored issues
show
Bug introduced by
It seems like $content defined by $request->getContent() on line 223 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...
228
        if (!$metaData->has('type')) {
229
            throw new NotAcceptableHttpException('Invalid payload');
230
        }
231
232
        try {
233
            $taskId = $this->getTensideTasks()->queue($metaData->get('type'), $metaData);
234
        } catch (\InvalidArgumentException $exception) {
235
            throw new NotAcceptableHttpException($exception->getMessage());
236
        }
237
238
        return $this->createJsonResponse(
239
            [$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...
Documentation introduced by
array($this->convertTask...s()->getTask($taskId))) is of type array<integer,array,{"0":"array"}>, but the function expects a array<integer,object<Tenside\Core\Task\Task>>.

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...
240
            'OK',
241
            JsonResponse::HTTP_CREATED
242
        );
243
    }
244
245
    /**
246
     * Remove a task from the list.
247
     *
248
     * @param string $taskId The id of the task to remove.
249
     *
250
     * @return JsonResponse
251
     *
252
     * @throws NotFoundHttpException      When the given task could not be found.
253
     * @throws NotAcceptableHttpException When trying to delete a running task.
254
     *
255
     * @ApiDoc(
256
     *   section="tasks",
257
     *   statusCodes = {
258
     *     200 = "When everything worked out ok"
259
     *   },
260
     *   authentication = true,
261
     *   authenticationRoles = {
262
     *     "ROLE_MANIPULATE_REQUIREMENTS"
263
     *   },
264
     * )
265
     * @ApiDescription(
266
     *   response={
267
     *     "status" = {
268
     *       "dataType" = "string",
269
     *       "description" = "OK on success"
270
     *     }
271
     *   }
272
     * )
273
     */
274
    public function deleteTaskAction($taskId)
275
    {
276
        $list = $this->getTensideTasks();
277
        $task = $list->getTask($taskId);
278
279
        if (!$task) {
280
            throw new NotFoundHttpException('Task id ' . $taskId . ' not found');
281
        }
282
283
        if ($task->getStatus() === Task::STATE_RUNNING) {
284
            throw new NotAcceptableHttpException('Task id ' . $taskId . ' is running and can not be deleted');
285
        }
286
287
        $task->removeAssets();
288
        $list->remove($task->getId());
289
290
        return JsonResponse::create(
291
            [
292
                'status' => 'OK'
293
            ]
294
        );
295
    }
296
297
    /**
298
     * Starts the next pending task if any.
299
     *
300
     * @return JsonResponse
301
     *
302
     * @throws NotFoundHttpException      When no task could be found.
303
     * @throws NotAcceptableHttpException When a task is already running and holds the lock.
304
     *
305
     * @ApiDoc(
306
     *   section="tasks",
307
     *   statusCodes = {
308
     *     200 = "When everything worked out ok",
309
     *     404 = "When no pending task has been found",
310
     *     406 = "When another task is still running"
311
     *   },
312
     *   authentication = true,
313
     *   authenticationRoles = {
314
     *     "ROLE_MANIPULATE_REQUIREMENTS"
315
     *   },
316
     * )
317
     * @ApiDescription(
318
     *   response={
319
     *     "status" = {
320
     *       "dataType" = "string",
321
     *       "description" = "OK on success"
322
     *     },
323
     *     "task" = {
324
     *         "id" = {
325
     *           "dataType" = "string",
326
     *           "description" = "The task id."
327
     *         },
328
     *         "status" = {
329
     *           "dataType" = "string",
330
     *           "description" = "The task status."
331
     *         },
332
     *         "type" = {
333
     *           "dataType" = "string",
334
     *           "description" = "The type of the task."
335
     *         },
336
     *         "created_at" = {
337
     *           "dataType" = "string",
338
     *           "description" = "The date the task was created in ISO 8601 format."
339
     *         },
340
     *         "output" = {
341
     *            "dataType" = "string",
342
     *            "description" = "The command line output of the task."
343
     *         }
344
     *   }
345
     * )
346
     */
347
    public function runAction()
348
    {
349
        $lock = $this->container->get('tenside.taskrun_lock');
350
351
        if ($this->getTensideConfig()->isForkingAvailable() && !$lock->lock()) {
0 ignored issues
show
Bug introduced by
The method isForkingAvailable() does not seem to exist on object<Tenside\Core\Config\TensideJsonConfig>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
352
            throw new NotAcceptableHttpException('Task already running');
353
        }
354
355
        // Fetch the next queued task.
356
        $task = $this->getTensideTasks()->getNext();
357
358
        if (!$task) {
359
            throw new NotFoundHttpException('Task not found');
360
        }
361
362
        if ($task::STATE_PENDING !== $task->getStatus()) {
363
            return $this->createJsonResponse([$this->convertTaskToArray($task)]);
0 ignored issues
show
Documentation introduced by
array($this->convertTaskToArray($task)) is of type array<integer,array,{"0":"array"}>, but the function expects a array<integer,object<Tenside\Core\Task\Task>>.

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...
364
        }
365
366
        // Now spawn a runner.
367
        try {
368
            $this->spawn($task);
369
        } finally {
370
            if ($this->getTensideConfig()->isForkingAvailable()) {
0 ignored issues
show
Bug introduced by
The method isForkingAvailable() does not seem to exist on object<Tenside\Core\Config\TensideJsonConfig>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
371
                $lock->release();
372
            }
373
        }
374
375
        return $this->createJsonResponse([$this->convertTaskToArray($task)]);
0 ignored issues
show
Documentation introduced by
array($this->convertTaskToArray($task)) is of type array<integer,array,{"0":"array"}>, but the function expects a array<integer,object<Tenside\Core\Task\Task>>.

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...
376
    }
377
378
    /**
379
     * Spawn a detached process for a task.
380
     *
381
     * @param Task $task The task to spawn a process for.
382
     *
383
     * @return void
384
     *
385
     * @throws \RuntimeException When the task could not be started.
386
     */
387
    private function spawn(Task $task)
388
    {
389
        $config      = $this->getTensideConfig();
390
        $home        = $this->get('tenside.home')->homeDir();
391
        $commandline = PhpProcessSpawner::create($config, $home)->spawn(
392
            [
393
                $this->get('tenside.cli_script')->cliExecutable(),
394
                'tenside:runtask',
395
                $task->getId(),
396
                '-v',
397
                '--no-interaction'
398
            ]
399
        );
400
401
        $commandline->start();
402
        if (!$commandline->isRunning()) {
403
            // We might end up here when the process has been forked.
404
            // If exit code is neither 0 nor null, we have a problem here.
405
            if ($exitCode = $commandline->getExitCode()) {
406
                /** @var LoggerInterface $logger */
407
                $logger = $this->get('logger');
408
                $logger->error('Failed to execute "' . $commandline->getCommandLine() . '"');
409
                $logger->error('Exit code: ' . $commandline->getExitCode());
410
                $logger->error('Output: ' . $commandline->getOutput());
411
                $logger->error('Error output: ' . $commandline->getErrorOutput());
412
                throw new \RuntimeException(
413
                    sprintf(
414
                        'Spawning process task %s resulted in exit code %s',
415
                        $task->getId(),
416
                        $exitCode
417
                    )
418
                );
419
            }
420
        }
421
    }
422
423
    /**
424
     * Convert a task to an array.
425
     *
426
     * @param Task $task
427
     * @param null $outputOffset
428
     *
429
     * @return array
430
     */
431
    private function convertTaskToArray(Task $task, $outputOffset = null)
432
    {
433
        return [
434
            'id'         => $task->getId(),
435
            'status'     => $task->getStatus(),
436
            'type'       => $task->getType(),
437
            'created_at' => $task->getCreatedAt()->format(\DateTime::ISO8601),
438
            'output'     => $task->getOutput($outputOffset)
439
        ];
440
    }
441
442
    /**
443
     * Create a JsonResponse based on an array of tasks.
444
     *
445
     * @param Task[] $tasks
446
     * @param string $status
447
     * @param int    $httpStatus
448
     *
449
     * @return JsonResponse
450
     */
451
    private function createJsonResponse(array $tasks, $status = 'OK', $httpStatus = JsonResponse::HTTP_OK)
452
    {
453
        $data = [
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 7 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
454
            'status' => $status
455
        ];
456
        $key = 1 === count($tasks) ? 'task' : 'tasks';
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
457
        $data[$key] = $tasks;
458
459
        return JsonResponse::create($data, $httpStatus)
460
            ->setEncodingOptions((JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_FORCE_OBJECT));
461
    }
462
}
463