From c21c1875bb189324b18dbef9ac151ab5f0bad9e1 Mon Sep 17 00:00:00 2001 From: Michael Martin Date: Mon, 11 Dec 2023 16:46:03 -0800 Subject: [PATCH] chore(*): remove minijinja This cuts the dist binary size in half (<500k) at the cost of making some of the nginx.conf generation code a bit uglier. --- Cargo.lock | 30 --- Cargo.toml | 1 - src/cli.rs | 258 +++------------------ src/nginx.conf.tpl | 172 -------------- src/nginx.rs | 563 +++++++++++++++++++++++++++++++++++++++++---- src/types.rs | 62 +++++ 6 files changed, 614 insertions(+), 472 deletions(-) delete mode 100644 src/nginx.conf.tpl diff --git a/Cargo.lock b/Cargo.lock index 5642053..358ca1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,15 +26,6 @@ version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" -[[package]] -name = "minijinja" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "208758577ef2c86cf5dd3e85730d161413ec3284e2d73b2ef65d9a24d9971bcb" -dependencies = [ - "serde", -] - [[package]] name = "nix" version = "0.27.1" @@ -75,7 +66,6 @@ name = "rusty-cli" version = "0.1.0-pre-6" dependencies = [ "libc", - "minijinja", "nix", "shlex", "strum", @@ -83,26 +73,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "serde" -version = "1.0.190" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.190" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "shlex" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index bd33a42..99ceb53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ publish = true [dependencies] -minijinja = "1.0.10" shlex = "1.2.0" strum = { version = "0.25", features = ["derive"] } strum_macros = "0.25" diff --git a/src/cli.rs b/src/cli.rs index 579e0c2..42ad55a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,5 @@ use crate::lua::*; +use crate::nginx; use crate::nginx::*; use crate::run::run; use crate::types::*; @@ -9,9 +10,8 @@ use std::env; use std::fmt::Display; use std::fs; use std::fs::File; -use std::process; +use std::io::prelude::*; use std::process::Command; -use thiserror::Error as ThisError; const VERSION: &str = "0.1.0"; @@ -158,223 +158,6 @@ fn main_conf(user: &mut UserArgs) -> Vec { conf } -pub struct NginxExec { - prefix: String, - runner: Runner, - bin: String, - label: Option, -} - -impl From for Command { - fn from(ngx: NginxExec) -> Self { - let root = ngx.prefix; - - // resty CLI always adds a trailing slash - let prefix = format!("{}/", root.trim_end_matches('/')); - - let nginx = ngx.bin; - - let mut nginx_args = vec![ - String::from("-p"), - prefix, - String::from("-c"), - String::from("conf/nginx.conf"), - ]; - - if let Some(label) = ngx.label { - nginx_args.insert(0, String::from("-g")); - nginx_args.insert(1, label); - } - - let bin: String; - let mut args: Vec = vec![]; - - match ngx.runner { - Runner::Default => { - bin = nginx; - args.append(&mut nginx_args); - } - Runner::RR => { - bin = String::from("rr"); - args.push(String::from("record")); - args.push(nginx); - args.append(&mut nginx_args); - } - Runner::Stap(opts) => { - bin = String::from("stap"); - args = vec![]; - if let Some(opts) = opts { - args.append(&mut split_shell_args(&opts)); - } - args.push("-c".to_owned()); - nginx_args.insert(0, nginx); - args.push(join_shell_args(&nginx_args)); - } - Runner::Valgrind(opts) => { - bin = "valgrind".to_owned(); - args = vec![]; - if let Some(opts) = opts { - args.append(&mut split_shell_args(&opts)); - } - args.push(nginx); - args.append(&mut nginx_args); - } - Runner::Gdb(opts) => { - bin = String::from("gdb"); - if let Some(opts) = opts { - args.append(&mut split_shell_args(&opts)); - } - args.push("--args".to_owned()); - args.push(nginx); - args.append(&mut nginx_args); - } - Runner::User(runner) => { - let mut user_args = split_shell_args(&runner); - bin = user_args.remove(0); - args.append(&mut user_args); - args.push(nginx); - args.append(&mut nginx_args); - } - }; - - let mut c = process::Command::new(bin); - - c.args(args); - c - } -} - -#[derive(Default, Debug)] -pub(crate) enum Runner { - #[default] - Default, - RR, - Stap(Option), - Valgrind(Option), - Gdb(Option), - User(String), -} - -impl Runner { - fn arg_name(&self) -> String { - match self { - Self::RR => "--rr", - Self::Stap(_) => "--stap", - Self::Gdb(_) => "--gdb", - Self::Valgrind(_) => "--valgrind", - Self::User(_) => "--user-runner", - _ => unreachable!(), - } - .to_owned() - } - - fn opt_name(&self) -> String { - self.arg_name() + "-opts" - } - - fn same(&self, other: &Runner) -> bool { - std::mem::discriminant(self) == std::mem::discriminant(other) - } - - fn takes_opts(&self) -> bool { - match self { - Self::Stap(_) => true, - Self::Gdb(_) => true, - Self::Valgrind(_) => true, - Self::User(_) => false, - Self::RR => false, - Self::Default => false, - } - } - - fn has_opts(&self) -> bool { - match self { - Self::Stap(o) | Self::Gdb(o) | Self::Valgrind(o) => o.is_some(), - Self::User(_) => true, - Self::RR => false, - Self::Default => false, - } - } - - fn update(&mut self, new: Runner) -> Result<(), ArgError> { - if let Runner::Default = self { - *self = new; - Ok(()) - } else if self.same(&new) { - // e.g. we already saw --gdb and are now adding opts with --gdb-opts - if self.takes_opts() && !self.has_opts() && new.has_opts() { - *self = new; - Ok(()) - } else { - Err(ArgError::Duplicate(new.opt_name())) - } - } else { - Err(ArgError::Conflict(self.arg_name(), new.arg_name())) - } - } -} - -#[derive(ThisError, Debug)] -pub enum ArgError { - #[error("ERROR: could not find {0} include file '{1}'")] - MissingInclude(String, String), - - #[error("ERROR: options {0} and {1} cannot be specified at the same time.")] - Conflict(String, String), - - #[error("ERROR: Invalid {arg} option value: {value}\n ({err})")] - InvalidValue { - arg: String, - value: String, - err: String, - }, - - #[error("unknown argument: `{0}`")] - UnknownArgument(String), - - #[error("option {0} takes an argument but found none.")] - MissingValue(String), - - #[error("Neither Lua input file nor -e \"\" option specified.")] - NoLuaInput, - - #[error("duplicate {0} options")] - Duplicate(String), - - #[error("Lua input file {0} not found.")] - LuaFileNotFound(String), -} - -impl ArgError { - pub fn exit_code(&self) -> i32 { - match self { - // I/O error - Self::MissingInclude(_, _) => 2, - - // yup, resty-cli returns 25 (ENOTTY) for mutually-exclusive - // arguments - // - // not on purpose though, it's just a side effect of errno - // having been set from a previous and unrelated error - Self::Conflict(_, _) => 25, - - Self::UnknownArgument(_) => 1, - - Self::InvalidValue { - arg: _, - value: _, - err: _, - } => 255, - Self::MissingValue(_) => 255, - - Self::NoLuaInput => 2, - Self::LuaFileNotFound(_) => 2, - - Self::Duplicate(_) => 255, - } - } -} - pub trait CliOpt { fn get_arg(&self, optarg: &mut Option) -> Result; @@ -489,8 +272,10 @@ impl Action { user.inline_lua.insert(0, jit.to_lua()); } + let resty_compat_version = get_resty_compat_version(); let mut label = None; - if get_resty_compat_version() >= 30 { + + if resty_compat_version >= 30 { let mut s = String::from("# "); if !user.inline_lua.is_empty() { s.push_str("-e '"); @@ -526,22 +311,35 @@ impl Action { } }; - let vars = Vars { - main_conf: main_conf(&mut user), - stream_enabled: !user.no_stream, - stream_conf: stream_conf(&mut user), - http_conf: http_conf(&mut user), - events_conf: vec![format!("worker_connections {};", user.worker_connections)], - lua_loader, + let conf_path = prefix.conf.join("nginx.conf"); + let mut file = match fs::File::create(conf_path) { + Ok(file) => file, + Err(e) => { + eprintln!("failed opening nginx.conf for writing: {}", e); + return 2; + } }; - let conf_path = prefix.conf.join("nginx.conf"); - if let Err(e) = fs::write(conf_path, render_config(vars)) { + let events_conf = vec![format!("worker_connections {};", user.worker_connections)]; + + let res = nginx::ConfBuilder::new() + .main(main_conf(&mut user)) + .events(events_conf) + .stream(stream_conf(&mut user), !user.no_stream) + .http(http_conf(&mut user)) + .lua(lua_loader) + .resty_compat_version(resty_compat_version) + .render(&mut file) + .and_then(|_| file.flush()); + + if let Err(e) = res { eprintln!("failed writing nginx.conf file: {}", e); return 2; } - let ngx = NginxExec { + drop(file); + + let ngx = nginx::Exec { bin: find_nginx_bin(user.nginx_bin).to_str().unwrap().to_string(), prefix: prefix.root.to_str().unwrap().to_string(), runner: user.runner, diff --git a/src/nginx.conf.tpl b/src/nginx.conf.tpl deleted file mode 100644 index b793f84..0000000 --- a/src/nginx.conf.tpl +++ /dev/null @@ -1,172 +0,0 @@ -# generated by rusty-cli -# resty-cli compat: v0.{{ resty_compat_version }} - -daemon off; -master_process off; -worker_processes 1; -pid logs/nginx.pid; - -{% for line in main_conf %} -{{ line }} -{%- endfor %} - -events { - {% for line in events_conf %} - {{ line }} - {%- endfor %} -} - -{% if stream_enabled %} -stream { - access_log off; - lua_socket_log_errors off; - lua_regex_cache_max_entries 40960; - {% for line in stream_conf %} - {{ line }} - {%- endfor %} -} -{% endif %} - -http { - access_log off; - lua_socket_log_errors off; - lua_regex_cache_max_entries 40960; - - {% for line in http_conf %} - {{ line }} - {%- endfor %} - - init_by_lua_block { - ngx.config.is_console = true - - local stdout = io.stdout - local ngx_null = ngx.null - local maxn = table.maxn - local unpack = unpack - local concat = table.concat - - local expand_table - function expand_table(src, inplace) - local n = maxn(src) - local dst = inplace and src or {} - for i = 1, n do - local arg = src[i] - local typ = type(arg) - if arg == nil then - dst[i] = "nil" - - elseif typ == "boolean" then - if arg then - dst[i] = "true" - else - dst[i] = "false" - end - - elseif arg == ngx_null then - dst[i] = "null" - - elseif typ == "table" then - dst[i] = expand_table(arg, false) - - elseif typ ~= "string" then - dst[i] = tostring(arg) - - else - dst[i] = arg - end - end - return concat(dst) - end - - local function output(...) - local args = {...} - - return stdout:write(expand_table(args, true)) - end - - ngx.orig_print = ngx.print - ngx.print = output - - ngx.orig_say = ngx.say - ngx.say = function (...) - local ok, err = output(...) - if ok then - return output("\n") - end - return ok, err - end - print = ngx.say - - ngx.flush = function (...) return stdout:flush() end - -- we cannot close stdout here due to a bug in Lua: - ngx.eof = function (...) return true end - - {% if resty_compat_version >= 29 %} - ngx.orig_exit = ngx.exit - {% endif %} - - ngx.exit = os.exit - } - - init_worker_by_lua_block { - local exit = os.exit - local stderr = io.stderr - local ffi = require "ffi" - - local function handle_err(err) - if err then - err = string.gsub(err, "^init_worker_by_lua:%d+: ", "") - stderr:write("ERROR: ", err, "\n") - end - return exit(1) - end - - local ok, err = pcall(function () - if not ngx.config - or not ngx.config.ngx_lua_version - or ngx.config.ngx_lua_version < 10009 - then - error("at least ngx_lua 0.10.9 is required") - end - - local signal_graceful_exit = - require("ngx.process").signal_graceful_exit - if not signal_graceful_exit then - error("lua-resty-core library is too old; " - .. "missing the signal_graceful_exit() function " - .. "in ngx.process") - end - - {% for line in lua_loader %} - {{ line }} - {%- endfor %} - - -- print("calling timer.at...") - local ok, err = ngx.timer.at(0, function () - -- io.stderr:write("timer firing") - local ok, err = xpcall(gen, function (err) - -- level 3: we skip this function and the - -- error() call itself in our stacktrace - local trace = debug.traceback(err, 3) - return handle_err(trace) - end) - if not ok then - return handle_err(err) - end - if ffi.abi("win") then - return exit(0) - end - signal_graceful_exit() - end) - if not ok then - return handle_err(err) - end - -- print("timer created") - end) - - if not ok then - return handle_err(err) - end - } -} -# vim:set ft=nginx ts=4 sw=4 sts=4 et: diff --git a/src/nginx.rs b/src/nginx.rs index 29f6072..ad9c4c1 100644 --- a/src/nginx.rs +++ b/src/nginx.rs @@ -1,48 +1,378 @@ -use minijinja::{context, Environment}; +use crate::types::ArgError; +use crate::util::*; use std::env; +use std::io; use std::path::PathBuf; +use std::process::Command; const RESTY_COMPAT_VAR: &str = "RESTY_CLI_COMPAT_VERSION"; const RESTY_COMPAT_LATEST: u64 = 30; -const TEMPLATE: &str = include_str!("nginx.conf.tpl"); -const TEMPLATE_NAME: &str = "nginx.conf"; +const BLOCK_OPEN: &str = "{"; +const BLOCK_CLOSE: &str = "}"; -pub struct Vars { - pub events_conf: Vec, - pub main_conf: Vec, - pub stream_enabled: bool, - pub stream_conf: Vec, - pub http_conf: Vec, - pub lua_loader: Vec, +#[derive(Debug, Default)] +pub struct ConfBuilder { + events: Option>, + main: Option>, + stream_enabled: bool, + stream: Option>, + http: Option>, + lua: Option>, + resty_compat_version: Option, } -fn init_template<'env, 'source>(env: &'env mut Environment) -> minijinja::Template<'env, 'source> { - env.add_template(TEMPLATE_NAME, TEMPLATE).unwrap(); - env.get_template(TEMPLATE_NAME).unwrap() +impl ConfBuilder { + pub fn new() -> Self { + Default::default() + } + + pub fn events(mut self, t: T) -> Self + where + T: IntoIterator, + { + self.events = Some(Vec::from_iter(t)); + self + } + + pub fn main(mut self, t: T) -> Self + where + T: IntoIterator, + { + self.main = Some(Vec::from_iter(t)); + self + } + + pub fn http(mut self, t: T) -> Self + where + T: IntoIterator, + { + self.http = Some(Vec::from_iter(t)); + self + } + + pub fn stream(mut self, t: T, enabled: bool) -> Self + where + T: IntoIterator, + { + self.stream = Some(Vec::from_iter(t)); + self.stream_enabled = enabled; + self + } + + pub fn lua(mut self, t: T) -> Self + where + T: IntoIterator, + { + self.lua = Some(Vec::from_iter(t)); + self + } + + pub fn resty_compat_version(mut self, v: u64) -> Self { + self.resty_compat_version = Some(v); + self + } + + pub fn render(self, buf: &mut T) -> io::Result<()> + where + T: io::Write, + { + let buf = &mut io::BufWriter::new(buf); + + Conf::from(self).render(buf) + } } -#[test] -fn verify_template() { - let mut env = Environment::new(); - init_template(&mut env); +impl From for Conf { + fn from(cb: ConfBuilder) -> Self { + let ConfBuilder { + events, + main, + stream_enabled, + stream, + http, + lua, + resty_compat_version, + } = cb; + + Self { + events: events.unwrap_or_default(), + main: main.unwrap_or_default(), + stream: stream.unwrap_or_default(), + stream_enabled, + http: http.unwrap_or_default(), + lua: lua.unwrap_or_default(), + resty_compat_version: resty_compat_version.unwrap_or_else(get_resty_compat_version), + } + } } -pub fn render_config(vars: Vars) -> String { - let mut env = Environment::new(); - let template = init_template(&mut env); - - let ctx = context! { - main_conf => vars.main_conf, - http_conf => vars.http_conf, - stream_enabled => vars.stream_enabled, - stream_conf => vars.stream_conf, - lua_loader => vars.lua_loader, - events_conf => vars.events_conf, - resty_compat_version => get_resty_compat_version(), - }; +struct Conf { + pub events: Vec, + pub main: Vec, + pub stream_enabled: bool, + pub stream: Vec, + pub http: Vec, + pub lua: Vec, + pub resty_compat_version: u64, +} + +impl Conf { + fn render(self, buf: &mut T) -> io::Result<()> + where + T: io::Write, + { + self.render_main(buf)?; + self.render_events(buf)?; + self.render_stream(buf)?; + self.render_http(buf)?; + + Ok(()) + } + + fn render_main(&self, buf: &mut T) -> io::Result<()> + where + T: io::Write, + { + writeln!(buf, "# generated by rusty-cli")?; + writeln!(buf, "# resty-cli compat: v0.{}", self.resty_compat_version)?; + writeln!(buf)?; + + writeln!(buf, "daemon off;")?; + writeln!(buf, "master_process off;")?; + writeln!(buf, "worker_processes 1;")?; + writeln!(buf, "pid logs/nginx.pid;")?; + writeln!(buf)?; + + for line in &self.main { + writeln!(buf, "{}", line)?; + } + + writeln!(buf)?; + + Ok(()) + } + + fn render_events(&self, buf: &mut T) -> io::Result<()> + where + T: io::Write, + { + writeln!(buf, "events {}", BLOCK_OPEN)?; + + for line in &self.events { + writeln!(buf, " {}", line)?; + } + + writeln!(buf, "{}", BLOCK_CLOSE)?; + writeln!(buf)?; + + Ok(()) + } + + fn render_stream(&self, buf: &mut T) -> io::Result<()> + where + T: io::Write, + { + if self.stream_enabled { + writeln!(buf, "stream {}", BLOCK_OPEN)?; + + writeln!(buf, " access_log off;")?; + writeln!(buf, " lua_socket_log_errors off;")?; + writeln!(buf, " lua_regex_cache_max_entries 40960;")?; + + for line in &self.stream { + writeln!(buf, " {}", line)?; + } + + writeln!(buf, "{}", BLOCK_CLOSE)?; + writeln!(buf)?; + } + + Ok(()) + } + + fn render_http(&self, buf: &mut T) -> io::Result<()> + where + T: io::Write, + { + writeln!(buf, "http {}", BLOCK_OPEN)?; + writeln!(buf, " access_log off;")?; + writeln!(buf, " lua_socket_log_errors off;")?; + writeln!(buf, " lua_regex_cache_max_entries 40960;")?; + + for line in &self.http { + writeln!(buf, " {}", line)?; + } + writeln!(buf)?; + + self.render_lua(buf)?; + + writeln!(buf, "{}", BLOCK_CLOSE)?; + + Ok(()) + } + + fn render_lua(&self, buf: &mut T) -> io::Result<()> + where + T: io::Write, + { + const INIT_BY_LUA_OPEN: &str = r##" + init_by_lua_block { + ngx.config.is_console = true + + local stdout = io.stdout + local ngx_null = ngx.null + local maxn = table.maxn + local unpack = unpack + local concat = table.concat + + local expand_table + function expand_table(src, inplace) + local n = maxn(src) + local dst = inplace and src or {} + for i = 1, n do + local arg = src[i] + local typ = type(arg) + if arg == nil then + dst[i] = "nil" + + elseif typ == "boolean" then + if arg then + dst[i] = "true" + else + dst[i] = "false" + end + + elseif arg == ngx_null then + dst[i] = "null" - template.render(ctx).unwrap() + elseif typ == "table" then + dst[i] = expand_table(arg, false) + + elseif typ ~= "string" then + dst[i] = tostring(arg) + + else + dst[i] = arg + end + end + return concat(dst) + end + + local function output(...) + local args = {...} + + return stdout:write(expand_table(args, true)) + end + + ngx.orig_print = ngx.print + ngx.print = output + + ngx.orig_say = ngx.say + ngx.say = function (...) + local ok, err = output(...) + if ok then + return output("\n") + end + return ok, err + end + print = ngx.say + + ngx.flush = function (...) return stdout:flush() end + -- we cannot close stdout here due to a bug in Lua: + ngx.eof = function (...) return true end +"##; + + const INIT_BY_LUA_CLOSE: &str = " + ngx.exit = os.exit + } +"; + + const INIT_WORKER_BY_LUA_OPEN: &str = r##" + init_worker_by_lua_block { + local exit = os.exit + local stderr = io.stderr + local ffi = require "ffi" + + local function handle_err(err) + if err then + err = string.gsub(err, "^init_worker_by_lua:%d+: ", "") + stderr:write("ERROR: ", err, "\n") + end + return exit(1) + end + + local ok, err = pcall(function () + if not ngx.config + or not ngx.config.ngx_lua_version + or ngx.config.ngx_lua_version < 10009 + then + error("at least ngx_lua 0.10.9 is required") + end + + local signal_graceful_exit = + require("ngx.process").signal_graceful_exit + if not signal_graceful_exit then + error("lua-resty-core library is too old; " + .. "missing the signal_graceful_exit() function " + .. "in ngx.process") + end +"##; + + const INIT_WORKER_BY_LUA_CLOSE: &str = r##" + -- print("calling timer.at...") + local ok, err = ngx.timer.at(0, function () + -- io.stderr:write("timer firing") + local ok, err = xpcall(gen, function (err) + -- level 3: we skip this function and the + -- error() call itself in our stacktrace + local trace = debug.traceback(err, 3) + return handle_err(trace) + end) + if not ok then + return handle_err(err) + end + if ffi.abi("win") then + return exit(0) + end + signal_graceful_exit() + end) + if not ok then + return handle_err(err) + end + -- print("timer created") + end) + + if not ok then + return handle_err(err) + end + } +"##; + + writeln!(buf, "{}", INIT_BY_LUA_OPEN)?; + writeln!(buf)?; + + if self.resty_compat_version >= 29 { + writeln!(buf, " ngx.orig_exit = ngx.exit")?; + writeln!(buf)?; + } + + writeln!(buf, "{}", INIT_BY_LUA_CLOSE)?; + writeln!(buf)?; + + writeln!(buf, "{}", INIT_WORKER_BY_LUA_OPEN)?; + writeln!(buf)?; + + for line in &self.lua { + writeln!(buf, " {}", line)?; + } + writeln!(buf)?; + + writeln!(buf, "{}", INIT_WORKER_BY_LUA_CLOSE)?; + writeln!(buf)?; + + Ok(()) + } } pub fn find_nginx_bin(nginx: Option) -> PathBuf { @@ -71,18 +401,173 @@ pub fn find_nginx_bin(nginx: Option) -> PathBuf { pub fn get_resty_compat_version() -> u64 { // TODO: maybe make this a build config item? - match env::var_os(RESTY_COMPAT_VAR) { - Some(value) => { - let value = value.to_str().unwrap(); + if let Some(value) = env::var_os(RESTY_COMPAT_VAR) { + let value = value.to_str().unwrap(); + + let value = value.strip_prefix('v').unwrap_or(value); + + let items: Vec<&str> = value.splitn(3, '.').collect(); + + let value = if items.len() > 1 { items[1] } else { items[0] }; - let value = value.strip_prefix('v').unwrap_or(value); + value.parse::().unwrap_or(RESTY_COMPAT_LATEST) + } else { + RESTY_COMPAT_LATEST + } +} + +pub struct Exec { + pub prefix: String, + pub runner: Runner, + pub bin: String, + pub label: Option, +} + +impl From for Command { + fn from(ngx: Exec) -> Self { + let root = ngx.prefix; + + // resty CLI always adds a trailing slash + let prefix = format!("{}/", root.trim_end_matches('/')); + + let nginx = ngx.bin; + + let mut nginx_args = vec![ + String::from("-p"), + prefix, + String::from("-c"), + String::from("conf/nginx.conf"), + ]; - let items: Vec<&str> = value.splitn(3, '.').collect(); + if let Some(label) = ngx.label { + nginx_args.insert(0, String::from("-g")); + nginx_args.insert(1, label); + } + + let bin: String; + let mut args: Vec = vec![]; + + match ngx.runner { + Runner::Default => { + bin = nginx; + args.append(&mut nginx_args); + } + Runner::RR => { + bin = String::from("rr"); + args.push(String::from("record")); + args.push(nginx); + args.append(&mut nginx_args); + } + Runner::Stap(opts) => { + bin = String::from("stap"); + args = vec![]; + if let Some(opts) = opts { + args.append(&mut split_shell_args(&opts)); + } + args.push("-c".to_owned()); + nginx_args.insert(0, nginx); + args.push(join_shell_args(&nginx_args)); + } + Runner::Valgrind(opts) => { + bin = "valgrind".to_owned(); + args = vec![]; + if let Some(opts) = opts { + args.append(&mut split_shell_args(&opts)); + } + args.push(nginx); + args.append(&mut nginx_args); + } + Runner::Gdb(opts) => { + bin = String::from("gdb"); + if let Some(opts) = opts { + args.append(&mut split_shell_args(&opts)); + } + args.push("--args".to_owned()); + args.push(nginx); + args.append(&mut nginx_args); + } + Runner::User(runner) => { + let mut user_args = split_shell_args(&runner); + bin = user_args.remove(0); + args.append(&mut user_args); + args.push(nginx); + args.append(&mut nginx_args); + } + }; + + let mut c = Command::new(bin); + + c.args(args); + c + } +} - let value = if items.len() > 1 { items[1] } else { items[0] }; +#[derive(Default, Debug)] +pub enum Runner { + #[default] + Default, + RR, + Stap(Option), + Valgrind(Option), + Gdb(Option), + User(String), +} + +impl Runner { + fn arg_name(&self) -> String { + match self { + Self::RR => "--rr", + Self::Stap(_) => "--stap", + Self::Gdb(_) => "--gdb", + Self::Valgrind(_) => "--valgrind", + Self::User(_) => "--user-runner", + _ => unreachable!(), + } + .to_owned() + } + + fn opt_name(&self) -> String { + self.arg_name() + "-opts" + } + + fn same(&self, other: &Runner) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } + + fn takes_opts(&self) -> bool { + match self { + Self::Stap(_) => true, + Self::Gdb(_) => true, + Self::Valgrind(_) => true, + Self::User(_) => false, + Self::RR => false, + Self::Default => false, + } + } + + fn has_opts(&self) -> bool { + match self { + Self::Stap(o) | Self::Gdb(o) | Self::Valgrind(o) => o.is_some(), + Self::User(_) => true, + Self::RR => false, + Self::Default => false, + } + } - value.parse::().unwrap_or(RESTY_COMPAT_LATEST) + pub fn update(&mut self, new: Runner) -> Result<(), ArgError> { + if let Runner::Default = self { + *self = new; + Ok(()) + } else if self.same(&new) { + // e.g. we already saw --gdb and are now adding opts with --gdb-opts + if self.takes_opts() && !self.has_opts() && new.has_opts() { + *self = new; + Ok(()) + } else { + Err(ArgError::Duplicate(new.opt_name())) + } + } else { + Err(ArgError::Conflict(self.arg_name(), new.arg_name())) } - None => RESTY_COMPAT_LATEST, } } diff --git a/src/types.rs b/src/types.rs index 2376ba7..88047ab 100644 --- a/src/types.rs +++ b/src/types.rs @@ -6,6 +6,7 @@ use std::io; use std::net; use std::path::PathBuf; use std::string::ToString; +use thiserror::Error as ThisError; const MKDTEMP_TEMPLATE: &str = "/tmp/resty_XXXXXX"; @@ -310,3 +311,64 @@ impl Buf { self.indent -= 1 } } + +#[derive(ThisError, Debug)] +pub enum ArgError { + #[error("ERROR: could not find {0} include file '{1}'")] + MissingInclude(String, String), + + #[error("ERROR: options {0} and {1} cannot be specified at the same time.")] + Conflict(String, String), + + #[error("ERROR: Invalid {arg} option value: {value}\n ({err})")] + InvalidValue { + arg: String, + value: String, + err: String, + }, + + #[error("unknown argument: `{0}`")] + UnknownArgument(String), + + #[error("option {0} takes an argument but found none.")] + MissingValue(String), + + #[error("Neither Lua input file nor -e \"\" option specified.")] + NoLuaInput, + + #[error("duplicate {0} options")] + Duplicate(String), + + #[error("Lua input file {0} not found.")] + LuaFileNotFound(String), +} + +impl ArgError { + pub fn exit_code(&self) -> i32 { + match self { + // I/O error + Self::MissingInclude(_, _) => 2, + + // yup, resty-cli returns 25 (ENOTTY) for mutually-exclusive + // arguments + // + // not on purpose though, it's just a side effect of errno + // having been set from a previous and unrelated error + Self::Conflict(_, _) => 25, + + Self::UnknownArgument(_) => 1, + + Self::InvalidValue { + arg: _, + value: _, + err: _, + } => 255, + Self::MissingValue(_) => 255, + + Self::NoLuaInput => 2, + Self::LuaFileNotFound(_) => 2, + + Self::Duplicate(_) => 255, + } + } +}