Completed
Push — master ( 31e4dd...c38430 )
by Christian
07:22
created

TaskRunnerController::getEnvironment()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 14
rs 9.4285
cc 3
eloc 7
nc 3
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
 * @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
     *     "<task-id>[]" = {
58
     *       "children" = {
59
     *         "id" = {
60
     *           "dataType" = "string",
61
     *           "description" = "The task id."
62
     *         },
63
     *         "type" = {
64
     *           "dataType" = "string",
65
     *           "description" = "The type of the task."
66
     *         }
67
     *       }
68
     *     }
69
     *   }
70
     * )
71
     */
72
    public function getTasksAction()
73
    {
74
        $result = [];
75
        $list   = $this->getTensideTasks();
76
        foreach ($list->getIds() as $taskId) {
77
            $result[$taskId] = [
78
                'id'   => $taskId,
79
                'type' => $list->getTask($taskId)->getType()
80
            ];
81
        }
82
83
        return JsonResponse::create($result)
84
            ->setEncodingOptions((JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_FORCE_OBJECT));
85
    }
86
87
    /**
88
     * Retrieve the given task task.
89
     *
90
     * @param string  $taskId  The id of the task to retrieve.
91
     *
92
     * @param Request $request The request.
93
     *
94
     * @return JsonResponse
95
     *
96
     * @throws NotFoundHttpException When the task could not be found.
97
     *
98
     * @ApiDoc(
99
     *   section="tasks",
100
     *   statusCodes = {
101
     *     200 = "When everything worked out ok"
102
     *   },
103
     *   authentication = true,
104
     *   authenticationRoles = {
105
     *     "ROLE_MANIPULATE_REQUIREMENTS"
106
     *   },
107
     *   filters = {
108
     *     {
109
     *       "name"="offset",
110
     *       "dataType" = "int",
111
     *       "description"="If present, the output will be returned from the given byte offset."
112
     *     }
113
     *   }
114
     * )
115
     * @ApiDescription(
116
     *   response={
117
     *     "status" = {
118
     *       "dataType" = "string",
119
     *       "description" = "The task status."
120
     *     },
121
     *     "type" = {
122
     *       "dataType" = "string",
123
     *       "description" = "The task type."
124
     *     },
125
     *     "output" = {
126
     *       "dataType" = "string",
127
     *       "description" = "The command line output of the task."
128
     *     }
129
     *   }
130
     * )
131
     */
132
    public function getTaskAction($taskId, Request $request)
133
    {
134
        // Retrieve the status file of the task.
135
        $task   = $this->getTensideTasks()->getTask($taskId);
136
        $offset = null;
137
138
        if (!$task) {
139
            throw new NotFoundHttpException('No such task.');
140
        }
141
142
        if ($request->query->has('offset')) {
143
            $offset = (int) $request->query->get('offset');
144
        }
145
146
        return JsonResponse::create(
147
            [
148
                'status' => $task->getStatus(),
149
                'type'   => $task->getType(),
150
                'output' => $task->getOutput($offset)
151
            ]
152
        );
153
    }
154
155
    /**
156
     * Queue a task in the list.
157
     *
158
     * @param Request $request The request.
159
     *
160
     * @return JsonResponse
161
     *
162
     * @throws NotAcceptableHttpException When the payload is invalid.
163
     *
164
     * @ApiDoc(
165
     *   section="tasks",
166
     *   statusCodes = {
167
     *     201 = "When everything worked out ok"
168
     *   },
169
     *   authentication = true,
170
     *   authenticationRoles = {
171
     *     "ROLE_MANIPULATE_REQUIREMENTS"
172
     *   },
173
     * )
174
     * @ApiDescription(
175
     *   response={
176
     *     "status" = {
177
     *       "dataType" = "string",
178
     *       "description" = "OK on success"
179
     *     },
180
     *     "task" = {
181
     *       "dataType" = "string",
182
     *       "description" = "The id of the created task."
183
     *     }
184
     *   }
185
     * )
186
     */
187
    public function addTaskAction(Request $request)
188
    {
189
        $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...
190
        $content  = $request->getContent();
191
        if (empty($content)) {
192
            throw new NotAcceptableHttpException('Invalid payload');
193
        }
194
        $metaData = new JsonArray($content);
0 ignored issues
show
Bug introduced by
It seems like $content defined by $request->getContent() on line 190 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...
195
        if (!$metaData->has('type')) {
196
            throw new NotAcceptableHttpException('Invalid payload');
197
        }
198
199
        try {
200
            $taskId = $this->getTensideTasks()->queue($metaData->get('type'), $metaData);
201
        } catch (\InvalidArgumentException $exception) {
202
            throw new NotAcceptableHttpException($exception->getMessage());
203
        }
204
205
        return JsonResponse::create(
206
            [
207
                'status' => 'OK',
208
                'task'   => $taskId
209
            ],
210
            JsonResponse::HTTP_CREATED
211
        );
212
    }
213
214
    /**
215
     * Remove a task from the list.
216
     *
217
     * @param string $taskId The id of the task to remove.
218
     *
219
     * @return JsonResponse
220
     *
221
     * @throws NotFoundHttpException      When the given task could not be found.
222
     * @throws NotAcceptableHttpException When trying to delete a running task.
223
     *
224
     * @ApiDoc(
225
     *   section="tasks",
226
     *   statusCodes = {
227
     *     200 = "When everything worked out ok"
228
     *   },
229
     *   authentication = true,
230
     *   authenticationRoles = {
231
     *     "ROLE_MANIPULATE_REQUIREMENTS"
232
     *   },
233
     * )
234
     * @ApiDescription(
235
     *   response={
236
     *     "status" = {
237
     *       "dataType" = "string",
238
     *       "description" = "OK on success"
239
     *     }
240
     *   }
241
     * )
242
     */
243
    public function deleteTaskAction($taskId)
244
    {
245
        $list = $this->getTensideTasks();
246
        $task = $list->getTask($taskId);
247
248
        if (!$task) {
249
            throw new NotFoundHttpException('Task id ' . $taskId . ' not found');
250
        }
251
252
        if ($task->getStatus() === Task::STATE_RUNNING) {
253
            throw new NotAcceptableHttpException('Task id ' . $taskId . ' is running and can not be deleted');
254
        }
255
256
        $task->removeAssets();
257
        $list->remove($task->getId());
258
259
        return JsonResponse::create(
260
            [
261
                'status' => 'OK'
262
            ]
263
        );
264
    }
265
266
    /**
267
     * Starts the next pending task if any.
268
     *
269
     * @return JsonResponse
270
     *
271
     * @throws NotFoundHttpException      When no task could be found.
272
     * @throws NotAcceptableHttpException When a task is already running and holds the lock.
273
     *
274
     * @ApiDoc(
275
     *   section="tasks",
276
     *   statusCodes = {
277
     *     200 = "When everything worked out ok",
278
     *     404 = "When no pending task has been found",
279
     *     406 = "When another task is still running"
280
     *   },
281
     *   authentication = true,
282
     *   authenticationRoles = {
283
     *     "ROLE_MANIPULATE_REQUIREMENTS"
284
     *   },
285
     * )
286
     * @ApiDescription(
287
     *   response={
288
     *     "status" = {
289
     *       "dataType" = "string",
290
     *       "description" = "OK on success"
291
     *     },
292
     *     "type" = {
293
     *       "dataType" = "string",
294
     *       "description" = "The type of the started task."
295
     *     },
296
     *     "task" = {
297
     *       "dataType" = "string",
298
     *       "description" = "The id of the started task."
299
     *     }
300
     *   }
301
     * )
302
     */
303
    public function runAction()
304
    {
305
        $lock = $this->container->get('tenside.taskrun_lock');
306
307
        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...
308
            throw new NotAcceptableHttpException('Task already running');
309
        }
310
311
        // Fetch the next queued task.
312
        $task = $this->getTensideTasks()->getNext();
313
314
        if (!$task) {
315
            throw new NotFoundHttpException('Task not found');
316
        }
317
318
        if ($task::STATE_PENDING !== $task->getStatus()) {
319
            return JsonResponse::create(
320
                [
321
                    'status' => $task->getStatus(),
322
                    'type'   => $task->getType(),
323
                    'task'   => $task->getId()
324
                ],
325
                JsonResponse::HTTP_OK
326
            );
327
        }
328
329
        // Now spawn a runner.
330
        try {
331
            $this->spawn($task);
332
        } finally {
333
            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...
334
                $lock->release();
335
            }
336
        }
337
338
        return JsonResponse::create(
339
            [
340
                'status' => 'OK',
341
                'type'   => $task->getType(),
342
                'task'   => $task->getId()
343
            ],
344
            JsonResponse::HTTP_OK
345
        );
346
    }
347
348
    /**
349
     * Spawn a detached process for a task.
350
     *
351
     * @param Task $task The task to spawn a process for.
352
     *
353
     * @return void
354
     *
355
     * @throws \RuntimeException When the task could not be started.
356
     */
357
    private function spawn(Task $task)
358
    {
359
        $config      = $this->getTensideConfig();
360
        $home        = $this->get('tenside.home')->homeDir();
361
        $commandline = PhpProcessSpawner::create($config, $home)->spawn(
362
            [
363
                $this->get('tenside.cli_script')->cliExecutable(),
364
                'tenside:runtask',
365
                $task->getId(),
366
                '-v',
367
                '--no-interaction'
368
            ]
369
        );
370
371
        $commandline->start();
372
        if (!$commandline->isRunning()) {
373
            // We might end up here when the process has been forked.
374
            // If exit code is neither 0 nor null, we have a problem here.
375
            if ($exitCode = $commandline->getExitCode()) {
376
                /** @var LoggerInterface $logger */
377
                $logger = $this->get('logger');
378
                $logger->error('Failed to execute "' . $commandline->getCommandLine() . '"');
379
                $logger->error('Exit code: ' . $commandline->getExitCode());
380
                $logger->error('Output: ' . $commandline->getOutput());
381
                $logger->error('Error output: ' . $commandline->getErrorOutput());
382
                throw new \RuntimeException(
383
                    sprintf(
384
                        'Spawning process task %s resulted in exit code %s',
385
                        $task->getId(),
386
                        $exitCode
387
                    )
388
                );
389
            }
390
        }
391
    }
392
}
393