From c74c3c23d4e52883912d42f38360a176ad4da42f Mon Sep 17 00:00:00 2001 From: JaguarJack Date: Wed, 16 Sep 2020 10:06:20 +0800 Subject: [PATCH] =?UTF-8?q?add:=E6=96=B0=E5=A2=9E=E7=9B=91=E6=8E=A7?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- catch/monitor/MonitorService.php | 29 ++ catch/monitor/command/CatchCrontabCommand.php | 175 +++++++++ catch/monitor/command/process/Attributes.php | 145 +++++++ catch/monitor/command/process/Master.php | 361 ++++++++++++++++++ catch/monitor/command/process/ParseTask.php | 47 +++ catch/monitor/command/process/Process.php | 213 +++++++++++ .../command/process/RegisterSignal.php | 156 ++++++++ catch/monitor/command/process/Store.php | 243 ++++++++++++ catch/monitor/controller/Crontab.php | 105 +++++ catch/monitor/controller/CrontabLog.php | 50 +++ .../migrations/20200914203553_crontab.php | 46 +++ .../migrations/20200915101135_crontab_log.php | 51 +++ catch/monitor/model/Crontab.php | 43 +++ catch/monitor/model/CrontabLog.php | 53 +++ .../monitor/model/search/CrontabLogSearch.php | 39 ++ catch/monitor/model/search/CrontabSearch.php | 24 ++ catch/monitor/module.json | 15 + catch/monitor/route.php | 22 ++ extend/catcher/base/CatchCronTask.php | 10 + .../catcher/command/CatchScheduleCommand.php | 104 ----- 20 files changed, 1827 insertions(+), 104 deletions(-) create mode 100644 catch/monitor/MonitorService.php create mode 100644 catch/monitor/command/CatchCrontabCommand.php create mode 100644 catch/monitor/command/process/Attributes.php create mode 100644 catch/monitor/command/process/Master.php create mode 100644 catch/monitor/command/process/ParseTask.php create mode 100644 catch/monitor/command/process/Process.php create mode 100644 catch/monitor/command/process/RegisterSignal.php create mode 100644 catch/monitor/command/process/Store.php create mode 100644 catch/monitor/controller/Crontab.php create mode 100644 catch/monitor/controller/CrontabLog.php create mode 100644 catch/monitor/database/migrations/20200914203553_crontab.php create mode 100644 catch/monitor/database/migrations/20200915101135_crontab_log.php create mode 100644 catch/monitor/model/Crontab.php create mode 100644 catch/monitor/model/CrontabLog.php create mode 100644 catch/monitor/model/search/CrontabLogSearch.php create mode 100644 catch/monitor/model/search/CrontabSearch.php create mode 100644 catch/monitor/module.json create mode 100644 catch/monitor/route.php create mode 100644 extend/catcher/base/CatchCronTask.php delete mode 100644 extend/catcher/command/CatchScheduleCommand.php diff --git a/catch/monitor/MonitorService.php b/catch/monitor/MonitorService.php new file mode 100644 index 0000000..7839860 --- /dev/null +++ b/catch/monitor/MonitorService.php @@ -0,0 +1,29 @@ +setName('catch:crontab') + ->addArgument('action', Argument::OPTIONAL, '[start|reload|stop|restart]', 'start') + ->addOption('daemon', '-d', Option::VALUE_NONE, 'daemon mode') + ->addOption('pid', '-p', Option::VALUE_REQUIRED, 'you can send signal to the process of pid') + ->addOption('static', '-s', Option::VALUE_REQUIRED, 'default static process number', 1) + ->addOption('dynamic', '-dy', Option::VALUE_REQUIRED, 'default dynamic process number', 10) + ->addOption('interval', '-i', Option::VALUE_REQUIRED, 'interval/seconds', 60) + ->setDescription('start catch crontab schedule'); + } + + protected function execute(Input $input, Output $output) + { + if (!$input->hasOption('pid')) { + $this->pid = Master::getMasterPid(); + } else { + $this->pid = $input->getOption('pid'); + } + + if (!extension_loaded('pcntl') || !extension_loaded('posix')) { + $output->error('you should install extension [pcntl && posix]'); + } else { + $this->{$input->getArgument('action')}(); + } + } + + /** + * 进程启动 + * + * @time 2020年09月14日 + * @return void + */ + protected function start() + { + $worker = new Master(); + + $worker->staticNumber($this->input->getOption('static')) + ->dynamic($this->input->getOption('dynamic')) + ->interval($this->input->getOption('interval')) + ->asDaemon($this->input->hasOption('daemon')) + ->run(); + } + + /** + * 停止任务 + * + * @time 2020年07月07日 + * @return void + */ + protected function stop() + { + $retryTimes = 0; + + if (Process::isAlive($this->pid)) { + $this->output->info('🔨 catch crontab is running.'); + Process::kill($this->pid, SIGTERM); + // 睡眠 1 秒 + $this->output->info('⌛️ killing catch crontab service, please waiting...'); + sleep(1); + if (!Process::isAlive($this->pid)) { + $this->output->info('🎉 catch crontab stopped!'); + } else { + while (true) { + Process::kill($this->pid, SIGKILL); + if (Process::isAlive($this->pid)) { + $retryTimes++; + $this->output->info('🔥 retry $retryTimes times'); + usleep(500 * 1000); + if ($retryTimes >= 3) { + $this->output->info('💔 catch crontab is running, stop failed, please use [kill -9 {$this->pid}] to Stop it'); + break; + } + } else { + $this->output->info('🎉 catch crontab stopped!'); + break; + } + } + } + Master::exitMasterDo(); + } else { + $this->output->error('🤔️ catch crontab is not running, Please Check it!'); + } + + $this->output->info('🎉 stop catch crontab successfully'); + } + + /** + * 重启任务 + * + * @time 2020年07月07日 + * @return void + */ + protected function reload() + { + Process::kill($this->pid, SIGUSR1); + } + + /** + * 重启 + * + * @time 2020年07月07日 + * @return void + */ + protected function restart() + { + $this->stop(); + + $this->output->info('catch crontab restarting...'); + + usleep(500 * 1000); + + $this->start(); + } + + /** + * status + * + * @time 2020年07月22日 + * @return void + */ + protected function status() + { + if ($this->pid) { + if (Process::isAlive($this->pid)) { + Process::kill($this->pid, SIGUSR2); + + usleep(100000); + + $worker = new Master; + $table = new Table; + + $table->setHeader(['PID', '名称', '内存', '处理任务', '开始时间', '运行时间', '状态'], Table::ALIGN_CENTER); + + $table->setRows($worker->getWorkerStatus(), Table::ALIGN_CENTER); + + $this->output->info($table->render()); + } else { + $this->output->error('🤔️ catch crontab is not running, Please Check it!'); + } + } else { + $this->output->error('🤔️ catch crontab is not running, Please Check it!'); + } + } +} diff --git a/catch/monitor/command/process/Attributes.php b/catch/monitor/command/process/Attributes.php new file mode 100644 index 0000000..e206ffd --- /dev/null +++ b/catch/monitor/command/process/Attributes.php @@ -0,0 +1,145 @@ +daemon = $daemon; + + return $this; + } + + public function staticNumber($n) + { + $this->static = $n; + + return $this; + } + + /** + * 可扩容 + * + * @time 2020年07月21日 + * @param $n + * @return $this + */ + public function dynamic($n) + { + $this->dynamic = $n; + + return $this; + } + + /** + * 定时 + * + * @time 2020年07月21日 + * @param $n + * @return $this + */ + public function interval($n) + { + $this->interval = $n; + + return $this; + } + + + /** + * 设置 name + * + * @time 2020年07月23日 + * @param $name + * @return $this + */ + public function name($name) + { + $this->name = $name; + + return $this; + } + + /** + * 设置报告错误 + * + * @time 2020年07月24日 + * @return void + */ + public function displayErrors() + { + ini_set('display_errors', 1); + + error_reporting(E_ALL & ~E_WARNING); + + ini_set('display_startup_errors', 1); + + ini_set('ignore_repeated_errors', 1); + } +} diff --git a/catch/monitor/command/process/Master.php b/catch/monitor/command/process/Master.php new file mode 100644 index 0000000..9202ef3 --- /dev/null +++ b/catch/monitor/command/process/Master.php @@ -0,0 +1,361 @@ +daemon) { + Process::daemon(); + } + + if ($this->interval) { + Process::alarm($this->interval); + } + $this->init(); + // 初始化进程池 + $this->initWorkers(); + // 设置进程名称 + Process::setWorkerName($this->name . ' master'); + // 注册信号 + $this->registerSignal(); + // 写入进程状态 + $this->setWorkerStatus($this->name . ' master'); + // 信号发送 + while (true) { + Process::dispatch(); + $pid = pcntl_waitpid(-1, $status, WNOHANG); + Process::dispatch(); + if ($pid > 0) { + if (isset($this->workerIds[$pid])) { + unset($this->workerIds[$pid]); + $this->deleteWorkerStatusFile($pid); + $this->worker_start_at = time(); + // 如果进程挂掉,正常退出码都是 0,当然这里可以自己定义,看 exit($status) 设置什么 + // 真实的 exit code pcntl_wexitstatus 函数获取 + // exit code > 0 都是由于异常导致的 + $exitCode = pcntl_wexitstatus($status); + if (!in_array($exitCode, [255, 250])) { + $this->forkStatic(); + } + } + // 如果静态工作进程全部退出,会发生 CPU 空转,所以这里需要 sleep 1 + if (!count($this->workerIds)) { + // sleep(1); + self::exitMasterDo(); + exit(0); + } + } + + usleep(500 * 1000); + } + } catch (\Throwable $exception) { + // todo + echo $exception->getMessage(); + } + } + + /** + * 初始化 + * @throws Exception + */ + protected function init() + { + $this->displayErrors(); + $this->start_at = $this->worker_start_at = time(); + // 记录 masterID + FileSystem::put(self::masterPidStorePath(), posix_getpid()); + // 保存信息 + $this->saveTaskInfo(); + // 初始化进程数量 + $this->allWorkersCount = $this->static; + // 显示UI + $this->display(); + // 重定向 + $this->dup(); + } + + /** + * 初始化进程池 + * + * @time 2020年07月21日 + * @return void + */ + protected function initWorkers() + { + $this->redis = $this->getRedisHandle(); + + for ($i = 0; $i < $this->static; $i++) { + $this->forkStatic(); + } + } + + /** + * fork 进程 + * + * @time 2020年07月21日 + * @return void + */ + protected function forkDynamic() + { + $process = new Process(function (Process $process) { + $redis = $this->getRedisHandle(); + while($crontab = $redis->rpop($this->crontabQueueName)) { + $task = $this->getTaskObject(\json_decode($crontab, true)); + $task->run(); + } + + $process->exit(); + }); + + $process->start(); + + $this->workerIds[$process->pid] = true; + } + + /** + * 静态进程 + * + * @time 2020年07月21日 + * @return void + */ + protected function forkStatic() + { + $process = new Process(function (Process $process) { + $process->initMemory(); + + $name = $this->name . ' worker'; + $this->setWorkerStatus($name, $this->dealNum, $this->status); + + Process::setWorkerName($name); + + Process::signal(SIGUSR2, function ($signal) use ($name) { + $this->setWorkerStatus($name, $this->dealNum, $this->status); + }); + + // 该信号保证进程完成任务后安全退出 + Process::signal(SIGTERM, function ($signal) { + $this->exitSafely = true; + }); + + while (true) { + /************** 任务 *********************/ + $this->status = 'busying'; + $this->setWorkerStatus($name, $this->dealNum, 'busying'); + + // 处理定时任务 + while ($crontab = $this->redis->rpop($this->crontabQueueName)) { + $task = $this->getTaskObject(\json_decode($crontab, true)); + $task->run(); + if ($task->shouldExit()) { + $process->exit(250); + } + $this->dealNum += 1; + } + + $this->status = 'waiting'; + $this->setWorkerStatus($name, $this->dealNum, 'waiting'); + /****************处理*********************/ + // 暂停一秒 让出CPU调度 + sleep(1); + // 信号调度 + Process::dispatch(); + // 是否需要安全退出 || 查看内存是否溢出 + if ($this->exitSafely || $process->isMemoryOverflow()) { + $process->exit(); + //exit(0); + } + } + }); + + $process->start(); + + $this->workerIds[$process->pid] = true; + } + + /** + * 重定向文件流 + * + * @time 2020年07月22日 + * @return void + * @throws Exception + */ + protected function dup() + { + if (!$this->daemon) { + return; + } + + global $stdout, $stderr; + + // 关闭标准输入输出 + fclose(STDOUT); + fclose(STDIN); + fclose(STDERR); + + // 重定向输出&错误 + $stdoutPath = self::$stdout ?: self::stdoutPath(); + + !file_exists($stdoutPath) && touch($stdoutPath); + // 等待 100 毫秒 + usleep(100 * 1000); + + $stdout = fopen($stdoutPath, 'a'); + + $stderr = fopen($stdoutPath, 'a'); + + return; + } + + /** + * 输出 + * + * @time 2020年07月21日 + * @return string + */ + public function output() + { + $isShowCtrlC = $this->daemon ? '' : 'Ctrl+c to stop' . "\r\n"; + + $info = <<output(); + + return fwrite(STDOUT, $this->renderStatus()); + } + + /** + * 获取 redis 句柄 + * + * @time 2020年09月15日 + * @return object|null + */ + protected function getRedisHandle() + { + return Cache::store('redis')->handler(); + } +} \ No newline at end of file diff --git a/catch/monitor/command/process/ParseTask.php b/catch/monitor/command/process/ParseTask.php new file mode 100644 index 0000000..38af4e6 --- /dev/null +++ b/catch/monitor/command/process/ParseTask.php @@ -0,0 +1,47 @@ +getTaskNamespace() . ucfirst(Str::camel($crontab['task'])); + + if (class_exists($class)) { + return app()->make($class)->setCrontab($crontab); + } + + throw new ClassNotFoundException('Task '. $crontab['task'] . ' not found'); + } + + /** + * 获取任务命名空间 + * + * @time 2020年09月15日 + * @return mixed + */ + protected function getTaskNamespace() + { + return config('catch.crontab.task_namespace'); + } +} \ No newline at end of file diff --git a/catch/monitor/command/process/Process.php b/catch/monitor/command/process/Process.php new file mode 100644 index 0000000..9109e1f --- /dev/null +++ b/catch/monitor/command/process/Process.php @@ -0,0 +1,213 @@ +callable = $callable; + } + + /** + * 守护进程 + * + * @time 2020年07月21日 + * @return void + * @throws FailedException + */ + public static function daemon() + { + $pid = pcntl_fork(); + + if ($pid < 0) { + throw new FailedException('fork process failed'); + } + // 退出父进程 + if ($pid > 0) { + exit(0); + } + // 设置新的会话组 + if (posix_setsid() < 0) { + exit(0); + } + chdir('/'); + // 重置掩码 权限问题 + umask(0); + } + + /** + * 启动进程 + * + * @time 2020年07月21日 + * @return void + */ + public function start() + { + $pid = pcntl_fork(); + + if ($this->pid < 0) { + exit('fork failed'); + } + + if ($pid > 0) { + $this->pid = $pid; + } else { + call_user_func_array($this->callable, [$this]); + } + } + + /** + * 信号 + * + * @time 2020年07月21日 + * @param $signal + * @param $callable + * @param $restartSysCalls + * @return void + */ + public static function signal($signal, $callable, $restartSysCalls = false) + { + pcntl_signal($signal, $callable, $restartSysCalls); + } + + /** + * default 1 second + * + * @param $interval + * @return mixed + */ + public static function alarm($interval = 1) + { + return pcntl_alarm($interval); + } + + /** + * linux 进程下设置进程名称 + * + * @time 2020年07月21日 + * @param $title + * @return void + */ + public static function setWorkerName($title) + { + if (strtolower(PHP_OS) === 'linux') { + cli_set_process_title($title); + } + } + + + /** + * 安全退出 + * + * @time 2020年07月21日 + * @param int $status + * @return void + */ + public function exit($status = 0) + { + exit($status); + } + + /** + * kill + * + * @time 2020年07月22日 + * @param $pid + * @param $signal + * @return bool + */ + public static function kill($pid, $signal) + { + return posix_kill($pid, $signal); + } + + /** + * + * @time 2020年07月22日 + * @return void + */ + public static function dispatch() + { + pcntl_signal_dispatch(); + } + + + /** + * 是否存活 + * + * @time 2020年07月22日 + * @param $pid + * @return bool + */ + public static function isAlive($pid) + { + return posix_kill($pid, 0); + } + + /** + * 初始化进程内存 + * + * @time 2020年07月22日 + * @param int $memory + * @return void + */ + public function initMemory($memory = 0) + { + if (ini_get('memory_limit') != $this->initMemory) { + // 这里申请一块稍微大的内存 + ini_set('memory_limit', $memory ?: $this->initMemory); + } + } + + /** + * 是否超过最大内存 + * + * @time 2020年07月22日 + * @return mixed + */ + public function isMemoryOverflow() + { + // 一旦超过了允许的内存 直接退出进程 + return memory_get_usage() > $this->allowMaxMemory; + } +} diff --git a/catch/monitor/command/process/RegisterSignal.php b/catch/monitor/command/process/RegisterSignal.php new file mode 100644 index 0000000..65dede9 --- /dev/null +++ b/catch/monitor/command/process/RegisterSignal.php @@ -0,0 +1,156 @@ +exit(); + // 等待子进程退出 + $this->waitWorkersExit(); + // 动态扩容 + $this->workerChecked(); + // 重启进程 + $this->reload(); + // 统计信息 + $this->showStatus(); + } + + /** + * 进程退出 + * + * @time 2020年07月21日 + * @return void + */ + protected function exit() + { + Process::signal(SIGTERM, function ($signal) { + foreach ($this->workerIds as $pid => $v) { + if (isset($this->workerIds[$pid])) { + unset($this->workerIds[$pid]); + } + Process::kill($pid, SIGTERM); + } + Process::kill(self::getMasterPid(), SIGKILL); + }); + + Process::signal(SIGINT, function ($signal) { + foreach ($this->workerIds as $pid => $v) { + if (isset($this->workerIds[$pid])) { + unset($this->workerIds[$pid]); + } + Process::kill($pid, SIGKILL); + } + Process::kill(self::getMasterPid(), SIGKILL); + }); + } + + /** + * 子进程退出 + * + * @time 2020年07月21日 + * @return void + */ + protected function waitWorkersExit() + { + Process::signal(SIGCHLD, function ($signal) { + + }); + } + + + /** + * 进程检测 + * + * @time 2020年07月21日 + * @return void + */ + protected function workerChecked() + { + Process::signal(SIGALRM, function ($signal) { + $process = new Process(function (Process $process) { + $crontabs = Crontab::where('status', Crontab::ENABLE) + ->where('tactics', '<>', Crontab::EXECUTE_FORBIDDEN) + ->select()->toArray(); + // 任务 + foreach ($crontabs as $crontab) { + $can = CronExpression::factory(trim($crontab['cron'])) + ->getNextRunDate(date('Y-m-d H:i:s'), 0 , true) + ->getTimestamp() == time(); + if ($can) { + // 如果任务只执行一次 之后禁用该任务 + if ($crontab['tactics'] === Crontab::EXECUTE_ONCE) { + Crontab::where('id', $crontab['id'])->update([ + 'status' => Crontab::DISABLE, + ]); + } + + $redis = $this->getRedisHandle(); + + $redis->lpush($this->crontabQueueName, json_encode([ + 'id' => $crontab['id'], + 'task' => $crontab['task'], + ])); + } + } + + $process->exit(); + }); + + $process->start(); + + Process::alarm($this->interval); + }); + } + + /** + * 重启 + * + * @time 2020年07月21日 + * @return void + */ + protected function reload() + { + Process::signal(SIGUSR1, function ($signal) { + $this->worker_start_at = 0; + foreach ($this->workerIds as $pid => $v) { + Process::kill($pid, SIGTERM); + } + }, false); + } + + /** + * 预留信号 + * + * @time 2020年07月21日 + * @return void + */ + protected function showStatus() + { + Process::signal(SIGUSR2, function ($signal) { + $this->setWorkerStatus($this->name . ' master'); + foreach ($this->workerIds as $pid => $v) { + Process::kill($pid, SIGUSR2); + } + }); + } +} + + diff --git a/catch/monitor/command/process/Store.php b/catch/monitor/command/process/Store.php new file mode 100644 index 0000000..006262a --- /dev/null +++ b/catch/monitor/command/process/Store.php @@ -0,0 +1,243 @@ + $this->name, + 'static' => $this->static, + 'dynamic' => $this->dynamic, + 'interval' => $this->interval, + ], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)); + } + + /** + * worker master pid + * + * @time 2020年07月23日 + * @return string + */ + public static function masterPidStorePath() + { + return self::storeTaskPath() . 'master.pid'; + } + + /** + * worker master status + * + * @time 2020年07月23日 + * @return string + */ + public static function statusPath() + { + return self::storeTaskPath() . 'master.status'; + } + + + /** + * worker status + * + * @time 2020年07月23日 + * @param string $name + * @return string + */ + public static function workerStatusPath($name) + { + $path = self::storeTaskPath() . 'status/'; + + if (!FileSystem::exists($path)) { + FileSystem::makeDirectory($path, 0777, true); + } + + return $path . $name . '.status'; + } + + /** + * + * @time 2020年07月23日 + * @return string + */ + public static function getWorkerStatusPath() + { + return self::storeTaskPath() . 'status/'; + } + + /** + * worker log + * + * @time 2020年07月23日 + * @return string + */ + public static function stdoutPath() + { + return self::storeTaskPath() . 'errors.log'; + } + + + /** + * 获取 master pid + * + * @time 2020年07月21日 + * @return false|string + */ + public static function getMasterPid() + { + $pidFile = config('catch.crontab.master_pid_file'); + + if (!file_exists($pidFile)) { + return 0; + } + + return FileSystem::sharedGet($pidFile); + } + + /** + * status + * + * @time 2020年07月21日 + * @return false|string + */ + public function renderStatus() + { + return file_get_contents(self::statusPath()); + } + + /** + * 运行时间 + * + * @time 2020年07月23日 + * @param $runtime + * @return string + */ + protected function getRunningTime($runtime) + { + $day = 3600 * 24; + if ($runtime > $day) { + $days = floor($runtime / $day); + return $days . '天:' . gmstrftime('%H:%M:%S', $runtime % $day); + } else { + return gmstrftime('%H:%M:%S', $runtime); + } + } + + /** + * 获取工作进程 + * + * @time 2020年07月23日 + * @return mixed + */ + public function getWorkerStatus() + { + usleep(500 * 1000); + + $files = FileSystem::glob(self::storeTaskPath() . 'status/*.status'); + + $workerStatus = []; + + foreach ($files as $file) { + $workerStatus[] = explode("\t", FileSystem::sharedGet($file)); + } + + return $workerStatus; + } + + /** + * 设置进程状态 + * + * @time 2020年07月23日 + * @param $name + * @param int $dealNum + * @param string $status + * @return void + */ + protected function setWorkerStatus($name, $dealNum = 0, $status = 'running') + { + $startAt = strpos($name, 'worker') ? $this->worker_start_at : $this->start_at; + + if ($this->daemon) { + FileSystem::put($this->workerStatusPath($this->workerStatusFileName($name)), implode("\t", [ + posix_getpid(), + $name, + floor(memory_get_usage() / 1024 / 1024) . 'M', + $dealNum, + date('Y-m-d H:i:s', $startAt), + $this->getRunningTime(time() - $startAt), + $status + ])); + } + } + + /** + * 进程名称 + * + * @time 2020年09月15日 + * @param $name + * @return string + */ + protected function workerStatusFileName($name) + { + return $name . '_' . posix_getpid(); + } + + /** + * 删除进程状态文件 + * + * @time 2020年09月15日 + * @param $pid + * @return void + */ + protected function deleteWorkerStatusFile($pid) + { + @unlink(self::workerStatusPath($this->name . ' worker_' . $pid)); + } + + /** + * 退出 + * + * @time 2020年09月15日 + * @return void + */ + public static function exitMasterDo() + { + @unlink(self::masterPidStorePath()); + @unlink(self::statusPath()); + Filesystem::deleteDirectory(self::getWorkerStatusPath()); + } +} \ No newline at end of file diff --git a/catch/monitor/controller/Crontab.php b/catch/monitor/controller/Crontab.php new file mode 100644 index 0000000..57458a6 --- /dev/null +++ b/catch/monitor/controller/Crontab.php @@ -0,0 +1,105 @@ +model = $model; + } + + /** + * 列表 + * + * @time 2020/09/14 20:35 + * + * @return \think\Response + */ + public function index() + { + return CatchResponse::paginate($this->model->getList()); + } + + /** + * 保存 + * + * @time 2020/09/14 20:35 + * @param Request Request + * @return \think\Response + */ + public function save(Request $request) + { + CronExpression::factory($request->post('cron')); + + return CatchResponse::success($this->model->storeBy($request->post())); + } + + /** + * 读取 + * + * @time 2020/09/14 20:35 + * @param $id + * @return \think\Response + */ + public function read($id) + { + return CatchResponse::success($this->model->findBy($id)); + } + + /** + * 更新 + * + * @time 2020/09/14 20:35 + * @param Request $request + * @return \think\Response + */ + public function update(Request $request, $id) + { + CronExpression::factory($request->post('cron')); + + return CatchResponse::success($this->model->updateBy($id, $request->post())); + } + + /** + * 删除 + * + * @time 2020/09/14 20:35 + * @param $id + * @return \think\Response + */ + public function delete($id) + { + return CatchResponse::success($this->model->deleteBy($id)); + } + + /** + * 禁用启用 + * + * @time 2020年09月15日 + * @param $id + * @return \think\response\Json + */ + public function disOrEnable($id) + { + return CatchResponse::success($this->model->disOrEnable($id)); + } + +} \ No newline at end of file diff --git a/catch/monitor/controller/CrontabLog.php b/catch/monitor/controller/CrontabLog.php new file mode 100644 index 0000000..f88703f --- /dev/null +++ b/catch/monitor/controller/CrontabLog.php @@ -0,0 +1,50 @@ +model = $model; + } + + /** + * 日志列表 + * + * @time 2020年09月15日 + * @throws \think\db\exception\DbException + * @return \think\response\Json + */ + public function index() + { + return CatchResponse::paginate($this->model->getList()); + } + + /** + * 删除日志 + * + * @time 2020年09月15日 + * @param $id + * @return \think\response\Json + */ + public function delete($id) + { + return CatchResponse::success($this->model->deleteBy($id)); + } +} \ No newline at end of file diff --git a/catch/monitor/database/migrations/20200914203553_crontab.php b/catch/monitor/database/migrations/20200914203553_crontab.php new file mode 100644 index 0000000..b41ecae --- /dev/null +++ b/catch/monitor/database/migrations/20200914203553_crontab.php @@ -0,0 +1,46 @@ +table('crontab', ['engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => '定时任务' ,'id' => 'id','signed' => true ,'primary_key' => ['id']]); + $table->addColumn('name', 'string', ['limit' => 255,'null' => false,'default' => '','signed' => false,'comment' => '任务名称',]) + ->addColumn('group', 'boolean', ['null' => false,'default' => 1,'signed' => false,'comment' => '1 默认 2 系统',]) + ->addColumn('task', 'string', ['limit' => 255,'null' => false,'default' => '','signed' => false,'comment' => '任务名称',]) + ->addColumn('cron', 'string', ['limit' => 50,'null' => false,'default' => '','signed' => false,'comment' => 'cron 表达式',]) + ->addColumn('tactics', 'boolean', ['null' => false,'default' => 1,'signed' => false,'comment' => '1 立即执行 2 执行一次 3 放弃执行',]) + ->addColumn('status', 'boolean', ['null' => false,'default' => 1,'signed' => false,'comment' => '1 正常 2 禁用',]) + ->addColumn('remark', 'string', ['limit' => 1000,'null' => false,'default' => '','signed' => false,'comment' => '备注',]) + ->addColumn('creator_id', 'integer', ['limit' => MysqlAdapter::INT_REGULAR,'null' => false,'default' => 0,'signed' => true,'comment' => '创建人ID',]) + ->addColumn('created_at', 'integer', ['limit' => MysqlAdapter::INT_REGULAR,'null' => false,'default' => 0,'signed' => true,'comment' => '创建时间',]) + ->addColumn('updated_at', 'integer', ['limit' => MysqlAdapter::INT_REGULAR,'null' => false,'default' => 0,'signed' => true,'comment' => '更新时间',]) + ->addColumn('deleted_at', 'integer', ['limit' => MysqlAdapter::INT_REGULAR,'null' => false,'default' => 0,'signed' => true,'comment' => '软删除',]) + ->create(); + } +} diff --git a/catch/monitor/database/migrations/20200915101135_crontab_log.php b/catch/monitor/database/migrations/20200915101135_crontab_log.php new file mode 100644 index 0000000..417a170 --- /dev/null +++ b/catch/monitor/database/migrations/20200915101135_crontab_log.php @@ -0,0 +1,51 @@ +table('crontab_log', ['engine' => 'Myisam', 'collation' => 'utf8mb4_general_ci', 'comment' => '定时任务日志' ,'id' => 'id','signed' => true ,'primary_key' => ['id']]); + $table->addColumn('crontab_id', 'integer', ['limit' => MysqlAdapter::INT_REGULAR,'null' => false,'default' => 0,'signed' => true,'comment' => 'crontab 任务ID',]) + ->addColumn('used_time', 'integer', ['limit' => MysqlAdapter::INT_REGULAR,'null' => false,'default' => 0,'signed' => true,'comment' => '任务消耗时间',]) + ->addColumn('status', 'boolean', ['null' => false,'default' => 1,'signed' => false,'comment' => '1 成功 2 失败',]) + ->addColumn('error_message', 'string', ['limit' => 1000,'null' => false,'default' => '','signed' => false,'comment' => '错误信息',]) + ->addColumn('created_at', 'integer', ['limit' => MysqlAdapter::INT_REGULAR,'null' => false,'default' => 0,'signed' => true,'comment' => '创建时间',]) + ->addColumn('updated_at', 'integer', ['limit' => MysqlAdapter::INT_REGULAR,'null' => false,'default' => 0,'signed' => true,'comment' => '更新时间',]) + ->addColumn('deleted_at', 'integer', ['limit' => MysqlAdapter::INT_REGULAR,'null' => false,'default' => 0,'signed' => true,'comment' => '软删除',]) + ->create(); + } +} diff --git a/catch/monitor/model/Crontab.php b/catch/monitor/model/Crontab.php new file mode 100644 index 0000000..77b6a9a --- /dev/null +++ b/catch/monitor/model/Crontab.php @@ -0,0 +1,43 @@ +catchLeftJoin(Crontab::class, 'id', 'crontab_id', ['name', 'group', 'task']) + ->catchSearch() + ->catchOrder() + ->field(['used_time', 'error_message', 'crontab_log.status', 'crontab_log.id', 'crontab_log.created_at']) + ->paginate(); + } +} \ No newline at end of file diff --git a/catch/monitor/model/search/CrontabLogSearch.php b/catch/monitor/model/search/CrontabLogSearch.php new file mode 100644 index 0000000..7ff03ab --- /dev/null +++ b/catch/monitor/model/search/CrontabLogSearch.php @@ -0,0 +1,39 @@ +where('crontab_id', $value); + } + + public function searchNameAttr($query, $value, $data) + { + return $query->whereLike('crontab.name', $value); + } + + public function searchStatusAttr($query, $value, $data) + { + return $query->where('status', $value); + } + + public function searchStartAtAttr($query, $value, $data) + { + return $query->where($this->aliasField('created_at'), '>=', strtotime($value)); + } + + public function searchEndAtAttr($query, $value, $data) + { + return $query->where($this->aliasField('created_at'), '<=', strtotime($value)); + } +} diff --git a/catch/monitor/model/search/CrontabSearch.php b/catch/monitor/model/search/CrontabSearch.php new file mode 100644 index 0000000..f24b20c --- /dev/null +++ b/catch/monitor/model/search/CrontabSearch.php @@ -0,0 +1,24 @@ +whereLike('name', $value); + } + + public function searchStatusAttr($query, $value, $data) + { + return $query->whereLike('status', $value); + } +} diff --git a/catch/monitor/module.json b/catch/monitor/module.json new file mode 100644 index 0000000..b11be66 --- /dev/null +++ b/catch/monitor/module.json @@ -0,0 +1,15 @@ +{ + "name": "系统监控", + "alias": "monitor", + "description": "系统监控模块", + "version": "1.0.0", + "keywords": [], + "order": 0, + "services": [ + "\\catchAdmin\\monitor\\MonitorService" + ], + "aliases": [], + "files": [], + "requires": [], + "enable": false +} \ No newline at end of file diff --git a/catch/monitor/route.php b/catch/monitor/route.php new file mode 100644 index 0000000..1426939 --- /dev/null +++ b/catch/monitor/route.php @@ -0,0 +1,22 @@ +group('monitor', function () use ($router){ + // crontab路由 + $router->resource('crontab', '\catchAdmin\monitor\controller\Crontab'); + $router->put('crontab/enable/', '\catchAdmin\monitor\controller\Crontab@disOrEnable'); + + // crontab 日志 + $router->get('crontab/log', '\catchAdmin\monitor\controller\CrontabLog@index'); + $router->delete('crontab/log/', '\catchAdmin\monitor\controller\CrontabLog@delete'); + +})->middleware('auth'); \ No newline at end of file diff --git a/extend/catcher/base/CatchCronTask.php b/extend/catcher/base/CatchCronTask.php new file mode 100644 index 0000000..ef90e0a --- /dev/null +++ b/extend/catcher/base/CatchCronTask.php @@ -0,0 +1,10 @@ +setName('catch:schedule') - ->addArgument('option', Argument::OPTIONAL, '[start|reload|stop|restart||status]', 'start') - ->addOption('daemon', '-d', Option::VALUE_NONE, 'daemon mode') - ->setDescription('start task schedule'); - } - - protected function execute(Input $input, Output $output) - { - if (!extension_loaded('swoole')) { - $output->error('Swoole Extension Not Installed! You can use [pecl] to install swoole'); - } else { - $master = new Master(); - if ($this->input->hasOption('daemon')) { - $master->daemon(); - } - $this->{$input->getArgument('option')}($master); - } - } - - /** - * 进程启动 - * - * @time 2020年07月07日 - * @param Master $process - * @return void - */ - protected function start(Master $process) - { - $process->start(); - $this->output->info($process->renderProcessesStatusToString()); - } - - /** - * 状态输出 - * - * @time 2020年07月07日 - * @param Master $process - * @return void - */ - protected function status(Master $process) - { - $process->status(); - - $this->output->info($process->output()); - } - - /** - * 停止任务 - * - * @time 2020年07月07日 - * @param Master $process - * @return void - */ - protected function stop(Master $process) - { - $process->stop(); - - $this->output->info('stop catch schedule successfully'); - } - - /** - * 重启任务 - * - * @time 2020年07月07日 - * @param Master $process - * @return void - */ - protected function reload(Master $process) - { - $process->reload(); - } - - /** - * 重启 - * - * @time 2020年07月07日 - * @param Master $process - * @return void - */ - protected function restart(Master $process) - { - $process->stop(); - - $process->start(); - } -}