From 07a72115263e762ce675d4fefb906df926931ca1 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Sat, 10 Oct 2026 13:25:51 +0800 Subject: [PATCH] Command: extends `useStdErr` to `useOutput` --- CHANGELOG.md | 3 ++ doc/json_schema.json | 22 +++++++++-- src/common/impl/processing_linux.c | 14 ++++--- src/common/impl/processing_windows.c | 15 ++++---- src/common/processing.h | 17 +++++++-- src/detection/command/command.c | 2 +- .../displayserver/displayserver_android.c | 2 +- src/modules/command/command.c | 37 +++++++++++++++++-- src/modules/command/option.h | 3 +- 9 files changed, 89 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b52a879e..2c3b9093a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changes: * LocalIP flags on Windows now describe the interface itself — its operational status, its media connection state and its type, for example `ETHERNET_CSMACD`, `IEEE80211` or `SOFTWARE_LOOPBACK` — instead of the `IP_ADAPTER_*` bits. * libpulse and libddcutil are no longer used on Linux. Package managers should remove them from their fastfetch package. * QuickJS format scripting has been removed (deprecated in v2.69.0). Migrate scripts to Lua if necessary. (General) +* The Command module option `useStdErr` has been replaced with `useOutput`, which selects which of the command's output streams to capture: `"stdout"`, `"stderr"` or `"both"`. + * It defaults to `"both"`, so a command that logs to stderr now has that output captured instead of discarded. + * Set it to `"stdout"` or `"stderr"` to restore the old single-stream behavior. (Command) Features: * Added Sound detection on Android. (Sound, Android) diff --git a/doc/json_schema.json b/doc/json_schema.json index 4d3c09ce3..d349c9f30 100644 --- a/doc/json_schema.json +++ b/doc/json_schema.json @@ -2381,10 +2381,24 @@ "description": "Command text to execute", "type": "string" }, - "useStdErr": { - "description": "Whether to use stderr instead of stdout for command output", - "type": "boolean", - "default": false + "useOutput": { + "description": "Which of the command's output streams to capture\nThe stream that is not captured is discarded", + "type": "string", + "oneOf": [ + { + "const": "stdout", + "description": "Capture stdout only and discard stderr" + }, + { + "const": "stderr", + "description": "Capture stderr only and discard stdout" + }, + { + "const": "both", + "description": "Capture both stdout and stderr" + } + ], + "default": "both" }, "parallel": { "description": "Whether to execute the command in parallel with other commands\nThis can improve performance when using multiple commands, but may cause issues with some commands", diff --git a/src/common/impl/processing_linux.c b/src/common/impl/processing_linux.c index e9989acc6..1b599fa5e 100644 --- a/src/common/impl/processing_linux.c +++ b/src/common/impl/processing_linux.c @@ -62,7 +62,7 @@ static inline int ffPipe2(int* readEnd, int* writeEnd, int flags) { } // Not thread-safe -const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle) { +const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FFNativeFD stdinFd, FFProcessHandle* outHandle) { FF_AUTO_CLOSE_FD int pipeRead = -1; FF_AUTO_CLOSE_FD int pipeWrite = -1; if (ffPipe2(&pipeRead, &pipeWrite, O_CLOEXEC) == -1) { @@ -72,6 +72,10 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF pid_t childPid = -1; int nullFile = ffGetNullFD(); + // Each stream is wired to the pipe when its bit is set and to the null device otherwise, so an + // unselected stream can neither mix into the captured text nor fill the pipe and block the child. + int stdoutFd = (useOutput & FF_PROCESS_OUTPUT_STDOUT_BIT) ? pipeWrite : nullFile; + int stderrFd = (useOutput & FF_PROCESS_OUTPUT_STDERR_BIT) ? pipeWrite : nullFile; #if !(__ANDROID__ || __OpenBSD__) @@ -83,8 +87,8 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF posix_spawn_file_actions_t file_actions; posix_spawn_file_actions_init(&file_actions); - posix_spawn_file_actions_adddup2(&file_actions, pipeWrite, useStdErr ? STDERR_FILENO : STDOUT_FILENO); - posix_spawn_file_actions_adddup2(&file_actions, nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO); + posix_spawn_file_actions_adddup2(&file_actions, stdoutFd, STDOUT_FILENO); + posix_spawn_file_actions_adddup2(&file_actions, stderrFd, STDERR_FILENO); if (ffIsValidNativeFD(stdinFd)) { posix_spawn_file_actions_adddup2(&file_actions, stdinFd, STDIN_FILENO); } @@ -149,8 +153,8 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF if (childPid == 0) { // Child process - dup2(pipeWrite, useStdErr ? STDERR_FILENO : STDOUT_FILENO); - dup2(nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO); + dup2(stdoutFd, STDOUT_FILENO); + dup2(stderrFd, STDERR_FILENO); if (ffIsValidNativeFD(stdinFd)) { dup2(stdinFd, STDIN_FILENO); } diff --git a/src/common/impl/processing_windows.c b/src/common/impl/processing_windows.c index 61467bf1d..ed4fcf07b 100644 --- a/src/common/impl/processing_windows.c +++ b/src/common/impl/processing_windows.c @@ -84,7 +84,7 @@ static wchar_t* createChildEnvironment(void) { return result; } -const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle) { +const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FFNativeFD stdinFd, FFProcessHandle* outHandle) { const int32_t timeout = instance.config.general.processingTimeout; wchar_t pipeName[32]; @@ -127,13 +127,12 @@ const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinF .cb = sizeof(siStartInfo), .dwFlags = STARTF_USESTDHANDLES, }; - if (useStdErr) { - siStartInfo.hStdOutput = ffGetNullFD(); - siStartInfo.hStdError = hChildPipeWrite; - } else { - siStartInfo.hStdOutput = hChildPipeWrite; - siStartInfo.hStdError = ffGetNullFD(); - } + + HANDLE nullFile = ffGetNullFD(); + // A stream wired to the pipe is captured; the other goes to the null device so it cannot mix + // into the captured text or fill the pipe and block the child. Both may share the pipe handle. + siStartInfo.hStdOutput = (useOutput & FF_PROCESS_OUTPUT_STDOUT_BIT) ? hChildPipeWrite : nullFile; + siStartInfo.hStdError = (useOutput & FF_PROCESS_OUTPUT_STDERR_BIT) ? hChildPipeWrite : nullFile; if (ffIsValidNativeFD(stdinFd)) { siStartInfo.hStdInput = stdinFd; } diff --git a/src/common/processing.h b/src/common/processing.h index b52070fdf..67e1c64dd 100644 --- a/src/common/processing.h +++ b/src/common/processing.h @@ -17,18 +17,29 @@ typedef struct FFProcessHandle { #endif } FFProcessHandle; +// Which of the child's two output streams are wired to the pipe that `ffProcessReadOutput()` drains. +// The stream that is not selected is pointed at the null device, so it can neither interleave with +// the captured text nor fill the pipe and block the child. This is a bitfield rather than a value +// because `BOTH` is a combination: each backend sets up the child's two stdio slots independently, +// testing one bit each. +typedef enum FFProcessOutputType: uint8_t { + FF_PROCESS_OUTPUT_STDOUT_BIT = 1 << 0, + FF_PROCESS_OUTPUT_STDERR_BIT = 1 << 1, + FF_PROCESS_OUTPUT_BOTH = FF_PROCESS_OUTPUT_STDOUT_BIT | FF_PROCESS_OUTPUT_STDERR_BIT, +} FFProcessOutputType; + // `stdinFd` is the fd the child process gets as its stdin. Pass `ffGetNullFD()` to detach it from // our stdin, or `FF_PROCESS_INHERIT_STDIN` to inherit ours, which is what the helpers below do. // Detaching matters on Android, where `/system/bin/cmd` forwards its stdin over binder to the // service: the kernel rejects the whole transaction when that fd is a terminal, and the tool then // only reports `Failure calling service : Failed transaction`. #define FF_PROCESS_INHERIT_STDIN ((FFNativeFD) -1) -const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFNativeFD stdinFd, FFProcessHandle* outHandle); +const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FFNativeFD stdinFd, FFProcessHandle* outHandle); const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer); // Destroys handle internally static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const argv[]) { FFProcessHandle handle; - const char* error = ffProcessSpawn(argv, false, FF_PROCESS_INHERIT_STDIN, &handle); + const char* error = ffProcessSpawn(argv, FF_PROCESS_OUTPUT_STDOUT_BIT, FF_PROCESS_INHERIT_STDIN, &handle); if (error) { return error; } @@ -42,7 +53,7 @@ static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const ar static inline const char* ffProcessAppendStdErr(FFstrbuf* buffer, char* const argv[]) { FFProcessHandle handle; - const char* error = ffProcessSpawn(argv, true, FF_PROCESS_INHERIT_STDIN, &handle); + const char* error = ffProcessSpawn(argv, FF_PROCESS_OUTPUT_STDERR_BIT, FF_PROCESS_INHERIT_STDIN, &handle); if (error) { return error; } diff --git a/src/detection/command/command.c b/src/detection/command/command.c index 4d2b12e36..c3e1fe1ed 100644 --- a/src/detection/command/command.c +++ b/src/detection/command/command.c @@ -27,7 +27,7 @@ static const char* spawnProcess(FFCommandOptions* options, FFProcessHandle* hand options->text.chars, nullptr } : (char* const[]) { options->shell.chars, options->text.chars, nullptr }, - options->useStdErr, + options->useOutput, FF_PROCESS_INHERIT_STDIN, handle); } diff --git a/src/detection/displayserver/displayserver_android.c b/src/detection/displayserver/displayserver_android.c index e294e73f4..14114c0c6 100644 --- a/src/detection/displayserver/displayserver_android.c +++ b/src/detection/displayserver/displayserver_android.c @@ -73,7 +73,7 @@ static bool detectWithCommand(FFDisplayServerResult* ds, char* const argv[], con // transaction when that fd is a terminal, which it is whenever fastfetch runs in a terminal. // Detaching the child from our stdin is only needed here, so the low level API is called instead // of `ffProcessAppendStdOut`. - if (ffProcessSpawn(argv, false, ffGetNullFD(), &handle) != nullptr) { + if (ffProcessSpawn(argv, FF_PROCESS_OUTPUT_STDOUT_BIT, ffGetNullFD(), &handle) != nullptr) { return false; // Neither command is available on every Android version } diff --git a/src/modules/command/command.c b/src/modules/command/command.c index e3f5dc175..9d89f413d 100644 --- a/src/modules/command/command.c +++ b/src/modules/command/command.c @@ -85,7 +85,24 @@ void ffParseCommandJsonObject(FFCommandOptions* options, yyjson_val* module) { } if (unsafe_yyjson_equals_str(key, "useStdErr")) { - options->useStdErr = yyjson_get_bool(val); + options->useOutput = yyjson_get_bool(val) ? FF_PROCESS_OUTPUT_STDERR_BIT : FF_PROCESS_OUTPUT_STDOUT_BIT; + ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "useStdErr is deprecated, use useOutput instead"); + continue; + } + + if (unsafe_yyjson_equals_str(key, "useOutput")) { + int value; + const char* error = ffJsonConfigParseEnum(val, &value, (FFKeyValuePair[]) { + { "stdout", FF_PROCESS_OUTPUT_STDOUT_BIT }, + { "stderr", FF_PROCESS_OUTPUT_STDERR_BIT }, + { "both", FF_PROCESS_OUTPUT_BOTH }, + {}, + }); + if (error) { + ffPrintError(FF_MODULE_GET_DISPLAY_NAME(Command), 0, &options->moduleArgs, FF_PRINT_TYPE_DEFAULT, "Invalid %s value: %s", unsafe_yyjson_get_str(key), error); + } else { + options->useOutput = (FFProcessOutputType) value; + } continue; } @@ -110,7 +127,19 @@ void ffGenerateCommandJsonConfig(FFCommandOptions* options, yyjson_mut_doc* doc, yyjson_mut_obj_add_strbuf(doc, module, "shell", &options->shell); yyjson_mut_obj_add_strbuf(doc, module, "param", &options->param); yyjson_mut_obj_add_strbuf(doc, module, "text", &options->text); - yyjson_mut_obj_add_bool(doc, module, "useStdErr", options->useStdErr); + switch (options->useOutput) { + case FF_PROCESS_OUTPUT_STDOUT_BIT: + yyjson_mut_obj_add_str(doc, module, "useOutput", "stdout"); + break; + case FF_PROCESS_OUTPUT_STDERR_BIT: + yyjson_mut_obj_add_str(doc, module, "useOutput", "stderr"); + break; + case FF_PROCESS_OUTPUT_BOTH: + yyjson_mut_obj_add_str(doc, module, "useOutput", "both"); + break; + default: + break; + } yyjson_mut_obj_add_bool(doc, module, "parallel", options->parallel); yyjson_mut_obj_add_bool(doc, module, "splitLines", options->splitLines); } @@ -168,7 +197,9 @@ void ffInitCommandOptions(FFCommandOptions* options) { #endif ); ffStrbufInit(&options->text); - options->useStdErr = false; + // Capture both streams by default: most tools log to stdout or stderr without saying which, and + // the previous single-stream default silently dropped one of them. + options->useOutput = FF_PROCESS_OUTPUT_BOTH; options->parallel = true; options->splitLines = false; } diff --git a/src/modules/command/option.h b/src/modules/command/option.h index 6b2108fb7..255433c91 100644 --- a/src/modules/command/option.h +++ b/src/modules/command/option.h @@ -1,6 +1,7 @@ #pragma once #include "common/option.h" +#include "common/processing.h" // FFProcessOutputType typedef struct FFCommandOptions { FFModuleArgs moduleArgs; @@ -8,7 +9,7 @@ typedef struct FFCommandOptions { FFstrbuf shell; FFstrbuf param; FFstrbuf text; - bool useStdErr; + FFProcessOutputType useOutput; bool parallel; bool splitLines; } FFCommandOptions;