AsyncJobs.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. <?php
  2. // /opt/local/pl.procesy5/p5build_SE/temp/WPS_Functions/default_db/CRM_PROCES_tree/relations-22
  3. // /opt/local/pl.procesy5/ -> /Library/New_Server/opt/local/pl.procesy5
  4. // /opt/local/pl.procesy5/async_jobs - TODO: APP_PATH_ASYNC_JOB from .htaccess or config
  5. // Config example:
  6. // cat SE/config/.cnf-biuro.biall-net.pl.ini.php:
  7. // APP_PATH_ASYNC_JOB="/opt/local/pl.procesy5/async_jobs"
  8. // register new job:
  9. // - $TODAY = date("Y-m-d");
  10. // - $JOB_ID = generateJobID if not already started?
  11. // - make folder APP_PATH_ASYNC_JOB / $TODAY / job-{$JOB_ID}
  12. // - make base files:
  13. // - log files: `out.log`, `error.log`
  14. // - `output`: if process creates file. @param --out_file=out
  15. // - `output_type`: output mime type or namespace
  16. // - `progress`: if process implement progress. @param --progress_file=progress
  17. class Core_AsyncJobs {
  18. static $VERSION = 1; // `CRM_CONFIG`.`CONF_KEY` = 'Core_AsyncJobs__version'
  19. static $CRM_CONFIG_VERSION_KEY = 'Core_AsyncJobs__version';
  20. static function getSimpleList() {
  21. $fullList = self::getFullList();
  22. return array_map(function ($jobInfo) {
  23. return [
  24. 'name' => $jobInfo['name'],
  25. 'pid' => $jobInfo['pid'],
  26. 'pm_id' => $jobInfo['pm_id'],
  27. 'pm2_env.status' => $jobInfo['pm2_env']['status'],
  28. 'monit.memory' => $jobInfo['monit']['memory'],
  29. 'monit.cpu' => $jobInfo['monit']['cpu'],
  30. // "pm_out_log_path": "/Library/WebServer/.pm2/logs/test-job-1-out.log",
  31. // "pm_err_log_path": "/Library/WebServer/.pm2/logs/test-job-1-error.log",
  32. // "pm_pid_path": "/Library/WebServer/.pm2/pids/test-job-0.pid",
  33. 'pm2_env.pm_out_log_path' => $jobInfo['pm2_env']['pm_out_log_path'],
  34. 'pm2_env.pm_err_log_path' => $jobInfo['pm2_env']['pm_err_log_path'],
  35. 'pm2_env.pm_pid_path' => $jobInfo['pm2_env']['pm_pid_path'],
  36. ];
  37. }, $fullList);
  38. }
  39. static function getFullList() {
  40. $jobListJson = V::shell_exec("pm2 jlist 2>&1");
  41. if (empty($jobListJson)) throw new Exception("Reading async job list failed");
  42. $parsedJobList = @json_decode($jobListJson, $assoc = true);
  43. if (null == $parsedJobList && 0 !== json_last_error()) throw new Exception("Parsing async job list failed: " . json_last_error());
  44. return $parsedJobList;
  45. }
  46. static function getJobLogs($jobNameOrID, $lastLines = 10) {
  47. if (empty($jobNameOrID) && $jobNameOrID !== 0) throw new Exception("Missing job name or id in getJobLogs");
  48. $lastLines = ((int)$lastLines > 0) ? (int)$lastLines : 10;
  49. // $cmd = "pm2 logs {$jobNameOrID} --lines {$lastLines} --nostream | tail -n {$lastLines}";
  50. $cmd = "pm2 logs {$jobNameOrID} --lines {$lastLines} --nostream";
  51. return V::shell_exec($cmd);
  52. // [TAILING] Tailing last 3 lines for [0] process (change the value with --lines option)
  53. // /Library/WebServer/.pm2/logs/test-job-1-out.log last 3 lines:
  54. // /Library/WebServer/
  55. }
  56. static function stopJob($jobNameOrID) {
  57. if (empty($jobNameOrID) && $jobNameOrID !== 0) throw new Exception("Missing job name or id in stopJob");
  58. $cmd = "pm2 stop {$jobNameOrID}";
  59. return V::shell_exec($cmd);
  60. }
  61. static function startJob($jobNameOrID) {
  62. if (empty($jobNameOrID) && $jobNameOrID !== 0) throw new Exception("Missing job name or id in startJob");
  63. $cmd = "pm2 start {$jobNameOrID}";
  64. return V::shell_exec($cmd);
  65. }
  66. static function startNewJob($jobName) {
  67. // for Ant:
  68. // index.php?_route=UrlAction_Ant&_task=ant&path=default_db.in7_dziennik_koresp/etykieta&typeName=default_db:IN7_DZIENNIK_KORESP&primaryKey=66263&primaryKeyField=ID
  69. // index.php?_route=UrlAction_Ant
  70. // & _task=ant
  71. // & path=default_db.in7_dziennik_koresp/test-bash
  72. // & typeName=default_db:IN7_DZIENNIK_KORESP
  73. // & primaryKey=66263
  74. // & primaryKeyField=ID
  75. // index.php?_route=UrlAction_Ant
  76. // & _task=ant
  77. // & path=default_db.in7_dziennik_koresp/test-bash
  78. // & template=test-loop
  79. // & typeName=default_db:IN7_DZIENNIK_KORESP
  80. // & primaryKey=66263
  81. // & primaryKeyField=ID
  82. // ant=default_db:IN7_DZIENNIK_KORESP/test-bash & task=test-loop & ns=default_db:IN7_DZIENNIK_KORESP & pk=66263
  83. if (empty($jobName)) throw new Exception("Missing job name");
  84. // $jobName - check if already started? pm2 will return failed
  85. $outLogPath = "p5-async-jobs/jobX/logs/out.log";
  86. $errorLogPath = "p5-async-jobs/jobX/logs/error.log";
  87. $jobExecPath = ""; // TODO: path to exec
  88. $args = ""; // args for script
  89. $cmd = implode(" ", [
  90. "pm2 start '{$jobExecPath}'",
  91. "--name '{$jobName}'",
  92. "--no-autorestart",
  93. "--output '{$outLogPath}'",
  94. "--error '{$errorLogPath}'",
  95. "--time", // prefix time to log entry
  96. "-- {$args}",
  97. ]);
  98. return V::shell_exec($cmd);
  99. }
  100. static function deleteStopped() {
  101. // TODO: script to remove stopped in loop with delay
  102. // pm2 start app.js --restart-delay=3000
  103. // $ pm2 list | grep '^│' | awk -F'│' '{ gsub(/ /, "", $2); gsub(/ /, "", $10); if ("stopped" == $10) { print $2" # STOPPED" } else { print $10 } }' | grep '# STOPPED' | xargs -n1 pm2 delete
  104. $testCmd = implode(" | ", [
  105. "pm2 list",
  106. "grep '^│'",
  107. "awk -F'│' '{ gsub(/ /, \"\", \$2); gsub(/ /, \"\", \$10); if (\"stopped\" == \$10) { print \$2\" # STOPPED\" } else { print \$10 } }'",
  108. "grep '# STOPPED'",
  109. "awk '{print \$1}'",
  110. ]);
  111. V::exec($testCmd . " 2>&1", $out, $ret);
  112. echo "cmd: <code>{$cmd}</code><br>RETURN CODE: '{$ret}'<br><pre>OUTPUT:\n" . implode("\n", $out) . "</pre>";
  113. $cmd = implode(" | ", [
  114. "pm2 list",
  115. "grep '^│'",
  116. "awk -F'│' '{ gsub(/ /, \"\", \$2); gsub(/ /, \"\", \$10); if (\"stopped\" == \$10) { print \$2\" # STOPPED\" } else { print \$10 } }'",
  117. "grep '# STOPPED'",
  118. "awk '{print \$1}'",
  119. "xargs -n1 pm2 delete",
  120. ]);
  121. }
  122. static function getNodePath() { return "/usr/local/bin/node"; }
  123. // npm_path="/usr/local/bin/npm"
  124. static function getPm2Path() { return "/usr/local/bin/pm2"; }
  125. static function getPm2WwwUserPath() { return "/Library/WebServer/.pm2/"; }
  126. static function isInstalled() {
  127. $confAsyncPath = Config::get('APP_PATH_ASYNC_JOB');
  128. if (!$confAsyncPath) throw new Exception("Missing Config APP_PATH_ASYNC_JOB");
  129. if (!file_exists($confAsyncPath)) {
  130. mkdir($confAsyncPath, $mode = 0777, $recursive = TRUE);
  131. }
  132. if (!file_exists($confAsyncPath)) throw new Exception("Folder not exists APP_PATH_ASYNC_JOB");
  133. // V::exec("/usr/local/bin/pm2 --version 2>&1", $out, $ret);
  134. V::exec("/usr/local/bin/pm2 ping 2>&1", $out, $ret); // expected "{ msg: 'pong' }"
  135. // echo UI::h('pre', [], "ret({$ret}):\n" . implode("\n", $out));
  136. if ($ret === 0) {
  137. // [PM2] Spawning PM2 daemon with pm2_home=/Library/WebServer/.pm2
  138. // [PM2] PM2 Successfully daemonized
  139. // { msg: 'pong' }
  140. }
  141. if ($ret !== 0) {
  142. if (!file_exists(self::getNodePath())) throw new Exception("pm2 not installed");
  143. // if [ ! -f "$node_path" ]; then
  144. // echo "$node_path not exists"
  145. // wget https://nodejs.org/dist/v12.15.0/node-v12.15.0.pkg
  146. // sudo installer -verbose -pkg node-v12.15.0.pkg -target /
  147. // fi
  148. // # node -v # expected v12.15.0
  149. if (!file_exists(self::getPm2Path())) throw new Exception("pm2 not installed");
  150. // npm install -g pm2
  151. // sudo npm install -g pm2
  152. // pm2 -version # expected 4.2.3
  153. if (!file_exists(self::getPm2WwwUserPath())) throw new Exception("pm2 user folder not exists");
  154. // FIX for Mac OS:
  155. // $ sudo mkdir /Library/WebServer/.pm2/
  156. // $ sudo chown _www /Library/WebServer/.pm2/
  157. throw new Exception("Error pm2"); // unknown error
  158. }
  159. if (!self::checkAsyncJobDatabase()) throw new Exception("Error database schema for AsyncJobs");
  160. return true;
  161. }
  162. static function checkAsyncJobDatabase() {
  163. // `CRM_CONFIG`.`CONF_KEY` = 'Core_AsyncJobs__version'
  164. $dbVersion = (int)DB::getPDO()->fetchValue(" select CONV_VAL from CRM_CONFIG where CONF_KEY = :key ", [ ':key' => self::$CRM_CONFIG_VERSION_KEY ]);
  165. if ($dbVersion < 1) self::upgradeAsyncJobDatabaseToVersion1();
  166. // $dbVersion = (int)DB::getPDO()->fetchValue(" select CONV_VAL from CRM_CONFIG where CONF_KEY = :key ", [ ':key' => 'Core_AsyncJobs__version' ]);
  167. // if ($dbVersion < 2) self::upgradeAsyncJobDatabaseToVersion2();
  168. $dbVersion = (int)DB::getPDO()->fetchValue(" select CONV_VAL from CRM_CONFIG where CONF_KEY = :key ", [ ':key' => self::$CRM_CONFIG_VERSION_KEY ]);
  169. return ($dbVersion < self::$VERSION) ? false : true;
  170. }
  171. static function upgradeAsyncJobDatabaseToVersion1() {
  172. // - insertOrUpdate new row in `CRM_ASYNC_FUNCTIONS` ( $TODAY, $version, $user, $jobName )
  173. // - $JOB_ID = fetchValue select ID from `CRM_ASYNC_FUNCTIONS` where JOB_NAME = $jobName
  174. // - `CRM_ASYNC_FUNCTIONS`.`A_STATUS` default 'WAITING' - not started
  175. // - `CRM_ASYNC_FUNCTIONS`.`A_STATUS`: 'NORMAL' - started
  176. // - `CRM_ASYNC_FUNCTIONS`.`A_STATUS`: 'OFF_HARD' - not running
  177. // - `CRM_ASYNC_FUNCTIONS`.`A_STATUS`: 'DELETED' - removed
  178. $sql = "
  179. CREATE TABLE IF NOT EXISTS `CRM_ASYNC_FUNCTIONS` ( -- list of async function definitions / config
  180. `ID` int(11) NOT NULL AUTO_INCREMENT,
  181. `ID_ZASOB` int(11) NOT NULL DEFAULT 0, -- TODO - register function in CRM_LISTA_ZASOBOW URL_ACTION to set perms
  182. `JOB_NAME` varchar(200) NOT NULL,
  183. `VERSION` int(11) NOT NULL DEFAULT 0,
  184. `LOCK_TYPE` enum('ROW', 'TABLE', 'SYSTEM', 'NO_LOCK') DEFAULT 'ROW',
  185. -- ROW - only one active job per row, eg. create pdf, close FV, @require primaryKey in JOB.LOCK_VALUE
  186. -- TABLE - ony one active job per table, eg. update columns, make report, sync
  187. -- SYSTEM - only one active job
  188. -- TODO USER_... - lock per user
  189. -- NO_LOCK - allow multiple jobs
  190. -- `USER` varchar(20) NOT NULL,
  191. -- `DATE` date NOT NULL,
  192. -- `A_STATUS` enum('WAITING','NORMAL','WARNING','OFF_SOFT','OFF_HARD','DELETED') DEFAULT 'WAITING',
  193. `A_RECORD_CREATE_DATE` datetime NOT NULL,
  194. `A_RECORD_CREATE_AUTHOR` varchar(20) NOT NULL,
  195. `A_RECORD_UPDATE_DATE` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  196. `A_RECORD_UPDATE_AUTHOR` varchar(20) NOT NULL,
  197. PRIMARY KEY (`ID`),
  198. UNIQUE KEY `JOB_NAME` (`JOB_NAME`),
  199. KEY `DATE` (`DATE`),
  200. KEY `A_STATUS` (`A_STATUS`),
  201. KEY `A_RECORD_UPDATE_DATE` (`A_RECORD_UPDATE_DATE`)
  202. ) ENGINE=MyISAM DEFAULT CHARSET=latin2;
  203. ";
  204. $sql = "
  205. CREATE TABLE IF NOT EXISTS `CRM_ASYNC_JOB_LOG` (
  206. `ID` int(11) NOT NULL AUTO_INCREMENT,
  207. `ID_FUNCTION` int(11) NOT NULL,
  208. `ID_SOURCE_JOB` int(11) NOT NULL DEFAULT 0, -- id job
  209. `P_ID` int(11) NOT NULL DEFAULT 0, -- parent job id
  210. `DATE` date NOT NULL,
  211. `VERSION` int(11) NOT NULL DEFAULT 0,
  212. `JOB_NAME` varchar(200) NOT NULL, -- copy from CRM_ASYNC_FUNCTIONS
  213. `LOCK_VALUE` varchar(200) NOT NULL DEFAULT '', -- second part for unique JOB_NAME eg. primaryKey
  214. `USER` varchar(20) NOT NULL, -- user who start this function
  215. `A_STATUS` enum('WAITING','NORMAL','WARNING','OFF_SOFT','OFF_HARD','DELETED') DEFAULT 'WAITING',
  216. `A_RECORD_CREATE_DATE` datetime NOT NULL,
  217. `A_RECORD_CREATE_AUTHOR` varchar(20) NOT NULL,
  218. `A_RECORD_UPDATE_DATE` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  219. `A_RECORD_UPDATE_AUTHOR` varchar(20) NOT NULL,
  220. PRIMARY KEY (`ID`),
  221. UNIQUE KEY `JOB_LOCK` (`JOB_NAME`, `LOCK_VALUE`, `USER`), -- add USER to store user click and respond that job is running by other user
  222. KEY `DATE` (`DATE`),
  223. KEY `A_STATUS` (`A_STATUS`),
  224. KEY `A_RECORD_UPDATE_DATE` (`A_RECORD_UPDATE_DATE`)
  225. ) ENGINE=MyISAM DEFAULT CHARSET=latin2;
  226. ";
  227. // @usage:
  228. // `CRM_ASYNC_FUNCTIONS`.`JOB_NAME` = '
  229. // route = ant
  230. // -- & path = default_db.in7_dziennik_koresp/test-bash
  231. // & template = test-loop
  232. // & typeName = default_db:IN7_DZIENNIK_KORESP
  233. // & primaryKey = 66263
  234. // & primaryKeyField = ID
  235. // -> `CRM_ASYNC_FUNCTIONS`.`JOB_NAME` =
  236. // namespace = default_db/IN7_DZIENNIK_KORESP
  237. // ant = test-bash
  238. // template = test-loop
  239. // primaryKey = 66263
  240. // -> `CRM_ASYNC_FUNCTIONS`.`JOB_NAME` Format: "{$type}|..."
  241. // -> `CRM_ASYNC_FUNCTIONS`.`JOB_NAME` Format: "Ant|{$namespace}"
  242. // -> `CRM_ASYNC_JOB_LOG`.`LOCK_VALUE` Format: "{$primaryKey}|{$func_name}|..."
  243. // -> `CRM_ASYNC_JOB_LOG`.`LOCK_VALUE` Format: "{$primaryKey}|{$func_name}|{$template}"
  244. // -> Example: JOB_NAME = 'Ant|default_db/IN7_DZIENNIK_KORESP', LOCK_VALUE = '66263|test-bash|test-loop'
  245. // Add fields:
  246. // - function config at execution time:
  247. // - eg LOCK by user | feature | namespace | no_lock -- should contain in name
  248. // - process result - last result - NO - result from file, separate request needed
  249. // IDEA: 2 tables:
  250. // - `Config` - for function config (create row if not exists)
  251. // - `Log` - for log where JOB_ID is created or used from Config table
  252. // then createNewJob:
  253. // 1. fetch job config from CRM_ASYNC_FUNCTIONS_CONFIG
  254. // 1.1 IF 404 then create row, read config from function (how?)
  255. // 1.2 IF exists then check status and lock config
  256. // 1.2.1 IF online and lock then return error
  257. // 1.2.2 IF !online and !lock then create new row in Log table and return correct JOB_ID
  258. self::_upgdateAsyncJobDatabaseVersion($version = 1);
  259. }
  260. static function upgradeAsyncJobDatabaseToVersion2() {
  261. // ...
  262. self::_upgdateAsyncJobDatabaseVersion($version = 2);
  263. }
  264. static function _upgdateAsyncJobDatabaseVersion($version) {
  265. DB::getPDO()->insertOrUpdate('CRM_CONFIG', [
  266. 'CONF_KEY' => self::$CRM_CONFIG_VERSION_KEY,
  267. '@insert' => [
  268. 'CONF_VAL' => $version,
  269. ],
  270. '@update' => [
  271. 'CONF_VAL' => $version,
  272. ]
  273. ]);
  274. }
  275. // pm2 logs -h
  276. //
  277. // Usage: logs [options] [id|name]
  278. // stream logs file. Default stream all logs
  279. // Options:
  280. // --json json log output
  281. // --format formated log output
  282. // --raw raw output
  283. // --err only shows error output
  284. // --out only shows standard output
  285. // --lines <n> output the last N lines, instead of the last 15 by default
  286. // --timestamp [format] add timestamps (default format YYYY-MM-DD-HH:mm:ss)
  287. // --nostream print logs without lauching the log stream
  288. // --highlight [value] highlights the given value
  289. // -h, --help output usage information
  290. // [0] => Array:
  291. // [pid] => 71728
  292. // [name] => test-job-1
  293. // [pm2_env] => Array:
  294. // [exit_code] => 0
  295. // [versioning] =>
  296. // [version] => N/A
  297. // [unstable_restarts] => 0
  298. // [restart_time] => 53
  299. // [pm_id] => 0
  300. // [created_at] => 1581938950672
  301. // [axm_dynamic] => Array:
  302. // [axm_options] => Array:
  303. // [axm_monitor] => Array:
  304. // [axm_actions] => Array:
  305. // [pm_uptime] => 1581939524854
  306. // [status] => online
  307. // [unique_id] => 80fefcff-8605-44cc-9829-bbcba4de4e7c
  308. // [PM2_HOME] => /Library/WebServer/.pm2
  309. // [PATH] => /usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/local/bin:/opt/local/lib/mysql55/bin:/Applications/Server.app/Contents/ServerRoot/usr/bin:/Applications/Server.app/Contents/ServerRoot/usr/sbin:/Users/pl/programy/bin
  310. // [PWD] => /Users/plabudda/rsync.se.master/SE
  311. // [XPC_FLAGS] => 0x80
  312. // [XPC_SERVICE_NAME] => 0
  313. // [HOME] => /Library/WebServer
  314. // [SHLVL] => 1
  315. // [SERVER_INSTALL_PATH_PREFIX] => /Applications/Server.app/Contents/ServerRoot
  316. // [XPC_SERVICES_UNAVAILABLE] => 1
  317. // [MODULE_INSTALL_PATH_PREFIX] =>
  318. // [_] => /usr/local/bin/pm2
  319. // [__CF_USER_TEXT_ENCODING] => 0x46:0:0
  320. // [PM2_USAGE] => CLI
  321. // [NODE_APP_INSTANCE] => 0
  322. // [vizion_running] =>
  323. // [km_link] =>
  324. // [pm_pid_path] => /Library/WebServer/.pm2/pids/test-job-0.pid
  325. // [pm_err_log_path] => /Library/WebServer/.pm2/logs/test-job-1-error.log
  326. // [pm_out_log_path] => /Library/WebServer/.pm2/logs/test-job-1-out.log
  327. // [instances] => 1
  328. // [exec_mode] => fork_mode
  329. // [exec_interpreter] => php
  330. // [pm_cwd] => /Users/plabudda/rsync.se.master/SE
  331. // [pm_exec_path] => /Users/plabudda/rsync.se.master/sbin/test-sleep-loop.php
  332. // [node_args] => Array:
  333. // [name] => test-job-1
  334. // [namespace] => default
  335. // [env] => Array:
  336. // [unique_id] => 80fefcff-8605-44cc-9829-bbcba4de4e7c
  337. // [test-job-1] => Array:
  338. // [PM2_HOME] => /Library/WebServer/.pm2
  339. // [PATH] => /usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/local/bin:/opt/local/lib/mysql55/bin:/Applications/Server.app/Contents/ServerRoot/usr/bin:/Applications/Server.app/Contents/ServerRoot/usr/sbin:/Users/pl/programy/bin
  340. // [PWD] => /Users/plabudda/rsync.se.master/SE
  341. // [XPC_FLAGS] => 0x80
  342. // [XPC_SERVICE_NAME] => 0
  343. // [HOME] => /Library/WebServer
  344. // [SHLVL] => 1
  345. // [SERVER_INSTALL_PATH_PREFIX] => /Applications/Server.app/Contents/ServerRoot
  346. // [XPC_SERVICES_UNAVAILABLE] => 1
  347. // [MODULE_INSTALL_PATH_PREFIX] =>
  348. // [_] => /usr/local/bin/pm2
  349. // [__CF_USER_TEXT_ENCODING] => 0x46:0:0
  350. // [PM2_USAGE] => CLI
  351. // [merge_logs] => 1
  352. // [vizion] => 1
  353. // [autorestart] => 1
  354. // [watch] =>
  355. // [instance_var] => NODE_APP_INSTANCE
  356. // [pmx] => 1
  357. // [automation] => 1
  358. // [treekill] => 1
  359. // [username] => _www
  360. // [windowsHide] => 1
  361. // [kill_retry_time] => 100
  362. // [pm_id] => 0
  363. // [monit] => Array:
  364. // [memory] => 15720448
  365. // [cpu] => 0
  366. }