diff --git a/src/Command/DaemonCommand.php b/src/Command/DaemonCommand.php index 3296a66..af1a1a6 100644 --- a/src/Command/DaemonCommand.php +++ b/src/Command/DaemonCommand.php @@ -33,13 +33,15 @@ class DaemonCommand extends Command $this ->setDescription('The deamon slayer of webcron') ->setHelp('This command is the daemon process of webcron, enabling webcron to actually run jobs on time') - ->addOption('time-limit', 't', InputOption::VALUE_REQUIRED, 'Time limit in seconds before stopping the daemon.'); + ->addOption('time-limit', 't', InputOption::VALUE_REQUIRED, 'Time limit in seconds before stopping the daemon.') + ->addOption('async', 'a', InputOption::VALUE_NEGATABLE, 'Time limit in seconds before stopping the daemon.'); } protected function execute(InputInterface $input, OutputInterface $output) { $jobRepo = $this->doctrine->getRepository(Job::class); $timelimit = $input->getOption('time-limit') ?? false; + $async = $input->getOption('async') ?? function_exists('pcntl_fork'); if ($timelimit === false) { $endofscript = false; } elseif(is_numeric($timelimit)) { @@ -55,27 +57,29 @@ class DaemonCommand extends Command $jobsToRun = $jobRepo->getJobsDue(); if(!empty($jobsToRun)) { foreach($jobsToRun as $job) { - $jobObj = $jobRepo->getJob($job['id']); - if($jobObj['data']['crontype'] == 'reboot') { + if($job->getData('crontype') == 'reboot') { $str = @file_get_contents('/proc/uptime'); $num = floatval($str); - $rebootedself = ($num < $jobObj['data']['reboot-duration'] * 60); - $consolerun = $jobRepo->getTempVar($job['id'], 'consolerun', false); + $rebootedself = ($num < $job->getData('reboot-duration') * 60); + $consolerun = $jobRepo->getTempVar($job->getId(), 'consolerun', false); if($consolerun && !$rebootedself) continue; } - $jobRepo->setJobRunning($job['id'], true); - $output->writeln('Running Job ' . $job['id']); - declare(ticks = 1); - pcntl_signal(SIGCHLD, SIG_IGN); - $pid = pcntl_fork(); - $this->doctrine->getConnection()->close(); - $jobRepo = $this->doctrine->getRepository(Job::class); - if($pid == -1) { - $jobRepo->RunJob($job['id'], $job['running'] == 2); - $jobRepo->setJobRunning($job['id'], false); + $jobRepo->setJobRunning($job->getId(), true); + $output->writeln('Running Job ' . $job->getId()); + if($async) { + declare(ticks = 1); + pcntl_signal(SIGCHLD, SIG_IGN); + $pid = pcntl_fork(); + $this->doctrine->getConnection()->close(); + $jobRepo = $this->doctrine->getRepository(Job::class); + } + + if(!$async || $pid == -1) { + $jobRepo->RunJob($job->getId(), $job->getRunning() == 2); + $jobRepo->setJobRunning($job->getId(), false); } elseif ($pid == 0) { - $jobRepo->RunJob($job['id'], $job['running'] == 2); - $jobRepo->setJobRunning($job['id'], false); + $jobRepo->RunJob($job->getId(), $job->getRunning() == 2); + $jobRepo->setJobRunning($job->getId(), false); exit; } } diff --git a/src/Controller/JobController.php b/src/Controller/JobController.php index 0e1f422..9cdbbb3 100644 --- a/src/Controller/JobController.php +++ b/src/Controller/JobController.php @@ -42,7 +42,7 @@ class JobController extends AbstractController if($request->getMethod() == 'GET') { $jobRepo = $doctrine->getRepository(Job::class); $job = $jobRepo->getJob($id, true); - return $this->render('job/edit.html.twig', $job); + return $this->render('job/edit.html.twig', ['job' => $job]); } elseif($request->getMethod() == 'POST') { $allValues = $request->request->all(); $jobRepo = $doctrine->getRepository(Job::class); diff --git a/src/Entity/Job.php b/src/Entity/Job.php index c6ae04d..4988ac5 100644 --- a/src/Entity/Job.php +++ b/src/Entity/Job.php @@ -5,6 +5,7 @@ namespace App\Entity; use App\Repository\JobRepository; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use InvalidArgumentException; #[ORM\Entity(repositoryClass: JobRepository::class)] class Job @@ -100,33 +101,78 @@ class Job return $this; } - /** - * @return array - */ - public function getData(): array + public function getData(?string $name = ''): mixed { - return json_decode($this->data, true); + $data = json_decode($this->data, true); + if(!empty($name)) { + $names = explode('.', $name); + foreach($names as $item) { + if(!isset($data[$item])) { + return NULL; + } + $data = $data[$item]; + } + } + return $data; } - /** - * @param array $data - * @return Job - */ public function setData(array $data): Job { $this->data = json_encode($data); return $this; } - public function addData(string $name, mixed $value): Job + public function addData(string $name, mixed $value): mixed { $data = json_decode($this->data, true); - $data[$name] = $value; + if (!empty($name)) { + $this->addDataItem($data, $name, $value); + } $this->data = json_encode($data); - return $this; } + private function addDataItem(array &$data, array|string $name, mixed $value): bool + { + $names = is_array($name) ? $name : explode('.', $name); + $current = $names[0]; + if(isset($data[$current]) && is_array($data[$current])) { + unset($names[0]); + $this->addDataItem($data[$current], array_values($names), $value); + } else { + $data[$names[0]] = $value; + } + return true; + } + + public function removeData(?string $name = ''): mixed + { + $data = json_decode($this->data, true); + if (!empty($name)) { + $this->removeDataItem($data, $name); + } + return $this; + } + + private function removeDataItem(array &$data, array|string $name): bool + { + $names = is_array($name) ? $name : explode('.', $name); + $current = $names[0]; + if(is_array($data[$current])) { + unset($names[0]); + $this->removeDataItem($data[$current], array_values($names)); + } elseif(!isset($data[$current])) { + return false; + } else { + unset($names[0]); + } + return true; + } + + public function hasData($name): bool + { + return !empty($this->getData($name)); + } /** * @return int */ diff --git a/src/Repository/JobRepository.php b/src/Repository/JobRepository.php index 342d05c..23777fe 100644 --- a/src/Repository/JobRepository.php +++ b/src/Repository/JobRepository.php @@ -25,7 +25,7 @@ class JobRepository extends EntityRepository $return = []; foreach($jobs as $job) { - if($job->getData()['needschecking']) { + if($job->getData('needschecking')) { $return[] = $job; } } @@ -35,13 +35,14 @@ class JobRepository extends EntityRepository public function getRunningJobs(bool $idiskey = false): array { $qb = $this->createQueryBuilder('job'); - return $qb->where('job.running != 0')->getQuery()->getResult(); + return $qb + ->where('job.running != 0') + ->getQuery()->getResult(); } public function getAllJobs(bool $idiskey = false) { $qb = $this->createQueryBuilder('job'); - /** @var Job[] $jobs */ $jobs = $qb ->orderBy('job.name') @@ -79,19 +80,29 @@ class JobRepository extends EntityRepository public function getJobsDue() { - $jobsSql = "SELECT id, running - FROM job - WHERE ( - nextrun <= :timestamp - AND (lastrun IS NULL OR lastrun > :timestamplastrun) - AND running IN (0,2) - ) - OR (running NOT IN (0,1,2) AND running < :timestamprun) - OR (running = 2)"; - $jobsStmt = $this->getEntityManager()->getConnection()->prepare($jobsSql); - $jobsRslt = $jobsStmt->executeQuery([':timestamp' => time(), ':timestamplastrun' => time(), ':timestamprun' => time()]); - $jobs = $jobsRslt->fetchAllAssociative(); - return $jobs; + $qb = $this->createQueryBuilder('job'); + return $qb + ->where( + $qb->expr()->andX( + $qb->expr()->lte('job.nextrun', ':timestamp'), + $qb->expr()->orX( + $qb->expr()->isNull('job.lastrun'), + $qb->expr()->gt('job.lastrun', ':timestamp') + ), + $qb->expr()->in('job.running', [0,2]) + ) + ) + ->orWhere( + $qb->expr()->andX( + $qb->expr()->notIn('job.running', [0,1,2]), + $qb->expr()->lt('job.running', ':timestamp') + ) + ) + ->orWhere('job.running = 2') + ->orderBy('job.running', 'DESC') + ->addOrderBy('job.nextrun', 'ASC') + ->setParameter(':timestamp', time()) + ->getQuery()->getResult(); } public function getTimeOfNextRun() @@ -186,25 +197,25 @@ class JobRepository extends EntityRepository return $result['temp_vars'][$name] ?? $default; } - private function runHttpJob(array $job): array + private function runHttpJob(Job $job): array { $client = new Client(); - if(!empty($job['data']['vars'])) { - foreach($job['data']['vars'] as $key => $var) { - if (!empty($job['data']['basicauth-username'])) $job['data']['basicauth-username'] = str_replace('{' . $key . '}', $var['value'], $job['data']['basicauth-username']); - $job['data']['url'] = str_replace('{' . $key . '}', $var['value'], $job['data']['url']); + if(!empty($job->getData('vars'))) { + foreach($job->getData('vars') as $key => $var) { + if (!empty($job->getData('basicauth-username'))) $job->addData('basicauth-username', str_replace('{' . $key . '}', $var['value'], $job->getData('basicauth-username'))); + $job->addData('url', str_replace('{' . $key . '}', $var['value'], $job->getData('url'))); } } - $url = $job['data']['url']; + $url = $job->getData('url'); $options['http_errors'] = false; - $options['auth'] = !empty($job['data']['basicauth-username']) ? [$job['data']['basicauth-username'], $job['data']['basicauth-password']] : NULL; + $options['auth'] = !empty($job->getData('basicauth-username')) ? [$job->getData('basicauth-username'), $job->getData('basicauth-password')] : NULL; try { $res = $client->request('GET', $url, $options); $return['exitcode'] = $res->getStatusCode(); $return['output'] = $res->getBody(); - $return['failed'] = !in_array($return['exitcode'], $job['data']['http-status']); + $return['failed'] = !in_array($return['exitcode'], $job->getData('http-status')); } catch(GuzzleException $exception) { $return['exitcode'] = $exception->getCode(); $return['output'] = $exception->getMessage(); @@ -214,25 +225,25 @@ class JobRepository extends EntityRepository return $return; } - private function runCommandJob(array $job): array + private function runCommandJob(Job $job): array { - if(!empty($job['data']['vars'])) { - foreach ($job['data']['vars'] as $key => $var) { - $job['data']['command'] = str_replace('{' . $key . '}', $var['value'], $job['data']['command']); + if(!empty($job->getData('vars'))) { + foreach ($job->getData('vars') as $key => $var) { + $job->addData('command', str_replace('{' . $key . '}', $var['value'], $job->getData('command'))); } } - $command = $job['data']['command']; - if ($job['data']['containertype'] == 'docker') { - $command = $this->prepareDockerCommand($command, $job['data']['service'], $job['data']['container-user']); + $command = $job->getData('command'); + if ($job->getData('containertype') == 'docker') { + $command = $this->prepareDockerCommand($command, $job->getData('service'), $job->getData('container-user')); } try { - if($job['data']['hosttype'] == 'local') { + if($job->getData('hosttype') == 'local') { $return = $this->runLocalCommand($command); - } elseif($job['data']['hosttype'] == 'ssh') { - $return = $this->runSshCommand($command, $job['data']['host'], $job['data']['user'], $job['data']['ssh-privkey'], $job['data']['privkey-password']); + } elseif($job->getData('hosttype') == 'ssh') { + $return = $this->runSshCommand($command, $job->getData('host'), $job->getData('user'), $job->getData('ssh-privkey'), $job->getData('privkey-password')); } - $return['failed'] = !in_array($return['exitcode'], $job['data']['response']); + $return['failed'] = !in_array($return['exitcode'], $job->getData('response')); } catch (\RuntimeException $exception) { $return['exitcode'] = $exception->getCode(); $return['output'] = $exception->getMessage(); @@ -253,7 +264,7 @@ class JobRepository extends EntityRepository return $return; } - private function runSshCommand(string $command, string $host, string $user, string $privkey, string $password): array + private function runSshCommand(string $command, string $host, string $user, ?string $privkey, ?string $password): array { $ssh = new SSH2($host); $key = null; @@ -378,14 +389,13 @@ class JobRepository extends EntityRepository public function runJob(int $job, bool $manual): array { - global $kernel; $starttime = microtime(true); $job = $this->getJob($job, true); - if ($job['data']['crontype'] == 'http') { + if ($job->getData('crontype') == 'http') { $result = $this->runHttpJob($job); - } elseif ($job['data']['crontype'] == 'command') { + } elseif ($job->getData('crontype') == 'command') { $result = $this->runCommandJob($job); - } elseif ($job['data']['crontype'] == 'reboot') { + } elseif ($job->getData('crontype') == 'reboot') { $result = $this->runRebootJob($job, $starttime, $manual); if(isset($result['status']) && $result['status'] == 'deferred') return $result; } @@ -405,8 +415,8 @@ class JobRepository extends EntityRepository } // Remove secrets from output - if(!empty($job['data']['vars'])) { - foreach($job['data']['vars'] as $key => $var) { + if(!empty($job->getData('vars'))) { + foreach($job->getData('vars') as $key => $var) { if ($var['issecret']) { $result['output'] = str_replace($var['value'], '{'.$key.'}', $result['output']); } @@ -415,20 +425,20 @@ class JobRepository extends EntityRepository // saving to database $this->getEntityManager()->getConnection()->close(); $runRepo = $this->getEntityManager()->getRepository(Run::class); - $runRepo->addRun($job['id'], $result['exitcode'], floor($starttime), $runtime, $result['output'], $flags); + $runRepo->addRun($job->getId(), $result['exitcode'], floor($starttime), $runtime, $result['output'], $flags); if (!$manual){ // setting nextrun to next run - $nextrun = $job['nextrun']; + $nextrun = $job->getNextrun(); do { - $nextrun = $nextrun + $job['interval']; + $nextrun = $nextrun + $job->getInterval(); } while ($nextrun < time()); $addRunSql = 'UPDATE job SET nextrun = :nextrun WHERE id = :id'; $addRunStmt = $this->getEntityManager()->getConnection()->prepare($addRunSql); - $addRunStmt->executeQuery([':id' => $job['id'], ':nextrun' => $nextrun]); + $addRunStmt->executeQuery([':id' => $job->getId(), ':nextrun' => $nextrun]); } - return ['job_id' => $job['id'], 'exitcode' => $result['exitcode'], 'timestamp' =>floor($starttime), 'runtime' => $runtime, 'output' => (string)$result['output'], 'flags' => implode("", $flags)]; + return ['job_id' => $job->getId(), 'exitcode' => $result['exitcode'], 'timestamp' =>floor($starttime), 'runtime' => $runtime, 'output' => (string)$result['output'], 'flags' => implode("", $flags)]; } public function unlockJob(int $id = 0): void @@ -618,50 +628,47 @@ class JobRepository extends EntityRepository } public function getJob(int $id, bool $withSecrets = false) { - $jobSql = "SELECT * FROM job WHERE id = :id"; - $jobStmt = $this->getEntityManager()->getConnection()->prepare($jobSql); - $jobRslt = $jobStmt->executeQuery([':id' => $id])->fetchAssociative(); + $job = $this->find($id); - $jobRslt['data'] = json_decode($jobRslt['data'], true); - - if(!empty($jobRslt['data']['vars'])) { - foreach ($jobRslt['data']['vars'] as $key => &$value) { + if(!empty($job->getData('vars'))) { + foreach ($job->getData('vars') as $key => &$value) { if ($value['issecret']) { - $value['value'] = ($withSecrets) ? Secret::decrypt(base64_decode($value['value'])) : ''; + $job->addData('vars.' . $key . '.value', ($withSecrets) ? Secret::decrypt(base64_decode($value['value'])) : ''); } } } - switch($jobRslt['data']['crontype']) { + switch($job->getData('crontype')) { case 'http': - if(isset($jobRslt['data']['vars']['basicauth-password']['value'])) { - $jobRslt['data']['basicauth-password'] = $jobRslt['data']['vars']['basicauth-password']['value']; - unset($jobRslt['data']['vars']['basicauth-password']); + if($job->hasData('vars.basicauth-password.value')) { + $job->addData('basicauth-password', $job->getData('vars.basicauth-password.value')); + $job->removeData('vars.basicauth-password'); } break; case 'reboot': - $jobRslt['data']['reboot-delay'] = $jobRslt['data']['vars']['reboot-delay']['value']; - $jobRslt['data']['reboot-delay-secs'] = $jobRslt['data']['vars']['reboot-delay-secs']['value']; - unset($jobRslt['data']['vars']['reboot-delay']); - unset($jobRslt['data']['vars']['reboot-delay-secs']); + $job->addData('reboot-delay', $job->getData('vars.reboot-delay.value')); + $job->addData('reboot-delay-secs', $job->getData('vars.reboot-delay-secs.value')); + + $job->removeData('vars.reboot-delay'); + $job->removeData('vars.reboot-delay-secs'); break; } - switch($jobRslt['data']['hosttype']) { + switch($job->getData('hosttype')) { case 'ssh': - if(isset($jobRslt['data']['vars']['ssh-privkey']['value'])) { - $jobRslt['data']['ssh-privkey'] = $jobRslt['data']['vars']['ssh-privkey']['value']; - unset($jobRslt['data']['vars']['ssh-privkey']); + if($job->hasData('vars.ssh-privkey.value')) { + $job->addData('ssh-privkey', $job->getData('vars.ssh-privkey.value')); + $job->removeData('vars.ssh-privkey'); } - if(isset($jobRslt['data']['vars']['privkey-password']['value'])) { - $jobRslt['data']['privkey-password'] = $jobRslt['data']['vars']['privkey-password']['value']; - unset($jobRslt['data']['vars']['privkey-password']); + if($job->hasData('vars.privkey-password.value')) { + $job->addData('privkey-password', $job->getData('vars.privkey-password.value')); + $job->removeData('vars.privkey-password'); } break; } - if($jobRslt['data']['crontype'] == 'http') { + if($job->getData('crontype') == 'http') { } - return $jobRslt; + return $job; } public function deleteJob(int $id) diff --git a/src/Repository/RunRepository.php b/src/Repository/RunRepository.php index e0c1cf4..f77bf27 100644 --- a/src/Repository/RunRepository.php +++ b/src/Repository/RunRepository.php @@ -16,20 +16,20 @@ class RunRepository extends EntityRepository public function getRunsForJob(int $id, bool $onlyfailed = false, int $maxage = NULL, bool $ordered = true): array { - $runsSql = "SELECT * FROM run WHERE job_id = :job"; - $params = [':job' => $id]; + $qb = $this->createQueryBuilder('run'); + $job = $this->getEntityManager()->getRepository(Job::class)->find($id); + $runs = $qb + ->where('run.job = :job') + ->setParameter(':job', $job); + if ($onlyfailed) { - $runsSql .= ' AND flags LIKE "%' . RunRepository::FAILED . '%"'; + $runs = $runs->andWhere('run.flags LIKE :flags')->setParameter(':flags', '%' . RunRepository::FAILED . '%'); } if($maxage !== NULL) { - $runsSql .= ' AND timestamp > :timestamp'; - $params[':timestamp'] = time() - ($maxage * 24 * 60 * 60); + $runs = $runs->andWhere('run.timestamp > :timestamp')->setParameter(':timestamp', time() - ($maxage * 24 * 60 * 60)); } - if ($ordered) $runsSql .= ' ORDER by timestamp DESC'; - $runsStmt = $this->getEntityManager()->getConnection()->prepare($runsSql); - $runsRslt = $runsStmt->executeQuery($params); - $runs = $runsRslt->fetchAllAssociative(); - return $runs; + if ($ordered) $runs->orderBy('run.timestamp', 'DESC'); + return $runs->getQuery()->getResult(); } public function addRun(int $jobid, string $exitcode, int $starttime, float $runtime, string $output, array $flags): void diff --git a/templates/job/edit.html.twig b/templates/job/edit.html.twig index f54d88b..5df5cd7 100644 --- a/templates/job/edit.html.twig +++ b/templates/job/edit.html.twig @@ -2,12 +2,12 @@ {% block title %}Add job{% endblock %} {% block content %}

Add a cronjob

-
+

General info

- + You can create colored tags by using [tag]
@@ -23,54 +23,54 @@
  • Every week
  • Every 4 weeks
  • - +
    - +
    - +
    Eternal - +
    - + How many days (at least) to keep runs of this job in the database
    -
    {% if attribute(data, 'fail-pct') is defined %}{{ attribute(data, 'fail-pct') }}{% else %}50{% endif %}%
    +
    {% if attribute(job.data, 'fail-pct') is defined %}{{ attribute(job.data, 'fail-pct') }}{% else %}50{% endif %}%
    - +
    - +

    Job details

    - - - {% if data.crontype == 'reboot' %} + {% if job.data.crontype == 'reboot' %}
    {% endif %} - -
    +

    Command details

    - +
    - +
    -
    +

    Reboot job details

    Use {reboot-delay} or {reboot-delay-secs} to add the delay in your command
    - +
    - +
    - + Delay between triggering reboot and actual reboot
    - + The amount of time the system takes to actually reboot
    -
    +

    HTTP request details

    - +
    - +
    - + This field is being saved as a secret
    - +
    -
    +

    Localhost details

    No options
    -
    +

    SSH host details

    - +
    - +
    - + - + Keep
    @@ -221,25 +221,25 @@
    - + If private key is empty this field is being used as ssh-password This field is being saved as a secret
    -
    +
    -
    +

    Docker container details

    - +
    - +
    @@ -255,8 +255,9 @@
    {% set key = 1 %} - {% if data.vars is defined %} - {% for id,var in data.vars %} + {% if job.data.vars is defined %} + Im defined! + {% for id,var in job.data.vars %}
    @@ -278,9 +279,9 @@ Add variable
    - - - + + +