summaryrefslogtreecommitdiff
path: root/src/classes
diff options
context:
space:
mode:
authorEvgeny Zinoviev <me@ch1p.io>2021-05-07 23:39:20 +0300
committerEvgeny Zinoviev <me@ch1p.io>2021-05-07 23:39:30 +0300
commit9a98ac50ff50dda2f2eed1ea825352c50c64440e (patch)
treecc5c7d36eee1ec934f8728e7f9104a9d2d760856 /src/classes
initial
Diffstat (limited to 'src/classes')
-rw-r--r--src/classes/Job.php51
-rw-r--r--src/classes/JobResult.php96
-rw-r--r--src/classes/jobs.php275
-rw-r--r--src/classes/model.php149
-rw-r--r--src/classes/mysql.php166
5 files changed, 737 insertions, 0 deletions
diff --git a/src/classes/Job.php b/src/classes/Job.php
new file mode 100644
index 0000000..4ecbf6c
--- /dev/null
+++ b/src/classes/Job.php
@@ -0,0 +1,51 @@
+<?php
+
+abstract class Job extends model {
+
+ // ENUM status
+ const STATUS_WAITING = 'waiting';
+ const STATUS_MANUAL = 'manual';
+ const STATUS_ACCEPTED = 'accepted';
+ const STATUS_IGNORED = 'ignored';
+ const STATUS_RUNNING = 'running';
+ const STATUS_DONE = 'done';
+
+ // ENUM result
+ const RESULT_OK = 'ok';
+ const RESULT_FAIL = 'fail';
+
+ const DB_TABLE = 'jobs';
+
+ protected static $Fields = [
+ 'id' => model::INTEGER,
+ 'target' => model::STRING,
+ 'name' => model::STRING,
+ 'time_created' => model::INTEGER,
+ 'time_started' => model::INTEGER,
+ 'time_finished' => model::INTEGER,
+ 'status' => model::STRING, // ENUM
+ 'result' => model::STRING, // ENUM
+ 'return_code' => model::INTEGER,
+ 'sig' => model::STRING,
+ 'stdout' => model::STRING,
+ 'stderr' => model::STRING,
+ 'input' => model::SERIALIZED,
+ ];
+
+ public $id;
+ public $target;
+ public $name;
+ public $timeCreated;
+ public $timeStarted;
+ public $timeFinished;
+ public $status;
+ public $result;
+ public $returnCode;
+ public $sig;
+ public $stdout;
+ public $stderr;
+ public $input;
+
+ abstract public function run();
+
+} \ No newline at end of file
diff --git a/src/classes/JobResult.php b/src/classes/JobResult.php
new file mode 100644
index 0000000..c42b6b0
--- /dev/null
+++ b/src/classes/JobResult.php
@@ -0,0 +1,96 @@
+<?php
+
+class JobResult {
+
+ /**
+ * @var string $result
+ */
+ protected $result;
+
+ /**
+ * @var int $returnCode
+ */
+ protected $returnCode;
+
+ /**
+ * @var string|null $signal
+ */
+ protected $signal;
+
+ /**
+ * @var string $stdout
+ */
+ protected $stdout;
+
+ /**
+ * @var string $stderr
+ */
+ protected $stderr;
+
+ /**
+ * @param string $result
+ * @param int $return_code
+ * @param string $stdout
+ * @param string $stderr
+ * @param null $signal
+ * @return $this
+ */
+ public function setResult(string $result,
+ int $return_code,
+ string $stdout,
+ string $stderr,
+ $signal = null): JobResult
+ {
+ $this->result = $result;
+ $this->returnCode = $return_code;
+ $this->stdout = $stdout;
+ $this->stderr = $stderr;
+ $this->signal = $signal;
+
+ return $this;
+ }
+
+ /**
+ * @param string $error
+ * @return $this
+ */
+ public function setError(string $error): JobResult
+ {
+ $this->result = Job::RESULT_FAIL;
+ $this->stderr = $error;
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isFailed(): bool
+ {
+ return $this->result == Job::RESULT_FAIL;
+ }
+
+ /**
+ * @return string
+ */
+ public function getStdout(): string
+ {
+ return $this->stdout;
+ }
+
+ /**
+ * @return mixed|null
+ */
+ public function getStdoutAsJSON() {
+ $json = jsonDecode($this->stdout);
+ return $json ? $json : null;
+ }
+
+ /**
+ * @return string
+ */
+ public function getError(): string {
+ return $this->stderr ?? '';
+ }
+
+} \ No newline at end of file
diff --git a/src/classes/jobs.php b/src/classes/jobs.php
new file mode 100644
index 0000000..9acbc50
--- /dev/null
+++ b/src/classes/jobs.php
@@ -0,0 +1,275 @@
+<?php
+
+class jobs
+{
+
+ /**
+ * @var jobs_destructor $destructor_instance
+ */
+ private static $destructor_instance;
+
+ /**
+ * @var array<int, array> $new_jobs
+ */
+ private static $new_jobs = [];
+
+ /**
+ * Automatically poke master on exit.
+ */
+ public static function destruct()
+ {
+ if (!empty(self::$new_jobs)) {
+ $targets = [];
+ foreach (self::$new_jobs as $new_job) {
+ if ($new_job['status'] === Job::STATUS_WAITING)
+ $targets[$new_job['target']] = true;
+ }
+
+ if (!empty($targets)) {
+ $targets = array_keys($targets);
+ self::poke($targets);
+ }
+ }
+ }
+
+ /**
+ * Create job.
+ *
+ * @param int|string $target
+ * @param string $name
+ * @param array $data
+ * @param string $status
+ * @return int|string Job ID
+ */
+ public static function add($target, string $name, array $data = [], string $status = Job::STATUS_WAITING): int
+ {
+ if (is_null(self::$destructor_instance))
+ self::$destructor_instance = new jobs_destructor();
+
+ if (strpos($name, '\\') !== false) {
+ $pos = strrpos($name, '\\');
+ $name = substr($name, $pos + 1);
+ }
+
+ $db = getMySQL();
+ $db->insert(JOBD_TABLE, [
+ 'target' => $target,
+ 'name' => $name,
+ 'time_created' => time(),
+ 'input' => serialize($data),
+ 'status' => $status
+ ]);
+ $id = $db->insertId();
+
+ self::$new_jobs[$id] = [
+ 'target' => $target,
+ 'status' => $status
+ ];
+
+ return $id;
+ }
+
+ /**
+ * Create manual job.
+ *
+ * @param int|string $target
+ * @param string $name
+ * @param array $data
+ * @return int
+ */
+ public static function manual($target, string $name, array $data = []): int
+ {
+ return self::add($target, $name, $data, Job::STATUS_MANUAL);
+ }
+
+ /**
+ * Run jobs with given ids and status=Job::STATUS_MANUAL and wait for results.
+ *
+ * If only one job was given and it's failed, an Exception will be thrown!
+ * If multiple jobs were given and some of them failed, an array of JobResults will be returned.
+ *
+ * @param int|int[] $job_ids
+ * @return array<int, JobResult>|JobResult
+ * @throws Exception
+ */
+ public static function run($job_ids)
+ {
+ if (!is_array($job_ids))
+ $job_ids = [$job_ids];
+
+ $job_ids_orig = $job_ids;
+ $job_ids = array_flip($job_ids);
+
+ $jobs = [];
+
+ // look for the given jobs in self::$new_jobs
+ foreach (self::$new_jobs as $id => $new_job) {
+ if ($new_job['status'] == Job::STATUS_MANUAL && isset($job_ids[$id])) {
+ $jobs[] = ['id' => $id, 'target' => $new_job['target']];
+ unset($job_ids[$id]);
+ }
+ }
+
+ // if some (or all) jobs were not found in self::$new_jobs, get them from the database
+ if (!empty($job_ids)) {
+ $job_ids = array_keys($job_ids);
+
+ $db = getMySQL();
+ $q = $db->query("SELECT id, target, status AS target FROM ".JOBD_TABLE." WHERE id IN (".implode(',', $job_ids).")");
+ $job_ids = array_flip($job_ids);
+
+ while ($row = $db->fetch($q)) {
+ // only manual jobs are allowed
+ if ($row['status'] != Job::STATUS_MANUAL)
+ throw new Exception("job id=${row['id']} has status = {$row['status']} != manual");
+
+ $jobs[] = [
+ 'id' => (int)$row['id'],
+ 'target' => $row['target']
+ ];
+
+ unset($job_ids[$row['id']]);
+ }
+
+ $q->free();
+
+ // we were given invalid ids, it seems. throw an exception and don't continue
+ if (!empty($job_ids))
+ throw new Exception("jobs with id ".implode(', ', array_keys($job_ids))." not found");
+ }
+
+ // connect to master and send run-manual request
+ $client = getJobdMaster();
+ $response = $client->runManual($jobs);
+
+ // master request failed
+ if (($error = $response->getError()) !== null)
+ throw new Exception("jobd returned error: ".$error);
+
+ // at this point, jobd-master request succeeded
+ // doesn't mean our jobs were successfully accepted and executed by workers,
+ // but at least we have some results
+
+ /**
+ * @var array<int, JobResult> $results
+ */
+ $results = [];
+ $data = $response->getData();
+
+ $client->close();
+
+ // collect results, successes and failures
+ if (!empty($data['jobs'])) {
+ foreach ($data['jobs'] as $job_id => $job_result_raw) {
+ $job_result = (new JobResult())->setResult(
+ $job_result_raw['result'],
+ $job_result_raw['code'],
+ $job_result_raw['stdout'],
+ $job_result_raw['stderr'],
+ $job_result_raw['signal']
+ );
+ $results[$job_id] = $job_result;
+ }
+ }
+ if (!empty($data['errors'])) {
+ foreach ($data['errors'] as $job_id => $job_result_raw) {
+ $job_result = (new JobResult())->setError($job_result_raw);
+ $results[$job_id] = $job_result;
+ }
+ }
+
+ // remove jobs from self::$new_jobs
+ foreach ($job_ids_orig as $id) {
+ if (isset(self::$new_jobs[$id]))
+ unset(self::$new_jobs[$id]);
+ }
+
+ // if the $job_ids arguments wasn't an array, return the JobResult instance
+ if (count($job_ids_orig) === 1 && count($results) === 1) {
+ $result = reset($results);
+ if ($result->isFailed())
+ throw new Exception($result->getError());
+ return $result;
+ }
+
+ // otherwise, return array of JobResult instances
+ return $results;
+ }
+
+ /**
+ * @param string|string[] $targets
+ */
+ public static function poke($targets)
+ {
+
+ $client = getJobdMaster();
+
+ if (!is_array($targets))
+ $targets = [$targets];
+
+ $client->poke($targets);
+ $targets = array_flip(array_unique($targets));
+
+ // remove poked targets from self::$new_jobs to avoid meaninglessly duplicating this poke from the destructor
+ if (!empty(self::$new_jobs)) {
+ foreach (self::$new_jobs as $new_job_id => $new_job) {
+ if ($new_job['status'] == Job::STATUS_WAITING && isset($targets[$new_job['target']]))
+ unset(self::$new_jobs[$new_job_id]);
+ }
+ }
+
+ $client->close();
+ return true;
+ }
+
+ /**
+ * @param int $id
+ * @return array
+ */
+ public static function get(int $id)
+ {
+ $db = getMySQL();
+ $q = $db->query("SELECT * FROM ".JOBD_TABLE." WHERE id=?", $id);
+ return $db->fetch($q);
+ }
+
+ /**
+ * Delete old succeeded jobs.
+ */
+ public static function cleanup()
+ {
+ $db = getMySQL();
+ $db->query("DELETE FROM ".JOBD_TABLE." WHERE status='done' AND result='ok' AND time_finished < ?",
+ time() - 86400);
+ }
+
+}
+
+
+class job_target
+{
+
+ const any = "any";
+
+ public static function high(int $server): string
+ {
+ return "$server/high";
+ }
+
+ public static function low(int $server): string
+ {
+ return "$server/low";
+ }
+
+}
+
+
+class jobs_destructor
+{
+
+ public function __destruct()
+ {
+ jobs::destruct();
+ }
+
+}
diff --git a/src/classes/model.php b/src/classes/model.php
new file mode 100644
index 0000000..ff44361
--- /dev/null
+++ b/src/classes/model.php
@@ -0,0 +1,149 @@
+<?php
+
+abstract class model {
+
+ const DB_TABLE = null;
+ const DB_KEY = 'id';
+
+ const STRING = 0;
+ const INTEGER = 1;
+ const FLOAT = 2;
+ const ARRAY = 3;
+ const BOOLEAN = 4;
+ const JSON = 5;
+ const SERIALIZED = 6;
+
+ protected static $Fields = [];
+
+ public static function create_instance(...$args) {
+ $cl = get_called_class();
+ return new $cl(...$args);
+ }
+
+ public function __construct($raw) {
+ foreach (static::$Fields as $name => $type)
+ $this->{toCamelCase($name)} = self::cast_to_type($type, $raw[$name]);
+
+ if (is_null(static::DB_TABLE))
+ trigger_error('class '.get_class($this).' doesn\'t have DB_TABLE defined');
+ }
+
+ /**
+ * @param $fields
+ */
+ public function edit($fields) {
+ $db = getMySQL();
+
+ $save = [];
+ foreach ($fields as $name => $value) {
+ switch (static::$Fields[$name]) {
+ case self::ARRAY:
+ if (is_array($value)) {
+ $fields[$name] = implode(',', $value);
+ $save[$name] = $value;
+ }
+ break;
+
+ case self::INTEGER:
+ $value = (int)$value;
+ $fields[$name] = $value;
+ $save[$name] = $value;
+ break;
+
+ case self::FLOAT:
+ $value = (float)$value;
+ $fields[$name] = $value;
+ $save[$name] = $value;
+ break;
+
+ case self::BOOLEAN:
+ $fields[$name] = $value ? 1 : 0;
+ $save[$name] = $value;
+ break;
+
+ case self::JSON:
+ $fields[$name] = jsonEncode($value);
+ $save[$name] = $value;
+ break;
+
+ case self::SERIALIZED:
+ $fields[$name] = serialize($value);
+ $save[$name] = $value;
+ break;
+
+ default:
+ $value = (string)$value;
+ $fields[$name] = $value;
+ $save[$name] = $value;
+ break;
+ }
+ }
+
+ if (!$db->update(static::DB_TABLE, $fields, static::DB_KEY."=?", $this->get_id())) {
+ //debugError(__METHOD__.': failed to update database');
+ return;
+ }
+
+ foreach ($save as $name => $value)
+ $this->{toCamelCase($name)} = $value;
+ }
+
+ /**
+ * @return int
+ */
+ public function get_id() {
+ return $this->{toCamelCase(static::DB_KEY)};
+ }
+
+ /**
+ * @param array $fields
+ * @param array $custom_getters
+ * @return array
+ */
+ public function as_array(array $fields = [], array $custom_getters = []) {
+ if (empty($fields))
+ $fields = array_keys(static::$Fields);
+
+ $array = [];
+ foreach ($fields as $field) {
+ if (isset($custom_getters[$field]) && is_callable($custom_getters[$field])) {
+ $array[$field] = $custom_getters[$field]();
+ } else {
+ $array[$field] = $this->{toCamelCase($field)};
+ }
+ }
+
+ return $array;
+ }
+
+ /**
+ * @param $type
+ * @param $value
+ * @return array|bool|false|float|int|string
+ */
+ protected static function cast_to_type($type, $value) {
+ switch ($type) {
+ case self::BOOLEAN:
+ return (bool)$value;
+
+ case self::INTEGER:
+ return (int)$value;
+
+ case self::FLOAT:
+ return (float)$value;
+
+ case self::ARRAY:
+ return array_filter(explode(',', $value));
+
+ case self::JSON:
+ return jsonDecode($value);
+
+ case self::SERIALIZED:
+ return unserialize($value);
+
+ default:
+ return (string)$value;
+ }
+ }
+
+}
diff --git a/src/classes/mysql.php b/src/classes/mysql.php
new file mode 100644
index 0000000..710390f
--- /dev/null
+++ b/src/classes/mysql.php
@@ -0,0 +1,166 @@
+<?php
+
+class mysql
+{
+
+ /** @var mysqli $link */
+ private $link = null;
+
+ public function __construct(string $host, string $user, string $password, string $name)
+ {
+ $this->link = new mysqli();
+ if (!$this->link->real_connect($host, $user, $password, $name)) {
+ $this->link = null;
+ throw new Exception('Could not connect to MySQL');
+ }
+ }
+
+ public function __destruct()
+ {
+ if ($this->link)
+ $this->link->close();
+ }
+
+ public function __get($k)
+ {
+ if ($k == 'error') {
+ return $this->link->error;
+ }
+ return $this->$k;
+ }
+
+ public function query(string $sql)
+ {
+ if (func_num_args() > 1) {
+ $mark_count = substr_count($sql, '?');
+ $positions = array();
+ $last_pos = -1;
+ for ($i = 0; $i < $mark_count; $i++) {
+ $last_pos = strpos($sql, '?', $last_pos + 1);
+ $positions[] = $last_pos;
+ }
+ for ($i = $mark_count - 1; $i >= 0; $i--) {
+ $arg_val = func_get_arg($i + 1);
+ if (is_null($arg_val)) {
+ $v = 'NULL';
+ } else {
+ $v = '\'' . $this->escape($arg_val) . '\'';
+ }
+ $sql = substr_replace($sql, $v, $positions[$i], 1);
+ }
+ }
+
+ $q = $this->link->query($sql);
+ if (!$q) {
+ $error = $this->link->error;
+ trigger_error($error, E_USER_WARNING);
+ return false;
+ }
+
+ return $q;
+ }
+
+ public function insert(string $table, array $fields)
+ {
+ return $this->performInsert('INSERT', $table, $fields);
+ }
+
+ public function replace(string $table, array $fields)
+ {
+ return $this->performInsert('REPLACE', $table, $fields);
+ }
+
+ protected function performInsert(string $command, string $table, array $fields)
+ {
+ $names = [];
+ $values = [];
+ $count = 0;
+ foreach ($fields as $k => $v) {
+ $names[] = $k;
+ $values[] = $v;
+ $count++;
+ }
+
+ $sql = "{$command} INTO `{$table}` (`" . implode('`, `', $names) . "`) VALUES (" . implode(', ', array_fill(0, $count, '?')) . ")";
+ array_unshift($values, $sql);
+
+ return call_user_func_array([$this, 'query'], $values);
+ }
+
+ public function multipleInsert(string $table, array $rows)
+ {
+ return $this->performMultipleInsert('INSERT', $table, $rows);
+ }
+
+ public function multipleReplace(string $table, array $rows)
+ {
+ return $this->performMultipleInsert('REPLACE', $table, $rows);
+ }
+
+ protected function performMultipleInsert(string $command, string $table, array $rows)
+ {
+ $names = [];
+ $sql_rows = [];
+ foreach ($rows as $i => $fields) {
+ $row_values = [];
+ foreach ($fields as $field_name => $field_val) {
+ if ($i == 0) {
+ $names[] = $field_name;
+ }
+ $row_values[] = $this->escape($field_val);
+ }
+ $sql_rows[] = "('" . implode("', '", $row_values) . "')";
+ }
+
+ $sql = "{$command} INTO `{$table}` (`" . implode('`, `', $names) . "`) VALUES " . implode(', ', $sql_rows);
+ return $this->query($sql);
+ }
+
+ public function update(string $table, arrow $rows, string ...$cond)
+ {
+ $fields = [];
+ $args = [];
+ foreach ($rows as $row_name => $row_value) {
+ $fields[] = "`{$row_name}`=?";
+ $args[] = $row_value;
+ }
+ $sql = "UPDATE `$table` SET " . implode(', ', $fields);
+ if (!empty($cond)) {
+ $sql .= " WHERE " . $cond[0];
+ if (count($cond) > 1)
+ $args = array_merge($args, array_slice($cond, 1));
+ }
+ return $this->query($sql, ...$args);
+ }
+
+ public function fetch(mysqli_result $q)
+ {
+ $row = $q->fetch_assoc();
+ if (!$row) {
+ $q->free();
+ return false;
+ }
+ return $row;
+ }
+
+ public function result(mysqli_result $q, int $field = 0)
+ {
+ return $q ? $q->fetch_row()[$field] : false;
+ }
+
+ public function insertId()
+ {
+ return $this->link->insert_id;
+ }
+
+ public function numRows(mysqli_result $query): int
+ {
+ return $query->num_rows;
+ }
+
+ public function escape(string $s): string
+ {
+ return $this->link->real_escape_string($s);
+ }
+
+}