-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcommand_runner.cpp
More file actions
570 lines (497 loc) · 16.1 KB
/
Copy pathcommand_runner.cpp
File metadata and controls
570 lines (497 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
#include "command_runner.hpp"
#include "os_registry.hpp"
#include <filesystem>
#include <thread>
#include <vector>
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#else
#include <csignal>
#include <cerrno>
#include <chrono>
#include <cstring>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#endif
namespace droidcli::cli {
bool looks_like_path(const core::String& value)
{
return value.find('\\') != core::String::npos || value.find('/') != core::String::npos;
}
#ifdef _WIN32
// Windows' "App Paths" registry mechanism - the same one Explorer/Win+R use
// to resolve a bare name like "chrome" to its actual install location even
// when the app was never added to PATH (most GUI app installers register
// here instead of touching PATH). Returns an empty string if no match is
// found in either hive. The actual open/read/close is os_registry's shared
// primitive (droidcli-infra) - not a private RegOpenKeyExA/RegQueryValueExA
// pair, since system_info/hardware_info need the identical shape.
core::String resolve_app_paths_registry(const core::String& name)
{
core::String key_name = name;
if (key_name.size() < 4 || key_name.compare(key_name.size() - 4, 4, ".exe") != 0)
{
key_name += ".exe";
}
const core::String subkey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" + key_name;
for (const RegistryRoot root : {RegistryRoot::CurrentUser, RegistryRoot::LocalMachine})
{
core::String resolved = read_registry_string(root, subkey);
if (!resolved.empty())
{
return resolved;
}
}
return {};
}
core::String resolve_system_executable(const core::String& name)
{
char system_dir[MAX_PATH] = {};
const UINT system_dir_length = GetSystemDirectoryA(system_dir, MAX_PATH);
if (system_dir_length > 0 && system_dir_length < MAX_PATH)
{
std::error_code error;
const std::filesystem::path candidate = std::filesystem::path(system_dir) / name;
if (std::filesystem::exists(candidate, error) && std::filesystem::is_regular_file(candidate, error))
{
return candidate.string();
}
}
// explorer.exe (and most of droidcli's ms-settings:/shell: URI targets,
// which are all launched via explorer.exe) lives in the Windows root,
// not System32 - checked second so a genuine System32 tool never has to
// fall through an extra stat for no reason.
char windows_dir[MAX_PATH] = {};
const UINT windows_dir_length = GetWindowsDirectoryA(windows_dir, MAX_PATH);
if (windows_dir_length > 0 && windows_dir_length < MAX_PATH)
{
std::error_code error;
const std::filesystem::path candidate = std::filesystem::path(windows_dir) / name;
if (std::filesystem::exists(candidate, error) && std::filesystem::is_regular_file(candidate, error))
{
return candidate.string();
}
}
return {};
}
CommandRunResult run_command_once(
const core::String& command,
const core::String& work_dir,
const int32_t timeout_ms,
const bool via_shell)
{
CommandRunResult result;
if (command.empty())
{
result.error_message = "command is empty";
return result;
}
SECURITY_ATTRIBUTES pipe_attributes {};
pipe_attributes.nLength = sizeof(pipe_attributes);
pipe_attributes.bInheritHandle = TRUE;
pipe_attributes.lpSecurityDescriptor = nullptr;
HANDLE stdout_read = nullptr;
HANDLE stdout_write = nullptr;
HANDLE stderr_read = nullptr;
HANDLE stderr_write = nullptr;
if (!CreatePipe(&stdout_read, &stdout_write, &pipe_attributes, 0))
{
result.error_message = "CreatePipe(stdout) failed";
return result;
}
SetHandleInformation(stdout_read, HANDLE_FLAG_INHERIT, 0);
if (!CreatePipe(&stderr_read, &stderr_write, &pipe_attributes, 0))
{
CloseHandle(stdout_read);
CloseHandle(stdout_write);
result.error_message = "CreatePipe(stderr) failed";
return result;
}
SetHandleInformation(stderr_read, HANDLE_FLAG_INHERIT, 0);
STARTUPINFOA startup {};
startup.cb = sizeof(startup);
startup.dwFlags |= STARTF_USESTDHANDLES;
startup.hStdOutput = stdout_write;
startup.hStdError = stderr_write;
startup.hStdInput = nullptr;
PROCESS_INFORMATION process {};
// via_shell=false skips cmd.exe's own `/c` re-tokenizing pass entirely -
// `command` is passed straight to CreateProcess, which parses it using
// standard argv-quoting rules (the same ones ffmpeg's own argument
// parser expects), rather than cmd.exe's separate and more fragile
// grammar. See the header comment on run_command_once for the incident
// this fixes.
core::String command_line = via_shell ? ("cmd.exe /c " + command) : command;
std::vector<char> mutable_command_line(command_line.begin(), command_line.end());
mutable_command_line.push_back('\0');
const char* cwd = work_dir.empty() ? nullptr : work_dir.c_str();
const BOOL created = CreateProcessA(
nullptr,
mutable_command_line.data(),
nullptr,
nullptr,
TRUE, // inherit handles (stdout_write/stderr_write)
CREATE_NO_WINDOW,
nullptr,
cwd,
&startup,
&process);
// The parent no longer needs its copies of the write ends - the child
// (if launched) has its own inherited handles. Closing these is what lets
// ReadFile on the read ends return 0 once the child exits.
CloseHandle(stdout_write);
CloseHandle(stderr_write);
if (created == FALSE)
{
const DWORD last_error = GetLastError();
CloseHandle(stdout_read);
CloseHandle(stderr_read);
result.error_message = "CreateProcess failed (" + std::to_string(last_error) + ")";
return result;
}
result.launched = true;
auto drain_pipe = [](HANDLE handle, core::String& out)
{
char buffer[4096];
DWORD bytes_read = 0;
while (ReadFile(handle, buffer, sizeof(buffer), &bytes_read, nullptr) && bytes_read > 0)
{
out.append(buffer, static_cast<size_t>(bytes_read));
}
};
std::thread stdout_thread([&]() { drain_pipe(stdout_read, result.stdout_text); });
std::thread stderr_thread([&]() { drain_pipe(stderr_read, result.stderr_text); });
const DWORD wait_result = WaitForSingleObject(
process.hProcess, timeout_ms > 0 ? static_cast<DWORD>(timeout_ms) : INFINITE);
if (wait_result == WAIT_TIMEOUT)
{
TerminateProcess(process.hProcess, 1);
WaitForSingleObject(process.hProcess, 2000);
result.error_message = "command timed out after " + std::to_string(timeout_ms) + "ms";
}
// Reading threads unblock once the process (and its inherited pipe write
// handles) is gone.
stdout_thread.join();
stderr_thread.join();
DWORD exit_code = 0;
GetExitCodeProcess(process.hProcess, &exit_code);
result.exit_code = static_cast<int32_t>(exit_code);
CloseHandle(process.hProcess);
CloseHandle(process.hThread);
CloseHandle(stdout_read);
CloseHandle(stderr_read);
return result;
}
LaunchAppResult launch_application(
const core::String& path_or_name,
const core::String& args,
const core::String& work_dir)
{
LaunchAppResult result;
if (path_or_name.empty())
{
result.error_message = "path_or_name is empty";
return result;
}
// Callers (DroidHost::resolve_open_application_target - see "Windows
// execution ruleset" in ARCHITECTURE.md) are required to have already
// resolved path_or_name to a real, verified path before calling this -
// this function does no resolution of its own, and refuses a bare name
// outright rather than falling back to CreateProcess's own unverified
// bare-name search (calling process's directory, cwd, system
// directories, PATH). This used to *be* a resolution step (an App Paths
// registry lookup, then a blind CreateProcess attempt on whatever was
// left); removed once every caller started pre-resolving - a live
// fallback here, reachable only if a caller's own resolution had a gap,
// would silently reintroduce the exact unverified-launch risk the
// ruleset exists to eliminate, undetected, the next time it happened to
// fire on a coincidental PATH match.
if (!looks_like_path(path_or_name))
{
result.error_message = "'" + path_or_name + "' is not a resolved path - launch_application "
"requires a caller to resolve a bare name first (see the Windows execution ruleset in "
"ARCHITECTURE.md); it does not do so itself.";
return result;
}
// Quote in case the path contains spaces; args are appended as given -
// caller's responsibility to quote individual arguments that need it.
core::String command_line = "\"" + path_or_name + "\"";
if (!args.empty())
{
command_line += " " + args;
}
std::vector<char> mutable_command_line(command_line.begin(), command_line.end());
mutable_command_line.push_back('\0');
STARTUPINFOA startup {};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process {};
const char* cwd = work_dir.empty() ? nullptr : work_dir.c_str();
// No lpApplicationName: CreateProcess resolves a bare executable name
// against the same search order a shell would (calling process's
// directory, current directory, Windows system directories, PATH) - the
// same intent as which_executable(), just performed by the OS itself.
// No stdout/stderr redirection and no wait: this is a detached,
// fire-and-forget GUI/app launch, not a captured one-shot command.
const BOOL created = CreateProcessA(
nullptr,
mutable_command_line.data(),
nullptr,
nullptr,
FALSE,
0,
nullptr,
cwd,
&startup,
&process);
if (created == FALSE)
{
const DWORD last_error = GetLastError();
result.error_message = "CreateProcess failed (" + std::to_string(last_error)
+ ") trying to launch the already-resolved path \"" + path_or_name
+ "\" - it may have been deleted or become inaccessible between resolution and launch.";
return result;
}
result.launched = true;
result.pid = static_cast<int64_t>(process.dwProcessId);
// Query the real path back from the live process handle rather than just
// echoing path_or_name - this is what the OS actually launched, and the
// two can still legitimately differ (a symlink, a reparse point). Any
// failure here (permissions, a process that has already exited by the
// time we ask) leaves resolved_path empty rather than falling back to a
// guess presented as fact.
char image_path_buffer[MAX_PATH] = {};
DWORD image_path_size = MAX_PATH;
if (QueryFullProcessImageNameA(process.hProcess, 0, image_path_buffer, &image_path_size))
{
result.resolved_path = core::String(image_path_buffer, image_path_size);
}
CloseHandle(process.hProcess);
CloseHandle(process.hThread);
return result;
}
#else // POSIX
// No Windows App Paths registry equivalent - always empty on this platform.
core::String resolve_app_paths_registry(const core::String&)
{
return {};
}
// No Windows System32/Windows-root concept on this platform.
core::String resolve_system_executable(const core::String&)
{
return {};
}
namespace {
// Reads whatever is currently available (non-blocking) from fd into out.
// Returns false once the peer has closed the pipe (EOF).
bool drain_available(int fd, core::String& out)
{
char buffer[4096];
for (;;)
{
const ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read > 0)
{
out.append(buffer, static_cast<size_t>(bytes_read));
continue;
}
if (bytes_read == 0)
{
return false; // EOF
}
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
return true; // no data right now, but pipe still open
}
return false; // real error - treat as closed
}
}
} // namespace
CommandRunResult run_command_once(
const core::String& command,
const core::String& work_dir,
const int32_t timeout_ms,
const bool via_shell)
{
// via_shell has no effect here - see the header comment. Both paths
// already go through `sh -c`, which doesn't share cmd.exe's
// nested-quote-mangling behavior.
(void)via_shell;
CommandRunResult result;
if (command.empty())
{
result.error_message = "command is empty";
return result;
}
int stdout_pipe[2] = {-1, -1};
int stderr_pipe[2] = {-1, -1};
if (pipe(stdout_pipe) != 0 || pipe(stderr_pipe) != 0)
{
result.error_message = "pipe() failed";
return result;
}
const pid_t child_pid = fork();
if (child_pid < 0)
{
result.error_message = "fork() failed";
return result;
}
if (child_pid == 0)
{
// Child.
dup2(stdout_pipe[1], STDOUT_FILENO);
dup2(stderr_pipe[1], STDERR_FILENO);
close(stdout_pipe[0]);
close(stdout_pipe[1]);
close(stderr_pipe[0]);
close(stderr_pipe[1]);
if (!work_dir.empty() && chdir(work_dir.c_str()) != 0)
{
_exit(127);
}
execl("/bin/sh", "sh", "-c", command.c_str(), static_cast<char*>(nullptr));
_exit(127);
}
// Parent.
close(stdout_pipe[1]);
close(stderr_pipe[1]);
fcntl(stdout_pipe[0], F_SETFL, fcntl(stdout_pipe[0], F_GETFL, 0) | O_NONBLOCK);
fcntl(stderr_pipe[0], F_SETFL, fcntl(stderr_pipe[0], F_GETFL, 0) | O_NONBLOCK);
result.launched = true;
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms > 0 ? timeout_ms : 0);
bool timed_out = false;
int status = 0;
bool exited = false;
while (true)
{
const bool stdout_open = drain_available(stdout_pipe[0], result.stdout_text);
const bool stderr_open = drain_available(stderr_pipe[0], result.stderr_text);
const pid_t wait_result = waitpid(child_pid, &status, WNOHANG);
if (wait_result == child_pid)
{
exited = true;
break;
}
if (!stdout_open && !stderr_open)
{
// Pipes closed but process not yet reaped - do a final blocking wait.
waitpid(child_pid, &status, 0);
exited = true;
break;
}
if (timeout_ms > 0 && std::chrono::steady_clock::now() >= deadline)
{
timed_out = true;
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
if (timed_out)
{
kill(child_pid, SIGKILL);
waitpid(child_pid, &status, 0);
result.error_message = "command timed out after " + std::to_string(timeout_ms) + "ms";
// Drain whatever was buffered before killing.
drain_available(stdout_pipe[0], result.stdout_text);
drain_available(stderr_pipe[0], result.stderr_text);
}
else if (exited)
{
if (WIFEXITED(status))
{
result.exit_code = WEXITSTATUS(status);
}
else if (WIFSIGNALED(status))
{
result.exit_code = 128 + WTERMSIG(status);
}
}
close(stdout_pipe[0]);
close(stderr_pipe[0]);
return result;
}
LaunchAppResult launch_application(
const core::String& path_or_name,
const core::String& args,
const core::String& work_dir)
{
LaunchAppResult result;
if (path_or_name.empty())
{
result.error_message = "path_or_name is empty";
return result;
}
// Double-fork so the launched app is fully detached and re-parented to
// init: a single fork()+exec() would leave `waitpid` below blocking
// until the *app itself* exits (exec replaces the child's image but
// keeps its PID), defeating fire-and-forget. The pipe hands the
// grandchild's PID back to this process, since fork()'s return value in
// the middle child isn't visible here.
int pid_pipe[2] = {-1, -1};
if (pipe(pid_pipe) != 0)
{
result.error_message = "pipe() failed";
return result;
}
const pid_t middle_pid = fork();
if (middle_pid < 0)
{
close(pid_pipe[0]);
close(pid_pipe[1]);
result.error_message = "fork() failed";
return result;
}
if (middle_pid == 0)
{
// Middle child: detach into its own session, fork the real
// grandchild, report its PID to the parent, then exit immediately -
// orphaning the grandchild to init rather than droidcli.
close(pid_pipe[0]);
setsid();
const pid_t app_pid = fork();
if (app_pid < 0)
{
_exit(1);
}
if (app_pid == 0)
{
if (!work_dir.empty() && chdir(work_dir.c_str()) != 0)
{
_exit(127);
}
core::String command_line = path_or_name;
if (!args.empty())
{
command_line += " " + args;
}
execl("/bin/sh", "sh", "-c", command_line.c_str(), static_cast<char*>(nullptr));
_exit(127);
}
const int64_t pid_to_report = static_cast<int64_t>(app_pid);
write(pid_pipe[1], &pid_to_report, sizeof(pid_to_report));
close(pid_pipe[1]);
_exit(0);
}
// Parent: reap the middle child (returns quickly, it exits right after
// forking) and read back the actual app's PID - not a blocking wait on
// the launched application.
close(pid_pipe[1]);
int64_t app_pid = 0;
const ssize_t bytes_read = read(pid_pipe[0], &app_pid, sizeof(app_pid));
close(pid_pipe[0]);
int status = 0;
waitpid(middle_pid, &status, 0);
result.launched = bytes_read == static_cast<ssize_t>(sizeof(app_pid)) && app_pid > 0;
result.pid = app_pid;
if (!result.launched)
{
result.error_message = "failed to launch " + path_or_name;
}
return result;
}
#endif
} // namespace droidcli::cli