diff --git a/core/register_command.php b/core/register_command.php index 58aed05ba68a6..eb0992ba5bc7e 100644 --- a/core/register_command.php +++ b/core/register_command.php @@ -3,7 +3,7 @@ declare(strict_types=1); /** - * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016-2025 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2013-2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ @@ -110,157 +110,294 @@ use OCP\Server; use Stecman\Component\Symfony\Console\BashCompletion\CompletionCommand; -$application->add(new CompletionCommand()); -$application->add(Server::get(Status::class)); -$application->add(Server::get(Check::class)); -$application->add(Server::get(CreateJs::class)); -$application->add(Server::get(SignApp::class)); -$application->add(Server::get(SignCore::class)); -$application->add(Server::get(CheckApp::class)); -$application->add(Server::get(CheckCore::class)); -$application->add(Server::get(ListRoutes::class)); -$application->add(Server::get(MatchRoute::class)); - -$config = Server::get(IConfig::class); - -if ($config->getSystemValueBool('installed', false)) { - $application->add(Server::get(Disable::class)); - $application->add(Server::get(Enable::class)); - $application->add(Server::get(Install::class)); - $application->add(Server::get(GetPath::class)); - $application->add(Server::get(ListApps::class)); - $application->add(Server::get(Remove::class)); - $application->add(Server::get(Update::class)); - - $application->add(Server::get(Cleanup::class)); - $application->add(Server::get(Enforce::class)); - $application->add(Server::get(Command\TwoFactorAuth\Enable::class)); - $application->add(Server::get(Command\TwoFactorAuth\Disable::class)); - $application->add(Server::get(State::class)); - - $application->add(Server::get(Mode::class)); - $application->add(Server::get(Job::class)); - $application->add(Server::get(ListCommand::class)); - $application->add(Server::get(Delete::class)); - $application->add(Server::get(JobWorker::class)); - - $application->add(Server::get(Test::class)); - - $application->add(Server::get(DeleteConfig::class)); - $application->add(Server::get(GetConfig::class)); - $application->add(Server::get(SetConfig::class)); - $application->add(Server::get(Import::class)); - $application->add(Server::get(ListConfigs::class)); - $application->add(Server::get(Preset::class)); - $application->add(Server::get(Command\Config\System\DeleteConfig::class)); - $application->add(Server::get(Command\Config\System\GetConfig::class)); - $application->add(Server::get(Command\Config\System\SetConfig::class)); - - $application->add(Server::get(File::class)); - $application->add(Server::get(Space::class)); - $application->add(Server::get(Storage::class)); - $application->add(Server::get(Storages::class)); - - $application->add(Server::get(ConvertType::class)); - $application->add(Server::get(ConvertMysqlToMB4::class)); - $application->add(Server::get(ConvertFilecacheBigInt::class)); - $application->add(Server::get(AddMissingColumns::class)); - $application->add(Server::get(AddMissingIndices::class)); - $application->add(Server::get(AddMissingPrimaryKeys::class)); - $application->add(Server::get(ExpectedSchema::class)); - $application->add(Server::get(ExportSchema::class)); - - $application->add(Server::get(GenerateMetadataCommand::class)); - $application->add(Server::get(PreviewCommand::class)); - if ($config->getSystemValueBool('debug', false)) { - $application->add(Server::get(StatusCommand::class)); - $application->add(Server::get(MigrateCommand::class)); - $application->add(Server::get(GenerateCommand::class)); - $application->add(Server::get(ExecuteCommand::class)); +/** + * This file registers Nextcloud core console commands for Nextcloud's CLI application (OCC). + * + * Core commands are explicitly added in this file, resolving them from the DI container via + * Server::get(...). + * + * Some commands are always registered; many are only registered when the instance is already + * installed. Other commands are only registered when `occ` is executed in debug mode. A + * single "install" command is registered when the instance is not yet installed. + * + * The Symfony Console `$application` instance ris provided by the including scope (see + * OC\Console\Application::loadCommands). + * + * TODO (maybe): + * - Refactor this into a real class/service/callable w/ clear dependency handling/etc. + * - Make each core command a tagged service and have the container or a service aggregator + * auto-register them. + */ + +// These variables are expected to be provided by the including scope (Application::loadCommands) +/** @var \Symfony\Component\Console\Application $application */ +/** @var bool $installed */ +/** @var bool $maintenance */ +/** @var bool $needUpgrade */ +/** @var bool $debug */ + +/* + * Commands that should always be registered (i.e. normal, pre-install, maintenance, upgrade needed) +*/ +$alwaysCommands = [ + CompletionCommand::class, + Status::class, + Check::class, + CreateJs::class, + SignApp::class, + SignCore::class, + CheckApp::class, + CheckCore::class, + ListRoutes::class, + MatchRoute::class, +]; + +/* + * Commands required when an upgrade is needed (besides above) + */ +$upgradeCommands = [ + Command\Maintenance\Mode::class, + Upgrade::class, +]; + +/* + * Commands available only when not installed + */ +$installerCommands = [ + Command\Maintenance\Install::class, +]; + +/* + * Commands allowed in maintenance mode (no apps loaded) + */ +$maintenanceCommands = [ + Command\Maintenance\Mode::class, +]; + +/* + * Commands for normal (installed/up-to-date/non-maintenance) operating mode + */ +$installedCommands = [ + // "app" + Disable::class, + Enable::class, + GetPath::class, + Install::class, + ListApps::class, + Remove::class, + Update::class, + + // "background" + Mode::class, + + // "background-job" + Delete::class, + Job::class, + JobWorker::class, + ListCommand::class, + + // "broadcast" + Test::class, + + // "config" + Import::class, + ListConfigs::class, + Preset::class, + + // "config:app" + DeleteConfig::class, + GetConfig::class, + SetConfig::class, + + // "config:system" + Command\Config\System\DeleteConfig::class, + Command\Config\System\GetConfig::class, + Command\Config\System\SetConfig::class, + + // "db" + AddMissingColumns::class, + AddMissingIndices::class, + AddMissingPrimaryKeys::class, + ConvertFilecacheBigInt::class, + ConvertMysqlToMB4::class, + ConvertType::class, + ExpectedSchema::class, + ExportSchema::class, + + // "encryption" + ChangeKeyStorageRoot::class, + DecryptAll::class, + Command\Encryption\Disable::class, + Command\Encryption\Enable::class, + EncryptAll::class, + ListModules::class, + MigrateKeyStorage::class, + SetDefaultModule::class, + ShowKeyStorageRoot::class, + Command\Encryption\Status::class, + + // "group" + Command\Group\Add::class, + AddUser::class, + Command\Group\Delete::class, + Command\Group\Info::class, + Command\Group\ListCommand::class, + RemoveUser::class, + + // "info" + File::class, + Space::class, + Storage::class, + Storages::class, + + // "log" + Command\Log\File::class, + Manage::class, + + // "maintenance" + DataFingerprint::class, + Command\Maintenance\Mode::class, + Repair::class, + RepairShareOwnership::class, + UpdateTheme::class, + UpdateHtaccess::class, + + // "maintenance:mimetype" + UpdateDB::class, + UpdateJS::class, + + // "memcache"" + RedisCommand::class, // TODO: Should probably be moved under debug; it's not currently gated + DistributedClear::class, // ditto + DistributedDelete::class, // ditto + DistributedGet::class, // ditto probably + DistributedSet::class, // ditto + + // "metadata" + Get::class, + + // "migrations" + GenerateMetadataCommand::class, + PreviewCommand::class, + + // "preview" + Command\Preview\Cleanup::class, + Generate::class, + ResetRenderedTexts::class, + + // "tag" + Command\SystemTag\Add::class, + Command\SystemTag\Delete::class, + Edit::class, + Command\SystemTag\ListCommand::class, + + // "twofactorauth" + Cleanup::class, + Command\TwoFactorAuth\Disable::class, + Command\TwoFactorAuth\Enable::class, + Enforce::class, + State::class, + + // "security:bruteforce" + BruteforceAttempts::class, + BruteforceResetAttempts::class, + + // "security:certificates" + ListCertificates::class, + ExportCertificates::class, + ImportCertificate::class, + RemoveCertificate::class, + + // "setupchecks" + SetupChecks::class, + + // "snowflake" + SnowflakeDecodeId::class, + + // "taskprocessing" + EnabledCommand::class, + Command\TaskProcessing\Cleanup::class, + GetCommand::class, + Command\TaskProcessing\ListCommand::class, + Statistics::class, + + // "user" + Add::class, + Command\User\AuthTokens\Add::class, + Command\User\AuthTokens\Delete::class, + Command\User\AuthTokens\ListCommand::class, + ClearGeneratedAvatarCacheCommand::class, + Command\User\Delete::class, + Command\User\Disable::class, + Command\User\Enable::class, + Info::class, + Verify::class, + LastSeen::class, + Command\User\ListCommand::class, + Profile::class, + Report::class, + ResetPassword::class, + Setting::class, + SyncAccountDataCommand::class, + Welcome::class, +]; + +/* + * Debug-mode only commands + */ +$debugCommands = [ + // "migrations" + ExecuteCommand::class, + GenerateCommand::class, + MigrateCommand::class, + StatusCommand::class, +]; + +/** + * Helper to resolve & add a list of command classes. + * + * Will abort registering if any app/service fails to load. + * + */ +/** @var \Symfony\Component\Console\Application $application */ +$addCommands = function (array $classes) use ($application) { + foreach ($classes as $class) { + // CompletionCommand is instantiated directly (not resolved from container). + if ($class === CompletionCommand::class) { + $application->add(new CompletionCommand()); + } else { + $application->add(Server::get($class)); + } } +}; + +/* + * Register commands according to state + */ + +// Register always available commands +$addCommands($alwaysCommands); + +if ($needUpgrade) { + // Register minimal extra commands needed to perform or diagnose the upgrade. + $addCommands($upgradeCommands); + return; +} + +if (!$installed) { + // Register pre-install only commands + $addCommands($installerCommands); + return; +} + +if ($maintenance) { + $addCommands($maintenanceCommands); + return; +} + +// Normal installed & not maintenance path +$addCommands($installedCommands); - $application->add(Server::get(Command\Encryption\Disable::class)); - $application->add(Server::get(Command\Encryption\Enable::class)); - $application->add(Server::get(ListModules::class)); - $application->add(Server::get(SetDefaultModule::class)); - $application->add(Server::get(Command\Encryption\Status::class)); - $application->add(Server::get(EncryptAll::class)); - $application->add(Server::get(DecryptAll::class)); - - $application->add(Server::get(Manage::class)); - $application->add(Server::get(Command\Log\File::class)); - - $application->add(Server::get(ChangeKeyStorageRoot::class)); - $application->add(Server::get(ShowKeyStorageRoot::class)); - $application->add(Server::get(MigrateKeyStorage::class)); - - $application->add(Server::get(DataFingerprint::class)); - $application->add(Server::get(UpdateDB::class)); - $application->add(Server::get(UpdateJS::class)); - $application->add(Server::get(Command\Maintenance\Mode::class)); - $application->add(Server::get(UpdateHtaccess::class)); - $application->add(Server::get(UpdateTheme::class)); - - $application->add(Server::get(Upgrade::class)); - $application->add(Server::get(Repair::class)); - $application->add(Server::get(RepairShareOwnership::class)); - - $application->add(Server::get(Command\Preview\Cleanup::class)); - $application->add(Server::get(Generate::class)); - $application->add(Server::get(ResetRenderedTexts::class)); - - $application->add(Server::get(Add::class)); - $application->add(Server::get(Command\User\Delete::class)); - $application->add(Server::get(Command\User\Disable::class)); - $application->add(Server::get(Command\User\Enable::class)); - $application->add(Server::get(LastSeen::class)); - $application->add(Server::get(Report::class)); - $application->add(Server::get(ResetPassword::class)); - $application->add(Server::get(Setting::class)); - $application->add(Server::get(Profile::class)); - $application->add(Server::get(Command\User\ListCommand::class)); - $application->add(Server::get(ClearGeneratedAvatarCacheCommand::class)); - $application->add(Server::get(Info::class)); - $application->add(Server::get(SyncAccountDataCommand::class)); - $application->add(Server::get(Command\User\AuthTokens\Add::class)); - $application->add(Server::get(Command\User\AuthTokens\ListCommand::class)); - $application->add(Server::get(Command\User\AuthTokens\Delete::class)); - $application->add(Server::get(Verify::class)); - $application->add(Server::get(Welcome::class)); - - $application->add(Server::get(Command\Group\Add::class)); - $application->add(Server::get(Command\Group\Delete::class)); - $application->add(Server::get(Command\Group\ListCommand::class)); - $application->add(Server::get(AddUser::class)); - $application->add(Server::get(RemoveUser::class)); - $application->add(Server::get(Command\Group\Info::class)); - - $application->add(Server::get(Command\SystemTag\ListCommand::class)); - $application->add(Server::get(Command\SystemTag\Delete::class)); - $application->add(Server::get(Command\SystemTag\Add::class)); - $application->add(Server::get(Edit::class)); - - $application->add(Server::get(ListCertificates::class)); - $application->add(Server::get(ExportCertificates::class)); - $application->add(Server::get(ImportCertificate::class)); - $application->add(Server::get(RemoveCertificate::class)); - $application->add(Server::get(BruteforceAttempts::class)); - $application->add(Server::get(BruteforceResetAttempts::class)); - $application->add(Server::get(SetupChecks::class)); - $application->add(Server::get(SnowflakeDecodeId::class)); - $application->add(Server::get(Get::class)); - - $application->add(Server::get(GetCommand::class)); - $application->add(Server::get(EnabledCommand::class)); - $application->add(Server::get(Command\TaskProcessing\ListCommand::class)); - $application->add(Server::get(Statistics::class)); - $application->add(Server::get(Command\TaskProcessing\Cleanup::class)); - - $application->add(Server::get(RedisCommand::class)); - $application->add(Server::get(DistributedClear::class)); - $application->add(Server::get(DistributedDelete::class)); - $application->add(Server::get(DistributedGet::class)); - $application->add(Server::get(DistributedSet::class)); -} else { - $application->add(Server::get(Command\Maintenance\Install::class)); +if ($debug) { + $addCommands($debugCommands); } diff --git a/lib/private/Console/Application.php b/lib/private/Console/Application.php index 4cf1e0da8ca13..5a2e3a1e3fb82 100644 --- a/lib/private/Console/Application.php +++ b/lib/private/Console/Application.php @@ -1,7 +1,7 @@ application = new SymfonyApplication($defaults->getName(), $serverVersion->getVersionString()); + $this->application = new SymfonyApplication( + $defaults->getName(), + $serverVersion->getVersionString() + ); } /** + * Loads relevant core and, if applicable, app commands. + * * @throws \Exception */ public function loadCommands( InputInterface $input, ConsoleOutputInterface $output, ): void { - // $application is required to be defined in the register_command scripts + $this->checkEnvironmentEssentials($input, $output); + + // Variables utilized by downstream `require_once */register_command.php` files (i.e. core) $application = $this->application; + // State flags used to determine which commands to load, checks to do, and warnings/errors to show. + $installed = (bool) $this->config->getSystemValueBool('installed', false); + $maintenance = (bool) ($installed && $this->config->getSystemValueBool('maintenance', false)); + $needUpgrade = (bool) ($installed && \OCP\Util::needUpgrade()); + /** + * @var bool $debug Used by core/register_command.php (required file reads this local) + * @psalm-suppress UnusedVariable + * @noinspection PhpUnusedLocalVariableInspection + */ + $debug = (bool) $this->config->getSystemValueBool('debug', false); + + // Add and handle `--no-warnings` by default regardless of command $inputDefinition = $application->getDefinition(); $inputDefinition->addOption( new InputOption( @@ -63,104 +81,138 @@ public function loadCommands( null ) ); - try { - $input->bind($inputDefinition); - } catch (\RuntimeException $e) { - //expected if there are extra options - } - if ($input->getOption('no-warnings')) { + // Note: environment errors (above) are still shown + if ($input->hasParameterOption('--no-warnings', true)) { $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); } - if ($this->memoryInfo->isMemoryLimitSufficient() === false) { - $output->getErrorOutput()->writeln( - 'The current PHP memory limit ' - . 'is below the recommended value of 512MB.' - ); + $this->writeMemoryCheckInfoIfLow($input, $output); + + // Load core commands + /** @var \Symfony\Component\Console\Application $application */ + require_once __DIR__ . '/../../../core/register_command.php'; + + if ($needUpgrade) { + $this->writeNeedsUpdateInfo($input, $output); + } elseif (!$installed) { + $this->writeNotInstalledInfo($input, $output); + } elseif ($maintenance) { + $this->writeMaintenanceModeInfo($input, $output); + } else { + // Normal installed path + $this->loadAppCommands($input, $output); } + } - try { - require_once __DIR__ . '/../../../core/register_command.php'; - if ($this->config->getSystemValueBool('installed', false)) { - if (\OCP\Util::needUpgrade()) { - throw new NeedsUpdateException(); - } elseif ($this->config->getSystemValueBool('maintenance')) { - $this->writeMaintenanceModeInfo($input, $output); - } else { - $this->appManager->loadApps(); - foreach ($this->appManager->getEnabledApps() as $app) { - try { - $appPath = $this->appManager->getAppPath($app); - } catch (AppPathNotFoundException) { - continue; - } - // load commands using info.xml - $info = $this->appManager->getAppInfo($app); - if (isset($info['commands'])) { - try { - $this->loadCommandsFromInfoXml($info['commands']); - } catch (\Throwable $e) { - $output->writeln('' . $e->getMessage() . ''); - $this->logger->error($e->getMessage(), [ - 'exception' => $e, - ]); - } - } - // load from register_command.php - \OC_App::registerAutoloading($app, $appPath); - $file = $appPath . '/appinfo/register_command.php'; - if (file_exists($file)) { - try { - require $file; - } catch (\Exception $e) { - $this->logger->error($e->getMessage(), [ - 'exception' => $e, - ]); - } - } - } - } - } elseif ($input->getArgument('command') !== '_completion' && $input->getArgument('command') !== 'maintenance:install') { + /** + * Essential environment checks that must pass except in rare cases. + * + * @throws \Exception if checks fail and command isn't whitelisted. + */ + private function checkEnvironmentEssentials(InputInterface $input, ConsoleOutputInterface $output): void { + $cmd = (string)$input->getFirstArgument(); + $errors = \OC_Util::checkServer(Server::get(SystemConfig::class)); + if (!empty($errors)) { + foreach ($errors as $error) { $errorOutput = $output->getErrorOutput(); - $errorOutput->writeln('Nextcloud is not installed - only a limited number of commands are available'); + $errorOutput->writeln('' . (string)$error['error'] . ''); + $errorOutput->writeln('' . (string)$error['hint'] . ''); + $errorOutput->writeln(''); } - } catch (NeedsUpdateException) { - if ($input->getArgument('command') !== '_completion' && $input->getArgument('command') !== 'upgrade') { - $errorOutput = $output->getErrorOutput(); - $errorOutput->writeln('Nextcloud or one of the apps require upgrade - only a limited number of commands are available'); - $errorOutput->writeln('You may use your browser or the occ upgrade command to do the upgrade'); + + // Command exceptions we let proceed even if environment fails essential checks + // Note: For (conservative) backwards compatibility; other than 'check' most of these are likely unnecessary... + // ...they can't be used to fix these types of errors anyhow! + // + // TODO: Remove all but 'check'. + $whitelist = [ 'check', 'upgrade', 'maintenance:mode', 'status', '_completion' ]; + if (!in_array($cmd, $whitelist, true)) { + throw new \Exception( + 'Environment not properly prepared for Nextcloud. Errors should be fixed before proceeding. ' . + 'Please refer to the Admin Manual to correct the above error(s) then retry your "occ" command.'); } } + } + + private function writeMemoryCheckInfoIfLow(InputInterface $input, ConsoleOutputInterface $output): void { + if ($this->memoryInfo->isMemoryLimitSufficient() === false) { + $currentLimit = trim((string)ini_get('memory_limit')); // we want the nearly raw ini value to show bogus values too + $recommendedLimit = '512M'; + $errorOutput = $output->getErrorOutput(); + $errorOutput->writeln( + 'The PHP-CLI memory limit (' . $currentLimit . ') is below the recommended ' . $recommendedLimit . + '. Some functions may fail.'); + $errorOutput->writeln( + 'Please adjust your "memory_limit" setting in the relevant php.ini file to at least ' . + $recommendedLimit . ' to prevent potential errors.'); + } + } + + /** + * @throws \Throwable if unable to load commands specified in an app's info.xml + */ + private function loadAppCommands(InputInterface $input, ConsoleOutputInterface $output): void { + $this->appManager->loadApps(); - if ($input->getFirstArgument() !== 'check') { - $errors = \OC_Util::checkServer(Server::get(SystemConfig::class)); - if (!empty($errors)) { - foreach ($errors as $error) { - $output->writeln((string)$error['error']); - $output->writeln((string)$error['hint']); - $output->writeln(''); + foreach ($this->appManager->getEnabledApps() as $app) { + try { + $appPath = $this->appManager->getAppPath($app); + } catch (AppPathNotFoundException) { + continue; + } + + // load commands using info.xml + $info = $this->appManager->getAppInfo($app); + if (isset($info['commands'])) { + try { + $this->loadCommandsFromInfoXml($info['commands']); + } catch (\Throwable $e) { + $errorOutput = $output->getErrorOutput(); + $errorOutput->writeln('' . $e->getMessage() . ''); + $this->logger->error($e->getMessage(), [ 'exception' => $e, ]); } - throw new \Exception('Environment not properly prepared.'); } + + // load from app's register_command.php if present + \OC_App::registerAutoloading($app, $appPath); + $file = $appPath . '/appinfo/register_command.php'; + if (file_exists($file)) { + try { + require $file; + } catch (\Throwable $e) { + $errorOutput = $output->getErrorOutput(); + $errorOutput->writeln('' . $e->getMessage() . ''); + $this->logger->error($e->getMessage(), [ 'exception' => $e, ]); + } + } + } + } + + private function writeNeedsUpdateInfo(InputInterface $input, ConsoleOutputInterface $output): void { + $cmd = (string)$input->getFirstArgument(); + if ($cmd !== '_completion' && $cmd !== 'upgrade') { + $errorOutput = $output->getErrorOutput(); + $errorOutput->writeln('Nextcloud or one of its apps requires an upgrade. Only a limited number of commands are available.'); + $errorOutput->writeln('Please use your browser or the "occ upgrade" command to finish the upgrade.'); + } + } + + private function writeNotInstalledInfo(InputInterface $input, ConsoleOutputInterface $output): void { + $cmd = (string)$input->getFirstArgument(); + if ($cmd !== '_completion' && $cmd !== 'maintenance:install') { + $errorOutput = $output->getErrorOutput(); + $errorOutput->writeln('Nextcloud is not installed. Only a limited number of commands are available.'); + $errorOutput->writeln('Please use your browser or the "occ maintenance:install" command to proceed with installation.'); } } - /** - * Write a maintenance mode info. - * The commands "_completion" and "maintenance:mode" are excluded. - * - * @param InputInterface $input The input implementation for reading inputs. - * @param ConsoleOutputInterface $output The output implementation - * for writing outputs. - * @return void - */ private function writeMaintenanceModeInfo(InputInterface $input, ConsoleOutputInterface $output): void { - if ($input->getArgument('command') !== '_completion' - && $input->getArgument('command') !== 'maintenance:mode' - && $input->getArgument('command') !== 'status') { - $errOutput = $output->getErrorOutput(); - $errOutput->writeln('Nextcloud is in maintenance mode, no apps are loaded.'); - $errOutput->writeln('Commands provided by apps are unavailable.'); + $cmd = (string)$input->getFirstArgument(); + if ($cmd !== '_completion' && $cmd !== 'maintenance:mode' && $cmd !== 'status') { + $errorOutput = $output->getErrorOutput(); + $errorOutput->writeln('Nextcloud is in maintenance mode. Only a limited number of commands are available.'); + $errorOutput->writeln('In maintenance mode logins are blocked and events may not be triggered since apps are not loaded.'); + $errorOutput->writeln('Proceed with maintenance activities then use the "occ maintenance:mode --off" to disable maintenance mode.'); } } @@ -174,10 +226,9 @@ public function setAutoExit(bool $boolean): void { } /** - * @return int * @throws \Exception */ - public function run(?InputInterface $input = null, ?OutputInterface $output = null) { + public function run(?InputInterface $input = null, ?OutputInterface $output = null): int { $event = new ConsoleEvent( ConsoleEvent::EVENT_RUN, $this->request->server['argv'] @@ -198,8 +249,8 @@ private function loadCommandsFromInfoXml(iterable $commands): void { if (class_exists($command)) { try { $c = new $command(); - } catch (ArgumentCountError) { - throw new \Exception("Failed to construct console command '$command': " . $e->getMessage(), 0, $e); + } catch (ArgumentCountError $ace) { + throw new \Exception("Failed to construct console command '$command': " . $ace->getMessage(), 0, $ace); } } else { throw new \Exception("Console command '$command' is unknown and could not be loaded");