master ohmu/fastfetch-openbsd / src / common / impl / processing_windows.c
ohmu  ·  2026-10-15
  1#include "fastfetch.h"
  2#include "common/debug.h"
  3#include "common/mallocHelper.h"
  4#include "common/processing.h"
  5#include "common/io.h"
  6#include "common/windows/unicode.h"
  7#include "common/windows/nt.h"
  8
  9#include <stdalign.h>
 10#include <windows.h>
 11#include <ntstatus.h>
 12
 13enum { FF_PIPE_BUFSIZ = 8192 };
 14
 15static void argvToCmdline(char* const argv[], FFstrbuf* result) {
 16    // From https://gist.github.com/jin-x/cdd641d98887524b091fb1f82a68717d
 17
 18    FF_STRBUF_AUTO_DESTROY temp = ffStrbufCreate();
 19    for (int i = 0; argv[i] != nullptr; i++) {
 20        ffStrbufSetS(&temp, argv[i]);
 21        // Add slash (\) before double quotes (") and duplicate slashes before it
 22        for (
 23            uint32_t pos = ffStrbufFirstIndexC(&temp, '"'), cnt;
 24            pos != temp.length;
 25            pos = ffStrbufNextIndexC(&temp, pos + cnt * 2, '"')) {
 26            cnt = 1;
 27            while (pos > 0 && temp.chars[pos - 1] == '\\') { ++cnt, --pos; }
 28            ffStrbufInsertNC(&temp, pos, cnt, '\\');
 29        }
 30
 31        // Add quotes around string if whitespace chars are present (with slash duplicating at the end of string)
 32        if (ffStrbufContainAnyC(&temp, " \t")) {
 33            uint32_t pos = temp.length;
 34            uint32_t cnt = 0;
 35            while (pos > 0 && temp.chars[pos - 1] == '\\') { ++cnt, --pos; }
 36            if (cnt > 0) {
 37                ffStrbufAppendNC(&temp, cnt, '\\');
 38            }
 39            ffStrbufPrependC(&temp, '"');
 40            ffStrbufAppendC(&temp, '"');
 41        }
 42
 43        // Add space delimiter
 44        if (i > 0) {
 45            ffStrbufAppendC(result, ' ');
 46        }
 47        ffStrbufAppend(result, &temp);
 48        ffStrbufClear(&temp);
 49    }
 50}
 51
 52static wchar_t* createChildEnvironment(void) {
 53    auto pparams = ffGetPeb()->ProcessParameters;
 54    const wchar_t* readStart = pparams->Environment;
 55
 56    if (!readStart) {
 57        return nullptr;
 58    }
 59
 60    RtlAcquirePebLock();
 61    wchar_t* result = malloc(pparams->EnvironmentSize + sizeof(L"LANG=C.UTF-8"));
 62
 63    const wchar_t* entry = readStart;
 64    wchar_t* writeStart = result;
 65
 66    // Copy all environment variables except LANG
 67    while (*entry) {
 68        assert((const uint8_t*) entry <= (const uint8_t*) pparams->Environment + pparams->EnvironmentSize);
 69        size_t entryLength = wcslen(entry);
 70        if (entryLength >= strlen("LANG=") && memcmp(entry, L"LANG=", strlen("LANG=") * sizeof(wchar_t)) == 0) {
 71            memcpy(writeStart, readStart, (size_t) (entry - readStart) * sizeof(wchar_t));
 72            writeStart += (size_t) (entry - readStart);
 73            readStart = entry + entryLength + 1;
 74        }
 75        entry += entryLength + 1;
 76    }
 77    // Copy the remaining environment variables
 78    memcpy(writeStart, readStart, (size_t) (entry - readStart) * sizeof(wchar_t));
 79    writeStart += (size_t) (entry - readStart);
 80    // Add LANG=C.UTF-8 and double null terminator
 81    memcpy(writeStart, L"LANG=C.UTF-8\0", sizeof(L"LANG=C.UTF-8\0"));
 82
 83    RtlReleasePebLock();
 84    return result;
 85}
 86
 87const char* ffProcessSpawn(char* const argv[], FFProcessOutputType useOutput, FFNativeFD stdinFd, FFProcessHandle* outHandle) {
 88    const int32_t timeout = instance.config.general.processingTimeout;
 89
 90    wchar_t pipeName[32];
 91    static unsigned pidCounter = 0;
 92    swprintf(pipeName, ARRAY_SIZE(pipeName), L"\\\\.\\pipe\\FASTFETCH-%u-%u", instance.state.platform.pid, ++pidCounter);
 93
 94    FF_AUTO_CLOSE_FD HANDLE hChildPipeRead = CreateNamedPipeW(
 95        pipeName,
 96        PIPE_ACCESS_INBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE | (timeout < 0 ? 0 : FILE_FLAG_OVERLAPPED),
 97        0,
 98        1,
 99        FF_PIPE_BUFSIZ,
100        FF_PIPE_BUFSIZ,
101        0,
102        nullptr);
103    if (hChildPipeRead == INVALID_HANDLE_VALUE) {
104        FF_DEBUG("CreateNamedPipeW() failed: %s", ffDebugWin32Error(GetLastError()));
105        return "CreateNamedPipeW(L\"\\\\.\\pipe\\FASTFETCH-$(PID)\") failed";
106    }
107
108    HANDLE hChildPipeWrite = CreateFileW(
109        pipeName,
110        GENERIC_WRITE,
111        0,
112        &(SECURITY_ATTRIBUTES) {
113            .nLength = sizeof(SECURITY_ATTRIBUTES),
114            .lpSecurityDescriptor = nullptr,
115            .bInheritHandle = TRUE,
116        },
117        OPEN_EXISTING,
118        0,
119        nullptr);
120    if (hChildPipeWrite == INVALID_HANDLE_VALUE) {
121        FF_DEBUG("CreateFileW() failed: %s", ffDebugWin32Error(GetLastError()));
122        return "CreateFileW(L\"\\\\.\\pipe\\FASTFETCH-$(PID)\") failed";
123    }
124
125    PROCESS_INFORMATION piProcInfo = {};
126    STARTUPINFOW siStartInfo = {
127        .cb = sizeof(siStartInfo),
128        .dwFlags = STARTF_USESTDHANDLES,
129    };
130
131    HANDLE nullFile = ffGetNullFD();
132    // A stream wired to the pipe is captured; the other goes to the null device so it cannot mix
133    // into the captured text or fill the pipe and block the child. Both may share the pipe handle.
134    siStartInfo.hStdOutput = (useOutput & FF_PROCESS_OUTPUT_STDOUT_BIT) ? hChildPipeWrite : nullFile;
135    siStartInfo.hStdError = (useOutput & FF_PROCESS_OUTPUT_STDERR_BIT) ? hChildPipeWrite : nullFile;
136    if (ffIsValidNativeFD(stdinFd)) {
137        siStartInfo.hStdInput = stdinFd;
138    }
139
140    FF_AUTO_FREE wchar_t* cmdline = nullptr;
141    {
142        FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
143        argvToCmdline(argv, &buf);
144        FF_DEBUG("Command line: %s", buf.chars);
145        uint32_t cmdlineBytes = (buf.length + 1) * sizeof(wchar_t);
146        cmdline = malloc(cmdlineBytes);
147        NTSTATUS status = RtlUTF8ToUnicodeN(cmdline, cmdlineBytes, nullptr, buf.chars, buf.length + 1);
148        if (!NT_SUCCESS(status)) {
149            FF_DEBUG("RtlUTF8ToUnicodeN() failed: %s", ffDebugNtStatus(status));
150            return "RtlUTF8ToUnicodeN() failed";
151        }
152    }
153
154    FF_AUTO_FREE wchar_t* environment = createChildEnvironment();
155    BOOL success = CreateProcessW(
156        nullptr,                    // application name
157        cmdline,                    // command line
158        nullptr,                    // process security attributes
159        nullptr,                    // primary thread security attributes
160        TRUE,                       // handles are inherited
161        CREATE_UNICODE_ENVIRONMENT, // creation flags
162        environment,                // child environment
163        nullptr,                    // use parent's current directory
164        &siStartInfo,               // STARTUPINFO pointer
165        &piProcInfo                 // receives PROCESS_INFORMATION
166    );
167
168    NtClose(hChildPipeWrite);
169    if (!success) {
170        DWORD error = GetLastError();
171        if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) {
172            FF_DEBUG("Command not found: %s (%s)", argv[0], ffDebugWin32Error(error));
173            return "command not found";
174        }
175        FF_DEBUG("CreateProcessW(%s) failed: %s", argv[0], ffDebugWin32Error(error));
176        return "CreateProcessW() failed";
177    }
178
179    NtClose(piProcInfo.hThread); // we don't need the thread handle
180    outHandle->pid = piProcInfo.hProcess;
181    outHandle->pipeRead = hChildPipeRead;
182    hChildPipeRead = INVALID_HANDLE_VALUE; // ownership transferred, don't close it
183
184    return nullptr;
185}
186
187static void terminateChildProcess(HANDLE hProcess, HANDLE hChildPipeRead, HANDLE hReadEvent, IO_STATUS_BLOCK* piosb) {
188    IO_STATUS_BLOCK cancelIosb = {};
189    if (NT_SUCCESS(NtCancelIoFileEx(hChildPipeRead, piosb, &cancelIosb))) {
190        if (hReadEvent) {
191            NtWaitForSingleObject(hReadEvent, FALSE, &(LARGE_INTEGER) { .QuadPart = -100000 }); // wait for cancellation to complete
192        }
193    }
194    NtTerminateProcess(hProcess, 1);
195}
196
197const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
198    assert(handle->pipeRead != INVALID_HANDLE_VALUE);
199    assert(handle->pid != INVALID_HANDLE_VALUE);
200
201    int32_t timeout = instance.config.general.processingTimeout;
202    FF_AUTO_CLOSE_FD HANDLE hProcess = handle->pid;
203    FF_AUTO_CLOSE_FD HANDLE hChildPipeRead = handle->pipeRead;
204    FF_AUTO_CLOSE_FD HANDLE hReadEvent = nullptr;
205    handle->pid = INVALID_HANDLE_VALUE;
206    handle->pipeRead = INVALID_HANDLE_VALUE;
207
208    NTSTATUS createStatus = STATUS_SUCCESS;
209    if (timeout >= 0 && !NT_SUCCESS(createStatus = NtCreateEvent(&hReadEvent, EVENT_ALL_ACCESS, nullptr, SynchronizationEvent, FALSE))) {
210        FF_DEBUG("NtCreateEvent() failed: %s", ffDebugNtStatus(createStatus));
211        return "NtCreateEvent() failed";
212    }
213
214    char str[FF_PIPE_BUFSIZ];
215    uint32_t nRead = 0;
216    IO_STATUS_BLOCK iosb = {};
217    do {
218        NTSTATUS status = NtReadFile(
219            hChildPipeRead,
220            hReadEvent,
221            nullptr,
222            nullptr,
223            &iosb,
224            str,
225            (ULONG) sizeof(str),
226            nullptr,
227            nullptr);
228        if (status == STATUS_PENDING) {
229            NTSTATUS waitStatus = NtWaitForSingleObject(hReadEvent, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) timeout * -10000 });
230            switch (waitStatus) {
231                case STATUS_WAIT_0:
232                    status = iosb.Status;
233                    break;
234
235                case STATUS_TIMEOUT: {
236                    terminateChildProcess(hProcess, hChildPipeRead, hReadEvent, &iosb);
237                    return "NtReadFile(hChildPipeRead) timed out";
238                }
239
240                default:
241                    FF_DEBUG("NtWaitForSingleObject(hReadEvent) failed: %s", ffDebugNtStatus(waitStatus));
242                    terminateChildProcess(hProcess, hChildPipeRead, hReadEvent, &iosb);
243                    return "NtWaitForSingleObject(hReadEvent) failed";
244            }
245        }
246
247        if (status == STATUS_PIPE_BROKEN || status == STATUS_END_OF_FILE) {
248            goto exit;
249        }
250
251        if (!NT_SUCCESS(status)) {
252            FF_DEBUG("NtReadFile(hChildPipeRead) failed: %s", ffDebugNtStatus(status));
253            terminateChildProcess(hProcess, hChildPipeRead, nullptr, &iosb);
254            return "NtReadFile(hChildPipeRead) failed";
255        }
256
257        nRead = (uint32_t) iosb.Information;
258        ffStrbufAppendNS(buffer, nRead, str);
259    } while (nRead > 0);
260
261exit: {
262    PROCESS_BASIC_INFORMATION info = {};
263    ULONG size;
264    NTSTATUS status = NtQueryInformationProcess(hProcess, ProcessBasicInformation, &info, sizeof(info), &size);
265    if (NT_SUCCESS(status)) {
266        assert(size == sizeof(info));
267        if (info.ExitStatus != STILL_ACTIVE && info.ExitStatus != 0) {
268            if (info.ExitStatus == 9009) {
269                // Note: CMD swallows 9009 errorlevel and returns 1 by default.
270                // One must use `& call exit %^ERRORLEVEL%` to propagate the correct error code.
271                FF_DEBUG("Child process exited with error code 9009 (command not found)");
272                return "command not found";
273            }
274            FF_DEBUG("Child process exited with an error: %x %s", (unsigned) info.ExitStatus, ffDebugNtStatus(info.ExitStatus));
275            return "Child process exited with an error";
276        }
277    } else {
278        FF_DEBUG("NtQueryInformationProcess(ProcessBasicInformation) failed: %s", ffDebugNtStatus(status));
279        return "NtQueryInformationProcess(ProcessBasicInformation) failed";
280    }
281}
282
283    return nullptr;
284}
285
286bool ffProcessGetInfoWindows(uint32_t pid, uint32_t* ppid, FFstrbuf* pname, FFstrbuf* exe, const char** exeName, FFstrbuf* exePath, bool* gui) {
287    FF_AUTO_CLOSE_FD HANDLE hProcess = NtCurrentProcess();
288    if (pid != 0) {
289        if (!NT_SUCCESS(NtOpenProcess(&hProcess, PROCESS_QUERY_LIMITED_INFORMATION, &(OBJECT_ATTRIBUTES) {
290                                                                                        .Length = sizeof(OBJECT_ATTRIBUTES),
291                                                                                    },
292                &(CLIENT_ID) { .UniqueProcess = (HANDLE) (uintptr_t) pid }))) {
293            return false;
294        }
295    }
296
297    if (ppid) {
298        PROCESS_BASIC_INFORMATION info = {};
299        ULONG size;
300        if (NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessBasicInformation, &info, sizeof(info), &size))) {
301            assert(size == sizeof(info));
302            *ppid = (uint32_t) info.InheritedFromUniqueProcessId;
303        } else {
304            return false;
305        }
306    }
307
308    if (exe) {
309        // TODO: It's possible to query the command line with `NtQueryInformationProcess(60/*ProcessCommandLineInformation*/)` since Windows 8.1
310
311        alignas(UNICODE_STRING) uint8_t buffer[4096];
312        ULONG size;
313        if (NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessImageFileNameWin32, &buffer, sizeof(buffer), &size))) {
314            UNICODE_STRING* imagePath = (UNICODE_STRING*) buffer;
315            ffStrbufSetNWS(exe, imagePath->Length / sizeof(wchar_t), imagePath->Buffer);
316
317            if (exePath) {
318                ffStrbufSet(exePath, exe);
319            }
320
321            if (pname && exeName) {
322                *exeName = exe->chars + ffStrbufLastIndexC(exe, '\\') + 1;
323                ffStrbufSetS(pname, *exeName);
324            }
325        } else {
326            return false;
327        }
328    }
329
330    if (gui) {
331        SECTION_IMAGE_INFORMATION info = {};
332        ULONG size;
333        if (NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessImageInformation, &info, sizeof(info), &size))) {
334            assert(size == sizeof(info));
335            *gui = info.SubSystemType == IMAGE_SUBSYSTEM_WINDOWS_GUI;
336        } else {
337            return false;
338        }
339    }
340
341    return true;
342}