fix(console): write app command loading errors to stderr - #62718
Conversation
281a0cc to
9345ce5
Compare
artonge
left a comment
There was a problem hiding this comment.
Makes sense, cc @icewind1991 and @bigcat88 as original author of those lines.
9345ce5 to
8e9efdc
Compare
|
@printminion-co can you fix the AI policy trailer in your commit message? |
@artonge Looks like it is some permission issue on your side with Per copilot analysis: Fix Current code (lines 147-151): YAML Recommended fix: YAML The gh pr edit command is the preferred method for adding labels to PRs and has better permission handling. Alternatively, ensure the token has proper scopes by verifying that secrets.COMMAND_BOT_PAT includes public_repo or repo scope with write:repository_hooks permissions. |
8e9efdc to
7a01a7e
Compare
When an app fails to load its commands from info.xml, the error was
written to stdout, while every other diagnostic in loadCommands() uses
$output->getErrorOutput(). The command itself then runs normally and
exits 0, so the message silently corrupts machine-readable output:
$ ./occ app:list --output=json
Connection refused
{"enabled":{...},"disabled":{...}}
$ echo $?
0
Anything piping `occ <cmd> --output=json` into a JSON parser breaks, with
no non-zero exit code to detect it by.
Observed with notify_push on a setup that has the phpredis extension
loaded but no Redis configured: RedisFactory::isAvailable() only checks
whether the extension is loaded, so constructing the app's console
commands ends up calling pconnect() and throws RedisException.
--no-warnings is not a workaround for this, as it sets VERBOSITY_QUIET
and suppresses the payload too.
Route the message to the error output instead. It is still reported via
logger->error() exactly as before.
Assisted-by: ClaudeCode:claude-opus-5
Signed-off-by: Misha M.-Kupriyanov <kupriyanov@strato.de>
7a01a7e to
0cd4ece
Compare
|
/backport stable33 |
|
/backport to stable32 |
|
/backport to stable33 |
|
/backport to stable34 |
Summary
When an app fails to load its console commands from
info.xml,OC\Console\Application::loadCommands()reports the failure with$output->writeln()— i.e. stdout — while every other diagnostic in the same method already uses$output->getErrorOutput()(insufficient memory limit, "Nextcloud is not installed", "requires upgrade", maintenance-mode notices).The command then runs normally and exits
0, so the message silently corrupts machine-readable output:Anything piping
occ <cmd> --output=jsoninto a JSON parser breaks, and there is no non-zero exit code to detect it by.--no-warningsis not a workaround, since it setsVERBOSITY_QUIETand suppresses the payload too.$outputis typedConsoleOutputInterface, sogetErrorOutput()is guaranteed by the signature. Two call sites are affected: theapp_apimaintenance-mode block and the regular installed-apps loop. Thelogger->error()calls are unchanged, so the failure is still logged with its stack trace.How this was hit
An app with the phpredis extension loaded but no Redis configured.
RedisFactory::isAvailable()only checksextension_loaded('redis'), not whether aredisconfig block exists, so the app's queue factory took the Redis branch, fell back to127.0.0.1:6379and threwRedisException: Connection refusedwhile its console commands were being constructed. Becauseenable_lazy_objectsdefaults totrue, the constructor runs inside$this->application->add($c)→Command::setApplication(), so the throw surfaces outsideloadCommandsFromInfoXml()'s innercatch (ContainerExceptionInterface)and lands in the outercatch (\Throwable).The app-side behaviour is arguably its own issue; the console bootstrap should not corrupt stdout regardless of which app fails to load.
Verification
Reproduced deterministically with a throwaway app whose
info.xmlnames a nonexistent command class:jqConsole command '...' is un…{"enabled":{...}The error remains in
nextcloud.log.On tests
No test is included. There is currently no test for
lib/private/Console/*, andgrep -rn getErrorOutput tests/returns nothing, so there is no existing harness or stdout-vs-stderr assertion precedent to extend.loadCommands()also does an unstubbablerequire_once core/register_command.php(~138Server::get()calls, re-fetching the realIConfigrather than an injected mock), which makes a genuine unit test a DB-group test whose result is order-dependent becauserequire_onceruns once per process.This change adds no logic — it only redirects an existing message from one stream to the other. Happy to add a
build/integrationscenario (CommandLine.phpalready captures stdout and stderr separately) or to extract thatrequire_oncebehind an injected collaborator to make the class unit-testable, if a maintainer would prefer either.Checklist
Signed-off-byis present (added by the contributor)AI disclosure
This change and this description were drafted with AI assistance (Claude Code, claude-opus-5), then reviewed by the submitting contributor. Disclosed per the Nextcloud AI Contribution Policy.