diff --git a/bin/console b/bin/console index dad1ccc91e..3916429b92 100755 --- a/bin/console +++ b/bin/console @@ -66,6 +66,7 @@ try { $app->add(new module_console_taskrun('task:run')); $app->add(new module_console_tasklist('task:list')); + $app->add(new module_console_taskState('task:state')); $app->add(new module_console_schedulerState('scheduler:state')); $app->add(new module_console_schedulerStop('scheduler:stop')); $app->add(new module_console_schedulerStart('scheduler:start')); diff --git a/lib/classes/module/console/schedulerState.class.php b/lib/classes/module/console/schedulerState.class.php index 4cb8bd755d..fcd22e3cc3 100644 --- a/lib/classes/module/console/schedulerState.class.php +++ b/lib/classes/module/console/schedulerState.class.php @@ -24,6 +24,15 @@ use Symfony\Component\Console\Command\Command; class module_console_schedulerState extends Command { + const EXITCODE_SETUP_ERROR = 1; + const EXITCODE_STATE_UNKNOWN = 21; + + private $stateToExitCode = array( + \task_manager::STATE_TOSTOP => 13, + \task_manager::STATE_STARTED => 10, + \task_manager::STATE_STOPPING => 12, + \task_manager::STATE_STOPPED => 11, + ); public function __construct($name = null) { @@ -35,9 +44,10 @@ class module_console_schedulerState extends Command 'short' , NULL , InputOption::VALUE_NONE - , 'print short result, ie: stopped | started(12345) | stopping' + , 'print short result, ie: stopped() | started(12345) | tostop(12345) | stopping(12345)' , NULL ); +// $this->setHelp(""); return $this; } @@ -47,7 +57,7 @@ class module_console_schedulerState extends Command if ( ! setup::is_installed()) { $output->writeln($input->getOption('short') ? 'setup_error' : 'Phraseanet is not set up'); - return 1; + return self::EXITCODE_SETUP_ERROR; } require_once __DIR__ . '/../../../../lib/bootstrap.php'; @@ -55,37 +65,29 @@ class module_console_schedulerState extends Command $appbox = appbox::get_instance(\bootstrap::getCore()); $task_manager = new task_manager($appbox); + $exitCode = 0; $state = $task_manager->getSchedulerState(); - if ($state['status'] == 'started') { - $output->writeln(sprintf( - 'Scheduler is %s on pid %d' - , $state['status'] - , $state['pid'] - )); + if ($input->getOption('short')) { + $output->writeln(sprintf('%s(%s)', $state['status'], $state['pid'])); } else { - $output->writeln(sprintf('Scheduler is %s', $state['status'])); + if ($state['pid'] != NULL) { + $output->writeln(sprintf( + 'Scheduler is %s on pid %d' + , $state['status'] + , $state['pid'] + )); + } else { + $output->writeln(sprintf('Scheduler is %s', $state['status'])); + } } - switch ($state['status']) { - case \task_manager::STATUS_SCHED_STARTED: - - return 10; - break; - case \task_manager::STATUS_SCHED_STOPPED: - - return 11; - break; - case \task_manager::STATUS_SCHED_STOPPING: - - return 12; - break; - case \task_manager::STATUS_SCHED_TOSTOP: - - return 13; - break; + if (array_key_exists($state['status'], $this->stateToExitCode)) { + $exitCode = $this->stateToExitCode[$state['status']]; + } else { + $exitCode = self::EXITCODE_STATE_UNKNOWN; } - return 1; + return $exitCode; } } diff --git a/lib/classes/module/console/schedulerStop.class.php b/lib/classes/module/console/schedulerStop.class.php index b721a276dc..c96f92b46b 100644 --- a/lib/classes/module/console/schedulerStop.class.php +++ b/lib/classes/module/console/schedulerStop.class.php @@ -47,7 +47,7 @@ class module_console_schedulerStop extends Command try { $appbox = appbox::get_instance(\bootstrap::getCore()); $task_manager = new task_manager($appbox); - $task_manager->setSchedulerState(task_manager::STATUS_SCHED_TOSTOP); + $task_manager->setSchedulerState(task_manager::STATE_TOSTOP); return 0; } catch (\Exception $e) { diff --git a/lib/classes/module/console/taskState.class.php b/lib/classes/module/console/taskState.class.php new file mode 100644 index 0000000000..c8086b15fe --- /dev/null +++ b/lib/classes/module/console/taskState.class.php @@ -0,0 +1,122 @@ + 13, + \task_abstract::STATE_STARTED => 10, + \task_abstract::STATE_TOSTART => 14, + \task_abstract::STATE_TORESTART => 15, + \task_abstract::STATE_STOPPED => 11, + \task_abstract::STATE_TODELETE => 16 + ); + + public function __construct($name = null) + { + parent::__construct($name); + + $this->addArgument('task_id', InputArgument::REQUIRED, 'The task_id to test'); + + $this->setDescription('Get task state'); + + $this->addOption( + 'short' + , NULL + , InputOption::VALUE_NONE + , 'print short result, ie: stopped() | started(12345) | tostop(12345) | ...' + , NULL + ); + + return $this; + } + + public function execute(InputInterface $input, OutputInterface $output) + { + if ( ! setup::is_installed()) { + $output->writeln($input->getOption('short') ? 'setup_error' : 'Phraseanet is not set up'); + + return self::EXITCODE_SETUP_ERROR; + } + $task_id = (int) $input->getArgument('task_id'); + if ($task_id <= 0 || strlen($task_id) !== strlen($input->getArgument('task_id'))) { + $output->writeln($input->getOption('short') ? 'bad_id' : 'Argument must be an ID'); + + return self::EXITCODE_BAD_ARGUMENT; + } + + require_once __DIR__ . '/../../../../lib/bootstrap.php'; + + $appbox = appbox::get_instance(\bootstrap::getCore()); + $task_manager = new task_manager($appbox); + + $taskPID = $taskState = NULL; + $exitCode = 0; + + $task = NULL; + try { + $task = $task_manager->getTask($task_id); + $taskPID = $task->getPID(); + $taskState = $task->getState(); + } catch (Exception_NotFound $e) { + $output->writeln($input->getOption('short') ? 'unknown_id' : $e->getMessage()); + + return self::EXITCODE_TASK_UNKNOWN; + } catch (Exception $e) { + $output->writeln($input->getOption('short') ? 'fatal_error' : $e->getMessage()); + + return self::EXITCODE_FATAL_ERROR; + } + + if ($input->getOption('short')) { + $output->writeln(sprintf('%s(%s)', $taskState, $taskPID)); + } else { + if ($taskPID !== NULL) { + $output->writeln(sprintf( + 'Task %d is %s on pid %d' + , $task_id + , $taskState + , $taskPID + )); + } else { + $output->writeln(sprintf('Task %d is %s', $task_id, $taskState)); + } + } + + if (array_key_exists($taskState, $this->stateToExitCode)) { + $exitCode = $this->stateToExitCode[$taskState]; + } else { + $exitCode = self::EXITCODE_STATE_UNKNOWN; + } + + return $exitCode; + } +} + diff --git a/lib/classes/task/Scheduler.class.php b/lib/classes/task/Scheduler.class.php index 45e63048d4..bb183d533c 100755 --- a/lib/classes/task/Scheduler.class.php +++ b/lib/classes/task/Scheduler.class.php @@ -128,8 +128,6 @@ class task_Scheduler // set every 'auto-start' task to start foreach ($task_manager->getTasks() as $task) { if ($task->isActive()) { - $tid = $task->getID(); - if ( ! $task->getPID()) { /* @var $task task_abstract */ $task->resetCrashCounter(); diff --git a/lib/classes/task/abstract.class.php b/lib/classes/task/abstract.class.php index 70f7ba145c..a322259d53 100755 --- a/lib/classes/task/abstract.class.php +++ b/lib/classes/task/abstract.class.php @@ -93,6 +93,11 @@ abstract class task_abstract "--help" => array("set" => false, "values" => array(), "usage" => " (no help available)") ); + /** + * get the state of the task (task_abstract::STATE_*) + * + * @return String + */ public function getState() { $conn = connection::getPDOConnection(); @@ -118,11 +123,22 @@ abstract class task_abstract return false; } + public function printInterfaceHTML() + { + return false; + } + public function getGraphicForm() { return false; } + /** + * set the state of the task (task_abstract::STATE_*) + * + * @param String $status + * @throws Exception_InvalidArgument + */ public function setState($status) { $av_status = array( @@ -179,6 +195,10 @@ abstract class task_abstract public function setSettings($settings) { + if (@simplexml_load_string($settings) === FALSE) { + throw new Exception_InvalidArgument('Bad XML'); + } + $conn = connection::getPDOConnection(); $sql = 'UPDATE task2 SET settings = :settings WHERE task_id = :taskid'; @@ -301,6 +321,10 @@ abstract class task_abstract public function setRunner($runner) { + if ($runner != self::RUNNER_MANUAL && $runner != self::RUNNER_SCHEDULER) { + throw new Exception_InvalidArgument(sprintf('unknown runner `%s`', $runner)); + } + $this->runner = $runner; $conn = connection::getPDOConnection(); @@ -364,12 +388,10 @@ abstract class task_abstract public function getPID() { $pid = NULL; - $taskid = $this->getID(); - $registry = registry::get_instance(); - system_file::mkdir($lockdir = $registry->get('GV_RootPath') . 'tmp/locks/'); + $lockfile = $this->getLockfilePath(); - if (($fd = fopen(($lockfile = ($lockdir . 'task_' . $taskid . '.lock')), 'a+')) != FALSE) { + if (($fd = fopen($lockfile, 'a+')) != FALSE) { if (flock($fd, LOCK_EX | LOCK_NB) === FALSE) { // already locked ? : task running $pid = fgets($fd); @@ -407,36 +429,50 @@ abstract class task_abstract } } + private function getLockfilePath() + { + $registry = registry::get_instance(); + $lockdir = $registry->get('GV_RootPath') . 'tmp/locks/'; + + system_file::mkdir($lockdir); + $lockfile = ($lockdir . 'task_' . $this->getID() . '.lock'); + + return($lockfile); + } + + private function lockTask() + { + $lockfile = $this->getLockfilePath(); + + $lockFD = fopen($lockfile, 'a+'); + + $locker = true; + if (flock($lockFD, LOCK_EX | LOCK_NB, $locker) === FALSE) { + $this->log("runtask::ERROR : task already running."); + fclose($lockFD); + + throw new Exception('task already running.', self::ERR_ALREADY_RUNNING); + } + + // here we run the task + ftruncate($lockFD, 0); + fwrite($lockFD, '' . getmypid()); + fflush($lockFD); + + // for windows : unlock then lock shared to allow OTHER processes to read the file + // too bad : no critical section nor atomicity + flock($lockFD, LOCK_UN); + flock($lockFD, LOCK_SH); + + return $lockFD; + } + final public function run($runner, $input = null, $output = null) { $this->input = $input; $this->output = $output; - $taskid = $this->getID(); - - $registry = registry::get_instance(); - system_file::mkdir($lockdir = $registry->get('GV_RootPath') . 'tmp/locks/'); - $locker = true; - $lockfile = ($lockdir . 'task_' . $taskid . '.lock'); - $tasklock = fopen($lockfile, 'a+'); - - if (flock($tasklock, LOCK_EX | LOCK_NB, $locker) === FALSE) { - $this->log("runtask::ERROR : task already running."); - fclose($tasklock); - - throw new Exception('task already running.', self::ERR_ALREADY_RUNNING); - return; - } - - // here we run the task - ftruncate($tasklock, 0); - fwrite($tasklock, '' . getmypid()); - fflush($tasklock); - - // for windows : unlock then lock shared to allow OTHER processes to read the file - // too bad : no critical section nor atomicity - flock($tasklock, LOCK_UN); - flock($tasklock, LOCK_SH); + $lockFD = $this->lockTask(); $this->setRunner($runner); $this->setState(self::STATE_STARTED); @@ -450,9 +486,21 @@ abstract class task_abstract } // in any case, exception or not, the task is ending so unlock the pid file - flock($tasklock, LOCK_UN | LOCK_NB); - ftruncate($tasklock, 0); - fclose($tasklock); + $this->unlockTask($lockFD); + + // if something went wrong, report + if ($exception) { + throw($exception); + } + } + + public function unlockTask($lockFD) + { + flock($lockFD, LOCK_UN | LOCK_NB); + ftruncate($lockFD, 0); + fclose($lockFD); + + $lockfile = $this->getLockfilePath(); @unlink($lockfile); switch ($this->getState()) { @@ -463,11 +511,6 @@ abstract class task_abstract $this->setState(self::STATE_STOPPED); break; } - - // if something went wrong, report - if ($exception) { - throw($exception); - } } abstract protected function run2(); diff --git a/lib/classes/task/manager.class.php b/lib/classes/task/manager.class.php index cc7a4f54df..2fe14a3d5b 100755 --- a/lib/classes/task/manager.class.php +++ b/lib/classes/task/manager.class.php @@ -16,10 +16,10 @@ */ class task_manager { - const STATUS_SCHED_STOPPED = 'stopped'; - const STATUS_SCHED_STOPPING = 'stopping'; - const STATUS_SCHED_STARTED = 'started'; - const STATUS_SCHED_TOSTOP = 'tostop'; + const STATE_STOPPED = 'stopped'; + const STATE_STOPPING = 'stopping'; + const STATE_STARTED = 'started'; + const STATE_TOSTOP = 'tostop'; protected $appbox; protected $tasks; @@ -91,7 +91,7 @@ class task_manager $tasks = $this->getTasks(); if ( ! isset($tasks[$task_id])) { - throw new Exception_NotFound('Unknown task_id'); + throw new Exception_NotFound('Unknown task_id ' . $task_id); } return $tasks[$task_id]; @@ -100,10 +100,10 @@ class task_manager public function setSchedulerState($status) { $av_status = array( - self::STATUS_SCHED_STARTED - , self::STATUS_SCHED_STOPPED - , self::STATUS_SCHED_STOPPING - , self::STATUS_SCHED_TOSTOP + self::STATE_STARTED, + self::STATE_STOPPED, + self::STATE_STOPPING, + self::STATE_TOSTOP ); if ( ! in_array($status, $av_status)) diff --git a/templates/web/admin/task.html b/templates/web/admin/task.html index 3872d4ba27..72eb8c5552 100644 --- a/templates/web/admin/task.html +++ b/templates/web/admin/task.html @@ -418,7 +418,7 @@ {% endif %} -
+
diff --git a/tests/task/task_abstractTest.php b/tests/task/task_abstractTest.php index c3de55283e..79e01da2bb 100644 --- a/tests/task/task_abstractTest.php +++ b/tests/task/task_abstractTest.php @@ -4,12 +4,170 @@ require_once __DIR__ . '/../PhraseanetPHPUnitAbstract.class.inc'; class task_abstractTest extends PhraseanetPHPUnitAbstract { + /** + * @var task_abstract + */ + protected static $task; + protected static $tid; - public function testCreate() + public static function setUpBeforeClass() { + parent::setUpBeforeClass(); $appbox = appbox::get_instance(\bootstrap::getCore()); - $task = task_abstract::create($appbox, 'task_period_apibridge'); - $task->delete(); + self::$task = task_abstract::create($appbox, 'task_period_test'); + self::$tid = self::$task->getID(); + } + + public static function tearDownAfterClass() + { + self::$task->delete(); + parent::tearDownAfterClass(); + } + + /** + * @covers \task_abstract::setActive + * @covers \task_abstract::isActive + */ + public function testActive() + { + self::$task->setActive(true); + self::assertTrue(self::$task->isActive()); + + self::$task->setActive(false); + self::assertFalse(self::$task->isActive()); + } + + /** + * @covers \task_abstract::setState + * @covers \task_abstract::getState + */ + public function testState() + { + self::$task->setState(\task_abstract::STATE_STOPPED); + self::assertEquals(\task_abstract::STATE_STOPPED, self::$task->getState()); + + self::$task->setState(\task_abstract::STATE_TOSTOP); + self::assertEquals(\task_abstract::STATE_TOSTOP, self::$task->getState()); + } + + /** + * @covers \task_abstract::setTitle + * @covers \task_abstract::getTitle + */ + public function testTitle() + { + self::$task->setTitle('a_test_title'); + self::assertEquals('a_test_title', self::$task->getTitle()); + } + + /** + * @covers \task_abstract::resetCrashCounter + * @covers \task_abstract::incrementCrashCounter + * @covers \task_abstract::getCrashCounter + */ + public function testCrashCounter() + { + self::$task->resetCrashCounter(); + self::$task->incrementCrashCounter(); + self::assertEquals(1, self::$task->getCrashCounter()); + + self::$task->incrementCrashCounter(); + self::assertEquals(2, self::$task->getCrashCounter()); + + self::$task->resetCrashCounter(); + self::assertEquals(0, self::$task->getCrashCounter()); + } + + /** + * @covers \task_abstract::setSettings + * @covers \task_abstract::getSettings + */ + public function testSettings() + { + $goodSettings = ""; + $sxGoodSettings = simplexml_load_string($goodSettings); + + self::$task->setSettings($goodSettings); + $settings = self::$task->getSettings(); + $sxSettings = @simplexml_load_string($settings); + + self::assertTrue($sxSettings !== FALSE); + self::assertEquals($sxGoodSettings->saveXML(), $sxSettings->saveXML()); + } + + /** + * @covers \task_abstract::setSettings + * @expectedException Exception_InvalidArgument + */ + public function testSettingsException() + { + self::$task->setSettings('this_is_bad_xml'); + } + + /** + * @covers \task_abstract::setRunner + * @covers \task_abstract::getRunner + */ + public function testRunner() + { + self::$task->setRunner(\task_abstract::RUNNER_MANUAL); + self::assertTrue(\task_abstract::RUNNER_MANUAL === self::$task->getRunner()); + + self::$task->setRunner(\task_abstract::RUNNER_SCHEDULER); + self::assertTrue(\task_abstract::RUNNER_SCHEDULER === self::$task->getRunner()); + } + + /** + * @covers \task_abstract::setRunner + * @expectedException Exception_InvalidArgument + */ + public function testRunnerException() + { + self::$task->setRunner('this_is_bad_runner'); + } + + /** + * @covers \task_abstract::lockTask + * @covers \task_abstract::unlockTask + */ + public function testLockTask() + { + $methodL = new ReflectionMethod(self::$task, 'lockTask'); + $methodL->setAccessible(TRUE); + + $methodU = new ReflectionMethod(self::$task, 'unlockTask'); + $methodU->setAccessible(TRUE); + + // test that task should not be locked + try { + $fd = $methodL->invoke(self::$task); + } catch (Exception $e) { + self::fail('file should not be locked'); + } + self::assertInternalType('resource', $fd); + + // now task should be locked + try { + $fd = $methodL->invoke(self::$task); + self::fail('file should be locked'); + } catch (Exception $e) { + + } + + // so we can unlock + $methodU->invokeArgs(self::$task, array($fd)); + + // task should not be locked + try { + $fd = $methodL->invoke(self::$task); + } catch (Exception $e) { + self::fail('file should not be locked'); + } + self::assertInternalType('resource', $fd); + + // leave the file unlocked + $methodU->invokeArgs(self::$task, array($fd)); + } } diff --git a/www/skins/icons/substitution.png b/www/skins/icons/substitution.png index 999d8c39af..9e5a71449e 100644 Binary files a/www/skins/icons/substitution.png and b/www/skins/icons/substitution.png differ