Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion WebFiori/Framework/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,8 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\VersionCommand',

'\\WebFiori\\Framework\\Cli\\Commands\\SchedulerCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\SchedulerRunCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\SchedulerRunCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\SchedulerDaemonCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\AddDbConnectionCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\AddSmtpConnectionCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\AddLangCommand',
Expand Down
113 changes: 113 additions & 0 deletions WebFiori/Framework/Cli/Commands/SchedulerDaemonCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php
/**
* This file is licensed under MIT License.
*
* Copyright (c) 2026-present WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
*
*/
namespace WebFiori\Framework\Cli\Commands;

use WebFiori\Cli\Argument;
use WebFiori\Cli\Command;
use WebFiori\Framework\Cli\CLIUtils;
use WebFiori\Framework\Scheduler\TasksManager;

/**
* A CLI command that runs the scheduler in a loop for a configurable duration.
*
* This command is intended for local development and testing. It calls
* the scheduler check every 60 seconds and self-terminates after the
* specified maximum number of minutes.
*
* Usage:
* php webfiori scheduler:daemon # runs for 60 minutes (default)
* php webfiori scheduler:daemon --max-minutes=5 # runs for 5 minutes
*
* For production environments, use `scheduler:run` with OS-level cron instead.
*
* @author Ibrahim
*/
class SchedulerDaemonCommand extends Command {
/**
* Creates a new instance of the command.
*
* Registers the `--max-minutes` argument with a default of 60 and
* optionally the scheduler password argument if a password is configured.
*/
public function __construct() {
parent::__construct('scheduler:daemon', [
new Argument('--max-minutes', 'Maximum number of minutes to keep the daemon running. Default: 60.', true),
new Argument('--show-log', 'If set, execution log will be shown after each run.', true),
], 'Run the scheduler in a loop for a limited duration.');

if (TasksManager::getPassword() != 'NO_PASSWORD') {
$this->addArg('p', [
'optional' => false,
'description' => 'Scheduler password.'
]);
}
}

/**
* Executes the daemon loop.
*
* The scheduler check is invoked every 60 seconds. The loop exits
* when the elapsed time exceeds the value of `--max-minutes`.
*
* @return int 0 on success, -1 if the password is incorrect or no tasks exist.
*/
public function exec(): int {
$count = count(TasksManager::getTasks());

if ($count == 0) {
$this->info('There are no scheduled tasks.');
return -1;
}

$maxMinutes = $this->getArgValue('--max-minutes');
$maxMinutes = $maxMinutes !== null ? intval($maxMinutes) : 60;

if ($maxMinutes <= 0) {
$this->error('--max-minutes must be a positive integer.');
return -1;
}

$pass = CLIUtils::resolvePassword($this->getArgValue('p')) ?? '';
$maxSeconds = $maxMinutes * 60;
$startTime = time();

$this->println("Scheduler daemon started. Will run for $maxMinutes minute(s).");
$this->println('Press Ctrl+C to stop.');
$this->println('---');

while ((time() - $startTime) < $maxSeconds) {
$this->println('[' . date('Y-m-d H:i:s') . '] Running scheduler check...');
$result = TasksManager::run($pass, null, false, $this);

if ($result == 'INV_PASS') {
$this->error('Provided password is incorrect.');
return -1;
}

$this->println('Executed: ' . $result['executed-count'] . '/' . $result['total-tasks'] . ' tasks.');

$remaining = $maxSeconds - (time() - $startTime);

if ($remaining <= 0) {
break;
}

$sleepTime = min(60, $remaining);
$this->println("Next check in $sleepTime second(s)...");
$this->println('---');
sleep($sleepTime);
}

$this->println('---');
$this->success('Daemon stopped after ' . $maxMinutes . ' minute(s).');
return 0;
}
}
1 change: 1 addition & 0 deletions tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function test00() {

" scheduler: Run tasks scheduler.\n",
" scheduler:run: Run the tasks scheduler check.\n",
" scheduler:daemon: Run the scheduler in a loop for a limited duration.\n",
" add:db-connection: Add a database connection.\n",
" add:smtp-connection: Add an SMTP account.\n",
" add:lang: Add a website language.\n",
Expand Down
81 changes: 81 additions & 0 deletions tests/WebFiori/Framework/Tests/Cli/SchedulerDaemonCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php
namespace WebFiori\Framework\Test\Cli;

use WebFiori\Framework\Cli\CLITestCase;
use WebFiori\Framework\Cli\Commands\SchedulerDaemonCommand;
use WebFiori\Framework\Scheduler\TasksManager;

class SchedulerDaemonCommandTest extends CLITestCase {

public function setUp(): void {
parent::setUp();
TasksManager::reset();
TasksManager::setPassword('123456');
TasksManager::registerTasks();
}

/**
* @test
* Covers: exec() — no tasks registered, returns -1
*/
public function testNoTasks() {
TasksManager::reset();

$output = $this->executeSingleCommand(new SchedulerDaemonCommand(), ['p' => '123456']);

$this->assertEquals(["Info: There are no scheduled tasks.\n"], $output);
$this->assertEquals(-1, $this->getExitCode());
}

/**
* @test
* Covers: exec() — --max-minutes=0 is rejected
*/
public function testInvalidMaxMinutesZero() {
$output = $this->executeSingleCommand(new SchedulerDaemonCommand(), [
'p' => '123456',
'--max-minutes' => '0'
]);

$this->assertEquals(["Error: --max-minutes must be a positive integer.\n"], $output);
$this->assertEquals(-1, $this->getExitCode());
}

/**
* @test
* Covers: exec() — negative --max-minutes is rejected
*/
public function testInvalidMaxMinutesNegative() {
$output = $this->executeSingleCommand(new SchedulerDaemonCommand(), [
'p' => '123456',
'--max-minutes' => '-5'
]);

$this->assertEquals(["Error: --max-minutes must be a positive integer.\n"], $output);
$this->assertEquals(-1, $this->getExitCode());
}

/**
* @test
* Covers: exec() — wrong password returns -1
*/
public function testWrongPassword() {
$output = $this->executeSingleCommand(new SchedulerDaemonCommand(), [
'p' => 'wrong',
'--max-minutes' => '1'
]);

$this->assertContains("Error: Provided password is incorrect.\n", $output);
$this->assertEquals(-1, $this->getExitCode());
}

/**
* @test
* Covers: __construct() — command metadata
*/
public function testCommandInfo() {
$cmd = new SchedulerDaemonCommand();
$this->assertEquals('scheduler:daemon', $cmd->getName());
$this->assertEquals('Run the scheduler in a loop for a limited duration.', $cmd->getDescription());
}
}
Loading