Checkpoint 2: integrate native Editor, MCP, gameplay builds and standalone export
This commit is contained in:
+92
-48
@@ -1,72 +1,116 @@
|
||||
#include <faset/core/hash.hpp>
|
||||
#include <faset/core/error.hpp>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstdint>
|
||||
#include <faset/core/error.hpp>
|
||||
#include <faset/core/hash.hpp>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
|
||||
namespace faset {
|
||||
namespace {
|
||||
constexpr std::array<std::uint32_t,64> constants = {
|
||||
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
|
||||
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
|
||||
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
|
||||
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
|
||||
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
|
||||
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
|
||||
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
||||
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
|
||||
};
|
||||
constexpr std::array<std::uint32_t, 64> constants = {
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2};
|
||||
class Digest {
|
||||
std::array<std::uint32_t,8> state_{0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19};
|
||||
std::array<std::uint8_t,64> pending_{};
|
||||
std::uint64_t count_=0;
|
||||
std::size_t used_=0;
|
||||
std::array<std::uint32_t, 8> state_{0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
||||
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};
|
||||
std::array<std::uint8_t, 64> pending_{};
|
||||
std::uint64_t count_ = 0;
|
||||
std::size_t used_ = 0;
|
||||
void block() {
|
||||
std::array<std::uint32_t,64> w{};
|
||||
for (int i=0;i<16;++i) w[i]=(std::uint32_t(pending_[i*4])<<24)|(std::uint32_t(pending_[i*4+1])<<16)|(std::uint32_t(pending_[i*4+2])<<8)|pending_[i*4+3];
|
||||
for (int i=16;i<64;++i) {
|
||||
const auto x=w[i-15],y=w[i-2];
|
||||
w[i]=w[i-16]+(std::rotr(x,7)^std::rotr(x,18)^(x>>3))+w[i-7]+(std::rotr(y,17)^std::rotr(y,19)^(y>>10));
|
||||
std::array<std::uint32_t, 64> w{};
|
||||
for (int i = 0; i < 16; ++i)
|
||||
w[i] = (std::uint32_t(pending_[i * 4]) << 24) |
|
||||
(std::uint32_t(pending_[i * 4 + 1]) << 16) |
|
||||
(std::uint32_t(pending_[i * 4 + 2]) << 8) | pending_[i * 4 + 3];
|
||||
for (int i = 16; i < 64; ++i) {
|
||||
const auto x = w[i - 15], y = w[i - 2];
|
||||
w[i] = w[i - 16] + (std::rotr(x, 7) ^ std::rotr(x, 18) ^ (x >> 3)) + w[i - 7] +
|
||||
(std::rotr(y, 17) ^ std::rotr(y, 19) ^ (y >> 10));
|
||||
}
|
||||
auto a=state_[0],b=state_[1],c=state_[2],d=state_[3],e=state_[4],f=state_[5],g=state_[6],h=state_[7];
|
||||
for (int i=0;i<64;++i) {
|
||||
const auto t1=h+(std::rotr(e,6)^std::rotr(e,11)^std::rotr(e,25))+((e&f)^(~e&g))+constants[i]+w[i];
|
||||
const auto t2=(std::rotr(a,2)^std::rotr(a,13)^std::rotr(a,22))+((a&b)^(a&c)^(b&c));
|
||||
h=g;g=f;f=e;e=d+t1;d=c;c=b;b=a;a=t1+t2;
|
||||
auto a = state_[0], b = state_[1], c = state_[2], d = state_[3], e = state_[4],
|
||||
f = state_[5], g = state_[6], h = state_[7];
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
const auto t1 = h + (std::rotr(e, 6) ^ std::rotr(e, 11) ^ std::rotr(e, 25)) +
|
||||
((e & f) ^ (~e & g)) + constants[i] + w[i];
|
||||
const auto t2 = (std::rotr(a, 2) ^ std::rotr(a, 13) ^ std::rotr(a, 22)) +
|
||||
((a & b) ^ (a & c) ^ (b & c));
|
||||
h = g;
|
||||
g = f;
|
||||
f = e;
|
||||
e = d + t1;
|
||||
d = c;
|
||||
c = b;
|
||||
b = a;
|
||||
a = t1 + t2;
|
||||
}
|
||||
state_[0]+=a;state_[1]+=b;state_[2]+=c;state_[3]+=d;state_[4]+=e;state_[5]+=f;state_[6]+=g;state_[7]+=h;
|
||||
state_[0] += a;
|
||||
state_[1] += b;
|
||||
state_[2] += c;
|
||||
state_[3] += d;
|
||||
state_[4] += e;
|
||||
state_[5] += f;
|
||||
state_[6] += g;
|
||||
state_[7] += h;
|
||||
}
|
||||
public:
|
||||
|
||||
public:
|
||||
void update(std::span<const std::byte> bytes) {
|
||||
count_+=bytes.size();
|
||||
for (auto byte:bytes) {
|
||||
pending_[used_++]=std::to_integer<std::uint8_t>(byte);
|
||||
if (used_==64) { block();used_=0; }
|
||||
count_ += bytes.size();
|
||||
for (auto byte : bytes) {
|
||||
pending_[used_++] = std::to_integer<std::uint8_t>(byte);
|
||||
if (used_ == 64) {
|
||||
block();
|
||||
used_ = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string finish() {
|
||||
const std::uint64_t bits=count_*8;
|
||||
pending_[used_++]=0x80;
|
||||
if (used_>56) { while(used_<64) pending_[used_++]=0;block();used_=0; }
|
||||
while(used_<56) pending_[used_++]=0;
|
||||
for (int i=7;i>=0;--i) pending_[used_++]=std::uint8_t(bits>>(i*8));
|
||||
const std::uint64_t bits = count_ * 8;
|
||||
pending_[used_++] = 0x80;
|
||||
if (used_ > 56) {
|
||||
while (used_ < 64)
|
||||
pending_[used_++] = 0;
|
||||
block();
|
||||
used_ = 0;
|
||||
}
|
||||
while (used_ < 56)
|
||||
pending_[used_++] = 0;
|
||||
for (int i = 7; i >= 0; --i)
|
||||
pending_[used_++] = std::uint8_t(bits >> (i * 8));
|
||||
block();
|
||||
constexpr char hex[]="0123456789abcdef";
|
||||
std::string result;result.reserve(64);
|
||||
for (auto word:state_) for (int i=7;i>=0;--i) result+=hex[(word>>(i*4))&15];
|
||||
constexpr char hex[] = "0123456789abcdef";
|
||||
std::string result;
|
||||
result.reserve(64);
|
||||
for (auto word : state_)
|
||||
for (int i = 7; i >= 0; --i)
|
||||
result += hex[(word >> (i * 4)) & 15];
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
std::string sha256(std::span<const std::byte> bytes) { Digest digest;digest.update(bytes);return digest.finish(); }
|
||||
std::string sha256_file(const std::filesystem::path& path) {
|
||||
std::ifstream stream(path,std::ios::binary);
|
||||
require(bool(stream),"io.open","Cannot open file for hashing: "+path.string());
|
||||
Digest digest;std::array<char,65536> buffer{};
|
||||
while(stream) { stream.read(buffer.data(),buffer.size());digest.update(std::as_bytes(std::span(buffer.data(),static_cast<std::size_t>(stream.gcount())))); }
|
||||
require(stream.eof(),"io.read","Cannot read file for hashing: "+path.string());
|
||||
} // namespace
|
||||
std::string sha256(std::span<const std::byte> bytes) {
|
||||
Digest digest;
|
||||
digest.update(bytes);
|
||||
return digest.finish();
|
||||
}
|
||||
std::string sha256_file(const std::filesystem::path& path) {
|
||||
std::ifstream stream(path, std::ios::binary);
|
||||
require(bool(stream), "io.open", "Cannot open file for hashing: " + path.string());
|
||||
Digest digest;
|
||||
std::array<char, 65536> buffer{};
|
||||
while (stream) {
|
||||
stream.read(buffer.data(), buffer.size());
|
||||
digest.update(
|
||||
std::as_bytes(std::span(buffer.data(), static_cast<std::size_t>(stream.gcount()))));
|
||||
}
|
||||
require(stream.eof(), "io.read", "Cannot read file for hashing: " + path.string());
|
||||
return digest.finish();
|
||||
}
|
||||
} // namespace faset
|
||||
|
||||
+97
-43
@@ -1,9 +1,9 @@
|
||||
#include <faset/core/io.hpp>
|
||||
#include <faset/core/error.hpp>
|
||||
#include <array>
|
||||
#include <faset/core/error.hpp>
|
||||
#include <faset/core/io.hpp>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#ifdef _WIN32
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
@@ -16,57 +16,111 @@ namespace faset {
|
||||
std::string new_id() {
|
||||
static std::mutex mutex;
|
||||
static std::random_device random;
|
||||
std::array<unsigned char,16> bytes{};
|
||||
{ std::lock_guard lock(mutex); for(auto& byte:bytes) byte=static_cast<unsigned char>(random()); }
|
||||
bytes[6]=(bytes[6]&0x0f)|0x40;bytes[8]=(bytes[8]&0x3f)|0x80;
|
||||
constexpr char hex[]="0123456789abcdef";
|
||||
std::string result;result.reserve(36);
|
||||
for(std::size_t i=0;i<bytes.size();++i) { if(i==4||i==6||i==8||i==10)result+='-';result+=hex[bytes[i]>>4];result+=hex[bytes[i]&15]; }
|
||||
std::array<unsigned char, 16> bytes{};
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
for (auto& byte : bytes)
|
||||
byte = static_cast<unsigned char>(random());
|
||||
}
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
constexpr char hex[] = "0123456789abcdef";
|
||||
std::string result;
|
||||
result.reserve(36);
|
||||
for (std::size_t i = 0; i < bytes.size(); ++i) {
|
||||
if (i == 4 || i == 6 || i == 8 || i == 10)
|
||||
result += '-';
|
||||
result += hex[bytes[i] >> 4];
|
||||
result += hex[bytes[i] & 15];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
std::string read_text(const std::filesystem::path& path) {
|
||||
std::ifstream stream(path,std::ios::binary);
|
||||
require(bool(stream),"io.open","Cannot open file: "+path.string());
|
||||
std::string value((std::istreambuf_iterator<char>(stream)),{});
|
||||
require(!stream.bad(),"io.read","Cannot read file: "+path.string());return value;
|
||||
std::ifstream stream(path, std::ios::binary);
|
||||
require(bool(stream), "io.open", "Cannot open file: " + path.string());
|
||||
std::string value((std::istreambuf_iterator<char>(stream)), {});
|
||||
require(!stream.bad(), "io.read", "Cannot read file: " + path.string());
|
||||
return value;
|
||||
}
|
||||
Json read_json(const std::filesystem::path& path) {
|
||||
try { return Json::parse(read_text(path)); }
|
||||
catch(const Json::exception& error) { throw Error("format.json","Invalid JSON in "+path.string(),{{"reason",error.what()}}); }
|
||||
try {
|
||||
return Json::parse(read_text(path));
|
||||
} catch (const Json::exception& error) {
|
||||
throw Error("format.json", "Invalid JSON in " + path.string(), {{"reason", error.what()}});
|
||||
}
|
||||
}
|
||||
void atomic_write(const std::filesystem::path& path,std::string_view bytes) {
|
||||
const auto parent=path.has_parent_path()?path.parent_path():std::filesystem::path(".");
|
||||
void atomic_write(const std::filesystem::path& path, std::string_view bytes) {
|
||||
const auto parent = path.has_parent_path() ? path.parent_path() : std::filesystem::path(".");
|
||||
std::filesystem::create_directories(parent);
|
||||
const auto temporary=parent/(path.filename().string()+".tmp-"+new_id());
|
||||
const auto temporary = parent / (path.filename().string() + ".tmp-" + new_id());
|
||||
try {
|
||||
#ifdef _WIN32
|
||||
HANDLE file=CreateFileW(temporary.c_str(),GENERIC_WRITE,0,nullptr,CREATE_NEW,FILE_ATTRIBUTE_NORMAL,nullptr);
|
||||
require(file!=INVALID_HANDLE_VALUE,"io.create","Cannot create temporary file");
|
||||
bool ok=true;std::size_t offset=0;
|
||||
while(offset<bytes.size()) { DWORD written=0; const auto count=static_cast<DWORD>(std::min<std::size_t>(bytes.size()-offset,1u<<30)); if(!WriteFile(file,bytes.data()+offset,count,&written,nullptr)||written==0){ok=false;break;} offset+=written; }
|
||||
ok=FlushFileBuffers(file)&&ok;CloseHandle(file);
|
||||
require(ok,"io.write","Cannot flush temporary file");
|
||||
require(MoveFileExW(temporary.c_str(),path.c_str(),MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH)!=0,"io.replace","Cannot publish file: "+path.string());
|
||||
HANDLE file = CreateFileW(temporary.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
require(file != INVALID_HANDLE_VALUE, "io.create", "Cannot create temporary file");
|
||||
bool ok = true;
|
||||
std::size_t offset = 0;
|
||||
while (offset < bytes.size()) {
|
||||
DWORD written = 0;
|
||||
const auto count =
|
||||
static_cast<DWORD>(std::min<std::size_t>(bytes.size() - offset, 1u << 30));
|
||||
if (!WriteFile(file, bytes.data() + offset, count, &written, nullptr) || written == 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
offset += written;
|
||||
}
|
||||
ok = FlushFileBuffers(file) && ok;
|
||||
CloseHandle(file);
|
||||
require(ok, "io.write", "Cannot flush temporary file");
|
||||
require(MoveFileExW(temporary.c_str(), path.c_str(),
|
||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0,
|
||||
"io.replace", "Cannot publish file: " + path.string());
|
||||
#else
|
||||
const int fd=::open(temporary.c_str(),O_WRONLY|O_CREAT|O_EXCL,0644);
|
||||
require(fd>=0,"io.create","Cannot create temporary file");
|
||||
bool ok=true;std::size_t offset=0;
|
||||
while(offset<bytes.size()) { const auto count=::write(fd,bytes.data()+offset,bytes.size()-offset);if(count<0&&errno==EINTR)continue;if(count<=0){ok=false;break;}offset+=static_cast<std::size_t>(count); }
|
||||
ok=(::fsync(fd)==0)&&ok;const auto closed=::close(fd);ok=ok&&(closed==0);
|
||||
require(ok,"io.write","Cannot flush temporary file");
|
||||
std::filesystem::rename(temporary,path);
|
||||
const int directory=::open(parent.c_str(),O_RDONLY|O_DIRECTORY);
|
||||
if(directory>=0){::fsync(directory);::close(directory);}
|
||||
const int fd = ::open(temporary.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0644);
|
||||
require(fd >= 0, "io.create", "Cannot create temporary file");
|
||||
bool ok = true;
|
||||
std::size_t offset = 0;
|
||||
while (offset < bytes.size()) {
|
||||
const auto count = ::write(fd, bytes.data() + offset, bytes.size() - offset);
|
||||
if (count < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (count <= 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
offset += static_cast<std::size_t>(count);
|
||||
}
|
||||
ok = (::fsync(fd) == 0) && ok;
|
||||
const auto closed = ::close(fd);
|
||||
ok = ok && (closed == 0);
|
||||
require(ok, "io.write", "Cannot flush temporary file");
|
||||
std::filesystem::rename(temporary, path);
|
||||
const int directory = ::open(parent.c_str(), O_RDONLY | O_DIRECTORY);
|
||||
if (directory >= 0) {
|
||||
::fsync(directory);
|
||||
::close(directory);
|
||||
}
|
||||
#endif
|
||||
} catch(...) { std::error_code ignored;std::filesystem::remove(temporary,ignored);throw; }
|
||||
} catch (...) {
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(temporary, ignored);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
void atomic_write_json(const std::filesystem::path& path,const Json& value) { atomic_write(path,value.dump(2)+"\n"); }
|
||||
std::filesystem::path project_path(const std::filesystem::path& root,const std::filesystem::path& relative) {
|
||||
require(!relative.is_absolute(),"path.outside_project","Expected a path relative to the project");
|
||||
const auto canonical=std::filesystem::weakly_canonical(root);
|
||||
const auto target=std::filesystem::weakly_canonical(canonical/relative);
|
||||
auto a=canonical.begin(),b=target.begin();
|
||||
for(;a!=canonical.end();++a,++b) require(b!=target.end()&&*a==*b,"path.outside_project","Path escapes the project root");
|
||||
void atomic_write_json(const std::filesystem::path& path, const Json& value) {
|
||||
atomic_write(path, value.dump(2) + "\n");
|
||||
}
|
||||
std::filesystem::path project_path(const std::filesystem::path& root,
|
||||
const std::filesystem::path& relative) {
|
||||
require(!relative.is_absolute(), "path.outside_project",
|
||||
"Expected a path relative to the project");
|
||||
const auto canonical = std::filesystem::weakly_canonical(root);
|
||||
const auto target = std::filesystem::weakly_canonical(canonical / relative);
|
||||
auto a = canonical.begin(), b = target.begin();
|
||||
for (; a != canonical.end(); ++a, ++b)
|
||||
require(b != target.end() && *a == *b, "path.outside_project",
|
||||
"Path escapes the project root");
|
||||
return target;
|
||||
}
|
||||
}
|
||||
} // namespace faset
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <faset/core/process.hpp>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#ifdef _WIN32
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <fcntl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
extern char** environ;
|
||||
#endif
|
||||
|
||||
namespace faset {
|
||||
namespace {
|
||||
#ifdef _WIN32
|
||||
std::wstring widen(const std::string& value) {
|
||||
if (value.empty())
|
||||
return {};
|
||||
int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(),
|
||||
static_cast<int>(value.size()), nullptr, 0);
|
||||
if (!length)
|
||||
throw std::runtime_error("Invalid UTF-8 process argument");
|
||||
std::wstring result(length, 0);
|
||||
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast<int>(value.size()),
|
||||
result.data(), length);
|
||||
return result;
|
||||
}
|
||||
std::wstring quote(const std::wstring& value) {
|
||||
std::wstring result = L"\"";
|
||||
std::size_t slashes{};
|
||||
for (wchar_t c : value) {
|
||||
if (c == L'\\') {
|
||||
++slashes;
|
||||
continue;
|
||||
}
|
||||
if (c == L'\"') {
|
||||
result.append(slashes * 2 + 1, L'\\');
|
||||
result += c;
|
||||
} else {
|
||||
result.append(slashes, L'\\');
|
||||
result += c;
|
||||
}
|
||||
slashes = 0;
|
||||
}
|
||||
result.append(slashes * 2, L'\\');
|
||||
result += L'\"';
|
||||
return result;
|
||||
}
|
||||
struct CaseInsensitive {
|
||||
bool operator()(const std::wstring& a, const std::wstring& b) const {
|
||||
return _wcsicmp(a.c_str(), b.c_str()) < 0;
|
||||
}
|
||||
};
|
||||
#else
|
||||
std::filesystem::path resolve_program(const std::string& name, const std::string& path,
|
||||
const std::filesystem::path& cwd) {
|
||||
if (name.find('/') != std::string::npos) {
|
||||
auto file = std::filesystem::path(name);
|
||||
if (file.is_relative())
|
||||
file = cwd / file;
|
||||
if (::access(file.c_str(), X_OK) == 0 && !std::filesystem::is_directory(file))
|
||||
return std::filesystem::absolute(file);
|
||||
throw std::runtime_error("Executable is missing or not executable: " + name);
|
||||
}
|
||||
std::size_t from{};
|
||||
do {
|
||||
auto end = path.find(':', from);
|
||||
auto directory = path.substr(from, end == std::string::npos ? end : end - from);
|
||||
auto file = (directory.empty() ? cwd : std::filesystem::path(directory)) / name;
|
||||
if (file.is_relative())
|
||||
file = cwd / file;
|
||||
if (::access(file.c_str(), X_OK) == 0 && !std::filesystem::is_directory(file))
|
||||
return std::filesystem::absolute(file);
|
||||
if (end == std::string::npos)
|
||||
break;
|
||||
from = end + 1;
|
||||
} while (true);
|
||||
throw std::runtime_error("Executable not found on PATH: " + name);
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
std::filesystem::path find_executable(const std::string& name) {
|
||||
#ifdef _WIN32
|
||||
auto wide = widen(name);
|
||||
std::vector<wchar_t> buffer(32768);
|
||||
DWORD length = SearchPathW(nullptr, wide.c_str(), L".exe", static_cast<DWORD>(buffer.size()),
|
||||
buffer.data(), nullptr);
|
||||
if (length == 0 || length >= buffer.size())
|
||||
throw std::runtime_error("Executable not found on PATH: " + name);
|
||||
return std::filesystem::path(std::wstring(buffer.data(), length));
|
||||
#else
|
||||
const char* path = std::getenv("PATH");
|
||||
return resolve_program(name, path ? path : "", std::filesystem::current_path());
|
||||
#endif
|
||||
}
|
||||
struct Process::Impl {
|
||||
#ifdef _WIN32
|
||||
HANDLE process{}, thread{}, job{}, output{};
|
||||
#else
|
||||
pid_t pid{-1};
|
||||
int output{-1};
|
||||
#endif
|
||||
bool running{};
|
||||
std::optional<int> exit_code;
|
||||
~Impl() {
|
||||
try {
|
||||
cancel();
|
||||
} catch (...) {
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (output)
|
||||
CloseHandle(output);
|
||||
if (thread)
|
||||
CloseHandle(thread);
|
||||
if (process)
|
||||
CloseHandle(process);
|
||||
if (job)
|
||||
CloseHandle(job);
|
||||
#else
|
||||
if (output >= 0)
|
||||
::close(output);
|
||||
#endif
|
||||
}
|
||||
ProcessPoll poll() {
|
||||
std::string text;
|
||||
char buffer[8192];
|
||||
#ifdef _WIN32
|
||||
if (output) {
|
||||
DWORD available{};
|
||||
while (PeekNamedPipe(output, nullptr, 0, nullptr, &available, nullptr) && available) {
|
||||
DWORD read{};
|
||||
if (!ReadFile(output, buffer, std::min<DWORD>(available, sizeof(buffer)), &read,
|
||||
nullptr) ||
|
||||
!read)
|
||||
break;
|
||||
text.append(buffer, read);
|
||||
}
|
||||
}
|
||||
if (running && WaitForSingleObject(process, 0) == WAIT_OBJECT_0) {
|
||||
DWORD code{};
|
||||
if (!GetExitCodeProcess(process, &code))
|
||||
throw std::runtime_error("Cannot read process exit status");
|
||||
running = false;
|
||||
exit_code = static_cast<int>(code);
|
||||
}
|
||||
#else
|
||||
if (output >= 0) {
|
||||
while (true) {
|
||||
auto count = ::read(output, buffer, sizeof(buffer));
|
||||
if (count > 0) {
|
||||
text.append(buffer, static_cast<std::size_t>(count));
|
||||
continue;
|
||||
}
|
||||
if (count < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (count == 0) {
|
||||
::close(output);
|
||||
output = -1;
|
||||
} else if (errno != EAGAIN && errno != EWOULDBLOCK)
|
||||
throw std::runtime_error("Cannot read child output");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (running) {
|
||||
int status{};
|
||||
pid_t result;
|
||||
do {
|
||||
result = ::waitpid(pid, &status, WNOHANG);
|
||||
} while (result < 0 && errno == EINTR);
|
||||
if (result == pid) {
|
||||
running = false;
|
||||
exit_code = WIFEXITED(status) ? WEXITSTATUS(status)
|
||||
: WIFSIGNALED(status) ? 128 + WTERMSIG(status)
|
||||
: 1;
|
||||
} else if (result < 0)
|
||||
throw std::runtime_error("Cannot collect child process");
|
||||
}
|
||||
#endif
|
||||
if (!running) {
|
||||
#ifdef _WIN32
|
||||
DWORD available{};
|
||||
while (output && PeekNamedPipe(output, nullptr, 0, nullptr, &available, nullptr) &&
|
||||
available) {
|
||||
DWORD read{};
|
||||
if (!ReadFile(output, buffer, std::min<DWORD>(available, sizeof(buffer)), &read,
|
||||
nullptr) ||
|
||||
!read)
|
||||
break;
|
||||
text.append(buffer, read);
|
||||
}
|
||||
#else
|
||||
if (output >= 0)
|
||||
while (true) {
|
||||
auto count = ::read(output, buffer, sizeof(buffer));
|
||||
if (count > 0) {
|
||||
text.append(buffer, static_cast<std::size_t>(count));
|
||||
continue;
|
||||
}
|
||||
if (count < 0 && errno == EINTR)
|
||||
continue;
|
||||
if (count == 0) {
|
||||
::close(output);
|
||||
output = -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
return {running, exit_code, std::move(text)};
|
||||
}
|
||||
void cancel() {
|
||||
if (!running)
|
||||
return;
|
||||
#ifdef _WIN32
|
||||
if (job)
|
||||
TerminateJobObject(job, 130);
|
||||
TerminateProcess(process, 130);
|
||||
WaitForSingleObject(process, INFINITE);
|
||||
DWORD code{};
|
||||
GetExitCodeProcess(process, &code);
|
||||
exit_code = static_cast<int>(code);
|
||||
running = false;
|
||||
#else
|
||||
::kill(-pid, SIGTERM);
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(200);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
int status{};
|
||||
auto result = ::waitpid(pid, &status, WNOHANG);
|
||||
if (result == pid) {
|
||||
running = false;
|
||||
exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status);
|
||||
::kill(-pid, SIGKILL);
|
||||
return;
|
||||
}
|
||||
if (result < 0 && errno != EINTR) {
|
||||
running = false;
|
||||
return;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
::kill(-pid, SIGKILL);
|
||||
int status{};
|
||||
while (::waitpid(pid, &status, 0) < 0 && errno == EINTR) {
|
||||
}
|
||||
running = false;
|
||||
exit_code = 130;
|
||||
#endif
|
||||
}
|
||||
};
|
||||
Process::Process(const ProcessOptions& options) : impl_(std::make_unique<Impl>()) {
|
||||
if (options.arguments.empty() || options.arguments[0].empty())
|
||||
throw std::invalid_argument("Process requires an executable");
|
||||
for (const auto& argument : options.arguments)
|
||||
if (argument.find('\0') != std::string::npos)
|
||||
throw std::invalid_argument("NUL in process argument");
|
||||
auto cwd = options.working_directory.empty()
|
||||
? std::filesystem::current_path()
|
||||
: std::filesystem::absolute(options.working_directory);
|
||||
if (!std::filesystem::is_directory(cwd))
|
||||
throw std::runtime_error("Process working directory does not exist");
|
||||
#ifdef _WIN32
|
||||
auto program = std::filesystem::path(widen(options.arguments[0]));
|
||||
if (program.has_parent_path() && program.is_relative())
|
||||
program = cwd / program;
|
||||
auto executable = program.has_parent_path() ? program : find_executable(options.arguments[0]);
|
||||
std::wstring command;
|
||||
for (const auto& argument : options.arguments) {
|
||||
if (!command.empty())
|
||||
command += L' ';
|
||||
command += quote(widen(argument));
|
||||
}
|
||||
std::map<std::wstring, std::wstring, CaseInsensitive> environment;
|
||||
auto block = GetEnvironmentStringsW();
|
||||
if (!block)
|
||||
throw std::runtime_error("Cannot read process environment");
|
||||
for (auto entry = block; *entry; entry += wcslen(entry) + 1) {
|
||||
std::wstring text(entry);
|
||||
auto equals = text.find(L'=', text[0] == L'=' ? 1 : 0);
|
||||
if (equals != std::wstring::npos)
|
||||
environment[text.substr(0, equals)] = text.substr(equals + 1);
|
||||
}
|
||||
FreeEnvironmentStringsW(block);
|
||||
for (auto& [key, value] : options.environment) {
|
||||
if (key.empty() || key.find('=') != std::string::npos ||
|
||||
key.find('\0') != std::string::npos || value.find('\0') != std::string::npos)
|
||||
throw std::invalid_argument("Invalid environment entry");
|
||||
environment[widen(key)] = widen(value);
|
||||
}
|
||||
std::vector<wchar_t> env;
|
||||
for (auto& [key, value] : environment) {
|
||||
auto item = key + L"=" + value;
|
||||
env.insert(env.end(), item.begin(), item.end());
|
||||
env.push_back(0);
|
||||
}
|
||||
env.push_back(0);
|
||||
SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
|
||||
HANDLE output_write{}, input{};
|
||||
if (!CreatePipe(&impl_->output, &output_write, &security, 0))
|
||||
throw std::runtime_error("Cannot create process pipe");
|
||||
SetHandleInformation(impl_->output, HANDLE_FLAG_INHERIT, 0);
|
||||
input = CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, &security,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
SIZE_T bytes{};
|
||||
InitializeProcThreadAttributeList(nullptr, 1, 0, &bytes);
|
||||
std::vector<std::byte> storage(bytes);
|
||||
auto* attributes = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(storage.data());
|
||||
bool initialized = InitializeProcThreadAttributeList(attributes, 1, 0, &bytes) != 0;
|
||||
HANDLE handles[] = {output_write, input};
|
||||
bool updated =
|
||||
initialized && UpdateProcThreadAttribute(attributes, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
||||
handles, sizeof(handles), nullptr, nullptr) != 0;
|
||||
STARTUPINFOEXW startup{};
|
||||
startup.StartupInfo.cb = sizeof(startup);
|
||||
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup.StartupInfo.hStdOutput = startup.StartupInfo.hStdError = output_write;
|
||||
startup.StartupInfo.hStdInput = input;
|
||||
startup.lpAttributeList = attributes;
|
||||
PROCESS_INFORMATION process{};
|
||||
bool launched =
|
||||
updated && CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, TRUE,
|
||||
CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED |
|
||||
CREATE_NEW_PROCESS_GROUP | EXTENDED_STARTUPINFO_PRESENT,
|
||||
env.data(), cwd.c_str(), &startup.StartupInfo, &process) != 0;
|
||||
if (initialized)
|
||||
DeleteProcThreadAttributeList(attributes);
|
||||
CloseHandle(output_write);
|
||||
if (input != INVALID_HANDLE_VALUE)
|
||||
CloseHandle(input);
|
||||
if (!launched)
|
||||
throw std::runtime_error("CreateProcess failed for " + options.arguments[0]);
|
||||
impl_->process = process.hProcess;
|
||||
impl_->thread = process.hThread;
|
||||
impl_->running = true;
|
||||
impl_->job = CreateJobObjectW(nullptr, nullptr);
|
||||
if (!impl_->job)
|
||||
throw std::runtime_error("Cannot create compiler job object");
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{};
|
||||
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
if (!SetInformationJobObject(impl_->job, JobObjectExtendedLimitInformation, &limits,
|
||||
sizeof(limits)) ||
|
||||
!AssignProcessToJobObject(impl_->job, impl_->process))
|
||||
throw std::runtime_error("Cannot isolate compiler process group");
|
||||
if (ResumeThread(impl_->thread) == DWORD(-1))
|
||||
throw std::runtime_error("Cannot start compiler process");
|
||||
#else
|
||||
std::map<std::string, std::string> environment;
|
||||
for (char** item = environ; *item; ++item) {
|
||||
std::string text(*item);
|
||||
auto separator = text.find('=');
|
||||
if (separator != std::string::npos)
|
||||
environment[text.substr(0, separator)] = text.substr(separator + 1);
|
||||
}
|
||||
for (const auto& [key, value] : options.environment) {
|
||||
if (key.empty() || key.find('=') != std::string::npos ||
|
||||
key.find('\0') != std::string::npos || value.find('\0') != std::string::npos)
|
||||
throw std::invalid_argument("Invalid environment entry");
|
||||
environment[key] = value;
|
||||
}
|
||||
auto executable = resolve_program(options.arguments[0], environment["PATH"], cwd);
|
||||
std::vector<std::string> env_storage;
|
||||
std::vector<char*> argv, envp;
|
||||
for (const auto& argument : options.arguments)
|
||||
argv.push_back(const_cast<char*>(argument.c_str()));
|
||||
argv.push_back(nullptr);
|
||||
for (const auto& [key, value] : environment)
|
||||
env_storage.push_back(key + "=" + value);
|
||||
for (auto& entry : env_storage)
|
||||
envp.push_back(entry.data());
|
||||
envp.push_back(nullptr);
|
||||
int pipes[2];
|
||||
if (::pipe2(pipes, O_CLOEXEC) < 0)
|
||||
throw std::runtime_error("Cannot create process pipe");
|
||||
int input = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
|
||||
if (input < 0) {
|
||||
::close(pipes[0]);
|
||||
::close(pipes[1]);
|
||||
throw std::runtime_error("Cannot open process input");
|
||||
}
|
||||
auto pid = ::fork();
|
||||
if (pid == 0) {
|
||||
::setpgid(0, 0);
|
||||
::close(pipes[0]);
|
||||
if (::dup2(input, STDIN_FILENO) < 0 || ::dup2(pipes[1], STDOUT_FILENO) < 0 ||
|
||||
::dup2(pipes[1], STDERR_FILENO) < 0 || ::chdir(cwd.c_str()) < 0)
|
||||
::_exit(126);
|
||||
::close(input);
|
||||
::close(pipes[1]);
|
||||
::execve(executable.c_str(), argv.data(), envp.data());
|
||||
constexpr char message[] = "Cannot execute child process\n";
|
||||
::write(STDERR_FILENO, message, sizeof(message) - 1);
|
||||
::_exit(127);
|
||||
}
|
||||
::close(input);
|
||||
::close(pipes[1]);
|
||||
if (pid < 0) {
|
||||
::close(pipes[0]);
|
||||
throw std::runtime_error("Cannot fork process");
|
||||
}
|
||||
::setpgid(pid, pid);
|
||||
impl_->pid = pid;
|
||||
impl_->output = pipes[0];
|
||||
impl_->running = true;
|
||||
int flags = ::fcntl(pipes[0], F_GETFL, 0);
|
||||
if (flags < 0 || ::fcntl(pipes[0], F_SETFL, flags | O_NONBLOCK) < 0)
|
||||
throw std::runtime_error("Cannot configure process output pipe");
|
||||
#endif
|
||||
}
|
||||
Process::~Process() = default;
|
||||
Process::Process(Process&&) noexcept = default;
|
||||
Process& Process::operator=(Process&&) noexcept = default;
|
||||
ProcessPoll Process::poll() {
|
||||
return impl_->poll();
|
||||
}
|
||||
void Process::cancel() {
|
||||
impl_->cancel();
|
||||
}
|
||||
} // namespace faset
|
||||
Reference in New Issue
Block a user