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}