diff --git a/Cargo.lock b/Cargo.lock index 7f74825761..ef92e52f36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -815,6 +815,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.5", ] @@ -1301,6 +1302,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "crypto-common", + "subtle", ] [[package]] @@ -1557,6 +1559,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -2038,6 +2055,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hmac" version = "0.10.1" @@ -2048,6 +2071,15 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "home" version = "0.5.9" @@ -2317,6 +2349,19 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.28", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "hyper-util" version = "0.1.3" @@ -2856,6 +2901,26 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "machineid-rs" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ceb4d434d69d7199abc3036541ba6ef86767a4356e3077d5a3419f85b70b14" +dependencies = [ + "hex", + "hmac 0.12.1", + "md-5 0.10.6", + "serde", + "serde_json", + "sha-1 0.10.1", + "sha2 0.10.8", + "sysinfo 0.29.11", + "uuid", + "whoami", + "winreg 0.11.0", + "wmi", +] + [[package]] name = "maplit" version = "1.0.2" @@ -3036,6 +3101,24 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -3181,12 +3264,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2 1.0.82", + "quote 1.0.36", + "syn 2.0.61", +] + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "opentelemetry" version = "0.21.0" @@ -3321,7 +3442,7 @@ dependencies = [ "indexmap 1.9.3", "nvml-wrapper", "opentelemetry 0.22.0", - "sysinfo", + "sysinfo 0.29.11", "tracing", ] @@ -3586,6 +3707,12 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "plotters" version = "0.3.5" @@ -3961,10 +4088,10 @@ checksum = "419a3ad8fa9f9d445e69d9b185a24878ae6e6f55c96e4512f4a0e28cd3bc5c56" dependencies = [ "blowfish 0.7.0", "byteorder", - "hmac", + "hmac 0.10.1", "md-5 0.9.1", "rand", - "sha-1", + "sha-1 0.9.8", "sha2 0.9.9", ] @@ -4178,11 +4305,13 @@ dependencies = [ "http-body 0.4.6", "hyper 0.14.28", "hyper-rustls 0.24.2", + "hyper-tls", "ipnet", "js-sys", "log", "mime", "mime_guess", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -4194,6 +4323,7 @@ dependencies = [ "sync_wrapper", "system-configuration", "tokio", + "tokio-native-tls", "tokio-rustls 0.24.1", "tower-service", "url", @@ -4201,7 +4331,7 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "webpki-roots", - "winreg", + "winreg 0.50.0", ] [[package]] @@ -4700,6 +4830,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sha1" version = "0.10.6" @@ -4986,6 +5127,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "sysinfo" +version = "0.30.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows 0.52.0", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -5097,6 +5253,7 @@ dependencies = [ "tailcall-fixtures", "tailcall-macros", "tailcall-prettier", + "tailcall-tracker", "temp-env", "tempfile", "thiserror", @@ -5194,6 +5351,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "tailcall-tracker" +version = "0.1.0" +dependencies = [ + "anyhow", + "lazy_static", + "machineid-rs", + "reqwest", + "serde", + "serde_json", + "sysinfo 0.30.12", + "tokio", + "tracing", +] + [[package]] name = "tailcall-upstream-grpc" version = "0.1.0" @@ -5434,6 +5606,16 @@ dependencies = [ "syn 2.0.61", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -5919,6 +6101,12 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74797339c3b98616c009c7c3eb53a0ce41e85c8ec66bd3db96ed132d20cfdee8" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -5956,6 +6144,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -6085,6 +6279,17 @@ dependencies = [ "winsafe", ] +[[package]] +name = "whoami" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +dependencies = [ + "redox_syscall", + "wasite", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" @@ -6116,6 +6321,27 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.5", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -6125,6 +6351,28 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "windows-implement" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2ee588991b9e7e6c8338edf3333fbe4da35dc72092643958ebb43f0ab2c49c" +dependencies = [ + "proc-macro2 1.0.82", + "quote 1.0.36", + "syn 1.0.109", +] + +[[package]] +name = "windows-interface" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6fb8df20c9bcaa8ad6ab513f7b40104840c8867d5751126e4df3b08388d0cc7" +dependencies = [ + "proc-macro2 1.0.82", + "quote 1.0.36", + "syn 1.0.109", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -6273,6 +6521,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a1a57ff50e9b408431e8f97d5456f2807f8eb2a2cd79b06068fc87f8ecf189" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "winreg" version = "0.50.0" @@ -6289,6 +6547,20 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "wmi" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daffb44abb7d2e87a1233aa17fdbde0d55b890b32a23a1f908895b87fa6f1a00" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror", + "windows 0.48.0", +] + [[package]] name = "worker" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 3fcf38d835..f25e78cd80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,14 +142,14 @@ dotenvy = "0.15.7" convert_case = "0.6.0" rand = "0.8.5" tailcall-macros = { path = "tailcall-macros" } +tailcall-tracker = { path = "tailcall-tracker", optional = true } tonic-types = "0.11.0" datatest-stable = "0.2.9" tokio-test = "0.4.4" base64 = "0.22.1" - [dev-dependencies] -tailcall-prettier = {path = "tailcall-prettier"} +tailcall-prettier = { path = "tailcall-prettier" } criterion = "0.5.1" httpmock = "0.7.0" pretty_assertions = "1.4.0" @@ -185,6 +185,7 @@ cli = [ "opentelemetry_sdk/rt-tokio", "dep:opentelemetry-otlp", "dep:opentelemetry-system-metrics", + "dep:tailcall-tracker" ] # Feature flag to enable all default features. @@ -203,8 +204,8 @@ members = [ "tailcall-prettier", "tailcall-query-plan", "tailcall-fixtures", - "tailcall-upstream-grpc" -] + "tailcall-upstream-grpc", + "tailcall-tracker"] # Boost execution_spec snapshot diffing performance [profile.dev.package] diff --git a/src/cli/command.rs b/src/cli/command.rs index c79fe9efbb..7b89104e2d 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -1,4 +1,5 @@ use clap::{Parser, Subcommand}; +use strum_macros::Display; use crate::{config, generator}; @@ -20,7 +21,7 @@ pub struct Cli { pub command: Command, } -#[derive(Subcommand)] +#[derive(Subcommand, Display)] pub enum Command { /// Starts the GraphQL server on the configured port Start { diff --git a/src/cli/tc.rs b/src/cli/tc.rs index 95998eeb5c..304445c34d 100644 --- a/src/cli/tc.rs +++ b/src/cli/tc.rs @@ -3,8 +3,10 @@ use std::path::Path; use anyhow::Result; use clap::Parser; +use convert_case::{Case, Casing}; use dotenvy::dotenv; use inquire::Confirm; +use lazy_static::lazy_static; use stripmargin::StripMargin; use super::command::{Cli, Command}; @@ -23,6 +25,9 @@ const FILE_NAME: &str = ".tailcallrc.graphql"; const YML_FILE_NAME: &str = ".graphqlrc.yml"; const JSON_FILE_NAME: &str = ".tailcallrc.schema.json"; +lazy_static! { + static ref TRACKER: tailcall_tracker::Tracker = tailcall_tracker::Tracker::default(); +} pub async fn run() -> Result<()> { if let Ok(path) = dotenv() { tracing::info!("Env file: {:?} loaded", path); @@ -31,6 +36,16 @@ pub async fn run() -> Result<()> { update_checker::check_for_update().await; let runtime = cli::runtime::init(&Blueprint::default()); let config_reader = ConfigReader::init(runtime.clone()); + + // Initialize ping event every 60 seconds + let _ = TRACKER + .init_ping(tokio::time::Duration::from_secs(60)) + .await; + + // Dispatch the command as an event + let _ = TRACKER + .dispatch(cli.command.to_string().to_case(Case::Snake).as_str()) + .await; match cli.command { Command::Start { file_paths } => { let config_module = config_reader.read_all(&file_paths).await?; diff --git a/tailcall-tracker/Cargo.toml b/tailcall-tracker/Cargo.toml new file mode 100644 index 0000000000..d068f81cd8 --- /dev/null +++ b/tailcall-tracker/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tailcall-tracker" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +reqwest = { version = "0.11", features = ["json", "rustls-tls"] } +anyhow = "1.0.82" +lazy_static = "1.4.0" +serde = { version = "1.0.200", features = ["derive"] } +serde_json = { version = "1.0.82", features = ["preserve_order"] } +machineid-rs = "1.2.4" +tokio = { version = "1.0.1", features = ["rt", "time"] } +tracing = "0.1.40" +sysinfo = "0.30.12" diff --git a/tailcall-tracker/src/check_tracking.rs b/tailcall-tracker/src/check_tracking.rs new file mode 100644 index 0000000000..0da3349945 --- /dev/null +++ b/tailcall-tracker/src/check_tracking.rs @@ -0,0 +1,52 @@ +use std::env; + +const LONG_ENV_FILTER_VAR_NAME: &str = "TAILCALL_TRACKER"; +const SHORT_ENV_FILTER_VAR_NAME: &str = "TC_TRACKER"; +const VERSION: &str = match option_env!("APP_VERSION") { + Some(version) => version, + _ => "0.1.0-dev", +}; + +/// Checks if tracking is enabled +pub fn check_tracking() -> bool { + let is_prod = !VERSION.contains("dev"); + let usage_enabled = env::var(LONG_ENV_FILTER_VAR_NAME) + .or(env::var(SHORT_ENV_FILTER_VAR_NAME)) + .map(|v| !v.eq_ignore_ascii_case("false")) + .ok(); + check_tracking_inner(is_prod, usage_enabled) +} + +fn check_tracking_inner(is_prod_build: bool, tracking_enabled: Option) -> bool { + if let Some(usage_enabled) = tracking_enabled { + usage_enabled + } else { + is_prod_build + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn usage_enabled_true() { + assert!(check_tracking_inner(true, Some(true))); + assert!(check_tracking_inner(false, Some(true))); + } + + #[test] + fn usage_enabled_false() { + assert!(!check_tracking_inner(true, Some(false))); + assert!(!check_tracking_inner(false, Some(false))); + } + + #[test] + fn usage_enabled_none_is_prod_true() { + assert!(check_tracking_inner(true, None)); + } + + #[test] + fn usage_enabled_none_is_prod_false() { + assert!(!check_tracking_inner(false, None)); + } +} diff --git a/tailcall-tracker/src/event.rs b/tailcall-tracker/src/event.rs new file mode 100644 index 0000000000..0ae3dd9d59 --- /dev/null +++ b/tailcall-tracker/src/event.rs @@ -0,0 +1,52 @@ +use machineid_rs::{Encryption, HWIDComponent, IdBuilder}; +use serde::{Deserialize, Serialize}; +use sysinfo::System; + +const PARAPHRASE: &str = "tc_key"; +const DEFAULT_CLIENT_ID: &str = ""; + +#[derive(Debug, Serialize, Deserialize)] +struct Params { + cpu_cores: String, + os_name: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct EventValue { + name: String, + params: Params, +} + +impl EventValue { + fn new(name: &str) -> EventValue { + let sys = System::new_all(); + let cores = sys.physical_core_count().unwrap_or(2).to_string(); + let os_name = System::long_os_version().unwrap_or("Unknown".to_string()); + EventValue { + name: name.to_string(), + params: Params { cpu_cores: cores, os_name }, + } + } +} + +/// Event structure to be sent to GA +#[derive(Debug, Serialize, Deserialize)] +pub struct Event { + client_id: String, + events: Vec, +} + +impl Event { + pub fn new(name: &str) -> Self { + let mut builder = IdBuilder::new(Encryption::SHA256); + builder + .add_component(HWIDComponent::SystemID) + .add_component(HWIDComponent::CPUCores); + + let id = builder + .build(PARAPHRASE) + .unwrap_or(DEFAULT_CLIENT_ID.to_string()); + + Self { client_id: id, events: vec![EventValue::new(name)] } + } +} diff --git a/tailcall-tracker/src/lib.rs b/tailcall-tracker/src/lib.rs new file mode 100644 index 0000000000..4a8bd0635f --- /dev/null +++ b/tailcall-tracker/src/lib.rs @@ -0,0 +1,4 @@ +mod check_tracking; +mod event; +mod tracker; +pub use tracker::Tracker; diff --git a/tailcall-tracker/src/tracker.rs b/tailcall-tracker/src/tracker.rs new file mode 100644 index 0000000000..a50d570177 --- /dev/null +++ b/tailcall-tracker/src/tracker.rs @@ -0,0 +1,76 @@ +use reqwest::header::{HeaderName, HeaderValue}; + +use crate::check_tracking::check_tracking; +use crate::event::Event; + +const API_SECRET: &str = "GVaEzXFeRkCI9YBIylbEjQ"; +const MEASUREMENT_ID: &str = "G-JEP3QDWT0G"; +const BASE_URL: &str = "https://www.google-analytics.com"; + +/// +/// Base structure to track usage of the CLI application +#[derive(Debug, Clone)] +pub struct Tracker { + base_url: String, + api_secret: String, + measurement_id: String, + is_tracking: bool, +} + +impl Default for Tracker { + fn default() -> Self { + Self { + base_url: BASE_URL.to_string(), + api_secret: API_SECRET.to_string(), + measurement_id: MEASUREMENT_ID.to_string(), + is_tracking: check_tracking(), + } + } +} + +impl Tracker { + /// Initializes the ping event to be sent after the provided duration + pub async fn init_ping(&'static self, duration: tokio::time::Duration) { + if self.is_tracking { + let mut interval = tokio::time::interval(duration); + tokio::task::spawn(async move { + loop { + interval.tick().await; + let _ = self.dispatch("ping").await; + } + }); + } + } + + fn create_request(&self, event_name: &str) -> anyhow::Result { + let event = Event::new(event_name); + tracing::debug!("Sending event: {:?}", event); + let mut url = reqwest::Url::parse(self.base_url.as_str())?; + url.set_path("/mp/collect"); + url.query_pairs_mut() + .append_pair("api_secret", self.api_secret.as_str()) + .append_pair("measurement_id", self.measurement_id.as_str()); + let mut request = reqwest::Request::new(reqwest::Method::POST, url); + let header_name = HeaderName::from_static("content-type"); + let header_value = HeaderValue::from_str("application/json")?; + request.headers_mut().insert(header_name, header_value); + + let _ = request + .body_mut() + .insert(reqwest::Body::from(serde_json::to_string(&event)?)); + Ok(request) + } + + pub async fn dispatch(&'static self, name: &str) -> anyhow::Result<()> { + if self.is_tracking { + let request = self.create_request(name)?; + let client = reqwest::Client::new(); + let response = client.execute(request).await?; + let status = response.status(); + let text = response.text().await?; + tracing::debug!("Tracker: {}, message: {:?}", status.as_str(), text); + } + + Ok(()) + } +}