diff --git a/src/process.cc b/src/process.cc index f250603..7d7a742 100644 --- a/src/process.cc +++ b/src/process.cc @@ -13,8 +13,9 @@ #include "ostd/process.hh" #ifdef OSTD_PLATFORM_WIN32 -# define WIN32_LEAN_AND_MEAN +# include "ostd/filesystem.hh" # include +namespace fs = ostd::filesystem; #else # include # include @@ -314,17 +315,344 @@ struct data { HANDLE process = nullptr, thread = nullptr; }; -OSTD_EXPORT void subprocess::open_impl( - std::string const &, std::vector const &, bool -) { - return; +struct pipe { + HANDLE p_r = nullptr, p_w = nullptr; + + ~pipe() { + if (p_r) { + CloseHandle(p_r); + } + if (p_w) { + CloseHandle(p_w); + } + } + + void open(process_stream use, SECURITY_ATTRIBUTES &sa, bool read) { + if (use != process_stream::PIPE) { + return; + } + if (!CreatePipe(&p_r, &p_w, &sa, 0)) { + throw process_error{EPIPE, std::generic_category()}; + } + if (!SetHandleInformation(read ? p_r : p_w, HANDLE_FLAG_INHERIT, 0)) { + throw process_error{EPIPE, std::generic_category()}; + } + } + + void fdopen(file_stream &s, bool read) { + int fd = _open_osfhandle( + reinterpret_cast(read ? p_r : p_w), + read ? _O_RDONLY : 0 + ); + if (fd < 0) { + throw process_error{EPIPE, std::generic_category()}; + } + if (read) { + p_r = nullptr; + } else { + p_w = nullptr; + } + auto p = _fdopen(fd, read ? "r" : "w"); + if (!p) { + _close(fd); + throw process_error{EPIPE, std::generic_category()}; + } + s.open(p, [](FILE *f) { + std::fclose(f); + }); + } +}; + +/* because there is no way to have CreateProcess do a lookup in standard + * paths AND specify a custom separate argv[0], we need to implement the + * path resolution ourselves; fortunately the standard filesystem API + * makes this kind of easy, but it's still a lot of code I'd rather + * not write... oh well + */ +static std::wstring resolve_file(wchar_t const *cmd) { + /* a reused buffer, TODO: allow longer paths */ + wchar_t buf[1024]; + + auto is_maybe_exec = [](fs::path const &p) { + auto st = fs::status(p); + if (fs::is_regular_file(st) || fs::is_symlink(st)) { + return true; + } + }; + + fs::path p{cmd}; + /* deal with some easy cases */ + if ((p.filename() != p) || (p == L".") || (p == L"..")) { + return cmd; + } + /* no extension appends .exe as is done normally */ + if (!p.has_extension()) { + p.replace_extension(L".exe"); + } + /* the directory from which the app loaded */ + if (GetModuleFileNameW(nullptr, buf, sizeof(buf))) { +#ifdef NTDDI_WIN8 + PathCchRemoveFileSpecW(buf, sizeof(buf)); +#else + PathRemoveFileSpecW(buf); +#endif + auto rp = fs::path{buf} / p; + if (is_maybe_exec(rp)) { + return rp.native(); + } + } + /* the current directory */ + { + auto rp = fs::path{L"."} / p; + if (is_maybe_exec(rp)) { + return rp.native(); + } + } + /* the system directory */ + if (GetSystemDirectoryW(buf, sizeof(buf))) { + auto rp = fs::path{buf} / p; + if (is_maybe_exec(rp)) { + return rp.native(); + } + } + /* the windows directory */ + if (GetWindowsDirectoryW(buf, sizeof(buf))) { + auto rp = fs::path{path} / p; + if (is_maybe_exec(rp)) { + return rp.native(); + } + } + /* the PATH envvar */ + std::size_t req = GetEnvironmentVariableW(L"PATH", buf, sizeof(buf)); + if (req) { + wchar_t *envp = buf; + std::vector dynbuf; + if (req > sizeof(buf)) { + dynbuf.reserve(req); + for (;;) { + req = GetEnvironmentVariable( + "PATH", dynbuf.data(), dynbuf.capacity() + ); + if (!req) { + return cmd; + } + if (req > dynbuf.capacity()) { + dynbuf.reserve(req); + } else { + envp = dynbuf.data(); + break; + } + } + } + for (;;) { + auto p = wcschr(envp, L';'); + fs::path rp; + if (!p) { + rp = fs::path{p} / p; + } else { + rp = fs::path{envp, p} / p; + envp = p + 1; + } + if (is_maybe_exec(rp)) { + return rp.native(); + } + } + } + /* nothing found */ + return cmd; } -OSTD_EXPORT int subprocess::close() { - throw process_error{ECHILD, std::generic_category()}; +/* windows follows a dumb set of rules for parsing command line params; + * a single \ is normally interpreted literally, unless it precedes a ", + * in which case it acts as an escape character for the quotation mark; + * if multiple backslashes precedes the quotation mark, each pair is + * treated as a single backslash + * + * we need to replicate this awful behavior here, hence the extra code + */ +static std::string concat_args(std::vector const &args) { + std::string ret; + for (auto &s: args) { + if (!ret.empty()) { + ret += ' '; + } + ret += '\"'; + for (char const *sp = s.data();;) { + char const *p = strpbrk(sp, "\"\\"); + if (!p) { + ret += sp; + break; + } + ret.append(sp, p); + if (*p == '\"') { + /* not preceded by \, so it's safe */ + ret += "\\\""; + } else { + /* handle any sequence of \ optionally followed by a " */ + std::size_t nbsl = 0; + while (*p++ == '\\') { + ++nbsl; + } + if (*p == '\"') { + /* double all the backslashes plus one for the " */ + ret.append(nbsl * 2 + 1, '\\'); + ret += '\"'; + } else { + ret.append(nbsl, '\\'); + } + } + sp = p + 1; + } + ret += '\"'; + } + return ret; +} + +OSTD_EXPORT void subprocess::open_impl( + std::string const &cmd, std::vector const &args, bool use_path +) { + if (use_in == process_stream::STDOUT) { + throw process_error{EINVAL, std::generic_category()}; + } + + /* pipes */ + + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.bInheritHandle = true; + sa.lpSecurityDescriptor = nullptr; + + pipe pipe_in, pipe_out, pipe_err; + + pipe_in.open(use_in, false); + pipe_out.open(use_out, true); + pipe_err.open(use_err, true) + + /* process creation */ + + PROCESS_INFORMATION pi; + STARTUPINFO si; + + memset(&pi, 0, sizeof(PROCESS_INFORMATION)); + memset(&si, 0, sizeof(STARTUPINFO)); + + si.cb = sizeof(STARTUPINFO); + + if (use_in == process_stream::PIPE) { + si.hStdInput = pipe_in.p_r; + pipe_in.fdopen(in, false); + } else { + si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + if (si.hStdInput == INVALID_HANDLE_VALUE) { + throw process_error{EINVAL, std::generic_category()}; + } + } + if (use_out == process_stream::PIPE) { + si.hStdOutput = pipe_out.p_w; + pipe_out.fdopen(out, true); + } else { + si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + if (si.hStdOutput == INVALID_HANDLE_VALUE) { + throw process_error{EINVAL, std::generic_category()}; + } + } + if (use_err == process_stream::PIPE) { + si.hStdError = pipe_err.p_w; + pipe_err.fdopen(err, true); + } else if (use_err = process_stream::STDOUT) { + si.hStdError = si.hStdOutput; + } else { + si.hStdError = GetStdHandle(STD_ERROR_HANDLE); + if (si.hStdError == INVALID_HANDLE_VALUE) { + throw process_error{EINVAL, std::generic_category()}; + } + } + si.dwFlags |= STARTF_USESTDHANDLES; + + std::wstring cmdpath; + /* convert and optionally resolve PATH and other lookup locations */ + { + std::unique_ptr wcmd{new wchar_t[cmd.size() + 1]}; + if (!MultiByteToWideChar( + CP_UTF8, 0, cmd.data(), cmd.size(), wcmd.data(), cmd.size() + 1 + )) { + throw process_error{EINVAL, std::generic_category()}; + } + if (use_path) { + cmdpath = wcmd.get(); + } else { + cmdpath = resolve_file(wcmd.get()); + } + } + + /* cmdline gets an ordinary conversion... */ + auto astr = concat_args(args); + + std::unique_ptr cmdline{new wchar_t[astr.size() + 1]}; + if (!MultiByteToWideChar( + CP_UTF8, 0, astr.data(), astr.size(), cmdline.data(), astr.size() + 1 + )) { + throw process_error{EINVAL, std::generic_category()}; + } + + /* owned by CreateProcess, do not close explicitly */ + pipe_in.p_r = nullptr; + pipe_out.p_w = nullptr; + pipe_err.p_w = nullptr; + + auto success = CreateProcessW( + cmdpath.data(), + cmdline.data(), + nullptr, /* process security attributes */ + nullptr, /* primary thread security attributes */ + true, /* inherit handles */ + 0, /* creation flags */ + nullptr, /* use parent env */ + nullptr, /* use parent cwd */ + &si, + &pi + ); + + p_current = ::new (reinterpret_cast(&p_data)) data{ + pi.hProcess, pi.hThread + }; + + if (!success) { + throw process_error{ECHILD, std::generic_category()}; + } } OSTD_EXPORT void subprocess::reset() { + p_current = nullptr; +} + +OSTD_EXPORT int subprocess::close() { + if (!p_current) { + throw process_error{ECHILD, std::generic_category()}; + } + + data *pd = static_cast(p_current); + + if (WaitForSingleObject(pd->process, INFINITE) == WAIT_FAILED) { + CloseHandle(pd->process); + CloseHandle(pd->thread); + reset(); + throw process_error{ECHILD, std::generic_category()}; + } + + int ec; + if (!GetExitCodeProcess(pd->process, &ec)) { + CloseHandle(pd->process); + CloseHandle(pd->thread); + reset(); + throw process_error{ECHILD, std::generic_category()}; + } + + CloseHandle(pd->process); + CloseHandle(pd->thread); + reset(); + + return ec; } #endif