Passed
Push — master ( fea572...1f758d )
by Ioannes
01:50
created

Cron::isActualJob()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
c 0
b 0
f 0
dl 0
loc 21
rs 9.2222
cc 6
nc 7
nop 1
1
<?php
2
namespace App\BxConsole;
3
4
use Doctrine\Common\Annotations\AnnotationReader;
5
use Symfony\Component\Console\Helper\Table;
6
use Symfony\Component\Console\Helper\TableCell;
7
use Symfony\Component\Console\Helper\TableSeparator;
8
use Symfony\Component\Console\Input\ArrayInput;
9
use Symfony\Component\Console\Input\InputInterface;
10
use Symfony\Component\Console\Input\InputOption;
11
use Symfony\Component\Console\Output\ConsoleOutputInterface;
12
use Symfony\Component\Console\Output\OutputInterface;
13
use App\BxConsole\Annotations\Agent;
14
15
class Cron extends BxCommand {
16
17
    use LockableTrait;
18
19
    const EXEC_STATUS_SUCCESS = 'SUCCESS';
20
    const EXEC_STATUS_ERROR = 'ERROR';
21
    const EXEC_STATUS_WORK = 'WORK';
22
23
    const SORT_NAME = 'name';
24
    const SORT_TIME = 'time';
25
26
    private $minAgentPeriod;
27
28
    protected function configure() {
29
30
        $this->setName('system:cron')
31
            ->setDescription('Job sheduler for application comands')
32
            ->addOption('status', 's', InputOption::VALUE_NONE, 'Show BX_CRONTAB status table')
33
            ->addOption('bytime', 't', InputOption::VALUE_NONE, 'Sort status table by exec time desc')
34
            ->addOption('clean', 'c', InputOption::VALUE_REQUIRED, 'Command to be clean crontab data (status, last exec)')
35
            ->addOption('all', 'a', InputOption::VALUE_NONE, 'Command to be clean all crontab data (status, last exec)');
36
    }
37
38
    protected function execute(InputInterface $input, OutputInterface $output): int
39
    {
40
        set_time_limit(EnvHelper::getCrontabTimeout());
41
42
        $logger = EnvHelper::getLogger('bx_cron');
43
        if($logger) {
44
            $this->setLogger($logger);
45
        }
46
47
        $showStatus = $input->getOption('status');
48
        $byTime = $input->getOption('bytime');
49
        if($showStatus) {
50
            $sort = ($byTime ? self::SORT_TIME : self::SORT_NAME);
51
            $this->showStatus($output, $sort);
52
            return 0;
53
        }
54
55
        if(EnvHelper::getSwitch('BX_CRONTAB_RUN', EnvHelper::SWITCH_STATE_OFF)) {
56
            if($this->logger) {
57
                $this->logger->alert('BxCron switch off');
58
            }
59
            return 0;
60
        }
61
62
        if(!$this->lock()) {
63
            $msg = 'The command is already running in another process.';
64
            $output->writeln($msg);
65
            if($this->logger) {
66
                $this->logger->warning($msg);
67
            }
68
            return 0;
69
        }
70
71
        if($sleepInterval = EnvHelper::checkSleepInterval()) {
72
            $msg = sprintf("Sleep in interval %s", $sleepInterval);
73
            $output->writeln($msg);
74
            if($this->logger) {
75
                $this->logger->warning($msg);
76
            }
77
            return 0;
78
        }
79
80
        $clean = $input->getOption('clean');
81
        if($clean) {
82
            $command = $this->getApplication()->find($clean);
83
            $this->cleanJob($command->getName());
84
            $output->writeln($command->getName() . " will be executed now");
85
            return 0;
86
        }
87
88
        $cleanAll = $input->getOption('all');
89
        if($cleanAll) {
90
            $this->cleanJob();
91
            $output->writeln("All commands will be executed now");
92
            return 0;
93
        }
94
95
        $this->executeJobs($output);
96
97
        $this->release();
98
99
        return 0;
100
    }
101
102
    protected function showStatus(OutputInterface $output, $sort) {
103
104
        $table = new Table($output);
105
        $table->setStyle('box-double');
106
107
        $isSwitchOff = EnvHelper::getSwitch('BX_CRONTAB_RUN', EnvHelper::SWITCH_STATE_OFF);
108
109
        $jobs = $this->getCronJobs();
110
        $this->sortCronTab($jobs, $sort);
0 ignored issues
show
Bug introduced by
It seems like $jobs can also be of type false; however, parameter $crontab of App\BxConsole\Cron::sortCronTab() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

110
        $this->sortCronTab(/** @scrutinizer ignore-type */ $jobs, $sort);
Loading history...
111
        $lastExec = 0;
112
        $hasError = false;
113
114
        foreach($jobs as $cmd => $job) {
115
            $execTime = $job['last_exec'];
116
            if($execTime > $lastExec) $lastExec = $execTime;
117
            if(!empty($job['error'])) {
118
                $hasError = true;
119
            }
120
        }
121
122
        $headStr = sprintf(
123
            "BX_CRONTAB_RUN: %s;  LAST_EXEC: %s;  AGENTS_COUNT: %d",
124
            ($isSwitchOff ? 'OFF' : 'ON'),
125
            ($lastExec ? date("d.m.Y H:i:s", $lastExec) : 'NONE'),
126
            count($jobs),
127
        );
128
129
        $header = [
130
            'Command',
131
            'Period',
132
            'Last Exec',
133
            'Status',
134
        ];
135
136
        if($hasError) {
137
            $header[] = 'Error';
138
        }
139
140
        $table->setHeaders([
141
            [new TableCell($headStr, ['colspan' => ($hasError ? 5 : 4)])],
142
            $header,
143
        ]);
144
145
        $cnt = 1;
146
        foreach($jobs as $cmd => $job) {
147
            if($cnt > 1) $table->addRow(new TableSeparator());
148
            $row = [
149
                $cmd,
150
                $job['period'],
151
                ($job['last_exec'] ? date("d.m.Y H:i:s", $job['last_exec']) : 'NONE'),
152
                $job['status'],
153
            ];
154
            if($hasError) {
155
                $row[] = $job['error'];
156
            }
157
            $table->addRow($row);
158
            $cnt++;
159
        }
160
161
        $table->render();
162
    }
163
164
    protected function cleanJob($command = false) {
165
166
        $crontab = [];
167
168
        if($command) {
169
            $crontab = $this->getCronTab();
170
            if($crontab === false) {
171
                return false;
172
            }
173
            unset($crontab[$command]);
174
        }
175
176
        $this->setCronTab($crontab);
177
    }
178
179
    protected function executeJobs(OutputInterface $output) {
180
181
        $jobs = $this->getCronJobs();
182
183
        if(is_array($jobs) && !empty($jobs)) {
184
185
            /*
186
             * Минимально допустимый период выполнения одной задачи
187
             * при котором гарантируется выполнение всех задач
188
             */
189
            $this->minAgentPeriod = (count($jobs) + 1) * EnvHelper::getBxCrontabPeriod();
190
191
            foreach($jobs as $cmd => $job) {
192
193
                if($this->isActualJob($job)) {
194
195
                    $job['status'] = self::EXEC_STATUS_WORK;
196
                    $this->updaateJob($cmd, $job);
197
198
                    $command = $this->getApplication()->find($cmd);
199
                    $cmdInput = new ArrayInput(['command' => $cmd]);
200
                    try {
201
202
                        $timeStart = microtime(true);
203
                        $returnCode = $command->run($cmdInput, $output);
204
205
                        if(!$returnCode) {
206
207
                            $job['status'] = self::EXEC_STATUS_SUCCESS;
208
209
                            $msg = sprintf("%s: SUCCESS [%.2f s]", $cmd, microtime(true) - $timeStart);
210
                            if($this->logger) {
211
                                $this->logger->alert($msg);
212
                            }
213
                            $output->writeln(PHP_EOL . $msg);
214
215
                        } else {
216
217
                            $job['status'] = self::EXEC_STATUS_ERROR;
218
                            $job['error_code'] = $returnCode;
219
220
                            $msg = sprintf("%s: ERROR [%.2f s]", $cmd, microtime(true) - $timeStart);
221
                            if($this->logger) {
222
                                $this->logger->alert($msg);
223
                            }
224
                            $output->writeln(PHP_EOL . $msg);
225
                        }
226
227
                    } catch (\Exception $e) {
228
229
                        $job['status'] = self::EXEC_STATUS_ERROR;
230
                        $job['error'] = $e->getMessage();
231
232
233
                        if($this->logger) {
234
                            $this->logger->error($e, ['command' => $cmd]);
235
                        }
236
                        $output->writeln(PHP_EOL . 'ERROR: ' . $e->getMessage());
237
238
                    } finally {
239
240
                        $job['last_exec'] = time();
241
                    }
242
243
                    $this->updaateJob($cmd, $job);
244
                    /*
245
                     * Let's do just one task
246
                     */
247
                    break;
248
                }
249
            } // foreach($jobs as $cmd => $job)
250
        } // if(!empty($jobs))
251
    }
252
253
    protected function isActualJob(&$job) {
254
255
        if($job['status'] == self::EXEC_STATUS_WORK) {
256
            return false;
257
        }
258
259
        $period = intval($job['period']);
260
261
        if($period > 0) {
262
            if($period < $this->minAgentPeriod) {
263
                $job['orig_period'] = $period;
264
                $period = $job['period'] = $this->minAgentPeriod;
265
            }
266
            if(time() - $job['last_exec'] >= $period) {
267
                return true;
268
            }
269
        } else if(!empty($job['times'])) {
270
            //TODO:
271
        }
272
273
        return false;
274
    }
275
276
    public function getCronJobs() {
277
278
        $crontab = $this->getCronTab();
279
        if($crontab === false) {
280
            return false;
281
        }
282
283
        /** @var Application $app */
284
        $app = $this->getApplication();
285
286
        $commands = $app->all();
287
288
        $selfCommands = [];
289
        foreach($commands as $command) {
290
            /** @var BxCommand $command */
291
            if($command instanceof BxCommand) {
292
                $name = $command->getName();
293
                $selfCommands[$name] = [
294
                    'object' => $command,
295
                ];
296
            }
297
        }
298
299
        $agents = [];
300
        $reader = new AnnotationReader();
301
        foreach($selfCommands as $cmd => $selfCommand) {
302
            $reflectionClass = new \ReflectionClass($selfCommand['object']);
303
            $annotations = $reader->getClassAnnotations($reflectionClass);
304
305
            foreach($annotations as $annotation) {
306
                if($annotation instanceof Agent) {
307
                    $agents[$cmd] = $annotation->toArray();
308
                }
309
            }
310
        }
311
312
        foreach($crontab as $cmd => $job) {
313
            if(is_array($job) && isset($agents[$cmd])) {
314
                $agents[$cmd] = array_merge($job, $agents[$cmd]);
315
            }
316
        }
317
318
        $this->setCronTab($agents);
319
320
        return $agents;
321
    }
322
323
    protected function updaateJob($cmd, $job) {
324
325
        return $this->updateCronTab([$cmd => $job]);
326
    }
327
328
    protected function updateCronTab(array $changedAgents) {
329
330
        $crontab = $this->getCronTab();
331
332
        if($crontab === false) {
333
            return false;
334
        } else {
335
            $crontab = array_merge($crontab, $changedAgents);
336
            return $this->setCronTab($crontab);
337
        }
338
    }
339
340
    protected function setCronTab(array $agents) {
341
342
        $isSuccess = true;
343
        $this->sortCronTab($agents);
344
345
        $filename = EnvHelper::getCrontabFile();
346
347
        $fh = fopen($filename, 'c');
348
        if (flock($fh, LOCK_EX)) {
349
            ftruncate($fh, 0);
350
            if(!fwrite($fh, json_encode($agents, JSON_PRETTY_PRINT))) {
351
                throw new \Exception('Unable to write BX_CRONTAB : ' . $filename);
352
            }
353
        } else {
354
            $isSuccess = false;
355
        }
356
        flock($fh, LOCK_UN);
357
        fclose($fh);
358
359
        return $isSuccess;
360
    }
361
362
    /**
363
     * @return array|false|mixed
364
     */
365
    protected function getCronTab() {
366
367
        $filename = EnvHelper::getCrontabFile();
368
369
        $fh = fopen($filename, 'r');
370
        if(flock($fh, LOCK_SH)) {
371
            $cronTab = [];
372
            if($data = fread($fh, filesize($filename))) {
373
                $decoded = json_decode($data, true);
374
                if(is_array($decoded)) {
375
                    $cronTab = $decoded;
376
                }
377
            }
378
        } else {
379
            $cronTab = false;
380
        }
381
        flock($fh, LOCK_UN);
382
        fclose($fh);
383
384
        return $cronTab;
385
    }
386
387
    protected function sortCronTab(array &$crontab, $sort = self::SORT_NAME) {
388
389
        if($sort == self::SORT_TIME) {
390
            $sorting = [];
391
            foreach($crontab as $cmd => $data) {
392
                $sorting[$cmd] = $data['last_exec'];
393
            }
394
            arsort($sorting, SORT_NUMERIC);
395
            $sorted = [];
396
            foreach($sorting as $cmd => $time) {
397
                $sorted[$cmd] = $crontab[$cmd];
398
            }
399
            $crontab = $sorted;
400
        } else {
401
            ksort($crontab, SORT_STRING);
402
        }
403
    }
404
}