taskman/taskman.inc.php

747 lines
16 KiB
PHP
Raw Normal View History

2022-05-16 14:14:49 +03:00
<?php
namespace {
$GLOBALS['TASKMAN_TASKS'] = array();
$GLOBALS['TASKMAN_CLOSURES'] = array();
$GLOBALS['TASKMAN_STACK'] = array();
$GLOBALS['TASKMAN_TASK_ALIASES'] = array();
$GLOBALS['TASKMAN_LOG_LEVEL'] = 1; //0 - important, 1 - normal, 2 - debug
$GLOBALS['TASKMAN_NO_DEPS'] = false;
$GLOBALS['TASKMAN_SCRIPT'] = '';
$GLOBALS['TASKMAN_CURRENT_TASK'] = null;
2022-06-06 18:36:02 +03:00
$GLOBALS['TASKMAN_HELP_FUNC'] = '_taskman_default_usage';
2022-05-16 14:14:49 +03:00
$GLOBALS['TASKMAN_LOGGER'] = '_taskman_default_logger';
$GLOBALS['TASKMAN_ERROR_HANDLER'] = null;
$GLOBALS['TASKMAN_START_TIME'] = 0;
2022-06-06 18:36:02 +03:00
function _taskman_default_logger($msg)
{
echo $msg;
}
function _taskman_default_usage($script_name = "<taskman-script>")
{
echo "\nUsage:\n $script_name [OPTIONS] <task-name1>[,<task-name2>,..] [-D PROP1=value [-D PROP2]]\n\n";
echo "Available options:\n";
echo " -c specify PHP script to be included (handy for setting props,config options,etc)\n";
echo " -V be super verbose\n";
echo " -q be quite, only system messages\n";
echo " -b batch mode: be super quite, don't even output any system messages\n";
echo " -- pass all options verbatim after this mark\n";
}
} //namespace global
namespace taskman {
2023-06-02 15:54:03 +03:00
use Exception;
2022-06-06 18:36:02 +03:00
2023-06-02 15:54:03 +03:00
class TaskmanException extends Exception
2022-05-16 14:14:49 +03:00
{}
class TaskmanTask
{
private $func;
private $name;
private $file;
private $line;
private $props = array();
private $is_running = false;
private $has_run = array();
private $args = array();
2022-06-06 18:36:02 +03:00
function __construct(\Closure $func, $name, $props = array())
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:42:26 +03:00
$refl = new \ReflectionFunction($func);
2022-05-16 14:14:49 +03:00
$this->file = $refl->getFileName();
$this->line = $refl->getStartLine();
$this->func = $func;
2022-06-06 18:36:02 +03:00
$this->name = $name;
$this->props = $props;
2022-05-16 14:14:49 +03:00
}
function validate()
{
try
{
foreach($this->_getBeforeDeps() as $dep_task)
2022-06-06 18:36:02 +03:00
get_task($dep_task);
2022-05-16 14:14:49 +03:00
foreach($this->_getDeps() as $dep_task)
2022-06-06 18:36:02 +03:00
get_task($dep_task);
2022-05-16 14:14:49 +03:00
foreach($this->_getAfterDeps() as $dep_task)
2022-06-06 18:36:02 +03:00
get_task($dep_task);
2022-05-16 14:14:49 +03:00
}
catch(Exception $e)
{
throw new Exception("Task '{$this->name}' validation error: " . $e->getMessage());
}
}
function getName()
{
return $this->name;
}
function getFile()
{
return $this->file;
}
function getLine()
{
return $this->line;
}
function getFunc()
{
return $this->func;
}
function getArgs()
{
return $this->args;
}
function getAliases()
{
$alias = $this->getPropOr("alias", "");
if(is_array($alias))
return $alias;
else if($alias && is_string($alias))
return explode(",", $alias);
else
return array();
}
function run($args = array())
{
global $TASKMAN_CURRENT_TASK;
global $TASKMAN_STACK;
global $TASKMAN_NO_DEPS;
global $TASKMAN_START_TIME;
$args_str = serialize($args);
if((isset($this->has_run[$args_str]) && $this->has_run[$args_str]) ||
$this->is_running)
return;
$this->is_running = true;
$this->args = $args;
2023-11-17 12:32:57 +03:00
$task_result = null;
2022-05-16 14:14:49 +03:00
try
{
2023-03-29 23:22:10 +03:00
$TASKMAN_STACK[] = $this;
$level = count($TASKMAN_STACK)-1;
msg_sys("***** ".str_repeat('-', $level)."task '" . $this->getName() . "' start *****\n");
2022-05-16 14:14:49 +03:00
if(!$TASKMAN_NO_DEPS)
{
2022-06-06 18:36:02 +03:00
run_many($this->_getBeforeDeps());
run_many($this->_getDeps());
2022-05-16 14:14:49 +03:00
}
2023-03-29 23:22:10 +03:00
$TASKMAN_CURRENT_TASK = $this;
2022-05-16 14:14:49 +03:00
$bench = microtime(true);
2023-11-17 12:32:57 +03:00
$task_result = call_user_func_array($this->func, array($this->args));
2022-05-16 14:14:49 +03:00
array_pop($TASKMAN_STACK);
if(!$TASKMAN_NO_DEPS)
2022-06-06 18:36:02 +03:00
run_many($this->_getAfterDeps());
2022-05-16 14:14:49 +03:00
2023-03-29 23:22:10 +03:00
msg_sys("***** ".str_repeat('-', $level)."task '" . $this->getName() . "' done(" .
2023-03-29 19:25:56 +03:00
round(microtime(true)-$bench,2) . '/' .round(microtime(true)-$TASKMAN_START_TIME,2) . " sec.) *****\n");
2022-05-16 14:14:49 +03:00
$this->has_run[$args_str] = true;
$this->is_running = false;
}
catch(Exception $e)
{
$error_handler = $GLOBALS['TASKMAN_ERROR_HANDLER'];
if($error_handler)
$error_handler($e);
else
throw $e;
}
2023-11-17 12:32:57 +03:00
return $task_result;
2022-05-16 14:14:49 +03:00
}
private function _getBeforeDeps()
{
return $this->_collectRelatedTasks("before");
}
private function _getAfterDeps()
{
return $this->_collectRelatedTasks("after");
}
private function _collectRelatedTasks($prop_name)
{
$arr = array();
2022-06-06 18:36:02 +03:00
foreach(get_tasks() as $task_obj)
2022-05-16 14:14:49 +03:00
{
if($this->getName() == $task_obj->getName())
continue;
$value = $task_obj->getPropOr($prop_name, "");
if($value == $this->getName() || in_array($value, $this->getAliases()))
$arr[] = $task_obj;
}
return $arr;
}
private function _getDeps()
{
$deps = $this->getPropOr('deps', "");
if(is_array($deps))
return $deps;
else if($deps && is_string($deps))
2022-06-06 18:36:02 +03:00
return _parse_taskstr($deps);
2022-05-16 14:14:49 +03:00
return array();
}
static function extractName($func)
{
if(strpos($func, "task_") === 0)
return substr($func, strlen('task_'), strlen($func));
else if(strpos($func, 'taskman\task_') === 0)
return substr($func, strlen('taskman\task_'), strlen($func));
}
function getPropOr($name, $def)
{
return isset($this->props[$name]) ? $this->props[$name] : $def;
}
function getProp($name)
{
return $this->getPropOr($name, null);
}
function hasProp($name)
{
return isset($this->props[$name]);
}
function getProps()
{
return $this->props;
}
}
2022-06-06 18:36:02 +03:00
function _collect_tasks()
2022-05-16 14:14:49 +03:00
{
global $TASKMAN_TASKS;
global $TASKMAN_TASK_ALIASES;
$TASKMAN_TASKS = array();
$TASKMAN_TASK_ALIASES = array();
2022-06-06 18:36:02 +03:00
$cands = _get_task_candidates();
2022-05-16 14:14:49 +03:00
foreach($cands as $name => $args)
{
if(isset($TASKMAN_TASKS[$name]))
throw new TaskmanException("Task '$name' is already defined");
2022-06-06 18:36:02 +03:00
if(is_array($args))
2022-05-16 14:14:49 +03:00
{
$props = array();
if(sizeof($args) > 2)
{
$props = $args[1];
$func = $args[2];
}
else
$func = $args[1];
$task = new TaskmanTask($func, $name, $props);
}
else
throw new Exception("Task '$name' is invalid");
$TASKMAN_TASKS[$name] = $task;
foreach($task->getAliases() as $alias)
{
if(isset($TASKMAN_TASKS[$alias]) || isset($TASKMAN_TASK_ALIASES[$alias]))
throw new TaskmanException("Alias '$alias' is already defined for task '$name'");
$TASKMAN_TASK_ALIASES[$alias] = $task;
}
}
foreach($TASKMAN_TASKS as $task)
$task->validate();
}
2022-06-06 18:36:02 +03:00
function _get_task_candidates()
2022-05-16 14:14:49 +03:00
{
global $TASKMAN_CLOSURES;
$cands = array();
2022-06-06 18:36:02 +03:00
//get tasks defined as closures
2022-05-16 14:14:49 +03:00
foreach($TASKMAN_CLOSURES as $name => $args)
{
if(isset($cands[$name]))
throw new Exception("Task '$name' is already defined");
$cands[$name] = $args;
}
ksort($cands);
return $cands;
}
2022-06-06 18:36:02 +03:00
function get_task($task)
2022-05-16 14:14:49 +03:00
{
global $TASKMAN_TASKS;
global $TASKMAN_TASK_ALIASES;
if(!is_scalar($task))
throw new TaskmanException("Bad task name");
if(isset($TASKMAN_TASKS[$task]))
return $TASKMAN_TASKS[$task];
if(isset($TASKMAN_TASK_ALIASES[$task]))
return $TASKMAN_TASK_ALIASES[$task];
throw new TaskmanException("Task with name/alias '{$task}' does not exist");
}
2022-06-06 18:36:02 +03:00
function _resolve_callable_prop($name)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
$prop = $GLOBALS['TASKMAN_PROP_' . $name];
if(!($prop instanceof \Closure))
return;
$value = $prop();
$GLOBALS['TASKMAN_PROP_' . $name] = $value;
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function get($name)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
if(!isset($GLOBALS['TASKMAN_PROP_' . $name]))
throw new TaskmanException("Property '$name' is not set");
_resolve_callable_prop($name);
return $GLOBALS['TASKMAN_PROP_' . $name];
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function getor($name, $def)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
if(!isset($GLOBALS['TASKMAN_PROP_' . $name]))
return $def;
_resolve_callable_prop($name);
return $GLOBALS['TASKMAN_PROP_' . $name];
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function set($name, $value)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
$GLOBALS['TASKMAN_PROP_' . $name] = $value;
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function setor($name, $value)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
if(!isset($GLOBALS['TASKMAN_PROP_' . $name]))
$GLOBALS['TASKMAN_PROP_' . $name] = $value;
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function is($name)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
return isset($GLOBALS['TASKMAN_PROP_' . $name]);
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function del($name)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
unset($GLOBALS['TASKMAN_PROP_' . $name]);
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function props()
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
$props = array();
foreach($GLOBALS as $key => $value)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
if(($idx = strpos($key, 'TASKMAN_PROP_')) === 0)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
$name = substr($key, strlen('TASKMAN_PROP_'));
$props[$name] = get($name);
2022-05-16 14:14:49 +03:00
}
}
2022-06-06 18:36:02 +03:00
return $props;
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function task($name)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
global $TASKMAN_CLOSURES;
$args = func_get_args();
$TASKMAN_CLOSURES[$name] = $args;
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function get_tasks()
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
global $TASKMAN_TASKS;
return $TASKMAN_TASKS;
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function current_task()
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
global $TASKMAN_CURRENT_TASK;
return $TASKMAN_CURRENT_TASK;
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function run($task, array $args = array())
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
if($task instanceof TaskmanTask)
$task_obj = $task;
else
$task_obj = get_task($task);
2022-05-16 14:14:49 +03:00
2023-11-17 12:32:57 +03:00
return $task_obj->run($args);
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function run_many($tasks, $args = array())
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
foreach($tasks as $task_spec)
{
if(is_array($task_spec))
run($task_spec[0], $task_spec[1]);
else
run($task_spec, $args);
}
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function msg_dbg($msg)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
_log($msg, 2);
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function msg($msg)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
_log($msg, 1);
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function msg_sys($msg)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
_log($msg, 0);
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function _log($msg, $level = 1)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
global $TASKMAN_LOG_LEVEL;
if($TASKMAN_LOG_LEVEL < $level)
return;
$logger = $GLOBALS['TASKMAN_LOGGER'];
call_user_func_array($logger, array($msg));
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function _($str)
{
if(strpos($str, '%') === false)
return $str;
$str = preg_replace_callback(
'~%\(([^\)]+)\)%~',
function($m) { return get($m[1]); },
$str
);
return $str;
}
function _isset_task($task)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
global $TASKMAN_TASKS;
global $TASKMAN_TASK_ALIASES;
return isset($TASKMAN_TASKS[$task]) || isset($TASKMAN_TASK_ALIASES[$task]);
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function _get_hints($task)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
global $TASKMAN_TASKS;
global $TASKMAN_TASK_ALIASES;
$tasks = array_merge(array_keys($TASKMAN_TASKS), array_keys($TASKMAN_TASK_ALIASES));
$found = array_filter($tasks, function($v) use($task) { return strpos($v, $task) === 0; });
$found = array_merge($found, array_filter($tasks, function($v) use($task) { $pos = strpos($v, $task); return $pos !== false && $pos > 0; }));
return $found;
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:42:26 +03:00
//e.g: run,build,zip
2022-06-06 18:36:02 +03:00
function _parse_taskstr($str)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
$task_spec = array();
$items = explode(',', $str);
foreach($items as $item)
{
$args = null;
$task = $item;
if(strpos($item, ' ') !== false)
@list($task, $args) = explode(' ', $item, 2);
if($args)
$task_spec[] = array($task, explode(' ', $args));
else
$task_spec[] = $task;
}
return $task_spec;
2022-05-16 14:14:49 +03:00
}
function _read_env_vars()
{
$envs = getenv(null);
foreach($envs as $k => $v)
{
if(strpos($k, 'TASKMAN_SET_') === 0)
{
$prop_name = substr($k, 12);
msg_sys("Setting prop '$prop_name' (with env '$k')\n");
set($prop_name, $v);
}
}
}
2022-06-06 18:36:02 +03:00
function _process_argv(&$argv)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
global $TASKMAN_LOG_LEVEL;
global $TASKMAN_BATCH;
global $TASKMAN_NO_DEPS;
$filtered = array();
$process_defs = false;
for($i=0;$i<sizeof($argv);++$i)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
$v = $argv[$i];
if($v == '--')
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
for($j=$i+1;$j<sizeof($argv);++$j)
$filtered[] = $argv[$j];
break;
}
else if($v == '-D')
{
$process_defs = true;
}
else if($v == '-V')
{
$TASKMAN_LOG_LEVEL = 2;
}
else if($v == '-q')
{
$TASKMAN_LOG_LEVEL = 0;
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
else if($v == '-b')
{
$TASKMAN_LOG_LEVEL = -1;
}
else if($v == '-O')
{
$TASKMAN_NO_DEPS = true;
}
else if($v == '-c')
{
if(!isset($argv[$i+1]))
throw new TaskmanException("Configuration file(-c option) is missing");
require_once($argv[$i+1]);
++$i;
}
else if($process_defs)
{
$eq_pos = strpos($v, '=');
if($eq_pos !== false)
{
$def_name = substr($v, 0, $eq_pos);
$def_value = substr($v, $eq_pos+1);
//TODO: this code must be more robust
if(strtolower($def_value) === 'true')
2022-06-06 18:36:02 +03:00
$def_value = true;
else if(strtolower($def_value) === 'false')
2022-06-06 18:36:02 +03:00
$def_value = false;
}
else
{
$def_name = $v;
$def_value = 1;
}
msg_sys("Setting prop '$def_name'=" . var_export($def_value, true) . "\n");
2022-07-21 17:16:16 +03:00
set($def_name, $def_value);
2022-06-06 18:36:02 +03:00
$process_defs = false;
}
else
$filtered[] = $v;
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
$argv = $filtered;
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function main($argv = array(), $help_func = null, $proc_argv = true, $read_env_vars = true)
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
$GLOBALS['TASKMAN_START_TIME'] = microtime(true);
if($help_func)
$GLOBALS['TASKMAN_HELP_FUNC'] = $help_func;
if($read_env_vars)
_read_env_vars();
2022-06-06 18:36:02 +03:00
if($proc_argv)
_process_argv($argv);
2022-06-06 18:36:02 +03:00
$GLOBALS['TASKMAN_SCRIPT'] = array_shift($argv);
_collect_tasks();
$always_tasks = array();
$default_task = null;
foreach(get_tasks() as $task_obj)
{
if($task_obj->hasProp('always'))
array_unshift($always_tasks, $task_obj);
if($task_obj->hasProp('default'))
{
if($default_task)
throw new TaskmanException("Assigned default task '" . $default_task->getName() . "' conflicts with '" . $task_obj->getName() . "'");
else
$default_task = $task_obj;
}
}
foreach($always_tasks as $always_task)
$always_task->run(array());
if(sizeof($argv) > 0)
{
$task_str = array_shift($argv);
2022-06-06 18:42:26 +03:00
$tasks = _parse_taskstr($task_str);
2022-06-06 18:36:02 +03:00
if(count($tasks) == 1 && !_isset_task($tasks[0]))
{
$pattern = $tasks[0];
if($pattern[0] == '~')
{
$pattern = substr($pattern, 1, strlen($pattern) - 1);
$is_similar = true;
}
elseif(substr($pattern, -1, 1) == '~')
{
$pattern = substr($pattern, 0, strlen($pattern) - 1);
$is_similar = true;
}
else
$is_similar = false;
$hints = _get_hints($pattern);
if($is_similar && count($hints) == 1)
$tasks = $hints;
else
{
printf("ERROR! Task %s not found\n", $tasks[0]);
if($hints)
{
printf("Similar tasks:\n");
foreach($hints as $hint)
printf(" %s\n", $hint);
}
exit(1);
}
}
run_many($tasks, $argv);
}
else if($default_task)
$default_task->run($argv);
2023-03-29 23:22:10 +03:00
msg_sys("***** All done (".round(microtime(true)-$GLOBALS['TASKMAN_START_TIME'],2)." sec.) *****\n");
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
function usage($script_name = "<taskman-script>")
2022-05-16 14:14:49 +03:00
{
2022-06-06 18:36:02 +03:00
\_taskman_default_usage($script_name);
2022-05-16 14:14:49 +03:00
}
2022-06-06 18:36:02 +03:00
task('help', function($args = array())
2022-05-16 14:14:49 +03:00
{
$filter = '';
if(isset($args[0]))
$filter = $args[0];
$maxlen = -1;
$tasks = array();
2022-06-06 18:36:02 +03:00
$all = get_tasks();
2022-05-16 14:14:49 +03:00
foreach($all as $task)
{
if($filter && (strpos($task->getName(), $filter) === false && strpos($task->getPropOr("alias", ""), $filter) === false))
continue;
if(strlen($task->getName()) > $maxlen)
$maxlen = strlen($task->getName());
$tasks[] = $task;
}
if(!$args)
{
$help_func = $GLOBALS['TASKMAN_HELP_FUNC'];
$help_func();
echo "\n";
}
echo "Available tasks:\n";
foreach($tasks as $task)
{
$props_string = '';
$pad = $maxlen - strlen($task->getName());
foreach($task->getProps() as $name => $value)
{
$props_string .= str_repeat(" ", $pad) .' @' . $name . ' ' . (is_string($value) ? $value : json_encode($value)) . "\n";
$pad = $maxlen + 1;
}
$props_string = rtrim($props_string);
echo "---------------------------------\n";
$file = $task->getFile();
$line = $task->getLine();
echo " " . $task->getName() . $props_string . " ($file@$line)\n";
}
echo "\n";
2022-06-06 18:36:02 +03:00
});
2022-05-16 14:14:49 +03:00
task('props', function($args = [])
2022-05-16 14:14:49 +03:00
{
$filter = '';
if(isset($args[0]))
$filter = $args[0];
$props = props();
echo "\n";
echo "Available props:\n";
foreach($props as $k => $v)
{
if($filter && stripos($k, $filter) === false)
continue;
echo "---------------------------------\n";
echo "$k : " . var_export($v, true) . "\n";
}
echo "\n";
2022-06-06 18:36:02 +03:00
});
2022-05-16 14:14:49 +03:00
} //namespace taskman
//}}}