From 199f9b0a79de0fb0fd57ce9307b36390339ee7e7 Mon Sep 17 00:00:00 2001 From: Picnoir Date: Fri, 5 Apr 2024 11:33:17 +0200 Subject: feat(tvix/nix-compat): read client setting from wire Add the primitives necessary to read the client settings from the Nix daemon wire protocol. Introducing the read_string primitive. This trivial primitive parses a read_bytes call, check the bytes are valid utf-8 bytes and wraps the result in a String. Change-Id: Ie1253523a6bd4e31e7924e9898a0898109da2fa0 Reviewed-on: https://cl.tvl.fyi/c/depot/+/11358 Reviewed-by: flokli Tested-by: BuildkiteCI --- tvix/nix-compat/src/wire/bytes.rs | 20 ++- tvix/nix-compat/src/wire/worker_protocol.rs | 208 +++++++++++++++++++++++++++- 2 files changed, 225 insertions(+), 3 deletions(-) diff --git a/tvix/nix-compat/src/wire/bytes.rs b/tvix/nix-compat/src/wire/bytes.rs index 1a25daca8b..070f2368fd 100644 --- a/tvix/nix-compat/src/wire/bytes.rs +++ b/tvix/nix-compat/src/wire/bytes.rs @@ -1,4 +1,7 @@ -use std::ops::RangeBounds; +use std::{ + io::{Error, ErrorKind}, + ops::RangeBounds, +}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -58,6 +61,21 @@ where Ok(buf) } +/// Read a Nix daemon string from the AsyncWrite, encoded as utf8. +/// Rejects reading more than `allowed_size` bytes +/// +/// A Nix daemon string is made up of two distincts parts: +/// 1. Its lenght, LE-encoded on 64 bits. +/// 2. Its content. 0-padded on 64 bits. +pub async fn read_string(r: &mut R, allowed_size: S) -> std::io::Result +where + R: AsyncReadExt + Unpin, + S: RangeBounds, +{ + let bytes = read_bytes(r, allowed_size).await?; + String::from_utf8(bytes).map_err(|e| Error::new(ErrorKind::InvalidData, e)) +} + /// Writes a sequence of sized bits to a (hopefully buffered) /// [AsyncWriteExt] handle. /// diff --git a/tvix/nix-compat/src/wire/worker_protocol.rs b/tvix/nix-compat/src/wire/worker_protocol.rs index ed47aedbca..77d5104250 100644 --- a/tvix/nix-compat/src/wire/worker_protocol.rs +++ b/tvix/nix-compat/src/wire/worker_protocol.rs @@ -1,4 +1,7 @@ -use std::io::{Error, ErrorKind}; +use std::{ + collections::HashMap, + io::{Error, ErrorKind}, +}; use enum_primitive_derive::Primitive; use num_traits::{FromPrimitive, ToPrimitive}; @@ -6,8 +9,16 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use crate::wire::primitive; +use super::bytes::read_string; + pub static STDERR_LAST: u64 = 0x616c7473; +/// Max length of a Nix setting name/value. In bytes. +/// +/// This value has been arbitrarily choosen after looking the nix.conf +/// manpage. Don't hesitate to increase it if it's too limiting. +pub static MAX_SETTING_SIZE: u64 = 1024; + /// Worker Operation /// /// These operations are encoded as unsigned 64 bits before being sent @@ -66,11 +77,102 @@ pub enum Operation { AddPermRoot = 47, } +/// Log verbosity. In the Nix wire protocol, the client requests a +/// verbosity level to the daemon, which in turns does not produce any +/// log below this verbosity. +#[derive(Debug, PartialEq, Primitive)] +pub enum Verbosity { + LvlError = 0, + LvlWarn = 1, + LvlNotice = 2, + LvlInfo = 3, + LvlTalkative = 4, + LvlChatty = 5, + LvlDebug = 6, + LvlVomit = 7, +} + +/// Settings requested by the client. These settings are applied to a +/// connection to between the daemon and a client. +#[derive(Debug, PartialEq)] +pub struct ClientSettings { + pub keep_failed: bool, + pub keep_going: bool, + pub try_fallback: bool, + pub verbosity: Verbosity, + pub max_build_jobs: u64, + pub max_silent_time: u64, + pub verbose_build: bool, + pub build_cores: u64, + pub use_substitutes: bool, + /// Key/Value dictionary in charge of overriding the settings set + /// by the Nix config file. + /// + /// Some settings can be safely overidden, + /// some other require the user running the Nix client to be part + /// of the trusted users group. + pub overrides: HashMap, +} + +/// Reads the client settings from the wire. +/// +/// Note: this function **only** reads the settings. It does not +/// manage the log state with the daemon. You'll have to do that on +/// your own. A minimal log implementation will consist in sending +/// back [STDERR_LAST] to the client after reading the client +/// settings. +/// +/// FUTUREWORK: write serialization. +pub async fn read_client_settings( + r: &mut R, + client_version: u64, +) -> std::io::Result { + let keep_failed = primitive::read_bool(r).await?; + let keep_going = primitive::read_bool(r).await?; + let try_fallback = primitive::read_bool(r).await?; + let verbosity_uint = primitive::read_u64(r).await?; + let verbosity = Verbosity::from_u64(verbosity_uint).ok_or_else(|| { + Error::new( + ErrorKind::InvalidData, + format!("Can't convert integer {} to verbosity", verbosity_uint), + ) + })?; + let max_build_jobs = primitive::read_u64(r).await?; + let max_silent_time = primitive::read_u64(r).await?; + _ = primitive::read_u64(r).await?; // obsolete useBuildHook + let verbose_build = primitive::read_bool(r).await?; + _ = primitive::read_u64(r).await?; // obsolete logType + _ = primitive::read_u64(r).await?; // obsolete printBuildTrace + let build_cores = primitive::read_u64(r).await?; + let use_substitutes = primitive::read_bool(r).await?; + let mut overrides = HashMap::new(); + if client_version >= 12 { + let num_overrides = primitive::read_u64(r).await?; + for _ in 0..num_overrides { + let name = read_string(r, 0..MAX_SETTING_SIZE).await?; + let value = read_string(r, 0..MAX_SETTING_SIZE).await?; + overrides.insert(name, value); + } + } + Ok(ClientSettings { + keep_failed, + keep_going, + try_fallback, + verbosity, + max_build_jobs, + max_silent_time, + verbose_build, + build_cores, + use_substitutes, + overrides, + }) +} + /// Read a worker [Operation] from the wire. pub async fn read_op(r: &mut R) -> std::io::Result { let op_number = primitive::read_u64(r).await?; Operation::from_u64(op_number).ok_or(Error::new( - ErrorKind::Other, + ErrorKind::InvalidData, format!("Invalid OP number {}", op_number), )) } @@ -105,3 +207,105 @@ where Trust::NotTrusted => primitive::write_u64(conn, 2).await, } } + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + use tokio_test::io::Builder; + + #[tokio::test] + async fn test_read_client_settings_without_overrides() { + // Client settings bits captured from a Nix 2.3.17 run w/ sockdump (protocol version 21). + let wire_bits = hex!( + "00 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00 \ + 02 00 00 00 00 00 00 00 \ + 10 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00 \ + 01 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00 \ + 01 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00" + ); + let mut mock = Builder::new().read(&wire_bits).build(); + let settings = read_client_settings(&mut mock, 21) + .await + .expect("should parse"); + let expected = ClientSettings { + keep_failed: false, + keep_going: false, + try_fallback: false, + verbosity: Verbosity::LvlNotice, + max_build_jobs: 16, + max_silent_time: 0, + verbose_build: false, + build_cores: 0, + use_substitutes: true, + overrides: HashMap::new(), + }; + assert_eq!(settings, expected); + } + + #[tokio::test] + async fn test_read_client_settings_with_overrides() { + // Client settings bits captured from a Nix 2.3.17 run w/ sockdump (protocol version 21). + let wire_bits = hex!( + "00 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00 \ + 02 00 00 00 00 00 00 00 \ + 10 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00 \ + 01 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00 \ + 00 00 00 00 00 00 00 00 \ + 01 00 00 00 00 00 00 00 \ + 02 00 00 00 00 00 00 00 \ + 0c 00 00 00 00 00 00 00 \ + 61 6c 6c 6f 77 65 64 2d \ + 75 72 69 73 00 00 00 00 \ + 1e 00 00 00 00 00 00 00 \ + 68 74 74 70 73 3a 2f 2f \ + 62 6f 72 64 65 61 75 78 \ + 2e 67 75 69 78 2e 67 6e \ + 75 2e 6f 72 67 2f 00 00 \ + 0d 00 00 00 00 00 00 00 \ + 61 6c 6c 6f 77 65 64 2d \ + 75 73 65 72 73 00 00 00 \ + 0b 00 00 00 00 00 00 00 \ + 6a 65 61 6e 20 70 69 65 \ + 72 72 65 00 00 00 00 00" + ); + let mut mock = Builder::new().read(&wire_bits).build(); + let settings = read_client_settings(&mut mock, 21) + .await + .expect("should parse"); + let overrides = HashMap::from([ + ( + String::from("allowed-uris"), + String::from("https://bordeaux.guix.gnu.org/"), + ), + (String::from("allowed-users"), String::from("jean pierre")), + ]); + let expected = ClientSettings { + keep_failed: false, + keep_going: false, + try_fallback: false, + verbosity: Verbosity::LvlNotice, + max_build_jobs: 16, + max_silent_time: 0, + verbose_build: false, + build_cores: 0, + use_substitutes: true, + overrides, + }; + assert_eq!(settings, expected); + } +} -- cgit 1.4.1