use std::path::PathBuf;
use std::sync::Arc;
use kevy_config::{AppendFsync, Config, EvictionPolicy, LogLevel, LogOutput, parse_size};
use kevy_resp::{
ArgvView, RespVersion, encode_array_len, encode_bulk, encode_error, encode_map_header,
encode_simple_string,
};
use crate::config_global;
use super::{appendfsync_str, eviction_str, log_level_str, wrong_args};
pub(crate) fn cmd_config<A: ArgvView + ?Sized>(
cfg: &Config,
args: &A,
out: &mut Vec<u8>,
proto: RespVersion,
) {
let sub = match args.get(1) {
Some(s) => s.to_ascii_uppercase(),
None => return wrong_args(out, "config"),
};
match sub.as_slice() {
b"GET" => cmd_config_get(cfg, args, out, proto),
b"SET" => cmd_config_set(args, out),
b"REWRITE" => cmd_config_rewrite(out),
b"RESETSTAT" => encode_simple_string(out, "OK"),
_ => encode_error(
out,
&format!(
"ERR unknown CONFIG subcommand '{}'",
String::from_utf8_lossy(args.get(1).unwrap_or(&[][..]))
),
),
}
}
fn cmd_config_get<A: ArgvView + ?Sized>(
cfg: &Config,
args: &A,
out: &mut Vec<u8>,
proto: RespVersion,
) {
if args.len() < 3 {
return wrong_args(out, "config|get");
}
let mut hits: Vec<(&'static str, String)> = Vec::new();
for i in 2..args.len() {
let pat = args[i].to_ascii_lowercase();
for (key, val) in config_pairs(cfg) {
if glob_match(&pat, key.as_bytes()) && !hits.iter().any(|(k, _)| *k == key) {
hits.push((key, val));
}
}
}
match proto {
RespVersion::V2 => encode_array_len(out, (hits.len() * 2) as i64),
RespVersion::V3 => encode_map_header(out, hits.len() as i64),
}
for (k, v) in hits {
encode_bulk(out, k.as_bytes());
encode_bulk(out, v.as_bytes());
}
}
fn cmd_config_set<A: ArgvView + ?Sized>(args: &A, out: &mut Vec<u8>) {
if args.len() != 4 {
return wrong_args(out, "config|set");
}
let key = args[2].to_ascii_lowercase();
let value = &args[3];
let v_slice: &[u8] = value;
crate::audit_log::record(&[
&b"CONFIG"[..],
&b"SET"[..],
&key[..],
v_slice,
]);
let live = config_global::get();
let mut new_cfg = (*live).clone();
match apply_hot_set(&mut new_cfg, &key, value) {
Ok(()) => match config_global::replace(Arc::new(new_cfg)) {
Ok(()) => encode_simple_string(out, "OK"),
Err(e) => encode_error(out, &format!("ERR {e}")),
},
Err(SetError::ReadOnly(k)) => encode_error(
out,
&format!("ERR config setting '{k}' can't be changed at runtime, restart required"),
),
Err(SetError::Unknown(k)) => encode_error(
out,
&format!("ERR Unknown CONFIG SET parameter: '{k}'"),
),
Err(SetError::BadValue { key, reason }) => encode_error(
out,
&format!("ERR CONFIG SET failed for '{key}': {reason}"),
),
}
}
fn cmd_config_rewrite(out: &mut Vec<u8>) {
crate::audit_log::record(&[&b"CONFIG"[..], &b"REWRITE"[..]]);
let cfg = config_global::get();
let Some(path) = cfg.source_path.clone() else {
return encode_error(
out,
"ERR The server is running without a config file",
);
};
let text = rewrite_text(&cfg, &path);
match atomic_write(&path, text.as_bytes()) {
Ok(()) => encode_simple_string(out, "OK"),
Err(e) => encode_error(
out,
&format!(
"ERR CONFIG REWRITE could not write {}: {e}",
path.display()
),
),
}
}
fn rewrite_text(cfg: &Config, path: &PathBuf) -> String {
match std::fs::read_to_string(path) {
Ok(src) => match cfg.to_toml_string_preserving(&src) {
Ok(t) => t,
Err(e) => {
eprintln!(
"kevy: CONFIG REWRITE comment-preserving re-parse of {} \
failed ({e}); falling back to standard template (comments lost)",
path.display(),
);
cfg.to_toml_string()
}
},
Err(e) => {
eprintln!(
"kevy: CONFIG REWRITE could not read {} for comment preservation \
({e}); falling back to standard template (comments lost)",
path.display(),
);
cfg.to_toml_string()
}
}
}
fn atomic_write(path: &PathBuf, bytes: &[u8]) -> std::io::Result<()> {
use std::io::Write;
let mut tmp = path.clone();
let new_name = match path.file_name() {
Some(n) => {
let mut s = n.to_os_string();
s.push(".rewrite");
s
}
None => return Err(std::io::Error::other("CONFIG REWRITE path has no file name")),
};
tmp.set_file_name(new_name);
let mut f = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&tmp)?;
f.write_all(bytes)?;
f.sync_data()?;
drop(f);
std::fs::rename(&tmp, path)?;
Ok(())
}
#[derive(Debug)]
enum SetError {
ReadOnly(String),
Unknown(String),
BadValue { key: String, reason: String },
}
fn apply_hot_set(cfg: &mut Config, key: &[u8], value: &[u8]) -> Result<(), SetError> {
let key_str = std::str::from_utf8(key)
.map_err(|_| SetError::Unknown(String::from_utf8_lossy(key).into_owned()))?;
let value_str = std::str::from_utf8(value)
.map_err(|_| SetError::BadValue {
key: key_str.to_string(),
reason: "value is not valid UTF-8".to_string(),
})?;
match key_str {
"maxmemory" | "maxmemory-policy" => set_memory(cfg, key_str, value_str),
"appendfsync" | "auto-aof-rewrite-percentage" | "auto-aof-rewrite-min-size" => {
set_persistence(cfg, key_str, value_str)
}
"hz" | "maxmemory-samples" => set_expiry(cfg, key_str, value_str),
"loglevel" | "logfile" => set_log(cfg, key_str, value_str),
"bind" | "port" | "io-threads" | "dir" | "appendonly" => {
Err(SetError::ReadOnly(key_str.to_string()))
}
other => Err(SetError::Unknown(other.to_string())),
}
}
fn set_memory(cfg: &mut Config, key: &str, value: &str) -> Result<(), SetError> {
match key {
"maxmemory" => {
cfg.memory.maxmemory = parse_size(value).map_err(|reason| SetError::BadValue {
key: key.to_string(),
reason,
})?;
}
"maxmemory-policy" => {
cfg.memory.maxmemory_policy = EvictionPolicy::parse(value).ok_or_else(|| {
SetError::BadValue {
key: key.to_string(),
reason: "expected one of noeviction / allkeys-lru / \
allkeys-lfu / allkeys-random / volatile-lru / \
volatile-lfu / volatile-random / volatile-ttl"
.to_string(),
}
})?;
}
_ => return Err(SetError::Unknown(key.to_string())),
}
Ok(())
}
fn set_persistence(cfg: &mut Config, key: &str, value: &str) -> Result<(), SetError> {
match key {
"appendfsync" => {
cfg.persistence.appendfsync =
AppendFsync::parse(value).ok_or_else(|| SetError::BadValue {
key: key.to_string(),
reason: "expected one of always / everysec / no".to_string(),
})?;
}
"auto-aof-rewrite-percentage" => {
cfg.persistence.auto_aof_rewrite_percentage =
value.parse::<u32>().map_err(|_| SetError::BadValue {
key: key.to_string(),
reason: "expected a non-negative integer".to_string(),
})?;
}
"auto-aof-rewrite-min-size" => {
cfg.persistence.auto_aof_rewrite_min_size =
parse_size(value).map_err(|reason| SetError::BadValue {
key: key.to_string(),
reason,
})?;
}
_ => return Err(SetError::Unknown(key.to_string())),
}
Ok(())
}
fn set_expiry(cfg: &mut Config, key: &str, value: &str) -> Result<(), SetError> {
let n = value.parse::<u32>().map_err(|_| SetError::BadValue {
key: key.to_string(),
reason: "expected a non-negative integer".to_string(),
})?;
match key {
"hz" => cfg.expiry.hz = n,
"maxmemory-samples" => cfg.expiry.sample = n,
_ => return Err(SetError::Unknown(key.to_string())),
}
Ok(())
}
fn set_log(cfg: &mut Config, key: &str, value: &str) -> Result<(), SetError> {
match key {
"loglevel" => {
cfg.log.level = LogLevel::parse(value).ok_or_else(|| SetError::BadValue {
key: key.to_string(),
reason: "expected one of trace / debug / info / warning / error"
.to_string(),
})?;
}
"logfile" => {
match LogOutput::parse(value) {
LogOutput::Stdout => cfg.log.output = LogOutput::Stdout,
LogOutput::Stderr => cfg.log.output = LogOutput::Stderr,
LogOutput::File(_) => return Err(SetError::ReadOnly(key.to_string())),
}
}
_ => return Err(SetError::Unknown(key.to_string())),
}
Ok(())
}
fn config_pairs(cfg: &Config) -> Vec<(&'static str, String)> {
let mut v: Vec<(&'static str, String)> = Vec::new();
let [a, b, c, d] = cfg.server.bind;
v.push(("bind", format!("{a}.{b}.{c}.{d}")));
v.push(("port", cfg.server.port.to_string()));
v.push(("io-threads", cfg.server.threads.to_string()));
v.push(("dir", cfg.server.data_dir.display().to_string()));
v.push(("appendonly", yes_no(cfg.persistence.aof)));
v.push(("save", String::new()));
v.push((
"appendfsync",
appendfsync_str(cfg.persistence.appendfsync).to_string(),
));
v.push((
"auto-aof-rewrite-percentage",
cfg.persistence.auto_aof_rewrite_percentage.to_string(),
));
v.push((
"auto-aof-rewrite-min-size",
cfg.persistence.auto_aof_rewrite_min_size.to_string(),
));
v.push(("maxmemory", cfg.memory.maxmemory.to_string()));
v.push((
"maxmemory-policy",
eviction_str(cfg.memory.maxmemory_policy).to_string(),
));
v.push(("hz", cfg.expiry.hz.to_string()));
v.push(("maxmemory-samples", cfg.expiry.sample.to_string()));
v.push(("loglevel", log_level_str(cfg.log.level).to_string()));
v.push(("cluster-enabled", yes_no(cfg.cluster.enabled)));
v.push((
"cluster-port-base",
crate::cluster_port_base(cfg).to_string(),
));
v
}
fn glob_match(pat: &[u8], s: &[u8]) -> bool {
fn go(pat: &[u8], s: &[u8]) -> bool {
match pat.split_first() {
None => s.is_empty(),
Some((&b'*', rest)) => {
if rest.is_empty() {
return true;
}
(0..=s.len()).any(|i| go(rest, &s[i..]))
}
Some((&c, rest)) => match s.split_first() {
Some((&first, srest)) if first == c => go(rest, srest),
_ => false,
},
}
}
go(pat, s)
}
fn yes_no(b: bool) -> String {
if b { "yes".into() } else { "no".into() }
}
#[cfg(test)]
#[path = "config_tests.rs"]
mod tests;