diff --git a/lix/libutil/meson.build b/lix/libutil/meson.build index 31d53df4d..8acae3919 100644 --- a/lix/libutil/meson.build +++ b/lix/libutil/meson.build @@ -48,6 +48,7 @@ liblix_sources += files( 'terminal.cc', 'thread-name.cc', 'thread-pool.cc', + 'types-rpc.cc', 'unix-domain-socket.cc', 'url-name.cc', 'url.cc', diff --git a/lix/libutil/types-rpc.cc b/lix/libutil/types-rpc.cc new file mode 100644 index 000000000..a5ee328bd --- /dev/null +++ b/lix/libutil/types-rpc.cc @@ -0,0 +1,61 @@ +#include "lix/libutil/types-rpc.hh" +#include "libutil/error.hh" +#include "libutil/fmt.hh" +#include "libutil/rpc.hh" +#include "types.capnp.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nix::rpc::error::v1 { + +std::string encodeLossy(const ::nix::ErrorInfo & e) +{ + capnp::MallocMessageBuilder msg; + RPC_FILL(msg, initRoot, e); + + const auto text = e.msg.str(); + + kj::VectorOutputStream out; + capnp::writePackedMessage(out, msg); + return fmt( + "%s %s%s%s", + text.length() > 128 ? "(oversize message)" : text, + V1_ERRORS->getHeader().cStr(), + kj::encodeBase64(out.getArray()).cStr(), + V1_ERRORS->getTrailer().cStr() + ); +} + +std::optional<::nix::ErrorInfo> tryDecode(std::string_view source) +{ + const auto dataStart = source.rfind(V1_ERRORS->getHeader().cStr()); + if (dataStart == source.npos) { + return std::nullopt; + } + source.remove_prefix(dataStart + V1_ERRORS->getHeader().size()); + const auto dataEnd = source.find(V1_ERRORS->getTrailer().cStr()); + if (dataEnd == source.npos) { + return std::nullopt; + } + source = source.substr(0, dataEnd); + + auto decoded = kj::decodeBase64(kj::arrayPtr(source.begin(), source.end())); + if (decoded.hadErrors) { + return std::nullopt; + } + + try { + kj::ArrayInputStream stream{decoded}; + capnp::PackedMessageReader reader{stream}; + return from(reader.getRoot()); + } catch (kj::Exception & e) { // NOLINT(lix-foreign-exceptions): capnp packet format errors + return std::nullopt; + } +} +} diff --git a/lix/libutil/types-rpc.hh b/lix/libutil/types-rpc.hh index ad5e4e092..946618d57 100644 --- a/lix/libutil/types-rpc.hh +++ b/lix/libutil/types-rpc.hh @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include namespace nix::rpc { @@ -166,4 +168,9 @@ struct Convert> return result; } }; + +namespace error::v1 { +std::string encodeLossy(const ::nix::ErrorInfo & e); +std::optional<::nix::ErrorInfo> tryDecode(std::string_view source); +} } diff --git a/lix/libutil/types.capnp b/lix/libutil/types.capnp index 54181a51e..2cdfea42f 100644 --- a/lix/libutil/types.capnp +++ b/lix/libutil/types.capnp @@ -17,6 +17,29 @@ enum Verbosity { vomit @7; } +# NOTE ON ERRORS +# +# interfaces in this system may throw errors in an encoded form. this encoded form is not +# itself meant to be read directly, but to be decoded and rethrown in a better error type +# than the (very limited) kj exceptions. transporting errors in result types inhibits all +# pipelining optimizations capnp can do for us (including early aborts in streaming types +# like encapsulated byte streams, or loggers). all interfaces should be annotated with an +# appropriate marker to document how their errors are transported. these annotations have +# NO IMPACT ON CODE GENERATION, error wrapping must still be done in each implementation. + +struct ErrorEncoding { + header @0 :Text; + trailer @1 :Text; +} + +const v1Errors :ErrorEncoding = ( + header = "{error:ODZmMTlmNjgtMjNiMy00MWE3LTgxYzUtMjY5YWUwN2ZkNDY1Cg:", + trailer = ":v1}", +); + +# annotations on interfaces also apply to methods and recursively to all child interfaces +annotation throws(interface, method) :ErrorEncoding; + struct Error { level @0 :Verbosity; message @1 :Data; diff --git a/tests/unit/libutil/rpc.cc b/tests/unit/libutil/rpc.cc new file mode 100644 index 000000000..178c112a0 --- /dev/null +++ b/tests/unit/libutil/rpc.cc @@ -0,0 +1,111 @@ +#include "lix/libutil/error.hh" +#include "lix/libutil/fmt.hh" +#include "lix/libutil/types-rpc.hh" +#include + +namespace nix { +TEST(RpcErrorV1, shortMessage) +{ + Error e{ErrorInfo{lvlWarn, HintFmt("test message %s", "data")}}; + e.addTrace(nullptr, HintFmt("trace 1 %s", "data")); + e.addTrace(nullptr, HintFmt("trace 2 %s", "data")); + + const auto encoded = rpc::error::v1::encodeLossy(e.info()); + ASSERT_EQ( + encoded, + "test message \x1B[35;1mdata\x1B[0m " + "{error:ODZmMTlmNjgtMjNiMy00MWE3LTgxYzUtMjY5YWUwN2ZkNDY1Cg:" + "EBBQAQIBAREF4hERFv90ZXN0IG1lcwJzYWdlIBtbMzU7MW1kYXRhDxtbMG0RBboRDbr/" + "dHJhY2UgMiAFG1szNTsxbWRhdGEbWzBtAHRyYWNlIDEgG1szNTsxbWRhdGEbWzBtAA==:v1}" + ); + + const auto decoded = rpc::error::v1::tryDecode("remote error: " + encoded); + ASSERT_TRUE(decoded.has_value()); + ASSERT_EQ(decoded->level, e.info().level); + ASSERT_EQ(decoded->msg.str(), e.info().msg.str()); + ASSERT_EQ(decoded->traces.size(), e.info().traces.size()); + for (auto ait = decoded->traces.begin(), bit = e.info().traces.begin(), aend = decoded->traces.end(); + ait != aend; + ++ait, ++bit) + { + ASSERT_EQ(ait->hint.str(), bit->hint.str()); + } +} + +TEST(RpcErrorV1, longMessage) +{ + Error e{ErrorInfo{lvlWarn, HintFmt("test message %s", std::string(1024, 'a'))}}; + e.addTrace(nullptr, HintFmt("trace 1 %s", "data")); + e.addTrace(nullptr, HintFmt("trace 2 %s", "data")); + + const auto encoded = rpc::error::v1::encodeLossy(e.info()); + ASSERT_EQ( + encoded, + "(oversize message) " + "{error:ODZmMTlmNjgtMjNiMy00MWE3LTgxYzUtMjY5YWUwN2ZkNDY1Cg:EI9QAQIBATEFwiATDQIW/" + "3Rlc3QgbWVzgnNhZ2UgG1szNTsxbWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY" + "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY" + "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY" + "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY" + "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY" + "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY" + "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY" + "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY" + "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY" + "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY" + "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY" + "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY" + "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhY" + "WFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEbWzBtE" + "QW6EQ26/3RyYWNlIDIgBRtbMzU7MW1kYXRhG1swbQB0cmFjZSAxIBtbMzU7MW1kYXRhG1swbQA=:v1}" + ); + + const auto decoded = rpc::error::v1::tryDecode("remote error: " + encoded); + ASSERT_TRUE(decoded.has_value()); + ASSERT_EQ(decoded->level, e.info().level); + ASSERT_EQ(decoded->msg.str(), e.info().msg.str()); + ASSERT_EQ(decoded->traces.size(), e.info().traces.size()); + for (auto ait = decoded->traces.begin(), bit = e.info().traces.begin(), aend = decoded->traces.end(); + ait != aend; + ++ait, ++bit) + { + ASSERT_EQ(ait->hint.str(), bit->hint.str()); + } +} + +TEST(RpcErrorV1, noDecode) +{ + // no marker + ASSERT_FALSE( + rpc::error::v1::tryDecode( + "{rror:ODZmMTlmNjgtMjNiMy00MWE3LTgxYzUtMjY5YWUwN2ZkNDY1Cg:" + "EBBQAQIBAREF4hERFv90ZXN0IG1lcwJzYWdlIBtbMzU7MW1kYXRhDxtbMG0RBboRDbr/" + "dHJhY2UgMiAFG1szNTsxbWRhdGEbWzBtAHRyYWNlIDEgG1szNTsxbWRhdGEbWzBtAA==:v1}" + ) + ); + // no trailer + ASSERT_FALSE( + rpc::error::v1::tryDecode( + "{error:ODZmMTlmNjgtMjNiMy00MWE3LTgxYzUtMjY5YWUwN2ZkNDY1Cg:" + "EBBQAQIBAREF4hERFv90ZXN0IG1lcwJzYWdlIBtbMzU7MW1kYXRhDxtbMG0RBboRDbr/" + "dHJhY2UgMiAFG1szNTsxbWRhdGEbWzBtAHRyYWNlIDEgG1szNTsxbWRhdGEbWzBtAA==}" + ) + ); + // bad base64 + ASSERT_FALSE( + rpc::error::v1::tryDecode( + "{error:ODZmMTlmNjgtMjNiMy00MWE3LTgxYzUtMjY5YWUwN2ZkNDY1Cg:" + "EBBQAQIBAREF4hERFv90ZXN0IG1lcwJzYWdlIBtbMzU7MW1kYXRhDxtbMG0RBboRDbr/" + "dHJhY2UgMiAFG1szNTsxbWRhdGEbWzBtAHRyYWNlIDEgG1szNTsxbWRhdGEbWzBtAA=:v1}" + ) + ); + // bad capnp data + ASSERT_FALSE( + rpc::error::v1::tryDecode( + "{error:ODZmMTlmNjgtMjNiMy00MWE3LTgxYzUtMjY5YWUwN2ZkNDY1Cg:" + "eBBQAQIBAREF4hERFv90ZXN0IG1lcwJzYWdlIBtbMzU7MW1kYXRhDxtbMG0RBboRDbr/" + "dHJhY2UgMiAFG1szNTsxbWRhdGEbWzBtAHRyYWNlIDEgG1szNTsxbWRhdGEbWzBtAA==:v1}" + ) + ); +} +} diff --git a/tests/unit/meson.build b/tests/unit/meson.build index c5415bf65..d7f950a94 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -67,6 +67,7 @@ libutil_tests_sources = files( 'libutil/paths-setting.cc', 'libutil/pool.cc', 'libutil/references.cc', + 'libutil/rpc.cc', 'libutil/rust.cc', 'libutil/serialise.cc', 'libutil/suggestions.cc',