diff options
author | Picnoir <picnoir@alternativebit.fr> | 2024-04-05T09·33+0200 |
---|---|---|
committer | picnoir picnoir <picnoir@alternativebit.fr> | 2024-04-07T20·30+0000 |
commit | 199f9b0a79de0fb0fd57ce9307b36390339ee7e7 (patch) | |
tree | 806fb4ec29232cd25b0b4c41b072948cd2239679 /tvix/nix-compat | |
parent | 289b3126db6248ea7dafddaf9931f4bf277c3d88 (diff) |
feat(tvix/nix-compat): read client setting from wire r/7867
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 <flokli@flokli.de> Tested-by: BuildkiteCI
Diffstat (limited to 'tvix/nix-compat')
-rw-r--r-- | tvix/nix-compat/src/wire/bytes.rs | 20 | ||||
-rw-r--r-- | 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 1a25daca8b4f..070f2368fdc3 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, S>(r: &mut R, allowed_size: S) -> std::io::Result<String> +where + R: AsyncReadExt + Unpin, + S: RangeBounds<u64>, +{ + 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 ed47aedbca46..77d510425016 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<String, String>, +} + +/// 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: AsyncReadExt + Unpin>( + r: &mut R, + client_version: u64, +) -> std::io::Result<ClientSettings> { + 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: AsyncReadExt + Unpin>(r: &mut R) -> std::io::Result<Operation> { 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); + } +} |