From 5b68bfe47d0f5b1ba31a4de7b00ec9c0b596d9e6 Mon Sep 17 00:00:00 2001 From: Ibrahim BinAlshikh Date: Sat, 13 Jun 2026 03:44:14 +0300 Subject: [PATCH 1/6] feat(runner): add signal handling support (SIGINT/SIGTERM) - Add SignalHandler class with register/remove/enable/disable API - Add enableSignalHandling() and setSignalHandler() to Runner - Add onSignal() per-command cleanup registration to Command - Default SIGINT: exits with code 130 (interactive: interrupts command only) - Default SIGTERM: sets shutdown flag, exits with code 143 - Graceful degradation on Windows (no-op when pcntl unavailable) - Interactive mode loop respects shutdown flag for clean exit - Per-command handlers auto-removed after command finishes Closes #45 --- WebFiori/Cli/Command.php | 3398 +++++++++-------- WebFiori/Cli/Runner.php | 2461 ++++++------ WebFiori/Cli/SignalHandler.php | 154 + .../WebFiori/Tests/Cli/SignalHandlerTest.php | 265 ++ .../Tests/Cli/SignalIntegrationTest.php | 487 +++ 5 files changed, 3918 insertions(+), 2847 deletions(-) create mode 100644 WebFiori/Cli/SignalHandler.php create mode 100644 tests/WebFiori/Tests/Cli/SignalHandlerTest.php create mode 100644 tests/WebFiori/Tests/Cli/SignalIntegrationTest.php diff --git a/WebFiori/Cli/Command.php b/WebFiori/Cli/Command.php index c98c4e2..59fd0c9 100644 --- a/WebFiori/Cli/Command.php +++ b/WebFiori/Cli/Command.php @@ -1,1674 +1,1724 @@ - - *
  • optional: A boolean. if set to true, it means that the argument - * is optional and can be ignored when running the command.
  • - *
  • default: An optional default value for the argument - * to use if it is not provided and is optional.
  • - *
  • description: A description of the argument which - * will be shown if the command 'help' is executed.
  • - *
  • values: A set of values that the argument can have. If provided, - * only the values on the list will be allowed. Note that if null or empty string - * is in the array, it will be ignored. Also, if boolean values are - * provided, true will be converted to the string 'y' and false will - * be converted to the string 'n'.
  • - * - * - * @param string $description A string that describes what does the job - * do. The description will appear when the command 'help' is executed. - * - * @param array $aliases An optional array of aliases for the command. - */ - public function __construct(string $commandName, array $args = [], string $description = '', array $aliases = []) { - if (!$this->setName($commandName)) { - $this->setName('new-command'); - } - $this->aliases = $aliases; - $this->addArgs($args); - - if (!$this->setDescription($description)) { - $this->setDescription(''); - } - } - /** - * Add command argument. - * - * An argument is a string that comes after the name of the command. The value - * of an argument can be set using equal sign. For example, if command name - * is 'do-it' and one argument has the name 'what-to-do', then the full - * CLI command would be "do-it what-to-do=say-hi". An argument can be - * also treated as an option. - * - * @param string $name The name of the argument. It must be non-empty string - * and does not contain spaces. Note that if the argument is already added and - * the developer is trying to add it again, the new options array will override - * the existing options array. - * - * @param array $options An optional array of options. Available options are: - * - * - * @return bool If the argument is added, the method will return true. - * Other than that, the method will return false. - * - */ - public function addArg(string $name, array $options = []) : bool { - $toAdd = Argument::create($name, $options); - - if ($toAdd === null) { - return false; - } - - return $this->addArgument($toAdd); - } - /** - * Adds multiple arguments to the command. - * - * @param array $arr An associative array of sub associative arrays. The - * key of each sub array is argument name. This can also be - * an array of objects of type 'CommandArgument'. For arrays, Each - * sub-array can have the following indices: - * - */ - public function addArgs(array $arr): void { - $this->commandArgs = []; - - foreach ($arr as $optionName => $options) { - if ($options instanceof Argument) { - $this->addArgument($options); - } else { - $this->addArg($optionName, $options); - } - } - } - /** - * Adds new command argument. - * - * @param Argument $arg The argument that will be added. - * - * @return bool If the argument is added, the method will return true. - * If not, false is returned. The argument will not be added only if an argument - * which has same name is added. - */ - public function addArgument(Argument $arg) : bool { - if (!$this->hasArg($arg->getName())) { - $this->commandArgs[] = $arg; - - return true; - } - - return false; - } - /** - * Clears the output before or after cursor position. - * - * This method will replace the visible characters with spaces. - * Note that support for this operation depends on terminal support for - * ANSI escape codes. - * - * @param int $numberOfCols Number of columns to clear. The columns that - * will be cleared are before and after cursor position. They don't include - * the character at which the cursor is currently pointing to. - * @param bool $beforeCursor If set to true, the characters which - * are before the cursor will be cleared. Default is true. - * - * @return Command The method will return the instance at which the - * method is called on. - * - */ - public function clear(int $numberOfCols = 1, bool $beforeCursor = true) : Command { - if ($numberOfCols >= 1 && $beforeCursor) { - for ($x = 0 ; $x < $numberOfCols ; $x++) { - $this->moveCursorLeft(); - $this->prints(" "); - $this->moveCursorLeft(); - } - $this->moveCursorRight($numberOfCols); - } else if ($numberOfCols >= 1) { - $this->moveCursorRight(); - - for ($x = 0 ; $x < $numberOfCols ; $x++) { - $this->prints(" "); - } - $this->moveCursorLeft($numberOfCols + 1); - } - - return $this; - } - /** - * Clears the whole content of the console. - * - * Note that support for this operation depends on terminal support for - * ANSI escape codes. - * - * @return Command The method will return the instance at which the - * method is called on. - */ - public function clearConsole() : Command { - $this->prints("\ec"); - - return $this; - } - /** - * Clears the line at which the cursor is in and move it back to the start - * of the line. - * - * Note that support for this operation depends on terminal support for - * ANSI escape codes. - * - */ - public function clearLine(): void { - $this->prints("\e[2K"); - $this->prints("\r"); - } - /** - * Asks the user to conform something. - * - * This method will display the question and wait for the user to confirm the - * action by entering 'y' or 'n' in the terminal. If the user give something - * other than 'Y' or 'n', it will show an error and ask him to confirm - * again. If a default answer is provided, it will appear in upper case in the - * terminal. For example, if default is set to true, at the end of the prompt, - * the string that shows the options would be like '(Y/n)'. - * - * @param string $confirmTxt The text of the question of which will be asked. - * - * @param bool|null $default Default answer to use if empty input is given. - * It can be true for 'y' and false for 'n'. Default value is null which - * means no default will be used. - * - * @return bool If the user choose 'y', the method will return true. If - * he chooses 'n', the method will return false. - * - * - */ - public function confirm(string $confirmTxt, ?bool $default = null) : bool { - $answer = null; - - do { - if ($default === true) { - $optionsStr = '(Y/n)'; - } else if ($default === false) { - $optionsStr = '(y/N)'; - } else { - $optionsStr = '(y/n)'; - } - $this->prints(trim($confirmTxt), [ - 'color' => 'gray', - 'bold' => true - ]); - $this->println($optionsStr, [ - 'color' => 'light-blue' - ]); - - $input = strtolower(trim($this->readln())); - - if ($input == 'n') { - $answer = false; - } else if ($input == 'y') { - $answer = true; - } else if (strlen($input) == 0 && $default !== null) { - return $default === true; - } else { - $this->error('Invalid answer. Choose \'y\' or \'n\'.'); - } - } while ($answer === null); - - return $answer; - } - - /** - * Creates and returns a new progress bar instance. - * - * @param int $total Total number of steps - * @return ProgressBar - */ - public function createProgressBar(int $total = 100): ProgressBar { - return new ProgressBar($this->getOutputStream(), $total); - } - /** - * Display a message that represents an error. - * - * The message will be prefixed with the string 'Error:' in - * red. - * - * @param string $message The message that will be shown. - * - */ - public function error(string $message): void { - $this->printMsg($message, 'Error', 'light-red'); - } - /** - * Execute the command. - * - * This method should not be called manually by the developer. - * - * @return int If the command is executed, the method will return 0. - * Other than that, it will return a number which depends on the return value of - * the method 'Command::exec()'. - * - */ - public function excCommand() : int { - $retVal = -1; - - $runner = $this->getOwner(); - - if ($runner !== null) { - foreach ($runner->getArgs() as $arg) { - $this->addArgument($arg); - } - } - - if ($this->parseArgsHelper()) { - // Check for help first, before validating required arguments - if ($this->isArgProvided('help') || $this->isArgProvided('-h')) { - $help = $runner->getCommandByName('help'); - $help->setArgValue('--command', $this->getName()); - $help->setOwner($runner); - $help->setOutputStream($runner->getOutputStream()); - $this->removeArgument('help'); - - return $help->exec(); - } else if ($this->checkIsArgsSetHelper()) { - $retVal = $this->exec(); - } - - } - - if ($runner !== null) { - foreach ($runner->getArgs() as $arg) { - $this->removeArgument($arg->getName()); - $arg->resetValue(); - } - } - - return $retVal; - } - /** - * Execute the command. - * - * The implementation of this method should contain the code that will run - * when the command is executed. - * - * @return int The developer should implement this method in a way it returns 0 - * if the command is executed successfully and return -1 if the - * command did not execute successfully. - * - */ - public abstract function exec() : int; - /** - * Execute a registered command using a sub-runner. - * - * This method can be used to execute a registered command within the runner - * using another - * runner instance which shares argsv, input and output streams with the - * main runner. It can be used to invoke another command from within a - * running command. - * - * @param string $name The name of the command. It must be a part of - * registered commands. - * - * @param array $additionalArgs An associative array that represents additional arguments - * to be passed to the command. - * - * @return int The method will return an integer that represent exit status - * code of the command after execution. - */ - public function execSubCommand(string $name, array $additionalArgs = []) : int { - $runner = $this->getOwner(); - - if ($runner === null) { - return -1; - } - - return $runner->runCommandAsSub($name, $additionalArgs); - } - /** - * Returns an array of aliases for the command. - * - * @return array An array of aliases. - */ - public function getAliases() : array { - return $this->aliases; - } - - /** - * Sets the aliases for the command. - * - * @param array $aliases An array of aliases. - */ - public function setAliases(array $aliases): void { - $this->aliases = $aliases; - } - - /** - * Adds an alias to the command. - * - * @param string $alias The alias to add. - */ - public function addAlias(string $alias): void { - if (!in_array($alias, $this->aliases)) { - $this->aliases[] = $alias; - } - } - /** - * Returns an object that holds argument info if the command. - * - * @param string $name The name of command argument. - * - * @return Argument|null If the command has an argument with the - * given name, it will be returned. Other than that, null is returned. - */ - public function getArg(string $name): ?Argument { - foreach ($this->getArgs() as $arg) { - if ($arg->getName() == $name) { - return $arg; - } - } - - return null; - } - /** - * Returns an associative array that contains command args. - * - * @return array An associative array. The indices of the array are - * the names of the arguments and the values are sub-associative arrays. - * the sub arrays will have the following indices: - *