libstore: initialize build hooks via rpc

this removes another file descriptor from the build hook. we are now
using only rpc to communicate between build hook and daemon, setting
the stage for a future world in which we don't even need build hooks

Change-Id: I706d9d3f2a420abd42fc7762c205931a45d3c8c5
This commit is contained in:
eldritch horrors
2025-10-17 11:33:00 +00:00
parent 9b886e2acf
commit 21d7cb8e1c
7 changed files with 120 additions and 35 deletions
+37 -16
View File
@@ -6,11 +6,14 @@
#include "lix/libutil/logging.hh"
#include "lix/libutil/rpc.hh"
#include "lix/libutil/types-rpc.hh"
#include "lix/libutil/types.hh"
#include <algorithm>
#include <capnp/rpc-twoparty.h>
#include <chrono>
#include <cstring>
#include <exception>
#include <future>
#include <kj/async.h>
#include <kj/time.h>
#include <set>
#include <memory>
@@ -44,8 +47,9 @@ namespace {
struct Instance final : rpc::build_remote::HookInstance::Server
{
unsigned int maxBuildJobs;
bool initialized = false;
Instance(unsigned int maxBuildJobs) : maxBuildJobs(maxBuildJobs) {}
kj::Promise<void> init(InitContext context) override;
kj::Promise<void> build(BuildContext context) override;
};
@@ -375,30 +379,47 @@ static int main_build_remote(AsyncIoRoot & aio, std::string programName, Strings
verbosity = (Verbosity) std::stoll(argv.front());
FdSource source(STDIN_FILENO);
/* Read the parent's settings. */
while (readNum<unsigned>(source)) {
auto name = readString(source);
auto value = readString(source);
settings.set(name, value);
}
auto maxBuildJobs = settings.maxBuildJobs;
settings.maxBuildJobs.set("1"); // hack to make tests with local?root= work
initPlugins();
auto conn = aio.kj.lowLevelProvider->wrapUnixSocketFd(1);
capnp::TwoPartyServer srv(kj::heap<Instance>(maxBuildJobs));
capnp::TwoPartyServer srv(kj::heap<Instance>());
srv.accept(*conn, 1).wait(aio.kj.waitScope);
return 0;
}
}
kj::Promise<void> Instance::init(InitContext context)
{
try {
if (initialized) {
throw Error("build hook can only be initialized once");
}
/* Read the parent's settings. */
for (const auto & [name, value] : rpc::to<StringMap>(context.getParams().getSettings())) {
settings.set(name, value);
}
maxBuildJobs = settings.maxBuildJobs;
settings.maxBuildJobs.set("1"); // hack to make tests with local?root= work
initPlugins();
initialized = true;
context.getResults().initResult().setGood();
} catch (...) {
RPC_FILL(context.getResults(), initResult, std::current_exception());
}
return kj::READY_NOW;
}
kj::Promise<void> Instance::build(BuildContext context)
{
try {
if (!initialized) {
throw Error("build hook not fully initialized");
}
// FIXME this does not open a daemon connection for historical reasons.
// we may create a lot of build hook instances, and having each of them
// also create a daemon instance is inefficient and wasteful. in future
+1 -1
View File
@@ -1118,7 +1118,7 @@ try {
// close the rpc connection to have the hook exit
hook->rpc = nullptr;
hook->client = std::nullopt;
hook->client = nullptr;
hook->conn = nullptr;
if (auto error = TRY_AWAIT(output)) {
+4 -1
View File
@@ -31,7 +31,10 @@ interface HookInstance {
}
}
build @0 (
init @0 (
settings :Types.Settings
) -> (result :Types.ResultV);
build @1 (
amWilling :Bool,
neededSystem :Data,
drvPath :StoreTypes.StorePath,
+17 -12
View File
@@ -4,7 +4,10 @@
#include "lix/libutil/file-system.hh"
#include "lix/libstore/globals.hh"
#include "lix/libstore/build/hook-instance.hh"
#include "lix/libutil/rpc.hh"
#include "lix/libutil/strings.hh"
#include "lix/libutil/types-rpc.hh" // IWYU pragma: keep
#include <memory>
namespace nix {
@@ -33,9 +36,6 @@ try {
fromHook_.create();
/* Create the communication pipes. */
Pipe toHook_;
toHook_.create();
auto [selfRPC, hookRPC] = SocketPair::stream();
printMsg(lvlChatty, "running build hook: %s", concatMapStringsSep(" ", args, shellEscape));
@@ -50,9 +50,6 @@ try {
if (chdir("/") == -1) throw SysError("changing into /");
/* Dup the communication pipes. */
if (dup2(toHook_.readSide.get(), STDIN_FILENO) == -1) {
throw SysError("dupping to-hook read side");
}
if (dup2(hookRPC.get(), STDOUT_FILENO) == -1) {
throw SysError("dupping to-hook read side");
}
@@ -66,15 +63,23 @@ try {
std::map<std::string, Config::SettingInfo> settings;
globalConfig.getSettings(settings, true);
FdSink sink(toHook_.writeSide.get());
for (auto & setting : settings) {
sink << 1 << setting.first << setting.second.value;
auto conn = AIO().lowLevelProvider.wrapUnixSocketFd(kj::AutoCloseFd(selfRPC.release()));
auto client = std::make_unique<capnp::TwoPartyClient>(*conn, 1);
auto rpc = client->bootstrap().castAs<rpc::build_remote::HookInstance>();
{
auto initReq = rpc.initRequest();
RPC_FILL(initReq, initSettings, settings);
TRY_AWAIT_RPC(initReq.send());
}
sink << 0;
sink.flush();
co_return std::make_unique<HookInstance>(
std::move(fromHook_.readSide), std::move(selfRPC), std::move(pid)
std::move(fromHook_.readSide),
std::move(conn),
std::move(client),
std::move(rpc),
std::move(pid)
);
} catch (...) {
co_return result::current_exception();
+12 -5
View File
@@ -7,6 +7,7 @@
#include "lix/libutil/serialise.hh"
#include <capnp/rpc-twoparty.h>
#include <kj/async-io.h>
#include <memory>
#include <utility>
namespace nix {
@@ -19,7 +20,7 @@ struct HookInstance
AutoCloseFD fromHook;
kj::Own<kj::AsyncCapabilityStream> conn;
std::optional<capnp::TwoPartyClient> client;
std::unique_ptr<capnp::TwoPartyClient> client;
rpc::build_remote::HookInstance::Client rpc;
/**
@@ -31,11 +32,17 @@ struct HookInstance
static kj::Promise<Result<std::unique_ptr<HookInstance>>> create();
HookInstance(AutoCloseFD fromHook, AutoCloseFD rpc, Pid pid)
HookInstance(
AutoCloseFD fromHook,
kj::Own<kj::AsyncCapabilityStream> conn,
std::unique_ptr<capnp::TwoPartyClient> client,
rpc::build_remote::HookInstance::Client rpc,
Pid pid
)
: fromHook(std::move(fromHook))
, conn(AIO().lowLevelProvider.wrapUnixSocketFd(kj::AutoCloseFd(rpc.release())))
, client(std::in_place, *this->conn, 1)
, rpc(client->bootstrap().castAs<rpc::build_remote::HookInstance>())
, conn(std::move(conn))
, client(std::move(client))
, rpc(std::move(rpc))
, pid(std::move(pid))
{
}
+39
View File
@@ -2,6 +2,7 @@
///@file RPC helper functions for `types.hh`
#include "error.hh"
#include "lix/libutil/config.hh"
#include "lix/libutil/result.hh"
#include "lix/libutil/types.capnp.h"
#include "rpc.hh"
@@ -127,4 +128,42 @@ struct Fill<Result<T>, std::exception_ptr>
detail::makeBadResult(rb, e);
}
};
template<>
struct Fill<Settings::Setting, std::pair<const std::string, Config::SettingInfo>>
{
static void fill(
Settings::Setting::Builder sb,
const std::pair<const std::string, Config::SettingInfo> & s,
auto &&... args
)
{
LIX_RPC_FILL(sb, setName, s.first);
LIX_RPC_FILL(sb, setValue, s.second.value);
}
};
template<>
struct Fill<Settings, std::map<std::string, Config::SettingInfo>>
{
static void fill(
Settings::Builder sb, const std::map<std::string, Config::SettingInfo> & s, auto &&... args
)
{
LIX_RPC_FILL(sb, initMap, s);
}
};
template<>
struct Convert<Settings, std::map<std::string, std::string>>
{
static std::map<std::string, std::string> convert(const Settings::Reader & t, auto &&...)
{
std::map<std::string, std::string> result;
for (const auto & s : t.getMap()) {
result[rpc::to<std::string>(s.getName())] = rpc::to<std::string>(s.getValue());
}
return result;
}
};
}
+10
View File
@@ -34,3 +34,13 @@ struct ResultV {
bad @1 :Error;
}
}
struct Settings {
struct Setting {
name @0 :Data;
value @1 :Data;
}
# actually a map, but will treat it as a last-value-wins list of pairs for now.
map @0 :List(Setting);
}