Piotr Labudda 6 anni fa
parent
commit
072dc65ca1

+ 143 - 202
SE/se-lib/Core/AsyncJobs.php

@@ -17,9 +17,147 @@
 //   - `output_type`: output mime type or namespace
 //   - `progress`: if process implement progress. @param --progress_file=progress
 
+Lib::loadClass('Request');
+Lib::loadClass('Core_AsyncJobsFiles');
+Lib::loadClass('Core_AsyncJobsDB');
+Lib::loadClass('Core_AsyncJobsServer'); // pm2
+
+/**
+ * @usage: Core_AsyncJobs::startAsyncJob($idJob)
+ *      -> Core_AsyncJobsServer::startAsyncJob(execPath, name, [ 'out.log', 'error.log', 'cwd', 'args' ])
+ */
+
 class Core_AsyncJobs {
-	static $VERSION = 1; // `CRM_CONFIG`.`CONF_KEY` = 'Core_AsyncJobs__version'
-	static $CRM_CONFIG_VERSION_KEY = 'Core_AsyncJobs__version';
+
+	static function registerNewAntTask($props) {
+		$namespace = V::get('namespace', '', $props);
+		$primaryKey = V::get('primaryKey', '', $props);
+		$ant_path = V::get('ant_path', '', $props);
+		$ant_template = V::get('ant_template', '', $props);
+
+		if (empty($namespace)) throw new Exception("Missing namespace");
+		if (empty($primaryKey)) throw new Exception("Missing primaryKey");
+		if (empty($ant_path)) throw new Exception("Missing ant_path");
+		if (empty($ant_template)) throw new Exception("Missing ant_template");
+
+		// https://biuro.biall-net.pl/dev-pl/se-master/index.php?_route=UrlAction_Ant
+		// & _task=ant
+		// & path=default_db.in7_dziennik_koresp/test-async
+		// & template=test-loop
+		// & typeName=default_db:IN7_DZIENNIK_KORESP
+		// & primaryKey=66263
+		// & primaryKeyField=ID
+
+		Lib::loadClass('ACL');
+		$acl = ACL::getAclByNamespace($namespace);
+		$typeName = Api_WfsNs::typeName($namespace);
+		$pkField = $acl->getPrimaryKeyField();
+
+		// DB::getPDO()->tryHandleException
+		$idJob = V::tryHandleException($handler = [ 'Core_AsyncJobs', 'isInstalled' ], $callback = [ 'Core_AsyncJobsDB', 'insert' ], $args = [
+			[
+				'JOB_NAME' => "Ant|{$namespace}|{$ant_path}|{$ant_template}",
+				'LOCK_VALUE' => $primaryKey,
+				'USER' => User::getLogin(),
+			],
+		]);
+
+		{
+			$path = $ant_path; // default_db.in7_dziennik_koresp/test-async
+			$template = $ant_template; // test-loop
+
+			$webRootUrl = Request::getPathUri() . "schema/ant-url_action/{$path}"; // TODO: security - only for test
+			$outputFunctionUrl = Router::getRoute('UrlAction_Ant')->getLink('output') . "&path={$path}&file=";
+			$antFunctionUrl = Router::getRoute('UrlAction_Ant')->getLink('ant') . "&path={$path}&file={$file}&template={$template}&typeName={$typeName}&primaryKey={$primaryKey}&primaryKeyField={$pkField}"; //  &confirmAntfile={$confirmAntfile}&confirmAntfileTarget={$confirmAntfileTarget} sdo confirmacji potrzebne - wzglednie przetwarzac z tresci output URL_TASK - ale potrzebne parametry analogiczne aby chodzily
+			// $testUrl = Request::getPathUri() . "wfs-data.php/default_db/?SERVICE=WFS&VERSION=1.0.0&TYPENAME={$typeName}&SRSNAME=EPSG:3003&featureID={$objectName}.{$primaryKey}";// &REQUEST=GetFeature
+			$uuid = date("Y-m-d-H_i_s") . substr(md5(time()), 0, 6); // TODO: uniq id for every request
+			Lib::loadClass('Crypt');
+			$passwordBase64Basic = base64_encode(User::getLogin() . ":" . Crypt::decrypt($_SESSION['ADM_PASS_HASH']));
+			// /* TODO: ??? */ if( strlen($confirmAntfile) > 0 ) $cmd .= " -DconfirmAntfile={$confirmAntfile}";
+			// /* TODO: ??? */ if( strlen($confirmAntfileTarget) > 0 ) $cmd .= " -DconfirmAntfileTarget={$confirmAntfileTarget}";
+			$php_session_id = session_id();
+		}
+		Core_AsyncJobsFiles::createNewJobFiles($idJob, $jobProps = [
+			'webRootUrl' => $webRootUrl,
+			'outputFunctionUrl' => $outputFunctionUrl,
+			'antFunctionUrl' => $antFunctionUrl,
+			'uuid' => $uuid,
+			'passwordBase64Basic' => $passwordBase64Basic,
+			'php_session_id' => $passwordBase64Basic,
+			'xpath' => $pkField,
+			'xpath_value' => $primaryKey,
+			'template' => $template,
+			'api_url' => Request::getPathUri()."wfs-data.php", //potrzebuje ten parametr do dzialania w AMS itp
+			'typeName' => $typeName,
+		]);
+
+		$jobStartScript = "";
+		{
+			$propFilePath = Core_AsyncJobsFiles::propertyFile($idJob, $today = date("Y-m-d"));
+			$cmd = "cd " . APP_PATH_SCHEMA . "/ant-url_action/{$ant_path}";
+			$cmd .= " && ";
+			$cmd .= implode(" ", [
+				'ant',
+				'-S',
+				'-propertyfile', $propFilePath,
+				($ant_template) ? $ant_template : '',
+				'2>&1',
+			]);
+
+			$pathCmd = [];
+			$pathCmd[] = '/opt/local/bin/ant';
+			$pathCmd[] = '/bin';
+			$pathCmd[] = '/usr/bin';
+			$pathCmd[] = '/usr/local/bin';
+			$pathCmd[] = '/opt/local/bin';
+			$pathCmd[] = '/sbin';
+			$pathCmd[] = '/usr/sbin';
+			$pathCmd[] = '/opt/local/sbin/skrypty';
+			$pathCmd[] = '/opt/local/var/macports/software';
+			$pathCmd[] = '/Applications/Server.app/Contents/ServerRoot/usr/bin';
+			$pathCmd[] = '/Applications/Server.app/Contents/ServerRoot/usr/sbin';
+			$jobStartScript = implode("\n", [
+				'export PATH=' . implode(':', $pathCmd),
+				'JAVA_HOME="/usr/bin/java"',
+				'MAVEN_OPTS="-Xms256m -Xmx512m"',
+				$cmd,
+				"",
+			]);
+		}
+		Core_AsyncJobsFiles::createTaskStartScript($idJob, $jobDate = date("Y-m-d"), $jobStartScript);
+
+		return $idJob;
+	}
+
+	static function startAsyncJob($idJob, $item = []) {
+		if (empty($item)) $item = Core_AsyncJobsDB::fetch($idJob);
+		DBG::nicePrint($item, '$item');
+		// Core_AsyncJobsFiles::basePath($idJob, $jobDate = $item['DATE']);
+		// Core_AsyncJobsFiles::propertyFile($idJob, $jobDate = $item['DATE']);
+		// Core_AsyncJobsFiles::startScript($idJob, $jobDate = $item['DATE']);
+		// Core_AsyncJobsFiles::outLog($idJob, $jobDate = $item['DATE']);
+		// Core_AsyncJobsFiles::errorLog($idJob, $jobDate = $item['DATE']);
+		$jobDate = $item['DATE'];
+		$execPath = Core_AsyncJobsFiles::startScript($idJob, $jobDate);
+
+		// Core_AsyncJobsServer::startAsyncJob(execPath, name, [ 'out.log', 'error.log', 'cwd', 'args' ])
+		$out = [];
+		$ret = Core_AsyncJobsServer::startAsyncJob(
+			$execPath,
+			$name = implode("|", [ $item['JOB_NAME'], $item['LOCK_VALUE'] ]),
+			[
+				'out.log' => Core_AsyncJobsFiles::outLog($idJob, $jobDate),
+				'error.log' => Core_AsyncJobsFiles::errorLog($idJob, $jobDate),
+				'cwd' => dirname(Core_AsyncJobsFiles::basePath($idJob, $jobDate)),
+			],
+			$out
+		);
+		if (0 === $ret) {
+			throw new AlertInfoException("Uruchomiono zadanie. " . implode(" ", $out));
+		} else {
+			throw new Exception("Nie uruchomiono zadania. " . implode(" ", $out));
+		}
+	}
 
 	static function getSimpleList() {
 		$fullList = self::getFullList();
@@ -72,41 +210,6 @@ class Core_AsyncJobs {
 		return V::shell_exec($cmd);
 	}
 
-	static function startNewJob($jobName) {
-		// for Ant: 
-		// index.php?_route=UrlAction_Ant&_task=ant&path=default_db.in7_dziennik_koresp/etykieta&typeName=default_db:IN7_DZIENNIK_KORESP&primaryKey=66263&primaryKeyField=ID
-		// index.php?_route=UrlAction_Ant
-		// 	& _task=ant
-		// 	& path=default_db.in7_dziennik_koresp/test-bash
-		// 	& typeName=default_db:IN7_DZIENNIK_KORESP
-		// 	& primaryKey=66263
-		// 	& primaryKeyField=ID
-		// index.php?_route=UrlAction_Ant
-		// 	& _task=ant
-		// 	& path=default_db.in7_dziennik_koresp/test-bash
-		// 	& template=test-loop
-		// 	& typeName=default_db:IN7_DZIENNIK_KORESP
-		// 	& primaryKey=66263
-		// 	& primaryKeyField=ID
-		// ant=default_db:IN7_DZIENNIK_KORESP/test-bash & task=test-loop & ns=default_db:IN7_DZIENNIK_KORESP & pk=66263
-		if (empty($jobName)) throw new Exception("Missing job name");
-		// $jobName - check if already started? pm2 will return failed
-		$outLogPath = "p5-async-jobs/jobX/logs/out.log";
-		$errorLogPath = "p5-async-jobs/jobX/logs/error.log";
-		$jobExecPath = ""; // TODO: path to exec
-		$args = ""; // args for script
-		$cmd = implode(" ", [
-			"pm2 start '{$jobExecPath}'",
-			"--name '{$jobName}'",
-			"--no-autorestart",
-			"--output '{$outLogPath}'",
-			"--error '{$errorLogPath}'",
-			"--time", // prefix time to log entry
-			"-- {$args}",
-		]);
-		return V::shell_exec($cmd);
-	}
-
 	static function deleteStopped() {
 		// TODO: script to remove stopped in loop with delay
 		// pm2 start app.js --restart-delay=3000
@@ -131,178 +234,16 @@ class Core_AsyncJobs {
 		]);
 	}
 
-	static function getNodePath() { return "/usr/local/bin/node"; }
-	// npm_path="/usr/local/bin/npm"
-	static function getPm2Path() { return "/usr/local/bin/pm2"; }
-	static function getPm2WwwUserPath() { return "/Library/WebServer/.pm2/"; }
 	static function isInstalled() {
-		$confAsyncPath = Config::get('APP_PATH_ASYNC_JOB');
-		if (!$confAsyncPath) throw new Exception("Missing Config APP_PATH_ASYNC_JOB");
+		if (!Core_AsyncJobsFiles::isInstalled()) throw new Exception("AsyncJobs files not installed");
 
-		if (!file_exists($confAsyncPath)) {
-			mkdir($confAsyncPath, $mode = 0777, $recursive = TRUE);
-		}
-		if (!file_exists($confAsyncPath)) throw new Exception("Folder not exists APP_PATH_ASYNC_JOB");
-
-		// V::exec("/usr/local/bin/pm2 --version 2>&1", $out, $ret);
-		V::exec("/usr/local/bin/pm2 ping 2>&1", $out, $ret); // expected "{ msg: 'pong' }"
-		// echo UI::h('pre', [], "ret({$ret}):\n" . implode("\n", $out));
-		if ($ret === 0) {
-			// [PM2] Spawning PM2 daemon with pm2_home=/Library/WebServer/.pm2
-			// [PM2] PM2 Successfully daemonized
-			// { msg: 'pong' }
-		}
-		if ($ret !== 0) {
-			if (!file_exists(self::getNodePath())) throw new Exception("pm2 not installed");
-			// if [ ! -f "$node_path" ]; then
-			// 	echo "$node_path not exists"
-			// 	wget https://nodejs.org/dist/v12.15.0/node-v12.15.0.pkg
-			// 	sudo installer -verbose -pkg node-v12.15.0.pkg -target /
-			// fi
-			// # node -v # expected v12.15.0
-			if (!file_exists(self::getPm2Path())) throw new Exception("pm2 not installed");
-			// npm install -g pm2
-			// sudo npm install -g pm2
-			// pm2 -version # expected 4.2.3
-			if (!file_exists(self::getPm2WwwUserPath())) throw new Exception("pm2 user folder not exists");
-			// FIX for Mac OS:
-			// $ sudo mkdir /Library/WebServer/.pm2/
-			// $ sudo chown _www /Library/WebServer/.pm2/
-			throw new Exception("Error pm2"); // unknown error
-		}
+		if (!Core_AsyncJobsServer::isInstalled()) throw new Exception("AsyncJobs server not installed");
 
-		if (!self::checkAsyncJobDatabase()) throw new Exception("Error database schema for AsyncJobs");
+		if (!Core_AsyncJobsDB::checkAsyncJobDatabase()) throw new Exception("AsyncJobs database schema not installed");
 
 		return true;
 	}
 
-	static function checkAsyncJobDatabase() {
-		// `CRM_CONFIG`.`CONF_KEY` = 'Core_AsyncJobs__version'
-		$dbVersion = (int)DB::getPDO()->fetchValue(" select CONV_VAL from CRM_CONFIG where CONF_KEY = :key ", [ ':key' => self::$CRM_CONFIG_VERSION_KEY ]);
-		if ($dbVersion < 1) self::upgradeAsyncJobDatabaseToVersion1();
-
-		// $dbVersion = (int)DB::getPDO()->fetchValue(" select CONV_VAL from CRM_CONFIG where CONF_KEY = :key ", [ ':key' => 'Core_AsyncJobs__version' ]);
-		// if ($dbVersion < 2) self::upgradeAsyncJobDatabaseToVersion2();
-
-		$dbVersion = (int)DB::getPDO()->fetchValue(" select CONV_VAL from CRM_CONFIG where CONF_KEY = :key ", [ ':key' => self::$CRM_CONFIG_VERSION_KEY ]);
-
-		return ($dbVersion < self::$VERSION) ? false : true;
-	}
-	static function upgradeAsyncJobDatabaseToVersion1() {
-
-		// - insertOrUpdate new row in `CRM_ASYNC_FUNCTIONS` ( $TODAY, $version, $user, $jobName )
-		// - $JOB_ID = fetchValue select ID from `CRM_ASYNC_FUNCTIONS` where JOB_NAME = $jobName
-		//   - `CRM_ASYNC_FUNCTIONS`.`A_STATUS` default 'WAITING' - not started
-		//   - `CRM_ASYNC_FUNCTIONS`.`A_STATUS`: 'NORMAL' - started
-		//   - `CRM_ASYNC_FUNCTIONS`.`A_STATUS`: 'OFF_HARD' - not running
-		//   - `CRM_ASYNC_FUNCTIONS`.`A_STATUS`: 'DELETED' - removed
-		$sql = "
-			CREATE TABLE IF NOT EXISTS `CRM_ASYNC_FUNCTIONS` ( -- list of async function definitions / config
-				`ID` int(11) NOT NULL AUTO_INCREMENT,
-				`ID_ZASOB` int(11) NOT NULL DEFAULT 0, -- TODO - register function in CRM_LISTA_ZASOBOW URL_ACTION to set perms
-				`JOB_NAME` varchar(200) NOT NULL,
-				`VERSION` int(11) NOT NULL DEFAULT 0,
-				`LOCK_TYPE` enum('ROW', 'TABLE', 'SYSTEM', 'NO_LOCK') DEFAULT 'ROW',
-				-- ROW - only one active job per row, eg. create pdf, close FV, @require primaryKey in JOB.LOCK_VALUE
-				-- TABLE - ony one active job per table, eg. update columns, make report, sync
-				-- SYSTEM - only one active job
-				-- TODO USER_... - lock per user
-				-- NO_LOCK - allow multiple jobs
-
-		--		`USER` varchar(20) NOT NULL,
-		--		`DATE` date NOT NULL,
-		--		`A_STATUS` enum('WAITING','NORMAL','WARNING','OFF_SOFT','OFF_HARD','DELETED') DEFAULT 'WAITING',
-
-				`A_RECORD_CREATE_DATE` datetime NOT NULL,
-				`A_RECORD_CREATE_AUTHOR` varchar(20) NOT NULL,
-				`A_RECORD_UPDATE_DATE` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-				`A_RECORD_UPDATE_AUTHOR` varchar(20) NOT NULL,
-				PRIMARY KEY (`ID`),
-				UNIQUE KEY `JOB_NAME` (`JOB_NAME`),
-				KEY `DATE` (`DATE`),
-				KEY `A_STATUS` (`A_STATUS`),
-				KEY `A_RECORD_UPDATE_DATE` (`A_RECORD_UPDATE_DATE`)
-			) ENGINE=MyISAM DEFAULT CHARSET=latin2;
-		";
-		$sql = "
-			CREATE TABLE IF NOT EXISTS `CRM_ASYNC_JOB_LOG` (
-				`ID` int(11) NOT NULL AUTO_INCREMENT,
-				`ID_FUNCTION` int(11) NOT NULL,
-				`ID_SOURCE_JOB` int(11) NOT NULL DEFAULT 0, -- id job
-				`P_ID` int(11) NOT NULL DEFAULT 0, -- parent job id
-				`DATE` date NOT NULL,
-				`VERSION` int(11) NOT NULL DEFAULT 0,
-				`JOB_NAME` varchar(200) NOT NULL, -- copy from CRM_ASYNC_FUNCTIONS
-				`LOCK_VALUE` varchar(200) NOT NULL DEFAULT '', -- second part for unique JOB_NAME eg. primaryKey
-				`USER` varchar(20) NOT NULL, -- user who start this function
-				`A_STATUS` enum('WAITING','NORMAL','WARNING','OFF_SOFT','OFF_HARD','DELETED') DEFAULT 'WAITING',
-
-				`A_RECORD_CREATE_DATE` datetime NOT NULL,
-				`A_RECORD_CREATE_AUTHOR` varchar(20) NOT NULL,
-				`A_RECORD_UPDATE_DATE` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
-				`A_RECORD_UPDATE_AUTHOR` varchar(20) NOT NULL,
-				PRIMARY KEY (`ID`),
-				UNIQUE KEY `JOB_LOCK` (`JOB_NAME`, `LOCK_VALUE`, `USER`), -- add USER to store user click and respond that job is running by other user
-				KEY `DATE` (`DATE`),
-				KEY `A_STATUS` (`A_STATUS`),
-				KEY `A_RECORD_UPDATE_DATE` (`A_RECORD_UPDATE_DATE`)
-			) ENGINE=MyISAM DEFAULT CHARSET=latin2;
-		";
-
-		// @usage:
-		// `CRM_ASYNC_FUNCTIONS`.`JOB_NAME` = '
-		//     route = ant
-		// --  & path = default_db.in7_dziennik_koresp/test-bash
-		//     & template = test-loop
-		//     & typeName = default_db:IN7_DZIENNIK_KORESP
-		//     & primaryKey = 66263
-		//     & primaryKeyField = ID
-		// ->  `CRM_ASYNC_FUNCTIONS`.`JOB_NAME` =
-		//     namespace = default_db/IN7_DZIENNIK_KORESP
-		//     ant = test-bash
-		//     template = test-loop
-		//     primaryKey = 66263
-		// ->  `CRM_ASYNC_FUNCTIONS`.`JOB_NAME` Format: "{$type}|..."
-		// ->  `CRM_ASYNC_FUNCTIONS`.`JOB_NAME` Format: "Ant|{$namespace}"
-		// ->  `CRM_ASYNC_JOB_LOG`.`LOCK_VALUE` Format: "{$primaryKey}|{$func_name}|..."
-		// ->  `CRM_ASYNC_JOB_LOG`.`LOCK_VALUE` Format: "{$primaryKey}|{$func_name}|{$template}"
-		// -> Example: JOB_NAME = 'Ant|default_db/IN7_DZIENNIK_KORESP', LOCK_VALUE = '66263|test-bash|test-loop'
-
-		// Add fields:
-		// - function config at execution time:
-		//   - eg LOCK by user | feature | namespace | no_lock -- should contain in name
-		// - process result - last result - NO - result from file, separate request needed
-
-		// IDEA: 2 tables:
-		// - `Config` - for function config (create row if not exists)
-		// - `Log` - for log where JOB_ID is created or used from Config table
-
-		// then createNewJob:
-		// 1. fetch job config from CRM_ASYNC_FUNCTIONS_CONFIG
-		// 1.1 IF 404 then create row, read config from function (how?)
-		// 1.2 IF exists then check status and lock config
-		// 1.2.1 IF online and lock then return error
-		// 1.2.2 IF !online and !lock then create new row in Log table and return correct JOB_ID
-
-		self::_upgdateAsyncJobDatabaseVersion($version = 1);
-	}
-	static function upgradeAsyncJobDatabaseToVersion2() {
-		// ...
-
-		self::_upgdateAsyncJobDatabaseVersion($version = 2);
-	}
-	static function _upgdateAsyncJobDatabaseVersion($version) {
-		DB::getPDO()->insertOrUpdate('CRM_CONFIG', [
-			'CONF_KEY' => self::$CRM_CONFIG_VERSION_KEY,
-			'@insert' => [
-				'CONF_VAL' => $version,
-			],
-			'@update' => [
-				'CONF_VAL' => $version,
-			]
-		]);
-	}
-
 	// pm2 logs -h
 	//
 	//   Usage: logs [options] [id|name]

+ 191 - 0
SE/se-lib/Core/AsyncJobsDB.php

@@ -0,0 +1,191 @@
+<?php
+
+/**
+ * 
+	`CRM_ASYNC_JOB_LOG`:
+		`ID`
+		`ID_FUNCTION`
+		`ID_SOURCE_JOB`
+		`P_ID`
+		`DATE`
+		`VERSION`
+		`JOB_NAME`
+		`LOCK_VALUE`
+		`USER`
+		`A_STATUS`
+		`A_RECORD_CREATE_DATE`
+		`A_RECORD_CREATE_AUTHOR`
+		`A_RECORD_UPDATE_DATE`
+		`A_RECORD_UPDATE_AUTHOR`
+ * 
+ * 
+ */
+
+Lib::loadClass('Request');
+
+class Core_AsyncJobsDB {
+
+	static $VERSION = 1; // `CRM_CONFIG`.`CONF_KEY` = 'Core_AsyncJobs__version'
+	static $CRM_CONFIG_VERSION_KEY = 'Core_AsyncJobs__version';
+
+	static function insert($item) {
+		if (empty($item['JOB_NAME'])) throw new Exception("Missing JOB_NAME in Core_AsyncJobsDB::insert");
+
+		return DB::getPDO()->insert('CRM_ASYNC_JOB_LOG', array_merge([
+			'ID_FUNCTION' => 0, // int(11) NOT NULL,
+			// 'ID_SOURCE_JOB' int(11) NOT NULL DEFAULT 0, -- id job
+			// 	'P_ID' int(11) NOT NULL DEFAULT 0, -- parent job id
+			'DATE' => date("Y-m-d"),// date NOT NULL,
+			// 'VERSION' => 0, // int(11) NOT NULL DEFAULT 0,
+			'JOB_NAME' => '', // varchar(200) NOT NULL, -- copy from CRM_ASYNC_FUNCTIONS
+			'LOCK_VALUE' => '', // varchar(200) NOT NULL DEFAULT '', -- second part for unique JOB_NAME eg. primaryKey
+			'USER' => User::getLogin(), // varchar(20) NOT NULL, -- user who start this function
+			'A_STATUS' => 'WAITING',
+			'A_RECORD_CREATE_DATE' => "NOW()",
+			'A_RECORD_CREATE_AUTHOR' => User::getLogin(),
+			// 	UNIQUE KEY 'JOB_LOCK' ('JOB_NAME', 'LOCK_VALUE', 'USER'), -- add USER to store user click and respond that job is running by other user
+			// 	KEY 'DATE' ('DATE'),
+			// 	KEY 'A_STATUS' ('A_STATUS'),
+			// 	KEY 'A_RECORD_UPDATE_DATE' ('A_RECORD_UPDATE_DATE')
+		], $item));
+	}
+
+	static function fetch($idJob) {
+		return DB::getPDO()->fetchFirst("
+			select j.*
+			from CRM_ASYNC_JOB_LOG j
+			where j.ID = :id
+		", [
+			':id' => $idJob,
+		]);
+	}
+
+	static function checkAsyncJobDatabase() {
+		// `CRM_CONFIG`.`CONF_KEY` = 'Core_AsyncJobs__version'
+		$dbVersion = (int)DB::getPDO()->fetchValue(" select CONF_VAL from CRM_CONFIG where CONF_KEY = :key ", [ ':key' => self::$CRM_CONFIG_VERSION_KEY ]);
+		if ($dbVersion < 1) self::upgradeAsyncJobDatabaseToVersion1();
+
+		// $dbVersion = (int)DB::getPDO()->fetchValue(" select CONF_VAL from CRM_CONFIG where CONF_KEY = :key ", [ ':key' => 'Core_AsyncJobs__version' ]);
+		// if ($dbVersion < 2) self::upgradeAsyncJobDatabaseToVersion2();
+
+		$dbVersion = (int)DB::getPDO()->fetchValue(" select CONF_VAL from CRM_CONFIG where CONF_KEY = :key ", [ ':key' => self::$CRM_CONFIG_VERSION_KEY ]);
+
+		return ($dbVersion < self::$VERSION) ? false : true;
+	}
+	static function upgradeAsyncJobDatabaseToVersion1() {
+		// - insertOrUpdate new row in `CRM_ASYNC_FUNCTIONS` ( $TODAY, $version, $user, $jobName )
+		// - $JOB_ID = fetchValue select ID from `CRM_ASYNC_FUNCTIONS` where JOB_NAME = $jobName
+		//   - `CRM_ASYNC_FUNCTIONS`.`A_STATUS` default 'WAITING' - not started
+		//   - `CRM_ASYNC_FUNCTIONS`.`A_STATUS`: 'NORMAL' - started
+		//   - `CRM_ASYNC_FUNCTIONS`.`A_STATUS`: 'OFF_HARD' - not running
+		//   - `CRM_ASYNC_FUNCTIONS`.`A_STATUS`: 'DELETED' - removed
+		$sql = "
+			CREATE TABLE IF NOT EXISTS `CRM_ASYNC_FUNCTIONS` ( -- list of async function definitions / config
+				`ID` int(11) NOT NULL AUTO_INCREMENT,
+				`ID_ZASOB` int(11) NOT NULL DEFAULT 0, -- TODO - register function in CRM_LISTA_ZASOBOW URL_ACTION to set perms
+				`JOB_NAME` varchar(200) NOT NULL,
+				`VERSION` int(11) NOT NULL DEFAULT 0,
+				`LOCK_TYPE` enum('ROW', 'TABLE', 'SYSTEM', 'NO_LOCK') DEFAULT 'ROW',
+				-- ROW - only one active job per row, eg. create pdf, close FV, @require primaryKey in JOB.LOCK_VALUE
+				-- TABLE - ony one active job per table, eg. update columns, make report, sync
+				-- SYSTEM - only one active job
+				-- TODO USER_... - lock per user
+				-- NO_LOCK - allow multiple jobs
+
+		--		`USER` varchar(20) NOT NULL,
+		--		`DATE` date NOT NULL,
+		--		`A_STATUS` enum('WAITING','NORMAL','WARNING','OFF_SOFT','OFF_HARD','DELETED') DEFAULT 'WAITING',
+
+				`A_RECORD_CREATE_DATE` datetime NOT NULL,
+				`A_RECORD_CREATE_AUTHOR` varchar(20) NOT NULL,
+				`A_RECORD_UPDATE_DATE` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+				`A_RECORD_UPDATE_AUTHOR` varchar(20) NOT NULL,
+				PRIMARY KEY (`ID`),
+				UNIQUE KEY `JOB_NAME` (`JOB_NAME`),
+				KEY `DATE` (`DATE`),
+				KEY `A_STATUS` (`A_STATUS`),
+				KEY `A_RECORD_UPDATE_DATE` (`A_RECORD_UPDATE_DATE`)
+			) ENGINE=MyISAM DEFAULT CHARSET=latin2;
+		";
+		// DB::getPDO()->execSql($sql);
+		$sql = "
+			CREATE TABLE IF NOT EXISTS `CRM_ASYNC_JOB_LOG` (
+				`ID` int(11) NOT NULL AUTO_INCREMENT,
+				`ID_FUNCTION` int(11) NOT NULL,
+				`ID_SOURCE_JOB` int(11) NOT NULL DEFAULT 0, -- id job
+				`P_ID` int(11) NOT NULL DEFAULT 0, -- parent job id
+				`DATE` date NOT NULL,
+				`VERSION` int(11) NOT NULL DEFAULT 0,
+				`JOB_NAME` varchar(200) NOT NULL, -- copy from CRM_ASYNC_FUNCTIONS
+				`LOCK_VALUE` varchar(200) NOT NULL DEFAULT '', -- second part for unique JOB_NAME eg. primaryKey
+				`USER` varchar(20) NOT NULL, -- user who start this function
+				`A_STATUS` enum('WAITING','NORMAL','WARNING','OFF_SOFT','OFF_HARD','DELETED') DEFAULT 'WAITING',
+
+				`A_RECORD_CREATE_DATE` datetime NOT NULL,
+				`A_RECORD_CREATE_AUTHOR` varchar(20) NOT NULL,
+				`A_RECORD_UPDATE_DATE` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+				`A_RECORD_UPDATE_AUTHOR` varchar(20) NOT NULL,
+				PRIMARY KEY (`ID`),
+			--	UNIQUE KEY `JOB_LOCK` (`JOB_NAME`, `LOCK_VALUE`, `USER`), -- add USER to store user click and respond that job is running by other user
+				KEY `DATE` (`DATE`),
+				KEY `A_STATUS` (`A_STATUS`),
+				KEY `A_RECORD_UPDATE_DATE` (`A_RECORD_UPDATE_DATE`)
+			) ENGINE=MyISAM DEFAULT CHARSET=latin2;
+		";
+		DB::getPDO()->execSql($sql);
+
+		// @usage:
+		// `CRM_ASYNC_FUNCTIONS`.`JOB_NAME` = '
+		//     route = ant
+		// --  & path = default_db.in7_dziennik_koresp/test-bash
+		//     & template = test-loop
+		//     & typeName = default_db:IN7_DZIENNIK_KORESP
+		//     & primaryKey = 66263
+		//     & primaryKeyField = ID
+		// ->  `CRM_ASYNC_FUNCTIONS`.`JOB_NAME` =
+		//     namespace = default_db/IN7_DZIENNIK_KORESP
+		//     ant = test-bash
+		//     template = test-loop
+		//     primaryKey = 66263
+		// ->  `CRM_ASYNC_FUNCTIONS`.`JOB_NAME` Format: "{$type}|..."
+		// ->  `CRM_ASYNC_FUNCTIONS`.`JOB_NAME` Format: "Ant|{$namespace}"
+		// ->  `CRM_ASYNC_JOB_LOG`.`LOCK_VALUE` Format: "{$primaryKey}|{$func_name}|..."
+		// ->  `CRM_ASYNC_JOB_LOG`.`LOCK_VALUE` Format: "{$primaryKey}|{$func_name}|{$template}"
+		// -> Example: JOB_NAME = 'Ant|default_db/IN7_DZIENNIK_KORESP', LOCK_VALUE = '66263|test-bash|test-loop'
+
+		// Add fields:
+		// - function config at execution time:
+		//   - eg LOCK by user | feature | namespace | no_lock -- should contain in name
+		// - process result - last result - NO - result from file, separate request needed
+
+		// IDEA: 2 tables:
+		// - `Config` - for function config (create row if not exists)
+		// - `Log` - for log where JOB_ID is created or used from Config table
+
+		// then createNewJob:
+		// 1. fetch job config from CRM_ASYNC_FUNCTIONS_CONFIG
+		// 1.1 IF 404 then create row, read config from function (how?)
+		// 1.2 IF exists then check status and lock config
+		// 1.2.1 IF online and lock then return error
+		// 1.2.2 IF !online and !lock then create new row in Log table and return correct JOB_ID
+
+		self::_upgdateAsyncJobDatabaseVersion($version = 1);
+	}
+	static function upgradeAsyncJobDatabaseToVersion2() {
+		// ...
+
+		self::_upgdateAsyncJobDatabaseVersion($version = 2);
+	}
+	static function _upgdateAsyncJobDatabaseVersion($version) {
+		DB::getPDO()->insertOrUpdate('CRM_CONFIG', [
+			'CONF_KEY' => self::$CRM_CONFIG_VERSION_KEY,
+			'@insert' => [
+				'CONF_VAL' => $version,
+			],
+			'@update' => [
+				'CONF_VAL' => $version,
+			]
+		]);
+	}
+
+}

+ 65 - 0
SE/se-lib/Core/AsyncJobsFiles.php

@@ -0,0 +1,65 @@
+<?php
+
+class Core_AsyncJobsFiles {
+
+	static function basePath($idJob, $jobDate) { // @return string base path
+		$confAsyncPath = Config::get('APP_PATH_ASYNC_JOB');
+		if (!$confAsyncPath) throw new Exception("Missing Config APP_PATH_ASYNC_JOB");
+		return "{$confAsyncPath}/{$jobDate}/{$idJob}";
+	}
+	static function propertyFile($idJob, $jobDate) { // @return string property file path
+		return self::basePath($idJob, $jobDate) . "/job.properties";
+	}
+	static function startScript($idJob, $jobDate) { // @return string start script path
+		return self::basePath($idJob, $jobDate) . "/start.sh";
+	}
+	static function outLog($idJob, $jobDate) { // @return string out log file path
+		return self::basePath($idJob, $jobDate) . "/out.log";
+	}
+	static function errorLog($idJob, $jobDate) { // @return string error log file path
+		return self::basePath($idJob, $jobDate) . "/error.log";
+	}
+
+	static function createNewJobFiles($idJob, $jobProps = []) {
+		if (!$idJob) throw new Exception("Missing job ID");
+		$today = date("Y-m-d");
+		$jobBasePath = self::basePath($idJob, $today); // "{$confAsyncPath}/{$today}/{$idJob}";
+		mkdir($jobBasePath, $mode = 0777, $recursive = TRUE);
+		if (!file_exists($jobBasePath)) throw new Exception("Cannot create folder for async job");
+		touch(self::outLog($idJob, $today)); // "{$jobBasePath}/out.log");
+		touch(self::errorLog($idJob, $today)); // "{$jobBasePath}/error.log");
+		// touch("{$jobBasePath}/start.sh");
+		$baseProps = [];
+		$baseProps['p5_async_job_base_path'] = $jobBasePath;
+		$baseProps['p5_async_job_output'] = "{$jobBasePath}/output";
+		$baseProps['p5_async_job_progress'] = "{$jobBasePath}/progress";
+		$baseProps['p5_se_base_path'] = APP_PATH_ROOT;
+		$allProps = array_merge($baseProps, $jobProps);
+		$outProperties = implode("\n", array_map(function ($value, $key) {
+			return "{$key}={$value}";
+		}, $allProps, array_keys($allProps)));
+		file_put_contents(self::propertyFile($idJob, $today), $outProperties);
+		touch("{$jobBasePath}/progress");
+		// touch("{$jobBasePath}/output");
+		// - make base files:
+		//   - log files: `out.log`, `error.log`
+		//   - `output`: if process creates file. @param --out_file=out
+		//   - `output_type`: output mime type or namespace
+		//   - `progress`: if process implement progress. @param --progress_file=progress
+	}
+
+	static function createTaskStartScript($idJob, $jobDate, $jobStartScript) {
+		file_put_contents(self::startScript($idJob, $jobDate), "#!/usr/bin/env bash" . "\n" . $jobStartScript);
+	}
+
+	static function isInstalled() {
+		$confAsyncPath = Config::get('APP_PATH_ASYNC_JOB');
+		if (!$confAsyncPath) throw new Exception("Missing Config APP_PATH_ASYNC_JOB");
+
+		if (!file_exists($confAsyncPath)) {
+			mkdir($confAsyncPath, $mode = 0777, $recursive = TRUE);
+		}
+		if (!file_exists($confAsyncPath)) throw new Exception("Folder not exists APP_PATH_ASYNC_JOB");
+	}
+
+}

+ 164 - 0
SE/se-lib/Core/AsyncJobsServer.php

@@ -0,0 +1,164 @@
+<?php
+
+Lib::loadClass('Request');
+
+/**
+ * @usage: Core_AsyncJobsServer::startAsyncJob(execPath, name, [ 'out.log', 'error.log', 'cwd', 'args' ])
+ * @usage: Core_AsyncJobsServer::isInstalled()
+ */
+
+class Core_AsyncJobsServer {
+
+	static function path_nodeJS() { return "/usr/local/bin/node"; }
+	// npm_path="/usr/local/bin/npm"
+	static function path_pm2() { return "/usr/local/bin/pm2"; }
+	static function path_pm2UserWww() { return "/Library/WebServer/.pm2/"; }
+
+	static function startAsyncJob($execPath, $name, $props = [], &$out) {
+		if (!$execPath) throw new Exception("Missing job exec path");
+		if (!$name) throw new Exception("Missing job name");
+
+		$pathLog = V::get('out.log', '', $props);
+		$pathError = V::get('error.log', '', $props);
+		// for Ant:
+		// index.php?_route=UrlAction_Ant&_task=ant&path=default_db.in7_dziennik_koresp/etykieta&typeName=default_db:IN7_DZIENNIK_KORESP&primaryKey=66263&primaryKeyField=ID
+		// index.php?_route=UrlAction_Ant
+		// 	& _task=ant
+		// 	& path=default_db.in7_dziennik_koresp/test-bash
+		// 	& typeName=default_db:IN7_DZIENNIK_KORESP
+		// 	& primaryKey=66263
+		// 	& primaryKeyField=ID
+		// index.php?_route=UrlAction_Ant
+		// 	& _task=ant
+		// 	& path=default_db.in7_dziennik_koresp/test-bash
+		// 	& template=test-loop
+		// 	& typeName=default_db:IN7_DZIENNIK_KORESP
+		// 	& primaryKey=66263
+		// 	& primaryKeyField=ID
+		// ant=default_db:IN7_DZIENNIK_KORESP/test-bash & task=test-loop & ns=default_db:IN7_DZIENNIK_KORESP & pk=66263
+		if (empty($pathLog)) throw new Exception("Missing job log path");
+		if (empty($pathError)) throw new Exception("Missing job error path");
+		$execArg = V::get('args', '', $props); // execArg for script
+		$cmd = implode(" ", [
+			"pm2 start '{$execPath}'",
+			(!empty($props['cwd'])) ? "--cwd=\"{$props['cwd']}\"" : "",
+			"--name '{$name}'",
+			"--no-autorestart",
+			"--output '{$pathLog}'",
+			"--error '{$pathError}'",
+			"--time", // prefix time to log entry
+			(!empty(trim($execArg))) ? "-- {$execArg}" : "",
+		]);
+		// return V::shell_exec($cmd);
+
+		V::exec($cmd . " 2>&1", $out, $ret);
+		// echo "cmd: <code>{$cmd}</code><br>RETURN CODE: '{$ret}'<br><pre>OUTPUT:\n" . implode("\n", $out). "</pre>";
+		if (0 !== $ret) {
+			// [PM2][ERROR] Script already launched, add -f option to force re-execution
+			$firstLine = reset($out);
+			if (0 === strpos($firstLine, '[PM2][ERROR] Script already launched')) {
+				$out = [ "[PM2][ERROR] Script already launched" ];
+			}
+			// throw new Exception("Nie uruchomiono zadania. " . implode(" ", $out));
+		} else {
+			// [PM2] Applying action restartProcessId on app [biall-backup-db](ids: [ 0 ])
+			// [PM2] [biall-backup-db](0) ✓
+			// [PM2] Process successfully started
+			$line0 = reset($out);
+			$line1 = next($out);
+			$line2 = next($out);
+			if (0 === strpos($line0, '[PM2] Applying action')
+				&& 0 === strpos($line1, '[PM2] [')
+				&& 0 === strpos($line2, '[PM2] Process successfully started')
+			) {
+				$out = [ "[PM2] Process successfully started" ];
+				return 0;
+			}
+			// [PM2] Starting /opt/local/pl.procesy5/async_jobs/2020-02-26/3/start.sh in fork_mode (1 instance) [PM2] Done.
+			if (0 === strpos($line0, '[PM2] Starting ')
+				&& 0 === strpos($line1, '[PM2] Done')
+			) {
+				$out = [ "[PM2] Process successfully started" ];
+				return 0;
+			}
+			// throw new AlertInfoException("Uruchomiono zadanie. " . implode(" ", $out));
+		}
+		return $ret;
+	}
+
+	static function runPm2Script($props, &$out) { // TODO: mv to AsyncJobs / @return bool $ret from exec
+		if (empty($props['jobName'])) throw new Exception("Missing jobName");
+		if (empty($props['path'])) throw new Exception("Missing path");
+		// cwd	(string)	“/var/www/”	the directory from which your app will be launched
+		$jobName = $props['jobName'];
+		$outLogPath = "/tmp/p5-async-jobs--{$jobName}--out.log";
+		$errLogPath = "/tmp/p5-async-jobs--{$jobName}--err.log";
+		$path = $props['path'];
+		$args = trim(V::get('args', '', $props));
+		$cmd = implode(" ", [
+			"pm2 start '{$path}'",
+			(!empty($props['cwd'])) ? "--cwd=\"{$props['cwd']}\"" : "",
+			"--name '{$jobName}'",
+			"--no-autorestart",
+			"--output '{$outLogPath}'",
+			"--error  '{$errLogPath}'",
+			"--time", // prefix time to log entry
+			(!empty($args)) ? "-- {$args}" : "",
+		]);
+		V::exec($cmd . " 2>&1", $out, $ret);
+		// echo "cmd: <code>{$cmd}</code><br>RETURN CODE: '{$ret}'<br><pre>OUTPUT:\n" . implode("\n", $out). "</pre>";
+		if (0 !== $ret) {
+			// [PM2][ERROR] Script already launched, add -f option to force re-execution
+			$firstLine = reset($out);
+			if (0 === strpos($firstLine, '[PM2][ERROR] Script already launched')) {
+				$out = [ "[PM2][ERROR] Script already launched" ];
+			}
+			// throw new Exception("Nie uruchomiono backupu. " . implode(" ", $out));
+		} else {
+			// [PM2] Applying action restartProcessId on app [biall-backup-db](ids: [ 0 ])
+			// [PM2] [biall-backup-db](0) ✓
+			// [PM2] Process successfully started
+			$line0 = reset($out);
+			$line1 = next($out);
+			$line2 = next($out);
+			if (0 === strpos($line0, '[PM2] Applying action')
+				&& 0 === strpos($line1, '[PM2] [')
+				&& 0 === strpos($line2, '[PM2] Process successfully started')
+			) {
+				$out = [ "[PM2] Process successfully started" ];
+			}
+			// throw new AlertInfoException("Uruchomiono backup. " . implode(" ", $out));
+		}
+		return $ret;
+	}
+
+	static function isInstalled() {
+		// V::exec("/usr/local/bin/pm2 --version 2>&1", $out, $ret);
+		V::exec("/usr/local/bin/pm2 ping 2>&1", $out, $ret); // expected "{ msg: 'pong' }"
+		// echo UI::h('pre', [], "ret({$ret}):\n" . implode("\n", $out));
+		if ($ret === 0) {
+			// [PM2] Spawning PM2 daemon with pm2_home=/Library/WebServer/.pm2
+			// [PM2] PM2 Successfully daemonized
+			// { msg: 'pong' }
+		}
+		if ($ret !== 0) {
+			if (!file_exists(self::path_nodeJS())) throw new Exception("pm2 not installed");
+			// if [ ! -f "$node_path" ]; then
+			// 	echo "$node_path not exists"
+			// 	wget https://nodejs.org/dist/v12.15.0/node-v12.15.0.pkg
+			// 	sudo installer -verbose -pkg node-v12.15.0.pkg -target /
+			// fi
+			// # node -v # expected v12.15.0
+			if (!file_exists(self::path_pm2())) throw new Exception("pm2 not installed");
+			// npm install -g pm2
+			// sudo npm install -g pm2
+			// pm2 -version # expected 4.2.3
+			if (!file_exists(self::path_pm2UserWww())) throw new Exception("pm2 user folder not exists");
+			// FIX for Mac OS:
+			// $ sudo mkdir /Library/WebServer/.pm2/
+			// $ sudo chown _www /Library/WebServer/.pm2/
+			throw new Exception("Error pm2"); // unknown error
+		}
+	}
+
+}

+ 31 - 0
SE/se-lib/Route/AsyncJobs.php

@@ -6,6 +6,7 @@ Lib::loadClass('Core_AsyncJobs');
 class Route_AsyncJobs extends RouteBase {
 
 	function isInstalled() { return Core_AsyncJobs::isInstalled(); }
+	function getSbinPath() { return realpath(APP_PATH_ROOT . "/../sbin/"); }
 
 	function defaultAction() { UI::layout([ $this, 'defaultView' ]); }
 	function defaultView() {
@@ -14,6 +15,13 @@ class Route_AsyncJobs extends RouteBase {
 
 		DBG::nicePrint(Config::getConfFile(), "Config::getConfFile()");
 
+		try {
+			Core_AsyncJobs::isInstalled();
+		} catch (Exception $e) {
+			UI::alert('danger', $e->getMessage());
+		}
+
+
 		echo UI::h('div', [ 'style' => "padding:12px 0; border-bottom:1px solid #ddd" ], [
 			UI::hButtonPost("Check", [
 				'class' => "btn btn-default",
@@ -77,6 +85,14 @@ class Route_AsyncJobs extends RouteBase {
 					'_postTask' => "mp2DeleteStopped",
 				]
 			]),
+			" ",
+			UI::hButtonPost("reinstall pm2-www", [
+				'class' => "btn btn-warning",
+				'data' => [
+					'_route' => 'AsyncJobs',
+					'_postTask' => "testReinstallPm2ByWWW",
+				]
+			]),
 		]);
 
 		$postTask = V::get('_postTask', '', $_POST);
@@ -88,6 +104,7 @@ class Route_AsyncJobs extends RouteBase {
 			case "mp2Start2": UI::tryCatchView([ $this, 'mp2Start2PostTask' ]); break;
 			case "mp2Start3": UI::tryCatchView([ $this, 'mp2Start3PostTask' ]); break;
 			case "mp2DeleteStopped": UI::tryCatchView([ $this, 'mp2DeleteStoppedPostTask' ]); break;
+			case "testReinstallPm2ByWWW": UI::tryCatchView([ $this, 'testReinstallPm2ByWWWPostTask' ]); break;
 		}
 
 	}
@@ -265,4 +282,18 @@ class Route_AsyncJobs extends RouteBase {
 		UI::alert("DONE");
 	}
 
+	function testReinstallPm2ByWWWPostTask() {
+		$seBinPath = $this->getSbinPath();
+		$out = [];
+		$cmd = "cd '{$seBinPath}' && bash se.sh sudo--install-pm2-www";
+		V::exec($cmd = "{$cmd} 2>&1", $out, $ret);
+		echo UI::h('pre', [], "RET({$ret}). OUTPUT:" . "\n" . implode("\n", $out));
+		if (0 !== $ret) throw new Exception( (empty($out)) ? "Error: backup failed!" : implode("\n", $out) );
+
+		if (!file_exists('/usr/local/bin/pm2-www')) {
+			throw new Exception("Nie udało się zainstalować pm2-www");
+		} else {
+			throw new AlertSuccessException("Zainstalowano pm2-www");
+		}
+	}
 }