| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, and/or sell copies of the Software, subject to the following conditions: |
| 10 | |
| 11 | 1. **Origin Attribution**: |
| 12 | - You must not misrepresent the origin of the Software; you must not claim |
| 13 | you created the original Software. |
| 14 | - If the Software is used in a product, you must either: |
| 15 | a. Provide clear attribution in the product's documentation, user interface, |
| 16 | or other visible areas, **OR** |
| 17 | b. Pay the original developers a fee they specify in writing. |
| 18 | 2. **Usage Restriction**: |
| 19 | - The Software, or any derivative works, dependencies, or libraries |
| 20 | incorporating it, must not be used for censorship or to suppress freedom of |
| 21 | speech, expression, or creativity. Prohibited uses include, but are not |
| 22 | limited to: |
| 23 | - Censorship of so-called "hate speech", visuals, non-mainstream opinions, |
| 24 | ideas, or objective reality. |
| 25 | - Tools or systems designed to restrict access to information or |
| 26 | artistic works. |
| 27 | 3. **Notice Preservation**: |
| 28 | - This license and the above copyright notice must remain intact in all copies |
| 29 | of the source code. |
| 30 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | |
| 39 | // システムのcrontabから毎分呼ぶ |
| 40 | // * * * * * php /var/www/htdocs/cron.php |
| 41 | // Webから呼ばれない様に(public/の外にあるけど念の為) |
| 42 | if (PHP_SAPI !== 'cli') { |
| 43 | http_response_code(404); |
| 44 | exit; |
| 45 | } |
| 46 | |
| 47 | require_once __DIR__.DIRECTORY_SEPARATOR.'/autoload.php'; |
| 48 | define('ROOT', realpath(__DIR__)); |
| 49 | require_once ROOT.'/config/config.php'; |
| 50 | require_once ROOT.'/util.php'; |
| 51 | mb_internal_encoding('UTF-8'); |
| 52 | |
| 53 | use Std\Lib\Cron; |
| 54 | use Std\Lib\CronTab; |
| 55 | |
| 56 | $cron = new Cron(); |
| 57 | |
| 58 | // タスクはutil.phpのregister_cron_tasks()で登録する(管理画面からも使う) |
| 59 | register_cron_tasks($cron); |
| 60 | |
| 61 | $usage = <<<TXT |
| 62 | 使い方: php cron.php [コマンド] |
| 63 | |
| 64 | (なし) 予定の来たジョブを実行する(crontabから毎分呼ぶ) |
| 65 | list ジョブの一覧 |
| 66 | tasks 登録されたタスク名の一覧 |
| 67 | add "<予定>" <タスク> ジョブを追加する 例: add "*/10 * * * *" apfederate |
| 68 | edit <ID> "<予定>" <タスク> ジョブを変更する 例: edit 1 "@hourly" apfederate |
| 69 | enable|disable|toggle <ID> ジョブを有効・無効にする |
| 70 | delete <ID> ジョブを削除する |
| 71 | run <ID> ジョブを今すぐ実行する |
| 72 | next <ID> [件数] 次の実行予定(既定5件) |
| 73 | |
| 74 | 予定は「分 時 日 月 曜日」又は @yearly @monthly @weekly @daily @hourly。 |
| 75 | 「*」がシェルに展開されない様に、予定は引用符で囲んで下さい。 |
| 76 | |
| 77 | TXT; |
| 78 | |
| 79 | $args = array_slice($argv, 1); |
| 80 | $cmd = array_shift($args) ?? ''; |
| 81 | |
| 82 | /** 結果を表示して終了コードを返す */ |
| 83 | $finish = function(Result $res): int { |
| 84 | if ($res->isSuccess) { |
| 85 | if ((string)$res->message !== '') echo $res->message, "\n"; |
| 86 | return 0; |
| 87 | } |
| 88 | fwrite(STDERR, $res->message."\n"); |
| 89 | return 1; |
| 90 | }; |
| 91 | |
| 92 | /** 引数のID */ |
| 93 | $id = function() use ($args, $usage): int { |
| 94 | if (!isset($args[0]) || !ctype_digit($args[0])) { |
| 95 | fwrite(STDERR, "IDを指定して下さい。\n\n{$usage}"); |
| 96 | exit(2); |
| 97 | } |
| 98 | return (int)$args[0]; |
| 99 | }; |
| 100 | |
| 101 | /** 引数のジョブ(未登録のタスクは警告だけ) */ |
| 102 | $job = function(array $parts, bool $isActive = true) use ($cron, $usage): CronTab { |
| 103 | try { |
| 104 | $ct = CronTab::fromString(implode(' ', $parts), $isActive); |
| 105 | } catch (\InvalidArgumentException $e) { |
| 106 | fwrite(STDERR, $e->getMessage()."\n\n{$usage}"); |
| 107 | exit(2); |
| 108 | } |
| 109 | if (!in_array($ct->command, $cron->tasks(), true)) { |
| 110 | fwrite(STDERR, "注意:タスク「{$ct->command}」は登録されていません(util.phpのregister_cron_tasks()を参照)。\n"); |
| 111 | } |
| 112 | return $ct; |
| 113 | }; |
| 114 | |
| 115 | $fmt = fn(?int $t) => $t === null ? '-' : date('Y-m-d H:i', $t); |
| 116 | // 全角文字があっても列が揃う様に、表示幅で埋める |
| 117 | $pad = fn(string $s, int $w) => $s.str_repeat(' ', max(1, $w - mb_strwidth($s))); |
| 118 | |
| 119 | switch ($cmd) { |
| 120 | case '': |
| 121 | // crontabはメールで出力を送るので、問題がなければ何も出力しない |
| 122 | $res = $cron->run(); |
| 123 | if (!$res->isSuccess) exit($finish($res)); |
| 124 | foreach ($res->data['failed'] as $jid => $err) fwrite(STDERR, "#{$jid}:{$err}\n"); |
| 125 | exit(empty($res->data['failed']) ? 0 : 1); |
| 126 | |
| 127 | case 'list': |
| 128 | $res = $cron->list(); |
| 129 | if (!$res->isSuccess) exit($finish($res)); |
| 130 | if (empty($res->data)) { |
| 131 | echo "ジョブはありません。\n"; |
| 132 | exit(0); |
| 133 | } |
| 134 | $now = new \DateTimeImmutable(); |
| 135 | $widths = [5, 5, 20, 16, 17, 17]; |
| 136 | $row = function(array $cols) use ($pad, $widths): void { |
| 137 | $line = ''; |
| 138 | foreach ($widths as $i => $w) $line .= $pad((string)$cols[$i], $w); |
| 139 | echo rtrim($line.$cols[count($widths)]), "\n"; |
| 140 | }; |
| 141 | $row(['ID', '有効', '予定', 'タスク', '次回', '前回', 'エラー']); |
| 142 | foreach ($res->data as $d) { |
| 143 | $ct = CronTab::fromArray($d); |
| 144 | $err = $ct->validate(); |
| 145 | $next = $err === null && $ct->isActive ? $ct->nextRun($now)?->getTimestamp() : null; |
| 146 | $row([$ct->getId(), $ct->isActive ? 'o' : '-', $ct->expression(), $ct->command, |
| 147 | $fmt($next), $fmt($ct->lastRun), $err ?? $ct->lastError ?? '']); |
| 148 | } |
| 149 | exit(0); |
| 150 | |
| 151 | case 'tasks': |
| 152 | $tasks = $cron->tasks(); |
| 153 | echo empty($tasks) ? "登録されたタスクはありません。\n" : implode("\n", $tasks)."\n"; |
| 154 | exit(0); |
| 155 | |
| 156 | case 'add': |
| 157 | exit($finish($cron->add($job($args)))); |
| 158 | |
| 159 | case 'edit': |
| 160 | // 有効・無効はそのまま |
| 161 | $old = $cron->show($id()); |
| 162 | if (!$old->isSuccess) exit($finish($old)); |
| 163 | exit($finish($cron->edit($id(), $job(array_slice($args, 1), (bool)($old->data['isActive'] ?? true))))); |
| 164 | |
| 165 | case 'enable': |
| 166 | case 'disable': |
| 167 | case 'toggle': |
| 168 | case 'delete': |
| 169 | exit($finish($cron->$cmd($id()))); |
| 170 | |
| 171 | case 'run': |
| 172 | exit($finish($cron->runNow($id()))); |
| 173 | |
| 174 | case 'next': |
| 175 | $res = $cron->show($id()); |
| 176 | if (!$res->isSuccess) exit($finish($res)); |
| 177 | $ct = CronTab::fromArray($res->data); |
| 178 | if (($err = $ct->validate()) !== null) exit($finish(Result::Error($err))); |
| 179 | $n = isset($args[1]) && ctype_digit($args[1]) ? max(1, min(100, (int)$args[1])) : 5; |
| 180 | $t = new \DateTimeImmutable(); |
| 181 | for ($i = 0; $i < $n && ($t = $ct->nextRun($t)) !== null; $i++) echo $t->format('Y-m-d H:i (D)'), "\n"; |
| 182 | if ($i === 0) echo "5年以内に実行予定はありません。\n"; |
| 183 | exit(0); |
| 184 | |
| 185 | case 'help': |
| 186 | case '-h': |
| 187 | case '--help': |
| 188 | echo $usage; |
| 189 | exit(0); |
| 190 | |
| 191 | default: |
| 192 | fwrite(STDERR, "不明なコマンド:{$cmd}\n\n{$usage}"); |
| 193 | exit(2); |
| 194 | } |