mirror of
https://github.com/ModOrganizer2/mob.git
synced 2026-07-27 14:07:05 -07:00
770 lines
13 KiB
C++
770 lines
13 KiB
C++
#include "pch.h"
|
|
#include "process.h"
|
|
#include "conf.h"
|
|
#include "net.h"
|
|
#include "context.h"
|
|
#include "op.h"
|
|
|
|
namespace mob
|
|
{
|
|
|
|
HANDLE get_bit_bucket()
|
|
{
|
|
SECURITY_ATTRIBUTES sa { .nLength = sizeof(sa), .bInheritHandle = TRUE };
|
|
return ::CreateFileA("NUL", GENERIC_WRITE, 0, &sa, OPEN_EXISTING, 0, 0);
|
|
}
|
|
|
|
|
|
async_pipe::async_pipe()
|
|
: pending_(false)
|
|
{
|
|
buffer_ = std::make_unique<char[]>(buffer_size);
|
|
std::memset(buffer_.get(), 0, buffer_size);
|
|
|
|
std::memset(&ov_, 0, sizeof(ov_));
|
|
}
|
|
|
|
handle_ptr async_pipe::create()
|
|
{
|
|
// creating pipe
|
|
handle_ptr out(create_pipe());
|
|
if (out.get() == INVALID_HANDLE_VALUE)
|
|
return {};
|
|
|
|
ov_.hEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
|
|
|
|
if (ov_.hEvent == NULL)
|
|
{
|
|
const auto e = GetLastError();
|
|
bail_out("CreateEvent failed", e);
|
|
}
|
|
|
|
event_.reset(ov_.hEvent);
|
|
|
|
return out;
|
|
}
|
|
|
|
std::string_view async_pipe::read()
|
|
{
|
|
if (pending_)
|
|
return check_pending();
|
|
else
|
|
return try_read();
|
|
}
|
|
|
|
HANDLE async_pipe::create_pipe()
|
|
{
|
|
static std::atomic<int> pipe_id(0);
|
|
|
|
const std::string pipe_name_prefix = "\\\\.\\pipe\\mob_pipe";
|
|
const std::string pipe_name = pipe_name_prefix + std::to_string(++pipe_id);
|
|
|
|
SECURITY_ATTRIBUTES sa = {};
|
|
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
|
|
sa.bInheritHandle = TRUE;
|
|
|
|
handle_ptr pipe;
|
|
|
|
// creating pipe
|
|
{
|
|
HANDLE pipe_handle = ::CreateNamedPipeA(
|
|
pipe_name.c_str(), PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED,
|
|
PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT,
|
|
1, 50'000, 50'000, pipe_timeout, &sa);
|
|
|
|
if (pipe_handle == INVALID_HANDLE_VALUE)
|
|
{
|
|
const auto e = GetLastError();
|
|
bail_out("CreateNamedPipe failed", e);
|
|
}
|
|
|
|
pipe.reset(pipe_handle);
|
|
}
|
|
|
|
{
|
|
// duplicating the handle to read from it
|
|
HANDLE output_read = INVALID_HANDLE_VALUE;
|
|
|
|
const auto r = DuplicateHandle(
|
|
GetCurrentProcess(), pipe.get(), GetCurrentProcess(), &output_read,
|
|
0, TRUE, DUPLICATE_SAME_ACCESS);
|
|
|
|
if (!r)
|
|
{
|
|
const auto e = GetLastError();
|
|
bail_out("DuplicateHandle for pipe", e);
|
|
}
|
|
|
|
stdout_.reset(output_read);
|
|
}
|
|
|
|
|
|
// creating handle to pipe which is passed to CreateProcess()
|
|
HANDLE output_write = ::CreateFileA(
|
|
pipe_name.c_str(), FILE_WRITE_DATA|SYNCHRONIZE, 0,
|
|
&sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
|
|
|
|
if (output_write == INVALID_HANDLE_VALUE)
|
|
{
|
|
const auto e = GetLastError();
|
|
bail_out("CreateFileW for pipe failed", e);
|
|
}
|
|
|
|
return output_write;
|
|
}
|
|
|
|
std::string_view async_pipe::try_read()
|
|
{
|
|
DWORD bytes_read = 0;
|
|
|
|
if (!::ReadFile(stdout_.get(), buffer_.get(), buffer_size, &bytes_read, &ov_))
|
|
{
|
|
const auto e = GetLastError();
|
|
|
|
switch (e)
|
|
{
|
|
case ERROR_IO_PENDING:
|
|
{
|
|
pending_ = true;
|
|
break;
|
|
}
|
|
|
|
case ERROR_BROKEN_PIPE:
|
|
{
|
|
// broken pipe probably means lootcli is finished
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
bail_out("async_pipe read failed", e);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
return {buffer_.get(), bytes_read};
|
|
}
|
|
|
|
std::string_view async_pipe::check_pending()
|
|
{
|
|
DWORD bytes_read = 0;
|
|
|
|
const auto r = WaitForSingleObject(event_.get(), pipe_timeout);
|
|
|
|
if (r == WAIT_FAILED) {
|
|
const auto e = GetLastError();
|
|
bail_out("WaitForSingleObject in async_pipe failed", e);
|
|
}
|
|
|
|
if (!::GetOverlappedResult(stdout_.get(), &ov_, &bytes_read, FALSE))
|
|
{
|
|
const auto e = GetLastError();
|
|
|
|
switch (e)
|
|
{
|
|
case ERROR_IO_INCOMPLETE:
|
|
{
|
|
break;
|
|
}
|
|
|
|
case WAIT_TIMEOUT:
|
|
{
|
|
break;
|
|
}
|
|
|
|
case ERROR_BROKEN_PIPE:
|
|
{
|
|
// broken pipe probably means lootcli is finished
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
bail_out("GetOverlappedResult failed in async_pipe", e);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
::ResetEvent(event_.get());
|
|
pending_ = false;
|
|
|
|
return {buffer_.get(), bytes_read};
|
|
}
|
|
|
|
|
|
process::impl::impl(const impl& i)
|
|
: interrupt(i.interrupt.load())
|
|
{
|
|
}
|
|
|
|
process::impl& process::impl::operator=(const impl& i)
|
|
{
|
|
interrupt = i.interrupt.load();
|
|
return *this;
|
|
}
|
|
|
|
|
|
process::process() :
|
|
cx_(&gcx()), flags_(process::noflags),
|
|
stdout_flags_(process::forward_to_log),
|
|
stdout_level_(context::level::trace),
|
|
stderr_flags_(process::forward_to_log),
|
|
stderr_level_(context::level::error),
|
|
code_(0)
|
|
{
|
|
}
|
|
|
|
process::~process()
|
|
{
|
|
try
|
|
{
|
|
join();
|
|
}
|
|
catch(...)
|
|
{
|
|
}
|
|
}
|
|
|
|
process process::raw(const context& cx, const std::string& cmd)
|
|
{
|
|
process p;
|
|
p.cx_ = &cx;
|
|
p.raw_ = cmd;
|
|
return p;
|
|
}
|
|
|
|
process& process::set_context(const context* cx)
|
|
{
|
|
cx_ = cx;
|
|
return *this;
|
|
}
|
|
|
|
process& process::name(const std::string& name)
|
|
{
|
|
name_ = name;
|
|
return *this;
|
|
}
|
|
|
|
std::string process::name() const
|
|
{
|
|
if (name_.empty())
|
|
return bin_.stem().string();
|
|
else
|
|
return name_;
|
|
}
|
|
|
|
process& process::binary(const fs::path& p)
|
|
{
|
|
bin_ = p;
|
|
return *this;
|
|
}
|
|
|
|
const fs::path& process::binary() const
|
|
{
|
|
return bin_;
|
|
}
|
|
|
|
process& process::cwd(const fs::path& p)
|
|
{
|
|
cwd_ = p;
|
|
return *this;
|
|
}
|
|
|
|
const fs::path& process::cwd() const
|
|
{
|
|
return cwd_;
|
|
}
|
|
|
|
process& process::stdout_flags(stream_flags s)
|
|
{
|
|
stdout_flags_ = s;
|
|
return *this;
|
|
}
|
|
|
|
process& process::stdout_level(context::level lv)
|
|
{
|
|
stdout_level_ = lv;
|
|
return *this;
|
|
}
|
|
|
|
process& process::stdout_filter(filter_fun f)
|
|
{
|
|
stdout_filter_ = f;
|
|
return *this;
|
|
}
|
|
|
|
process& process::stderr_flags(stream_flags s)
|
|
{
|
|
stderr_flags_ = s;
|
|
return *this;
|
|
}
|
|
|
|
process& process::stderr_level(context::level lv)
|
|
{
|
|
stderr_level_ = lv;
|
|
return *this;
|
|
}
|
|
|
|
process& process::stderr_filter(filter_fun f)
|
|
{
|
|
stderr_filter_ = f;
|
|
return *this;
|
|
}
|
|
|
|
process& process::external_error_log(const fs::path& p)
|
|
{
|
|
error_log_file_ = p;
|
|
return *this;
|
|
}
|
|
|
|
process& process::flags(flags_t f)
|
|
{
|
|
flags_ = f;
|
|
return *this;
|
|
}
|
|
|
|
process::flags_t process::flags() const
|
|
{
|
|
return flags_;
|
|
}
|
|
|
|
process& process::env(const mob::env& e)
|
|
{
|
|
env_ = e;
|
|
return *this;
|
|
}
|
|
|
|
std::string process::make_name() const
|
|
{
|
|
if (!name_.empty())
|
|
return name_;
|
|
|
|
return make_cmd();
|
|
}
|
|
|
|
std::string process::make_cmd() const
|
|
{
|
|
if (!raw_.empty())
|
|
return raw_;
|
|
|
|
return "\"" + bin_.string() + "\"" + cmd_;
|
|
}
|
|
|
|
void process::pipe_into(const process& p)
|
|
{
|
|
raw_ = make_cmd() + " | " + p.make_cmd();
|
|
}
|
|
|
|
void process::run()
|
|
{
|
|
if (!cwd_.empty())
|
|
cx_->debug(context::cmd, "> cd " + cwd_.string());
|
|
|
|
const auto what = make_cmd();
|
|
cx_->debug(context::cmd, "> " + what);
|
|
|
|
if (conf::dry())
|
|
return;
|
|
|
|
do_run(what);
|
|
}
|
|
|
|
void process::do_run(const std::string& what)
|
|
{
|
|
if (raw_.empty() && bin_.empty())
|
|
cx_->bail_out(context::cmd, "process: nothing to run");
|
|
|
|
if (fs::exists(error_log_file_))
|
|
{
|
|
cx_->trace(context::cmd,
|
|
"external error log file " +
|
|
error_log_file_.string() + " exists, deleting");
|
|
|
|
op::delete_file(*cx_, error_log_file_, op::optional);
|
|
}
|
|
|
|
STARTUPINFOA si = { .cb=sizeof(si) };
|
|
PROCESS_INFORMATION pi = {};
|
|
|
|
handle_ptr stdout_pipe, stderr_pipe;
|
|
|
|
switch (stdout_flags_)
|
|
{
|
|
case forward_to_log:
|
|
case keep_in_string:
|
|
{
|
|
stdout_pipe = impl_.stdout_pipe.create();
|
|
si.hStdOutput = stdout_pipe.get();
|
|
break;
|
|
}
|
|
|
|
case bit_bucket:
|
|
{
|
|
si.hStdOutput = get_bit_bucket();
|
|
break;
|
|
}
|
|
|
|
case inherit:
|
|
{
|
|
si.hStdOutput = ::GetStdHandle(STD_OUTPUT_HANDLE);
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (stderr_flags_)
|
|
{
|
|
case forward_to_log:
|
|
case keep_in_string:
|
|
{
|
|
stderr_pipe = impl_.stderr_pipe.create();
|
|
si.hStdError = stderr_pipe.get();
|
|
break;
|
|
}
|
|
|
|
case bit_bucket:
|
|
{
|
|
si.hStdError = get_bit_bucket();
|
|
break;
|
|
}
|
|
|
|
case inherit:
|
|
{
|
|
si.hStdError = ::GetStdHandle(STD_ERROR_HANDLE);
|
|
break;
|
|
}
|
|
}
|
|
|
|
si.hStdInput = get_bit_bucket();
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
|
|
const std::string cmd = this_env::get("COMSPEC");
|
|
const std::string args = "/C \"" + what + "\"";
|
|
|
|
const char* cwd_p = nullptr;
|
|
std::string cwd_s;
|
|
|
|
if (!cwd_.empty())
|
|
{
|
|
op::create_directories(*cx_, cwd_);
|
|
cwd_s = cwd_.string();
|
|
cwd_p = (cwd_s.empty() ? nullptr : cwd_s.c_str());
|
|
}
|
|
|
|
cx_->trace(context::cmd, "creating process");
|
|
|
|
const auto r = ::CreateProcessA(
|
|
cmd.c_str(), const_cast<char*>(args.c_str()),
|
|
nullptr, nullptr, TRUE, CREATE_NEW_PROCESS_GROUP,
|
|
env_.get_pointers(), cwd_p, &si, &pi);
|
|
|
|
if (!r)
|
|
{
|
|
const auto e = GetLastError();
|
|
cx_->bail_out(context::cmd, "failed to start '" + cmd + "'", e);
|
|
}
|
|
|
|
cx_->trace(context::cmd, "pid " + std::to_string(pi.dwProcessId));
|
|
|
|
::CloseHandle(pi.hThread);
|
|
impl_.handle.reset(pi.hProcess);
|
|
}
|
|
|
|
void process::interrupt()
|
|
{
|
|
impl_.interrupt = true;
|
|
cx_->trace(context::cmd, "will interrupt");
|
|
}
|
|
|
|
void process::join()
|
|
{
|
|
if (!impl_.handle)
|
|
return;
|
|
|
|
bool interrupted = false;
|
|
guard g([&] { impl_.handle = {}; });
|
|
|
|
cx_->trace(context::cmd, "joining");
|
|
|
|
for (;;)
|
|
{
|
|
const auto r = WaitForSingleObject(impl_.handle.get(), 100);
|
|
|
|
if (r == WAIT_OBJECT_0)
|
|
{
|
|
on_completed();
|
|
break;
|
|
}
|
|
else if (r == WAIT_TIMEOUT)
|
|
{
|
|
on_timeout(interrupted);
|
|
}
|
|
else
|
|
{
|
|
const auto e = GetLastError();
|
|
cx_->bail_out(context::cmd, "failed to wait on process", e);
|
|
}
|
|
}
|
|
|
|
if (interrupted)
|
|
cx_->trace(context::cmd, "process interrupted and finished");
|
|
}
|
|
|
|
void process::read_pipes()
|
|
{
|
|
// stdout
|
|
switch (stdout_flags_)
|
|
{
|
|
case forward_to_log:
|
|
{
|
|
std::string_view s = impl_.stdout_pipe.read();
|
|
|
|
for_each_line(s, [&](auto&& line)
|
|
{
|
|
filter f = {line, context::std_out, stdout_level_, false};
|
|
|
|
if (stdout_filter_)
|
|
{
|
|
stdout_filter_(f);
|
|
if (f.ignore)
|
|
return;
|
|
}
|
|
|
|
cx_->log(f.r, f.lv, f.line);
|
|
});
|
|
|
|
break;
|
|
}
|
|
|
|
case keep_in_string:
|
|
{
|
|
std::string_view s = impl_.stdout_pipe.read();
|
|
stdout_string_ += s;
|
|
break;
|
|
}
|
|
|
|
case bit_bucket:
|
|
case inherit:
|
|
break;
|
|
}
|
|
|
|
|
|
switch (stderr_flags_)
|
|
{
|
|
case forward_to_log:
|
|
{
|
|
std::string_view s = impl_.stderr_pipe.read();
|
|
|
|
for_each_line(s, [&](auto&& line)
|
|
{
|
|
filter f = {line, context::std_err, stderr_level_, false};
|
|
|
|
if (stderr_filter_)
|
|
{
|
|
stderr_filter_(f);
|
|
if (f.ignore)
|
|
return;
|
|
}
|
|
|
|
cx_->log(f.r, f.lv, f.line);
|
|
});
|
|
|
|
break;
|
|
}
|
|
|
|
case keep_in_string:
|
|
{
|
|
std::string_view s = impl_.stderr_pipe.read();
|
|
stderr_string_ += s;
|
|
break;
|
|
}
|
|
|
|
case bit_bucket:
|
|
case inherit:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void process::on_completed()
|
|
{
|
|
// one last time
|
|
read_pipes();
|
|
|
|
if (impl_.interrupt)
|
|
return;
|
|
|
|
if (!GetExitCodeProcess(impl_.handle.get(), &code_))
|
|
{
|
|
const auto e = GetLastError();
|
|
cx_->error(context::cmd, "failed to get exit code", e);
|
|
code_ = 0xffff;
|
|
}
|
|
|
|
// success
|
|
if (code_ == 0)
|
|
return;
|
|
|
|
if (flags_ & allow_failure)
|
|
{
|
|
cx_->trace(context::cmd,
|
|
"process failed but failure was allowed");
|
|
}
|
|
else
|
|
{
|
|
dump_error_log_file();
|
|
|
|
cx_->bail_out(context::cmd,
|
|
make_name() + " returned " + std::to_string(code_));
|
|
}
|
|
}
|
|
|
|
void process::on_timeout(bool& already_interrupted)
|
|
{
|
|
read_pipes();
|
|
|
|
if (impl_.interrupt && !already_interrupted)
|
|
{
|
|
if (flags_ & terminate_on_interrupt)
|
|
{
|
|
cx_->trace(context::cmd,
|
|
"terminating process (flag is set)");
|
|
|
|
::TerminateProcess(impl_.handle.get(), 0xffff);
|
|
}
|
|
else
|
|
{
|
|
const auto pid = GetProcessId(impl_.handle.get());
|
|
|
|
if (pid == 0)
|
|
{
|
|
cx_->trace(context::cmd,
|
|
"process id is 0, terminating instead");
|
|
|
|
::TerminateProcess(impl_.handle.get(), 0xffff);
|
|
}
|
|
else
|
|
{
|
|
cx_->trace(context::cmd,
|
|
"sending sigint to " + std::to_string(pid));
|
|
|
|
GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid);
|
|
}
|
|
}
|
|
|
|
already_interrupted = true;
|
|
}
|
|
}
|
|
|
|
void process::dump_error_log_file() noexcept
|
|
{
|
|
try
|
|
{
|
|
if (error_log_file_.empty())
|
|
return;
|
|
|
|
if (fs::exists(error_log_file_))
|
|
{
|
|
std::string log = op::read_text_file(
|
|
*cx_, error_log_file_, op::optional);
|
|
|
|
if (log.empty())
|
|
return;
|
|
|
|
cx_->error(context::cmd,
|
|
make_name() + " failed, "
|
|
"content of " + error_log_file_.string() + ":");
|
|
|
|
for_each_line(log, [&](auto&& line)
|
|
{
|
|
cx_->error(context::cmd, std::string(8, ' ') + std::string(line));
|
|
});
|
|
}
|
|
else
|
|
{
|
|
cx_->debug(context::cmd,
|
|
"external error log file " + error_log_file_.string() + " "
|
|
"doesn't exist");
|
|
}
|
|
}
|
|
catch(...)
|
|
{
|
|
// eat it
|
|
}
|
|
}
|
|
|
|
int process::exit_code() const
|
|
{
|
|
return static_cast<int>(code_);
|
|
}
|
|
|
|
std::string process::steal_stdout()
|
|
{
|
|
return std::move(stdout_string_);
|
|
}
|
|
|
|
std::string process::steal_stderr()
|
|
{
|
|
return std::move(stderr_string_);
|
|
}
|
|
|
|
void process::add_arg(const std::string& k, const std::string& v, arg_flags f)
|
|
{
|
|
if ((f & log_debug) && !conf::log_debug())
|
|
return;
|
|
|
|
if ((f & log_trace) && !conf::log_trace())
|
|
return;
|
|
|
|
if ((f & log_dump) && !conf::log_dump())
|
|
return;
|
|
|
|
if ((f & log_quiet) && conf::log_trace())
|
|
return;
|
|
|
|
if (k.empty() && v.empty())
|
|
return;
|
|
|
|
if (k.empty())
|
|
cmd_ += " " + v;
|
|
else if ((f & nospace) || k.back() == '=')
|
|
cmd_ += " " + k + v;
|
|
else
|
|
cmd_ += " " + k + " " + v;
|
|
}
|
|
|
|
std::string process::arg_to_string(const char* s, bool force_quote)
|
|
{
|
|
if (force_quote)
|
|
return "\"" + std::string(s) + "\"";
|
|
else
|
|
return s;
|
|
}
|
|
|
|
std::string process::arg_to_string(const std::string& s, bool force_quote)
|
|
{
|
|
if (force_quote)
|
|
return "\"" + std::string(s) + "\"";
|
|
else
|
|
return s;
|
|
}
|
|
|
|
std::string process::arg_to_string(const fs::path& p, bool)
|
|
{
|
|
return "\"" + p.string() + "\"";
|
|
}
|
|
|
|
std::string process::arg_to_string(const url& u, bool force_quote)
|
|
{
|
|
if (force_quote)
|
|
return "\"" + u.string() + "\"";
|
|
else
|
|
return u.string();
|
|
}
|
|
|
|
} // namespace
|