mirror of
https://github.com/privatevoid-net/nix-super.git
synced 2024-11-22 14:06:16 +02:00
Merge pull request #10769 from poweredbypie/mingw-spawn
Implement runProgram for Windows
This commit is contained in:
commit
df068734ac
3 changed files with 384 additions and 11 deletions
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
#include "types.hh"
|
#include "types.hh"
|
||||||
#include "error.hh"
|
#include "error.hh"
|
||||||
|
#include "file-descriptor.hh"
|
||||||
#include "logging.hh"
|
#include "logging.hh"
|
||||||
#include "ansicolor.hh"
|
#include "ansicolor.hh"
|
||||||
|
|
||||||
|
@ -23,26 +24,36 @@ namespace nix {
|
||||||
struct Sink;
|
struct Sink;
|
||||||
struct Source;
|
struct Source;
|
||||||
|
|
||||||
#ifndef _WIN32
|
|
||||||
class Pid
|
class Pid
|
||||||
{
|
{
|
||||||
|
#ifndef _WIN32
|
||||||
pid_t pid = -1;
|
pid_t pid = -1;
|
||||||
bool separatePG = false;
|
bool separatePG = false;
|
||||||
int killSignal = SIGKILL;
|
int killSignal = SIGKILL;
|
||||||
|
#else
|
||||||
|
AutoCloseFD pid = INVALID_DESCRIPTOR;
|
||||||
|
#endif
|
||||||
public:
|
public:
|
||||||
Pid();
|
Pid();
|
||||||
|
#ifndef _WIN32
|
||||||
Pid(pid_t pid);
|
Pid(pid_t pid);
|
||||||
~Pid();
|
|
||||||
void operator =(pid_t pid);
|
void operator =(pid_t pid);
|
||||||
operator pid_t();
|
operator pid_t();
|
||||||
|
#else
|
||||||
|
Pid(AutoCloseFD pid);
|
||||||
|
void operator =(AutoCloseFD pid);
|
||||||
|
#endif
|
||||||
|
~Pid();
|
||||||
int kill();
|
int kill();
|
||||||
int wait();
|
int wait();
|
||||||
|
|
||||||
|
// TODO: Implement for Windows
|
||||||
|
#ifndef _WIN32
|
||||||
void setSeparatePG(bool separatePG);
|
void setSeparatePG(bool separatePG);
|
||||||
void setKillSignal(int signal);
|
void setKillSignal(int signal);
|
||||||
pid_t release();
|
pid_t release();
|
||||||
};
|
|
||||||
#endif
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
#include "current-process.hh"
|
#include "current-process.hh"
|
||||||
#include "environment-variables.hh"
|
#include "environment-variables.hh"
|
||||||
|
#include "error.hh"
|
||||||
|
#include "file-descriptor.hh"
|
||||||
|
#include "file-path.hh"
|
||||||
#include "signals.hh"
|
#include "signals.hh"
|
||||||
#include "processes.hh"
|
#include "processes.hh"
|
||||||
#include "finally.hh"
|
#include "finally.hh"
|
||||||
#include "serialise.hh"
|
#include "serialise.hh"
|
||||||
|
#include "file-system.hh"
|
||||||
|
#include "util.hh"
|
||||||
|
#include "windows-error.hh"
|
||||||
|
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
@ -16,25 +22,347 @@
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
namespace nix {
|
namespace nix {
|
||||||
|
|
||||||
std::string runProgram(Path program, bool lookupPath, const Strings & args,
|
using namespace nix::windows;
|
||||||
const std::optional<std::string> & input, bool isInteractive)
|
|
||||||
|
Pid::Pid() {}
|
||||||
|
|
||||||
|
Pid::Pid(AutoCloseFD pid)
|
||||||
|
: pid(std::move(pid))
|
||||||
{
|
{
|
||||||
throw UnimplementedError("Cannot shell out to git on Windows yet");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Pid::~Pid()
|
||||||
|
{
|
||||||
|
if (pid.get() != INVALID_DESCRIPTOR)
|
||||||
|
kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Pid::operator=(AutoCloseFD pid)
|
||||||
|
{
|
||||||
|
if (this->pid.get() != INVALID_DESCRIPTOR && this->pid.get() != pid.get())
|
||||||
|
kill();
|
||||||
|
this->pid = std::move(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement (not needed for process spawning yet)
|
||||||
|
int Pid::kill()
|
||||||
|
{
|
||||||
|
assert(pid.get() != INVALID_DESCRIPTOR);
|
||||||
|
|
||||||
|
debug("killing process %1%", pid.get());
|
||||||
|
|
||||||
|
throw UnimplementedError("Pid::kill unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
int Pid::wait()
|
||||||
|
{
|
||||||
|
// https://github.com/nix-windows/nix/blob/windows-meson/src/libutil/util.cc#L1938
|
||||||
|
assert(pid.get() != INVALID_DESCRIPTOR);
|
||||||
|
DWORD status = WaitForSingleObject(pid.get(), INFINITE);
|
||||||
|
if (status != WAIT_OBJECT_0) {
|
||||||
|
debug("WaitForSingleObject returned %1%", status);
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD exitCode = 0;
|
||||||
|
if (GetExitCodeProcess(pid.get(), &exitCode) == FALSE) {
|
||||||
|
debug("GetExitCodeProcess failed on pid %1%", pid.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
pid.close();
|
||||||
|
return exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Merge this with Unix's runProgram since it's identical logic.
|
||||||
|
std::string runProgram(
|
||||||
|
Path program, bool lookupPath, const Strings & args, const std::optional<std::string> & input, bool isInteractive)
|
||||||
|
{
|
||||||
|
auto res = runProgram(RunOptions{
|
||||||
|
.program = program, .lookupPath = lookupPath, .args = args, .input = input, .isInteractive = isInteractive});
|
||||||
|
|
||||||
|
if (!statusOk(res.first))
|
||||||
|
throw ExecError(res.first, "program '%1%' %2%", program, statusToString(res.first));
|
||||||
|
|
||||||
|
return res.second;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Path> getProgramInterpreter(const Path & program)
|
||||||
|
{
|
||||||
|
// These extensions are automatically handled by Windows and don't require an interpreter.
|
||||||
|
static constexpr const char * exts[] = {".exe", ".cmd", ".bat"};
|
||||||
|
for (const auto ext : exts) {
|
||||||
|
if (hasSuffix(program, ext)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Open file and read the shebang
|
||||||
|
throw UnimplementedError("getProgramInterpreter unimplemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Not sure if this is needed in the unix version but it might be useful as a member func
|
||||||
|
void setFDInheritable(AutoCloseFD & fd, bool inherit)
|
||||||
|
{
|
||||||
|
if (fd.get() != INVALID_DESCRIPTOR) {
|
||||||
|
if (!SetHandleInformation(fd.get(), HANDLE_FLAG_INHERIT, inherit ? HANDLE_FLAG_INHERIT : 0)) {
|
||||||
|
throw WinError("Couldn't disable inheriting of handle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoCloseFD nullFD()
|
||||||
|
{
|
||||||
|
// Create null handle to discard reads / writes
|
||||||
|
// https://stackoverflow.com/a/25609668
|
||||||
|
// https://github.com/nix-windows/nix/blob/windows-meson/src/libutil/util.cc#L2228
|
||||||
|
AutoCloseFD nul = CreateFileW(
|
||||||
|
L"NUL",
|
||||||
|
GENERIC_READ | GENERIC_WRITE,
|
||||||
|
// We don't care who reads / writes / deletes this file since it's NUL anyways
|
||||||
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||||
|
NULL,
|
||||||
|
OPEN_EXISTING,
|
||||||
|
0,
|
||||||
|
NULL);
|
||||||
|
if (!nul.get()) {
|
||||||
|
throw WinError("Couldn't open NUL device");
|
||||||
|
}
|
||||||
|
// Let this handle be inheritable by child processes
|
||||||
|
setFDInheritable(nul, true);
|
||||||
|
return nul;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapted from
|
||||||
|
// https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
|
||||||
|
std::string windowsEscape(const std::string & str, bool cmd)
|
||||||
|
{
|
||||||
|
// TODO: This doesn't handle cmd.exe escaping.
|
||||||
|
if (cmd) {
|
||||||
|
throw UnimplementedError("cmd.exe escaping is not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str.find_first_of(" \t\n\v\"") == str.npos && !str.empty()) {
|
||||||
|
// No need to escape this one, the nonempty contents don't have a special character
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
std::string buffer;
|
||||||
|
// Add the opening quote
|
||||||
|
buffer += '"';
|
||||||
|
for (auto iter = str.begin();; ++iter) {
|
||||||
|
size_t backslashes = 0;
|
||||||
|
while (iter != str.end() && *iter == '\\') {
|
||||||
|
++iter;
|
||||||
|
++backslashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only escape backslashes if:
|
||||||
|
// - They come immediately before the closing quote
|
||||||
|
// - They come immediately before a quote in the middle of the string
|
||||||
|
// Both of these cases break the escaping if not handled. Otherwise backslashes are fine as-is
|
||||||
|
if (iter == str.end()) {
|
||||||
|
// Need to escape each backslash
|
||||||
|
buffer.append(backslashes * 2, '\\');
|
||||||
|
// Exit since we've reached the end of the string
|
||||||
|
break;
|
||||||
|
} else if (*iter == '"') {
|
||||||
|
// Need to escape each backslash and the intermediate quote character
|
||||||
|
buffer.append(backslashes * 2, '\\');
|
||||||
|
buffer += "\\\"";
|
||||||
|
} else {
|
||||||
|
// Don't escape the backslashes since they won't break the delimiter
|
||||||
|
buffer.append(backslashes, '\\');
|
||||||
|
buffer += *iter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the closing quote
|
||||||
|
return buffer + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
Pid spawnProcess(const Path & realProgram, const RunOptions & options, Pipe & out, Pipe & in)
|
||||||
|
{
|
||||||
|
// Setup pipes.
|
||||||
|
if (options.standardOut) {
|
||||||
|
// Don't inherit the read end of the output pipe
|
||||||
|
setFDInheritable(out.readSide, false);
|
||||||
|
} else {
|
||||||
|
out.writeSide = nullFD();
|
||||||
|
}
|
||||||
|
if (options.standardIn) {
|
||||||
|
// Don't inherit the write end of the input pipe
|
||||||
|
setFDInheritable(in.writeSide, false);
|
||||||
|
} else {
|
||||||
|
in.readSide = nullFD();
|
||||||
|
}
|
||||||
|
|
||||||
|
STARTUPINFOW startInfo = {0};
|
||||||
|
startInfo.cb = sizeof(startInfo);
|
||||||
|
startInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||||
|
startInfo.hStdInput = in.readSide.get();
|
||||||
|
startInfo.hStdOutput = out.writeSide.get();
|
||||||
|
startInfo.hStdError = out.writeSide.get();
|
||||||
|
|
||||||
|
std::string envline;
|
||||||
|
// Retain the current processes' environment variables.
|
||||||
|
for (const auto & envVar : getEnv()) {
|
||||||
|
envline += (envVar.first + '=' + envVar.second + '\0');
|
||||||
|
}
|
||||||
|
// Also add new ones specified in options.
|
||||||
|
if (options.environment) {
|
||||||
|
for (const auto & envVar : *options.environment) {
|
||||||
|
envline += (envVar.first + '=' + envVar.second + '\0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string cmdline = windowsEscape(realProgram, false);
|
||||||
|
for (const auto & arg : options.args) {
|
||||||
|
// TODO: This isn't the right way to escape windows command
|
||||||
|
// See https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw
|
||||||
|
cmdline += ' ' + windowsEscape(arg, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
PROCESS_INFORMATION procInfo = {0};
|
||||||
|
if (CreateProcessW(
|
||||||
|
// EXE path is provided in the cmdline
|
||||||
|
NULL,
|
||||||
|
string_to_os_string(cmdline).data(),
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
TRUE,
|
||||||
|
CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED,
|
||||||
|
string_to_os_string(envline).data(),
|
||||||
|
options.chdir.has_value() ? string_to_os_string(*options.chdir).data() : NULL,
|
||||||
|
&startInfo,
|
||||||
|
&procInfo)
|
||||||
|
== 0) {
|
||||||
|
throw WinError("CreateProcessW failed (%1%)", cmdline);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert these to use RAII
|
||||||
|
AutoCloseFD process = procInfo.hProcess;
|
||||||
|
AutoCloseFD thread = procInfo.hThread;
|
||||||
|
|
||||||
|
// Add current process and child to job object so child terminates when parent terminates
|
||||||
|
// TODO: This spawns one job per child process. We can probably keep this as a global, and
|
||||||
|
// add children a single job so we don't use so many jobs at once.
|
||||||
|
Descriptor job = CreateJobObjectW(NULL, NULL);
|
||||||
|
if (job == NULL) {
|
||||||
|
TerminateProcess(procInfo.hProcess, 0);
|
||||||
|
throw WinError("Couldn't create job object for child process");
|
||||||
|
}
|
||||||
|
if (AssignProcessToJobObject(job, procInfo.hProcess) == FALSE) {
|
||||||
|
TerminateProcess(procInfo.hProcess, 0);
|
||||||
|
throw WinError("Couldn't assign child process to job object");
|
||||||
|
}
|
||||||
|
if (ResumeThread(procInfo.hThread) == (DWORD) -1) {
|
||||||
|
TerminateProcess(procInfo.hProcess, 0);
|
||||||
|
throw WinError("Couldn't resume child process thread");
|
||||||
|
}
|
||||||
|
|
||||||
|
return process;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Merge this with Unix's runProgram since it's identical logic.
|
||||||
// Output = error code + "standard out" output stream
|
// Output = error code + "standard out" output stream
|
||||||
std::pair<int, std::string> runProgram(RunOptions && options)
|
std::pair<int, std::string> runProgram(RunOptions && options)
|
||||||
{
|
{
|
||||||
throw UnimplementedError("Cannot shell out to git on Windows yet");
|
StringSink sink;
|
||||||
}
|
options.standardOut = &sink;
|
||||||
|
|
||||||
|
int status = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
runProgram2(options);
|
||||||
|
} catch (ExecError & e) {
|
||||||
|
status = e.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {status, std::move(sink.s)};
|
||||||
|
}
|
||||||
|
|
||||||
void runProgram2(const RunOptions & options)
|
void runProgram2(const RunOptions & options)
|
||||||
{
|
{
|
||||||
throw UnimplementedError("Cannot shell out to git on Windows yet");
|
checkInterrupt();
|
||||||
|
|
||||||
|
assert(!(options.standardIn && options.input));
|
||||||
|
|
||||||
|
std::unique_ptr<Source> source_;
|
||||||
|
Source * source = options.standardIn;
|
||||||
|
|
||||||
|
if (options.input) {
|
||||||
|
source_ = std::make_unique<StringSource>(*options.input);
|
||||||
|
source = source_.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create a pipe. */
|
||||||
|
Pipe out, in;
|
||||||
|
// TODO: I copied this from unix but this is handled again in spawnProcess, so might be weird to split it up like
|
||||||
|
// this
|
||||||
|
if (options.standardOut)
|
||||||
|
out.create();
|
||||||
|
if (source)
|
||||||
|
in.create();
|
||||||
|
|
||||||
|
Path realProgram = options.program;
|
||||||
|
// TODO: Implement shebang / program interpreter lookup on Windows
|
||||||
|
auto interpreter = getProgramInterpreter(realProgram);
|
||||||
|
|
||||||
|
std::optional<Finally<std::function<void()>>> resumeLoggerDefer;
|
||||||
|
if (options.isInteractive) {
|
||||||
|
logger->pause();
|
||||||
|
resumeLoggerDefer.emplace([]() { logger->resume(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
Pid pid = spawnProcess(interpreter.has_value() ? *interpreter : realProgram, options, out, in);
|
||||||
|
|
||||||
|
// TODO: This is identical to unix, deduplicate?
|
||||||
|
out.writeSide.close();
|
||||||
|
|
||||||
|
std::thread writerThread;
|
||||||
|
|
||||||
|
std::promise<void> promise;
|
||||||
|
|
||||||
|
Finally doJoin([&] {
|
||||||
|
if (writerThread.joinable())
|
||||||
|
writerThread.join();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (source) {
|
||||||
|
in.readSide.close();
|
||||||
|
writerThread = std::thread([&] {
|
||||||
|
try {
|
||||||
|
std::vector<char> buf(8 * 1024);
|
||||||
|
while (true) {
|
||||||
|
size_t n;
|
||||||
|
try {
|
||||||
|
n = source->read(buf.data(), buf.size());
|
||||||
|
} catch (EndOfFile &) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
writeFull(in.writeSide.get(), {buf.data(), n});
|
||||||
|
}
|
||||||
|
promise.set_value();
|
||||||
|
} catch (...) {
|
||||||
|
promise.set_exception(std::current_exception());
|
||||||
|
}
|
||||||
|
in.writeSide.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.standardOut)
|
||||||
|
drainFD(out.readSide.get(), *options.standardOut);
|
||||||
|
|
||||||
|
/* Wait for the child to finish. */
|
||||||
|
int status = pid.wait();
|
||||||
|
|
||||||
|
/* Wait for the writer thread to finish. */
|
||||||
|
if (source)
|
||||||
|
promise.get_future().get();
|
||||||
|
|
||||||
|
if (status)
|
||||||
|
throw ExecError(status, "program '%1%' %2%", options.program, statusToString(status));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string statusToString(int status)
|
std::string statusToString(int status)
|
||||||
|
@ -45,10 +373,8 @@ std::string statusToString(int status)
|
||||||
return "succeeded";
|
return "succeeded";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool statusOk(int status)
|
bool statusOk(int status)
|
||||||
{
|
{
|
||||||
return status == 0;
|
return status == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
36
tests/unit/libutil/spawn.cc
Normal file
36
tests/unit/libutil/spawn.cc
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "processes.hh"
|
||||||
|
|
||||||
|
namespace nix {
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
TEST(SpawnTest, spawnEcho)
|
||||||
|
{
|
||||||
|
auto output = runProgram(RunOptions{.program = "cmd.exe", .args = {"/C", "echo", "hello world"}});
|
||||||
|
ASSERT_EQ(output.first, 0);
|
||||||
|
ASSERT_EQ(output.second, "\"hello world\"\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string windowsEscape(const std::string & str, bool cmd);
|
||||||
|
|
||||||
|
TEST(SpawnTest, windowsEscape)
|
||||||
|
{
|
||||||
|
auto empty = windowsEscape("", false);
|
||||||
|
ASSERT_EQ(empty, R"("")");
|
||||||
|
// There's no quotes in this argument so the input should equal the output
|
||||||
|
auto backslashStr = R"(\\\\)";
|
||||||
|
auto backslashes = windowsEscape(backslashStr, false);
|
||||||
|
ASSERT_EQ(backslashes, backslashStr);
|
||||||
|
|
||||||
|
auto nestedQuotes = windowsEscape(R"(he said: "hello there")", false);
|
||||||
|
ASSERT_EQ(nestedQuotes, R"("he said: \"hello there\"")");
|
||||||
|
|
||||||
|
auto middleQuote = windowsEscape(R"( \\\" )", false);
|
||||||
|
ASSERT_EQ(middleQuote, R"(" \\\\\\\" ")");
|
||||||
|
|
||||||
|
auto space = windowsEscape("hello world", false);
|
||||||
|
ASSERT_EQ(space, R"("hello world")");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
Loading…
Reference in a new issue