Command: extends useStdErr to useOutput

This commit is contained in:
Carter Li
2026-10-10 13:25:51 +08:00
parent 0806145f4d
commit 07a7211526
9 changed files with 89 additions and 26 deletions
+3
View File
@@ -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)
+18 -4
View File
@@ -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",
+9 -5
View File
@@ -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);
}
+7 -8
View File
@@ -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;
}
+14 -3
View File
@@ -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 <name>: 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;
}
+1 -1
View File
@@ -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);
}
@@ -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
}
+34 -3
View File
@@ -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;
}
+2 -1
View File
@@ -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;