Upgrade to clap v4 ()

Co-authored-by: Steve Fan <29133953+stevefan1999-personal@users.noreply.github.com>
This commit is contained in:
Finn Bear 2023-05-22 12:19:35 -07:00 committed by GitHub
parent 6b02c2f026
commit cdf97fcb96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 728 additions and 688 deletions

120
Cargo.lock generated
View file

@ -279,6 +279,55 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstream"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is-terminal",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
[[package]]
name = "anstyle-parse"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "anstyle-wincon"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
dependencies = [
"anstyle",
"windows-sys 0.48.0",
]
[[package]]
name = "any_ascii"
version = "0.3.2"
@ -610,7 +659,7 @@ dependencies = [
"bitflags",
"cexpr 0.6.0",
"clang-sys",
"clap",
"clap 3.2.25",
"env_logger 0.9.3",
"lazy_static",
"lazycell",
@ -893,13 +942,52 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
dependencies = [
"atty",
"bitflags",
"clap_lex",
"clap_lex 0.2.4",
"indexmap",
"strsim",
"termcolor",
"textwrap",
]
[[package]]
name = "clap"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc"
dependencies = [
"clap_builder",
"clap_derive",
"once_cell",
]
[[package]]
name = "clap_builder"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990"
dependencies = [
"anstream",
"anstyle",
"bitflags",
"clap_lex 0.5.0",
"strsim",
"terminal_size",
"unicase",
"unicode-width",
]
[[package]]
name = "clap_derive"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.16",
]
[[package]]
name = "clap_lex"
version = "0.2.4"
@ -909,6 +997,12 @@ dependencies = [
"os_str_bytes",
]
[[package]]
name = "clap_lex"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
[[package]]
name = "clipboard-win"
version = "4.5.0"
@ -929,6 +1023,12 @@ dependencies = [
"cc",
]
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "colored"
version = "1.9.3"
@ -1029,7 +1129,7 @@ dependencies = [
"atty",
"cast",
"ciborium",
"clap",
"clap 3.2.25",
"criterion-plot",
"itertools 0.10.5",
"lazy_static",
@ -4083,11 +4183,12 @@ dependencies = [
"bung",
"bytes",
"chrono",
"clap",
"clap 4.3.0",
"fern",
"futures 0.3.28",
"http",
"hyper",
"ipnet",
"jsonwebtoken",
"log",
"once_cell",
@ -4107,6 +4208,7 @@ dependencies = [
"thiserror",
"tokio",
"tokio-stream",
"tokio-util",
"tonic",
"tracing",
"tracing-futures",
@ -4289,6 +4391,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237"
dependencies = [
"rustix",
"windows-sys 0.48.0",
]
[[package]]
name = "textwrap"
version = "0.16.0"

View file

@ -34,11 +34,12 @@ base64 = "0.21.1"
bung = "0.1.0"
bytes = "1.4.0"
chrono = { version = "0.4.24", features = ["serde"] }
clap = { version = "3.2.25", features = ["env"] }
clap = { version = "4.2.1", features = ["env", "derive", "wrap_help", "unicode"] }
fern = { version = "0.6.2", features = ["colored"] }
futures = "0.3.28"
http = "0.2.9"
hyper = "0.14.26"
ipnet = "2.7.2"
jsonwebtoken = "8.3.0"
log = "0.4.17"
once_cell = "1.17.1"
@ -54,6 +55,7 @@ serde_json = "1.0.96"
surrealdb = { path = "lib", features = ["protocol-http", "protocol-ws", "rustls"] }
thiserror = "1.0.40"
tokio = { version = "1.28.1", features = ["macros", "signal"] }
tokio-util = { version = "0.7.8", features = ["io"] }
tracing = "0.1"
tracing-futures = "0.2.5"
tracing-opentelemetry = "0.18.0"

View file

@ -0,0 +1,32 @@
use clap::Args;
#[derive(Args, Debug)]
pub(crate) struct AuthArguments {
#[arg(help = "Database authentication username to use when connecting")]
#[arg(env = "SURREAL_USER", short = 'u', long = "username", visible_alias = "user")]
#[arg(default_value = "root")]
pub(crate) username: String,
#[arg(help = "Database authentication password to use when connecting")]
#[arg(short = 'p', long = "password", visible_alias = "pass")]
#[arg(env = "SURREAL_PASS", default_value = "root")]
pub(crate) password: String,
}
#[derive(Args, Debug)]
pub struct DatabaseSelectionArguments {
#[arg(help = "The namespace selected for the operation")]
#[arg(env = "SURREAL_NAMESPACE", long = "namespace", visible_alias = "ns")]
pub(crate) namespace: String,
#[arg(help = "The database selected for the operation")]
#[arg(env = "SURREAL_DATABASE", long = "database", visible_alias = "db")]
pub(crate) database: String,
}
#[derive(Args, Debug)]
pub struct DatabaseConnectionArguments {
#[arg(help = "Remote database server url to connect to")]
#[arg(short = 'e', long = "endpoint", visible_aliases = ["conn"])]
#[arg(default_value = "https://cloud.surrealdb.com")]
#[arg(value_parser = super::validator::endpoint_valid)]
pub(crate) endpoint: String,
}

View file

@ -1,115 +1,134 @@
use crate::cli::abstraction::AuthArguments;
use crate::cnf::SERVER_AGENT;
use crate::err::Error;
use reqwest::blocking::Body;
use reqwest::blocking::Client;
use clap::Args;
use futures::TryStreamExt;
use reqwest::header::CONTENT_TYPE;
use reqwest::header::USER_AGENT;
use std::fs::OpenOptions;
use std::io::copy;
use reqwest::{Body, Client, Response};
use std::io::ErrorKind;
use tokio::fs::OpenOptions;
use tokio::io::{copy, stdin, stdout, AsyncWrite, AsyncWriteExt};
use tokio_util::io::{ReaderStream, StreamReader};
const TYPE: &str = "application/octet-stream";
pub fn init(matches: &clap::ArgMatches) -> Result<(), Error> {
#[derive(Args, Debug)]
pub struct BackupCommandArguments {
#[arg(help = "Path to the remote database or file from which to export")]
#[arg(value_parser = super::validator::into_valid)]
from: String,
#[arg(help = "Path to the remote database or file into which to import")]
#[arg(default_value = "-")]
#[arg(value_parser = super::validator::into_valid)]
into: String,
#[command(flatten)]
auth: AuthArguments,
}
pub async fn init(
BackupCommandArguments {
from,
into,
auth: AuthArguments {
username: user,
password: pass,
},
}: BackupCommandArguments,
) -> Result<(), Error> {
// Initialize opentelemetry and logging
crate::o11y::builder().with_log_level("error").init();
// Try to parse the specified source file
let from = matches.value_of("from").unwrap();
// Try to parse the specified output file
let into = matches.value_of("into").unwrap();
// Process the source->destination response
if from.ends_with(".db") && into.ends_with(".db") {
backup_file_to_file(matches, from, into)
} else if from.ends_with(".db") {
backup_file_to_http(matches, from, into)
} else if into.ends_with(".db") {
backup_http_to_file(matches, from, into)
} else {
backup_http_to_http(matches, from, into)
let into_local = into.ends_with(".db");
let from_local = from.ends_with(".db");
match (from.as_str(), into.as_str()) {
// From Stdin -> Into Stdout (are you trying to make an ouroboros?)
("-", "-") => Err(Error::OperationUnsupported),
// From Stdin -> Into File (possible but meaningless)
("-", _) if into_local => Err(Error::OperationUnsupported),
// From File -> Into Stdout (possible but meaningless, could be useful for source validation but not for now)
(_, "-") if from_local => Err(Error::OperationUnsupported),
// From File -> Into File (also possible but meaningless,
// but since the original function had this, I would choose to keep it as of now)
(from, into) if from_local && into_local => {
tokio::fs::copy(from, into).await?;
Ok(())
}
// From File -> Into HTTP
(from, into) if from_local => {
// Copy the data to the destination
let from = OpenOptions::new().read(true).open(from).await?;
post_http_sync_body(from, into, &user, &pass).await
}
// From HTTP -> Into File
(from, into) if into_local => {
// Try to open the output file
let into =
OpenOptions::new().write(true).create(true).truncate(true).open(into).await?;
backup_http_to_file(from, into, &user, &pass).await
}
// From HTTP -> Into Stdout
(from, "-") => backup_http_to_file(from, stdout(), &user, &pass).await,
// From Stdin -> Into File
("-", into) => {
let from = Body::wrap_stream(ReaderStream::new(stdin()));
post_http_sync_body(from, into, &user, &pass).await
}
// From HTTP -> Into HTTP
(from, into) => {
// Copy the data to the destination
let from = get_http_sync_body(from, &user, &pass).await?;
post_http_sync_body(from, into, &user, &pass).await
}
}
}
fn backup_file_to_file(_: &clap::ArgMatches, from: &str, into: &str) -> Result<(), Error> {
// Try to open the source file
let mut from = OpenOptions::new().read(true).open(from)?;
// Try to open the output file
let mut into = OpenOptions::new().write(true).create(true).truncate(true).open(into)?;
// Copy the data to the destination
copy(&mut from, &mut into)?;
// Everything OK
Ok(())
}
fn backup_http_to_file(matches: &clap::ArgMatches, from: &str, into: &str) -> Result<(), Error> {
// Parse the specified username
let user = matches.value_of("user").unwrap();
// Parse the specified password
let pass = matches.value_of("pass").unwrap();
// Set the correct source URL
let from = format!("{from}/sync");
// Try to open the source http
let mut from = Client::new()
.get(from)
.basic_auth(user, Some(pass))
.header(USER_AGENT, SERVER_AGENT)
.header(CONTENT_TYPE, TYPE)
.send()?
.error_for_status()?;
// Try to open the output file
let mut into = OpenOptions::new().write(true).create(true).truncate(true).open(into)?;
// Copy the data to the destination
copy(&mut from, &mut into)?;
// Everything OK
Ok(())
}
fn backup_file_to_http(matches: &clap::ArgMatches, from: &str, into: &str) -> Result<(), Error> {
// Parse the specified username
let user = matches.value_of("user").unwrap();
// Parse the specified password
let pass = matches.value_of("pass").unwrap();
// Try to open the source file
let from = OpenOptions::new().read(true).open(from)?;
// Set the correct output URL
let into = format!("{into}/sync");
// Copy the data to the destination
async fn post_http_sync_body<B: Into<Body>>(
from: B,
into: &str,
user: &str,
pass: &str,
) -> Result<(), Error> {
Client::new()
.post(into)
.post(format!("{into}/sync"))
.basic_auth(user, Some(pass))
.header(USER_AGENT, SERVER_AGENT)
.header(CONTENT_TYPE, TYPE)
.body(from)
.send()?
.send()
.await?
.error_for_status()?;
// Everything OK
Ok(())
}
fn backup_http_to_http(matches: &clap::ArgMatches, from: &str, into: &str) -> Result<(), Error> {
// Parse the specified username
let user = matches.value_of("user").unwrap();
// Parse the specified password
let pass = matches.value_of("pass").unwrap();
// Set the correct source URL
let from = format!("{from}/sync");
// Set the correct output URL
let into = format!("{into}/sync");
// Try to open the source file
let from = Client::new()
.get(from)
async fn get_http_sync_body(from: &str, user: &str, pass: &str) -> Result<Response, Error> {
Ok(Client::new()
.get(format!("{from}/sync"))
.basic_auth(user, Some(pass))
.header(USER_AGENT, SERVER_AGENT)
.header(CONTENT_TYPE, TYPE)
.send()?
.error_for_status()?;
.send()
.await?
.error_for_status()?)
}
async fn backup_http_to_file<W: AsyncWrite + Unpin>(
from: &str,
mut into: W,
user: &str,
pass: &str,
) -> Result<(), Error> {
let mut from = StreamReader::new(
get_http_sync_body(from, user, pass)
.await?
.bytes_stream()
.map_err(|x| std::io::Error::new(ErrorKind::Other, x)),
);
// Copy the data to the destination
Client::new()
.post(into)
.basic_auth(user, Some(pass))
.header(USER_AGENT, SERVER_AGENT)
.header(CONTENT_TYPE, TYPE)
.body(Body::new(from))
.send()?
.error_for_status()?;
copy(&mut from, &mut into).await?;
into.flush().await?;
// Everything OK
Ok(())
}

View file

@ -1,5 +1,5 @@
use once_cell::sync::OnceCell;
use std::net::SocketAddr;
use std::{net::SocketAddr, path::PathBuf};
pub static CF: OnceCell<Config> = OnceCell::new();
@ -10,32 +10,6 @@ pub struct Config {
pub path: String,
pub user: String,
pub pass: Option<String>,
pub crt: Option<String>,
pub key: Option<String>,
}
pub fn init(matches: &clap::ArgMatches) {
// Parse the server binding address
let bind = matches.value_of("bind").unwrap().parse::<SocketAddr>().unwrap();
// Parse the database endpoint path
let path = matches.value_of("path").unwrap().to_owned();
// Parse the root username for authentication
let user = matches.value_of("user").unwrap().to_owned();
// Parse the root password for authentication
let pass = matches.value_of("pass").map(|v| v.to_owned());
// Parse any TLS server security options
let crt = matches.value_of("web-crt").map(|v| v.to_owned());
let key = matches.value_of("web-key").map(|v| v.to_owned());
// Check if database strict mode is enabled
let strict = matches.is_present("strict");
// Store the new config object
let _ = CF.set(Config {
strict,
bind,
path,
user,
pass,
crt,
key,
});
pub crt: Option<PathBuf>,
pub key: Option<PathBuf>,
}

View file

@ -1,28 +1,54 @@
use crate::cli::abstraction::{
AuthArguments, DatabaseConnectionArguments, DatabaseSelectionArguments,
};
use crate::cli::LOG;
use crate::err::Error;
use clap::Args;
use surrealdb::engine::any::connect;
use surrealdb::error::Api as ApiError;
use surrealdb::opt::auth::Root;
use surrealdb::Error as SurrealError;
#[tokio::main]
pub async fn init(matches: &clap::ArgMatches) -> Result<(), Error> {
#[derive(Args, Debug)]
pub struct ExportCommandArguments {
#[arg(help = "Path to the sql file to export. Use dash - to write into stdout.")]
#[arg(default_value = "-")]
#[arg(index = 1)]
file: String,
#[command(flatten)]
conn: DatabaseConnectionArguments,
#[command(flatten)]
auth: AuthArguments,
#[command(flatten)]
sel: DatabaseSelectionArguments,
}
pub async fn init(
ExportCommandArguments {
file,
conn: DatabaseConnectionArguments {
endpoint,
},
auth: AuthArguments {
username,
password,
},
sel: DatabaseSelectionArguments {
namespace: ns,
database: db,
},
}: ExportCommandArguments,
) -> Result<(), Error> {
// Initialize opentelemetry and logging
crate::o11y::builder().with_log_level("error").init();
// Try to parse the file argument
let file = matches.value_of("file").unwrap();
// Parse all other cli arguments
let username = matches.value_of("user").unwrap();
let password = matches.value_of("pass").unwrap();
let endpoint = matches.value_of("conn").unwrap();
let ns = matches.value_of("ns").unwrap();
let db = matches.value_of("db").unwrap();
// Connect to the database engine
let client = connect(endpoint).await?;
// Sign in to the server if the specified database engine supports it
let root = Root {
username,
password,
username: &username,
password: &password,
};
if let Err(error) = client.signin(root).await {
match error {

View file

@ -1,28 +1,52 @@
use crate::cli::abstraction::{
AuthArguments, DatabaseConnectionArguments, DatabaseSelectionArguments,
};
use crate::cli::LOG;
use crate::err::Error;
use clap::Args;
use surrealdb::engine::any::connect;
use surrealdb::error::Api as ApiError;
use surrealdb::opt::auth::Root;
use surrealdb::Error as SurrealError;
#[tokio::main]
pub async fn init(matches: &clap::ArgMatches) -> Result<(), Error> {
#[derive(Args, Debug)]
pub struct ImportCommandArguments {
#[arg(help = "Path to the sql file to import")]
#[arg(index = 1)]
file: String,
#[command(flatten)]
conn: DatabaseConnectionArguments,
#[command(flatten)]
auth: AuthArguments,
#[command(flatten)]
sel: DatabaseSelectionArguments,
}
pub async fn init(
ImportCommandArguments {
file,
conn: DatabaseConnectionArguments {
endpoint,
},
auth: AuthArguments {
username,
password,
},
sel: DatabaseSelectionArguments {
namespace: ns,
database: db,
},
}: ImportCommandArguments,
) -> Result<(), Error> {
// Initialize opentelemetry and logging
crate::o11y::builder().with_log_level("error").init();
// Try to parse the file argument
let file = matches.value_of("file").unwrap();
// Parse all other cli arguments
let username = matches.value_of("user").unwrap();
let password = matches.value_of("pass").unwrap();
let endpoint = matches.value_of("conn").unwrap();
let ns = matches.value_of("ns").unwrap();
let db = matches.value_of("db").unwrap();
// Connect to the database engine
let client = connect(endpoint).await?;
// Sign in to the server if the specified database engine supports it
let root = Root {
username,
password,
username: &username,
password: &password,
};
if let Err(error) = client.signin(root).await {
match error {

View file

@ -1,12 +1,23 @@
use crate::cli::abstraction::DatabaseConnectionArguments;
use crate::err::Error;
use clap::Args;
use surrealdb::engine::any::connect;
#[tokio::main]
pub async fn init(matches: &clap::ArgMatches) -> Result<(), Error> {
#[derive(Args, Debug)]
pub struct IsReadyCommandArguments {
#[command(flatten)]
conn: DatabaseConnectionArguments,
}
pub async fn init(
IsReadyCommandArguments {
conn: DatabaseConnectionArguments {
endpoint,
},
}: IsReadyCommandArguments,
) -> Result<(), Error> {
// Initialize opentelemetry and logging
crate::o11y::builder().with_log_level("error").init();
// Parse all other cli arguments
let endpoint = matches.value_of("conn").unwrap();
// Connect to the database engine
connect(endpoint).await?;
println!("OK");

View file

@ -1,3 +1,4 @@
pub(crate) mod abstraction;
mod backup;
mod config;
mod export;
@ -5,17 +6,20 @@ mod import;
mod isready;
mod sql;
mod start;
pub(crate) mod validator;
mod version;
pub use config::CF;
use crate::cnf::LOGO;
use clap::{Arg, Command};
use std::net::SocketAddr;
use std::path::Path;
use backup::BackupCommandArguments;
use clap::{Parser, Subcommand};
use export::ExportCommandArguments;
use import::ImportCommandArguments;
use isready::IsReadyCommandArguments;
use sql::SqlCommandArguments;
use start::StartCommandArguments;
use std::process::ExitCode;
use tracing::Level;
use tracing_subscriber::EnvFilter;
pub const LOG: &str = "surrealdb::cli";
@ -32,503 +36,51 @@ We would love it if you could star the repository (https://github.com/surrealdb/
----------
";
fn split_endpoint(v: &str) -> (&str, &str) {
match v {
"memory" => ("mem", ""),
v => match v.split_once("://") {
Some(parts) => parts,
None => v.split_once(':').unwrap_or_default(),
},
}
#[derive(Parser, Debug)]
#[command(name = "SurrealDB command-line interface and server", bin_name = "surreal")]
#[command(about = INFO, before_help = LOGO)]
#[command(disable_version_flag = true, arg_required_else_help = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
fn file_valid(v: &str) -> Result<(), String> {
match v {
v if !v.is_empty() => Ok(()),
_ => Err(String::from("Provide a valid path to a SQL file")),
}
#[derive(Debug, Subcommand)]
enum Commands {
#[command(about = "Start the database server")]
Start(StartCommandArguments),
#[command(about = "Backup data to or from an existing database")]
Backup(BackupCommandArguments),
#[command(about = "Import a SurrealQL script into an existing database")]
Import(ImportCommandArguments),
#[command(about = "Export an existing database as a SurrealQL script")]
Export(ExportCommandArguments),
#[command(about = "Output the command-line tool version information")]
Version,
#[command(about = "Start an SQL REPL in your terminal with pipe support")]
Sql(SqlCommandArguments),
#[command(
about = "Check if the SurrealDB server is ready to accept connections",
visible_alias = "isready"
)]
IsReady(IsReadyCommandArguments),
}
fn file_exists(file: &str) -> Result<(), String> {
let path = Path::new(file);
if !*path.try_exists().as_ref().map_err(ToString::to_string)? {
return Err(String::from("Ensure the file exists"));
}
if !path.is_file() {
return Err(String::from("Ensure the path is a file"));
}
Ok(())
}
fn bind_valid(v: &str) -> Result<(), String> {
match v.parse::<SocketAddr>() {
Ok(_) => Ok(()),
_ => Err(String::from("Provide a valid network bind parameter")),
}
}
fn path_valid(v: &str) -> Result<(), String> {
match v {
"memory" => Ok(()),
v if v.starts_with("file:") => Ok(()),
v if v.starts_with("rocksdb:") => Ok(()),
v if v.starts_with("tikv:") => Ok(()),
v if v.starts_with("fdb:") => Ok(()),
_ => Err(String::from("Provide a valid database path parameter")),
}
}
fn conn_valid(v: &str) -> Result<(), String> {
let scheme = split_endpoint(v).0;
match scheme {
"http" | "https" | "ws" | "wss" | "fdb" | "mem" | "rocksdb" | "file" | "tikv" => Ok(()),
_ => Err(String::from("Provide a valid database connection string")),
}
}
fn from_valid(v: &str) -> Result<(), String> {
match v {
v if v.ends_with(".db") => Ok(()),
v if v.starts_with("http://") => Ok(()),
v if v.starts_with("https://") => Ok(()),
_ => Err(String::from("Provide a valid database connection string, or the path to a file")),
}
}
fn into_valid(v: &str) -> Result<(), String> {
match v {
v if v.ends_with(".db") => Ok(()),
v if v.starts_with("http://") => Ok(()),
v if v.starts_with("https://") => Ok(()),
_ => Err(String::from("Provide a valid database connection string, or the path to a file")),
}
}
fn key_valid(v: &str) -> Result<(), String> {
match v.len() {
16 => Ok(()),
24 => Ok(()),
32 => Ok(()),
_ => Err(String::from("Ensure your database encryption key is 16, 24, or 32 bits long")),
}
}
fn log_valid(v: &str) -> Result<String, String> {
match v {
// Don't show any logs at all
"none" => Ok("none".to_string()),
// Check if we should show all log levels
"full" => Ok(Level::TRACE.to_string()),
// Otherwise, let's only show errors
"error" => Ok(Level::ERROR.to_string()),
// Specify the log level for each code area
"warn" | "info" | "debug" | "trace" => {
Ok(format!("error,surreal={v},surrealdb={v},surrealdb::txn=error"))
}
// Let's try to parse the custom log level
_ => match EnvFilter::builder().parse(v) {
// The custom log level parsed successfully
Ok(_) => Ok(v.to_owned()),
// There was an error parsing the custom log level
Err(_) => Err(String::from("Provide a valid log filter configuration string")),
},
}
}
pub fn init() -> ExitCode {
let setup = Command::new("SurrealDB command-line interface and server")
.about(INFO)
.before_help(LOGO)
.disable_version_flag(true)
.arg_required_else_help(true);
let setup = setup.subcommand(
Command::new("start")
.display_order(1)
.about("Start the database server")
.arg(
Arg::new("path")
.index(1)
.env("SURREAL_PATH")
.required(false)
.validator(path_valid)
.default_value("memory")
.help("Database path used for storing data"),
)
.arg(
Arg::new("user")
.short('u')
.env("SURREAL_USER")
.long("user")
.forbid_empty_values(true)
.default_value("root")
.help("The master username for the database"),
)
.arg(
Arg::new("pass")
.short('p')
.env("SURREAL_PASS")
.long("pass")
.takes_value(true)
.forbid_empty_values(true)
.help("The master password for the database"),
)
.arg(
Arg::new("addr")
.env("SURREAL_ADDR")
.long("addr")
.number_of_values(1)
.forbid_empty_values(true)
.multiple_occurrences(true)
.default_value("127.0.0.1/32")
.help("The allowed networks for master authentication"),
)
.arg(
Arg::new("bind")
.short('b')
.env("SURREAL_BIND")
.long("bind")
.validator(bind_valid)
.forbid_empty_values(true)
.default_value("0.0.0.0:8000")
.help("The hostname or ip address to listen for connections on"),
)
.arg(
Arg::new("key")
.short('k')
.env("SURREAL_KEY")
.long("key")
.takes_value(true)
.forbid_empty_values(true)
.validator(key_valid)
.help("Encryption key to use for on-disk encryption"),
)
.arg(
Arg::new("kvs-ca")
.env("SURREAL_KVS_CA")
.long("kvs-ca")
.takes_value(true)
.forbid_empty_values(true)
.validator(file_exists)
.help("Path to the CA file used when connecting to the remote KV store"),
)
.arg(
Arg::new("kvs-crt")
.env("SURREAL_KVS_CRT")
.long("kvs-crt")
.takes_value(true)
.forbid_empty_values(true)
.validator(file_exists)
.help(
"Path to the certificate file used when connecting to the remote KV store",
),
)
.arg(
Arg::new("kvs-key")
.env("SURREAL_KVS_KEY")
.long("kvs-key")
.takes_value(true)
.forbid_empty_values(true)
.validator(file_exists)
.help(
"Path to the private key file used when connecting to the remote KV store",
),
)
.arg(
Arg::new("web-crt")
.env("SURREAL_WEB_CRT")
.long("web-crt")
.takes_value(true)
.forbid_empty_values(true)
.validator(file_exists)
.help("Path to the certificate file for encrypted client connections"),
)
.arg(
Arg::new("web-key")
.env("SURREAL_WEB_KEY")
.long("web-key")
.takes_value(true)
.forbid_empty_values(true)
.validator(file_exists)
.help("Path to the private key file for encrypted client connections"),
)
.arg(
Arg::new("strict")
.short('s')
.env("SURREAL_STRICT")
.long("strict")
.required(false)
.takes_value(false)
.help("Whether strict mode is enabled on this database instance"),
)
.arg(
Arg::new("log")
.short('l')
.env("SURREAL_LOG")
.long("log")
.takes_value(true)
.default_value("info")
.forbid_empty_values(true)
.value_parser(log_valid)
.help("The logging level for the database server. One of error, warn, info, debug, trace, full."),
)
.arg(
Arg::new("no-banner")
.env("SURREAL_NO_BANNER")
.long("no-banner")
.required(false)
.takes_value(false)
.help("Whether to hide the startup banner"),
),
);
let setup = setup.subcommand(
Command::new("backup")
.display_order(2)
.about("Backup data to or from an existing database")
.arg(
Arg::new("from")
.index(1)
.required(true)
.validator(from_valid)
.help("Path to the remote database or file from which to export"),
)
.arg(
Arg::new("into")
.index(2)
.required(true)
.validator(into_valid)
.help("Path to the remote database or file into which to import"),
)
.arg(
Arg::new("user")
.short('u')
.long("user")
.forbid_empty_values(true)
.default_value("root")
.help("Database authentication username to use when connecting"),
)
.arg(
Arg::new("pass")
.short('p')
.long("pass")
.forbid_empty_values(true)
.default_value("root")
.help("Database authentication password to use when connecting"),
),
);
let setup = setup.subcommand(
Command::new("import")
.display_order(3)
.about("Import a SurrealQL script into an existing database")
.arg(
Arg::new("file")
.index(1)
.required(true)
.validator(file_valid)
.help("Path to the sql file to import"),
)
.arg(
Arg::new("ns")
.long("ns")
.required(true)
.takes_value(true)
.forbid_empty_values(true)
.help("The namespace to import the data into"),
)
.arg(
Arg::new("db")
.long("db")
.required(true)
.takes_value(true)
.forbid_empty_values(true)
.help("The database to import the data into"),
)
.arg(
Arg::new("conn")
.short('c')
.long("conn")
.alias("host")
.forbid_empty_values(true)
.validator(conn_valid)
.default_value("https://cloud.surrealdb.com")
.help("Remote database server url to connect to"),
)
.arg(
Arg::new("user")
.short('u')
.long("user")
.forbid_empty_values(true)
.default_value("root")
.help("Database authentication username to use when connecting"),
)
.arg(
Arg::new("pass")
.short('p')
.long("pass")
.forbid_empty_values(true)
.default_value("root")
.help("Database authentication password to use when connecting"),
),
);
let setup = setup.subcommand(
Command::new("export")
.display_order(4)
.about("Export an existing database as a SurrealQL script")
.arg(
Arg::new("file")
.index(1)
.required(true)
.validator(file_valid)
.help("Path to the sql file to export. Use dash - to write into stdout."),
)
.arg(
Arg::new("ns")
.long("ns")
.required(true)
.takes_value(true)
.forbid_empty_values(true)
.help("The namespace to export the data from"),
)
.arg(
Arg::new("db")
.long("db")
.required(true)
.takes_value(true)
.forbid_empty_values(true)
.help("The database to export the data from"),
)
.arg(
Arg::new("conn")
.short('c')
.long("conn")
.alias("host")
.forbid_empty_values(true)
.validator(conn_valid)
.default_value("https://cloud.surrealdb.com")
.help("Remote database server url to connect to"),
)
.arg(
Arg::new("user")
.short('u')
.long("user")
.forbid_empty_values(true)
.default_value("root")
.help("Database authentication username to use when connecting"),
)
.arg(
Arg::new("pass")
.short('p')
.long("pass")
.forbid_empty_values(true)
.default_value("root")
.help("Database authentication password to use when connecting"),
),
);
let setup = setup.subcommand(
Command::new("version")
.display_order(5)
.about("Output the command-line tool version information"),
);
let setup = setup.subcommand(
Command::new("sql")
.display_order(6)
.about("Start an SQL REPL in your terminal with pipe support")
.arg(
Arg::new("ns")
.long("ns")
.required(false)
.takes_value(true)
.forbid_empty_values(true)
.help("The namespace to export the data from"),
)
.arg(
Arg::new("db")
.long("db")
.required(false)
.takes_value(true)
.forbid_empty_values(true)
.help("The database to export the data from"),
)
.arg(
Arg::new("conn")
.short('c')
.long("conn")
.alias("host")
.forbid_empty_values(true)
.validator(conn_valid)
.default_value("wss://cloud.surrealdb.com")
.help("Remote database server url to connect to"),
)
.arg(
Arg::new("user")
.short('u')
.long("user")
.forbid_empty_values(true)
.default_value("root")
.help("Database authentication username to use when connecting"),
)
.arg(
Arg::new("multi")
.long("multi")
.required(false)
.takes_value(false)
.help("Whether omitting semicolon causes a newline"),
)
.arg(
Arg::new("pass")
.short('p')
.long("pass")
.forbid_empty_values(true)
.default_value("root")
.help("Database authentication password to use when connecting"),
)
.arg(
Arg::new("pretty")
.long("pretty")
.required(false)
.takes_value(false)
.help("Whether database responses should be pretty printed"),
),
);
let setup = setup.subcommand(
Command::new("isready")
.display_order(7)
.about("Check if the SurrealDB server is ready to accept connections")
.arg(
Arg::new("conn")
.short('c')
.long("conn")
.alias("host")
.forbid_empty_values(true)
.validator(conn_valid)
.default_value("http://localhost:8000")
.help("Remote database server url to connect to"),
),
);
let matches = setup.get_matches();
let output = match matches.subcommand() {
Some(("sql", m)) => sql::init(m),
Some(("start", m)) => start::init(m),
Some(("backup", m)) => backup::init(m),
Some(("import", m)) => import::init(m),
Some(("export", m)) => export::init(m),
Some(("version", m)) => version::init(m),
Some(("isready", m)) => isready::init(m),
_ => Ok(()),
pub async fn init() -> ExitCode {
let args = Cli::parse();
let output = match args.command {
Commands::Start(args) => start::init(args).await,
Commands::Backup(args) => backup::init(args).await,
Commands::Import(args) => import::init(args).await,
Commands::Export(args) => export::init(args).await,
Commands::Version => version::init(),
Commands::Sql(args) => sql::init(args).await,
Commands::IsReady(args) => isready::init(args).await,
};
if let Err(e) = output {
error!(target: LOG, "{}", e);
return ExitCode::FAILURE;
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
ExitCode::SUCCESS
}

View file

@ -1,4 +1,8 @@
use crate::cli::abstraction::{
AuthArguments, DatabaseConnectionArguments, DatabaseSelectionArguments,
};
use crate::err::Error;
use clap::Args;
use rustyline::error::ReadlineError;
use rustyline::validate::{ValidationContext, ValidationResult, Validator};
use rustyline::{Completer, Editor, Helper, Highlighter, Hinter};
@ -8,24 +12,49 @@ use surrealdb::opt::auth::Root;
use surrealdb::sql::{self, Statement, Value};
use surrealdb::{Error as SurrealError, Response};
#[tokio::main]
pub async fn init(matches: &clap::ArgMatches) -> Result<(), Error> {
#[derive(Args, Debug)]
pub struct SqlCommandArguments {
#[command(flatten)]
conn: DatabaseConnectionArguments,
#[command(flatten)]
auth: AuthArguments,
#[command(flatten)]
sel: DatabaseSelectionArguments,
/// Whether database responses should be pretty printed
#[arg(long)]
pretty: bool,
/// Whether omitting semicolon causes a newline
#[arg(long)]
multi: bool,
}
pub async fn init(
SqlCommandArguments {
auth: AuthArguments {
username,
password,
},
conn: DatabaseConnectionArguments {
endpoint,
},
sel: DatabaseSelectionArguments {
namespace,
database,
},
pretty,
multi,
..
}: SqlCommandArguments,
) -> Result<(), Error> {
// Initialize opentelemetry and logging
crate::o11y::builder().with_log_level("warn").init();
// Parse all other cli arguments
let username = matches.value_of("user").unwrap();
let password = matches.value_of("pass").unwrap();
let endpoint = matches.value_of("conn").unwrap();
let mut ns = matches.value_of("ns").map(str::to_string);
let mut db = matches.value_of("db").map(str::to_string);
// If we should pretty-print responses
let pretty = matches.is_present("pretty");
// Connect to the database engine
let client = connect(endpoint).await?;
// Sign in to the server if the specified database engine supports it
let root = Root {
username,
password,
username: &username,
password: &password,
};
if let Err(error) = client.signin(root).await {
match error {
@ -40,10 +69,13 @@ pub async fn init(matches: &clap::ArgMatches) -> Result<(), Error> {
let mut rl = Editor::new().unwrap();
// Set custom input validation
rl.set_helper(Some(InputValidator {
multi: matches.is_present("multi"),
multi,
}));
// Load the command-line history
let _ = rl.load_history("history.txt");
// Keep track of current namespace/database.
let mut ns = Some(namespace);
let mut db = Some(database);
// Configure the prompt
let mut prompt = "> ".to_owned();
// Loop over each command-line input

View file

@ -1,26 +1,119 @@
use super::config;
use super::config::Config;
use crate::cli::validator::parser::env_filter::CustomEnvFilter;
use crate::cli::validator::parser::env_filter::CustomEnvFilterParser;
use crate::cnf::LOGO;
use crate::dbs;
use crate::env;
use crate::err::Error;
use crate::iam;
use crate::net;
use futures::Future;
use clap::Args;
use ipnet::IpNet;
use std::net::SocketAddr;
use std::path::PathBuf;
pub fn init(matches: &clap::ArgMatches) -> Result<(), Error> {
with_enough_stack(init_impl(matches))
#[derive(Args, Debug)]
pub struct StartCommandArguments {
#[arg(help = "Database path used for storing data")]
#[arg(env = "SURREAL_PATH", index = 1)]
#[arg(default_value = "memory")]
#[arg(value_parser = super::validator::path_valid)]
path: String,
#[arg(help = "The master username for the database")]
#[arg(env = "SURREAL_USER", short = 'u', long = "username", visible_alias = "user")]
#[arg(default_value = "root")]
username: String,
#[arg(help = "The master password for the database")]
#[arg(env = "SURREAL_PASS", short = 'p', long = "password", visible_alias = "pass")]
password: Option<String>,
#[arg(help = "The allowed networks for master authentication")]
#[arg(env = "SURREAL_ADDR", long = "addr")]
#[arg(default_value = "127.0.0.1/32")]
allowed_networks: Vec<IpNet>,
#[arg(help = "The hostname or ip address to listen for connections on")]
#[arg(env = "SURREAL_BIND", short = 'b', long = "bind")]
#[arg(default_value = "0.0.0.0:8000")]
listen_addresses: Vec<SocketAddr>,
#[arg(help = "Encryption key to use for on-disk encryption")]
#[arg(env = "SURREAL_KEY", short = 'k', long = "key")]
#[arg(value_parser = super::validator::key_valid)]
key: Option<String>,
#[command(flatten)]
kvs: Option<StartCommandRemoteTlsOptions>,
#[command(flatten)]
web: Option<StartCommandWebTlsOptions>,
#[arg(help = "Whether strict mode is enabled on this database instance")]
#[arg(env = "SURREAL_STRICT", short = 's', long = "strict")]
#[arg(default_value_t = false)]
strict: bool,
#[arg(help = "The logging level for the database server")]
#[arg(env = "SURREAL_LOG", short = 'l', long = "log")]
#[arg(default_value = "info")]
#[arg(value_parser = CustomEnvFilterParser::new())]
log: CustomEnvFilter,
#[arg(help = "Whether to hide the startup banner")]
#[arg(env = "SURREAL_NO_BANNER", long)]
#[arg(default_value_t = false)]
no_banner: bool,
}
async fn init_impl(matches: &clap::ArgMatches) -> Result<(), Error> {
#[derive(Args, Debug)]
#[group(requires_all = ["kvs_ca", "kvs_crt", "kvs_key"], multiple = true)]
struct StartCommandRemoteTlsOptions {
#[arg(help = "Path to the CA file used when connecting to the remote KV store")]
#[arg(env = "SURREAL_KVS_CA", long = "kvs-ca", value_parser = super::validator::file_exists)]
kvs_ca: Option<PathBuf>,
#[arg(help = "Path to the certificate file used when connecting to the remote KV store")]
#[arg(env = "SURREAL_KVS_CRT", long = "kvs-crt", value_parser = super::validator::file_exists)]
kvs_crt: Option<PathBuf>,
#[arg(help = "Path to the private key file used when connecting to the remote KV store")]
#[arg(env = "SURREAL_KVS_KEY", long = "kvs-key", value_parser = super::validator::file_exists)]
kvs_key: Option<PathBuf>,
}
#[derive(Args, Debug)]
#[group(requires_all = ["web_crt", "web_key"], multiple = true)]
struct StartCommandWebTlsOptions {
#[arg(help = "Path to the certificate file for encrypted client connections")]
#[arg(env = "SURREAL_WEB_CRT", long = "web-crt", value_parser = super::validator::file_exists)]
web_crt: Option<PathBuf>,
#[arg(help = "Path to the private key file for encrypted client connections")]
#[arg(env = "SURREAL_WEB_KEY", long = "web-key", value_parser = super::validator::file_exists)]
web_key: Option<PathBuf>,
}
pub async fn init(
StartCommandArguments {
path,
username: user,
password: pass,
listen_addresses,
web,
strict,
log: CustomEnvFilter(log),
no_banner,
..
}: StartCommandArguments,
) -> Result<(), Error> {
// Initialize opentelemetry and logging
crate::o11y::builder().with_log_level(matches.get_one::<String>("log").unwrap()).init();
crate::o11y::builder().with_filter(log).init();
// Check if a banner should be outputted
if !matches.is_present("no-banner") {
if !no_banner {
// Output SurrealDB logo
println!("{LOGO}");
}
// Setup the cli options
config::init(matches);
let _ = config::CF.set(Config {
strict,
bind: listen_addresses.first().cloned().unwrap(),
path,
user,
pass,
crt: web.as_ref().and_then(|x| x.web_crt.clone()),
key: web.as_ref().and_then(|x| x.web_key.clone()),
});
// Initiate environment
env::init().await?;
// Initiate master auth
@ -32,19 +125,3 @@ async fn init_impl(matches: &clap::ArgMatches) -> Result<(), Error> {
// All ok
Ok(())
}
/// Rust's default thread stack size of 2MiB doesn't allow sufficient recursion depth.
fn with_enough_stack<T>(fut: impl Future<Output = T> + Send) -> T {
let stack_size = 8 * 1024 * 1024;
// Stack frames are generally larger in debug mode.
#[cfg(debug_assertions)]
let stack_size = stack_size * 2;
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_stack_size(stack_size)
.build()
.unwrap()
.block_on(fut)
}

64
src/cli/validator/mod.rs Normal file
View file

@ -0,0 +1,64 @@
use std::path::{Path, PathBuf};
pub(crate) mod parser;
pub(crate) fn path_valid(v: &str) -> Result<String, String> {
match v {
"memory" => Ok(v.to_string()),
v if v.starts_with("file:") => Ok(v.to_string()),
v if v.starts_with("rocksdb:") => Ok(v.to_string()),
v if v.starts_with("tikv:") => Ok(v.to_string()),
v if v.starts_with("fdb:") => Ok(v.to_string()),
_ => Err(String::from("Provide a valid database path parameter")),
}
}
pub(crate) fn file_exists(path: &str) -> Result<PathBuf, String> {
let path = Path::new(path);
if !*path.try_exists().as_ref().map_err(ToString::to_string)? {
return Err(String::from("Ensure the file exists"));
}
if !path.is_file() {
return Err(String::from("Ensure the path is a file"));
}
Ok(path.to_owned())
}
pub(crate) fn endpoint_valid(v: &str) -> Result<String, String> {
fn split_endpoint(v: &str) -> (&str, &str) {
match v {
"memory" => ("mem", ""),
v => match v.split_once("://") {
Some(parts) => parts,
None => v.split_once(':').unwrap_or_default(),
},
}
}
let scheme = split_endpoint(v).0;
match scheme {
"http" | "https" | "ws" | "wss" | "fdb" | "mem" | "rocksdb" | "file" | "tikv" => {
Ok(v.to_string())
}
_ => Err(String::from("Provide a valid database connection string")),
}
}
pub(crate) fn into_valid(v: &str) -> Result<String, String> {
match v {
v if v.ends_with(".db") => Ok(v.to_string()),
v if v.starts_with("http://") => Ok(v.to_string()),
v if v.starts_with("https://") => Ok(v.to_string()),
"-" => Ok(v.to_string()),
_ => Err(String::from("Provide a valid database connection string, or the path to a file")),
}
}
pub(crate) fn key_valid(v: &str) -> Result<String, String> {
match v.len() {
16 => Ok(v.to_string()),
24 => Ok(v.to_string()),
32 => Ok(v.to_string()),
_ => Err(String::from("Ensure your database encryption key is 16, 24, or 32 bytes long")),
}
}

View file

@ -0,0 +1,74 @@
use clap::builder::{NonEmptyStringValueParser, PossibleValue, TypedValueParser};
use clap::error::{ContextKind, ContextValue, ErrorKind};
use tracing::Level;
use tracing_subscriber::EnvFilter;
#[derive(Debug)]
pub struct CustomEnvFilter(pub EnvFilter);
impl Clone for CustomEnvFilter {
fn clone(&self) -> Self {
Self(EnvFilter::builder().parse(self.0.to_string()).unwrap())
}
}
#[derive(Clone)]
pub struct CustomEnvFilterParser;
impl CustomEnvFilterParser {
pub fn new() -> CustomEnvFilterParser {
Self
}
}
impl TypedValueParser for CustomEnvFilterParser {
type Value = CustomEnvFilter;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let inner = NonEmptyStringValueParser::new();
let v = inner.parse_ref(cmd, arg, value)?;
let filter = (match v.as_str() {
// Don't show any logs at all
"none" => Ok(EnvFilter::default()),
// Check if we should show all log levels
"full" => Ok(EnvFilter::default().add_directive(Level::TRACE.into())),
// Otherwise, let's only show errors
"error" => Ok(EnvFilter::default().add_directive(Level::ERROR.into())),
// Specify the log level for each code area
"warn" | "info" | "debug" | "trace" => EnvFilter::builder()
.parse(format!("error,surreal={v},surrealdb={v},surrealdb::txn=error")),
// Let's try to parse the custom log level
_ => EnvFilter::builder().parse(v),
})
.map_err(|e| {
let mut err = clap::Error::new(ErrorKind::ValueValidation).with_cmd(cmd);
err.insert(ContextKind::Custom, ContextValue::String(e.to_string()));
err.insert(
ContextKind::InvalidValue,
ContextValue::String("Provide a valid log filter configuration string".to_string()),
);
err
})?;
Ok(CustomEnvFilter(filter))
}
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
Some(Box::new(
[
PossibleValue::new("none"),
PossibleValue::new("full"),
PossibleValue::new("error"),
PossibleValue::new("warn"),
PossibleValue::new("info"),
PossibleValue::new("debug"),
PossibleValue::new("trace"),
]
.into_iter(),
))
}
}

View file

@ -0,0 +1 @@
pub(crate) mod env_filter;

View file

@ -1,7 +1,7 @@
use crate::env::release;
use crate::err::Error;
pub fn init(_: &clap::ArgMatches) -> Result<(), Error> {
pub fn init() -> Result<(), Error> {
println!("{}", release());
Ok(())
}

View file

@ -29,6 +29,9 @@ pub enum Error {
#[error("There was a problem connecting with the storage engine")]
InvalidStorage,
#[error("The operation is unsupported")]
OperationUnsupported,
#[error("There was a problem with the database: {0}")]
Db(#[from] SurrealError),

View file

@ -26,8 +26,26 @@ mod net;
mod o11y;
mod rpc;
use std::future::Future;
use std::process::ExitCode;
fn main() -> ExitCode {
cli::init() // Initiate the command line
// Initiate the command line
with_enough_stack(cli::init())
}
/// Rust's default thread stack size of 2MiB doesn't allow sufficient recursion depth.
fn with_enough_stack<T>(fut: impl Future<Output = T> + Send) -> T {
let stack_size = 8 * 1024 * 1024;
// Stack frames are generally larger in debug mode.
#[cfg(debug_assertions)]
let stack_size = stack_size * 2;
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_stack_size(stack_size)
.build()
.unwrap()
.block_on(fut)
}

View file

@ -1,12 +1,15 @@
mod logger;
mod tracers;
use crate::cli::validator::parser::env_filter::CustomEnvFilter;
use tracing::Subscriber;
use tracing_subscriber::{prelude::*, util::SubscriberInitExt};
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::{prelude::*, util::SubscriberInitExt, EnvFilter};
#[derive(Default, Debug, Clone)]
pub struct Builder {
log_level: String,
log_level: Option<String>,
filter: Option<CustomEnvFilter>,
}
pub fn builder() -> Builder {
@ -16,14 +19,30 @@ pub fn builder() -> Builder {
impl Builder {
/// Set the log level on the builder
pub fn with_log_level(mut self, log_level: &str) -> Self {
self.log_level = log_level.to_string();
self.log_level = Some(log_level.to_string());
self
}
/// Set the filter on the builder
pub fn with_filter(mut self, filter: EnvFilter) -> Self {
self.filter = Some(CustomEnvFilter(filter));
self
}
/// Build a dispatcher with the fmt subscriber (logs) and the chosen tracer subscriber
pub fn build(self) -> Box<dyn Subscriber + Send + Sync + 'static> {
Box::new(
tracing_subscriber::registry().with(logger::new(self.log_level)).with(tracers::new()),
)
let registry = tracing_subscriber::registry();
let registry = registry.with(self.filter.map(|filter| {
tracing_subscriber::fmt::layer()
.compact()
.with_ansi(true)
.with_span_events(FmtSpan::NONE)
.with_writer(std::io::stderr)
.with_filter(filter.0)
.boxed()
}));
let registry = registry.with(self.log_level.map(logger::new));
let registry = registry.with(tracers::new());
Box::new(registry)
}
/// Build a dispatcher and set it as global
pub fn init(self) {