multi-service commands: rss start/stop/restart/enable/disable mc-proxy mc-lobby

supports space-separated service names in start, stop, restart,
enable, disable commands. each service result returned on its own line.
This commit is contained in:
Niko Marmeladkov 2026-07-03 15:33:01 +03:00
parent f0d334b7dd
commit 672b676020
3 changed files with 111 additions and 74 deletions

View file

@ -14,15 +14,15 @@ pub struct Cli {
pub enum Commands {
/// Start a service
Start {
name: String,
names: Vec<String>,
},
/// Stop a service
Stop {
name: String,
names: Vec<String>,
},
/// Restart a service
Restart {
name: String,
names: Vec<String>,
},
/// Show service status
Status {
@ -38,13 +38,13 @@ pub enum Commands {
},
/// Enable auto-start for a service
Enable {
name: String,
names: Vec<String>,
#[arg(long, help = "Start the service immediately")]
now: bool,
},
/// Disable auto-start for a service
Disable {
name: String,
names: Vec<String>,
},
/// List all configured services
List,

View file

@ -364,48 +364,60 @@ fn handle_client(
let response = match line {
l if l.starts_with("start ") => {
let name = &l[6..];
if !config.service.contains_key(name) {
format!("error: service '{}' not found", name)
} else if procs.contains_key(name) {
format!("error: '{}' already running", name)
} else {
match spawn(name, &config.service[name]) {
Some(p) => {
let pid = p.pid;
procs.insert(name.to_string(), p);
format!("ok {}", pid)
let names: Vec<&str> = l[6..].split_whitespace().collect();
let mut out = Vec::new();
for name in &names {
if !config.service.contains_key(*name) {
out.push(format!("error: service '{}' not found", name));
} else if procs.contains_key(*name) {
out.push(format!("error: '{}' already running", name));
} else {
match spawn(name, &config.service[*name]) {
Some(p) => {
let pid = p.pid;
procs.insert(name.to_string(), p);
out.push(format!("ok {} pid={}", name, pid));
}
None => out.push(format!("error: failed to spawn '{}'", name)),
}
None => format!("error: failed to spawn '{}'", name),
}
}
out.join("\n")
}
l if l.starts_with("stop ") => {
let name = &l[5..];
if !procs.contains_key(name) {
format!("error: '{}' not running", name)
} else {
stop(name, procs);
"ok".to_string()
let names: Vec<&str> = l[5..].split_whitespace().collect();
let mut out = Vec::new();
for name in &names {
if !procs.contains_key(*name) {
out.push(format!("error: '{}' not running", name));
} else {
stop(name, procs);
out.push(format!("ok {}", name));
}
}
out.join("\n")
}
l if l.starts_with("restart ") => {
let name = &l[8..];
if procs.contains_key(name) {
stop(name, procs);
}
if let Some(cfg) = config.service.get(name) {
match spawn(name, cfg) {
Some(p) => {
let pid = p.pid;
procs.insert(name.to_string(), p);
format!("ok {}", pid)
}
None => format!("error: failed to spawn '{}'", name),
let names: Vec<&str> = l[8..].split_whitespace().collect();
let mut out = Vec::new();
for name in &names {
if procs.contains_key(*name) {
stop(name, procs);
}
if let Some(cfg) = config.service.get(*name) {
match spawn(name, cfg) {
Some(p) => {
let pid = p.pid;
procs.insert(name.to_string(), p);
out.push(format!("ok {} pid={}", name, pid));
}
None => out.push(format!("error: failed to spawn '{}'", name)),
}
} else {
out.push(format!("error: service '{}' not found", name));
}
} else {
format!("error: service '{}' not found", name)
}
out.join("\n")
}
"list" | "list " => {
let mut out = Vec::new();
@ -446,30 +458,39 @@ fn handle_client(
}
l if l.starts_with("enable ") => {
let rest = &l[7..];
let (name, now) = if let Some(n) = rest.strip_suffix(" now") {
let (names_str, now) = if let Some(n) = rest.strip_suffix(" now") {
(n, true)
} else {
(rest, false)
};
if !config.service.contains_key(name) {
format!("error: service '{}' not found", name)
} else if let Err(e) = crate::state::enable(name) {
format!("error: enable '{}': {}", name, e)
} else {
if now {
if !procs.contains_key(name) {
if let Some(p) = spawn(name, &config.service[name]) {
procs.insert(name.to_string(), p);
let names: Vec<&str> = names_str.split_whitespace().collect();
let mut out = Vec::new();
for name in &names {
if !config.service.contains_key(*name) {
out.push(format!("error: service '{}' not found", name));
} else if let Err(e) = crate::state::enable(name) {
out.push(format!("error: enable '{}': {}", name, e));
} else {
if now {
if !procs.contains_key(*name) {
if let Some(p) = spawn(name, &config.service[*name]) {
procs.insert(name.to_string(), p);
}
}
}
out.push(format!("ok {}", name));
}
"ok".to_string()
}
out.join("\n")
}
l if l.starts_with("disable ") => {
let name = &l[8..];
crate::state::disable(name);
"ok".to_string()
let names: Vec<&str> = l[8..].split_whitespace().collect();
let mut out = Vec::new();
for name in &names {
crate::state::disable(name);
out.push(format!("ok {}", name));
}
out.join("\n")
}
_ => format!("error: unknown command '{}'", line),
};

View file

@ -18,14 +18,14 @@ fn main() {
let config_path = resolve_config_path(cli.config.as_deref());
daemon::run(config_path);
}
Commands::Start { ref name } => cmd_start(name),
Commands::Stop { ref name } => cmd_stop(name),
Commands::Restart { ref name } => cmd_restart(name),
Commands::Start { ref names } => cmd_start(names),
Commands::Stop { ref names } => cmd_stop(names),
Commands::Restart { ref names } => cmd_restart(names),
Commands::Status { ref name } => cmd_status(name.as_deref()),
Commands::List => cmd_list(),
Commands::Logs { ref name, lines, follow } => cmd_logs(name, lines, follow),
Commands::Enable { ref name, now } => cmd_enable(name, now),
Commands::Disable { ref name } => cmd_disable(name),
Commands::Enable { ref names, now } => cmd_enable(names, now),
Commands::Disable { ref names } => cmd_disable(names),
Commands::Supervise { .. } => {
eprintln!("error: supervise is internal, use daemon");
std::process::exit(1);
@ -43,11 +43,12 @@ fn resolve_config_path(custom: Option<&str>) -> String {
state::config_dir().join("config.toml").to_string_lossy().to_string()
}
fn cmd_start(name: &str) {
match ipc::send_cmd(&format!("start {}", name)) {
fn cmd_start(names: &[String]) {
let cmd = format!("start {}", names.join(" "));
match ipc::send_cmd(&cmd) {
Ok(lines) => {
if let Some(first) = lines.first() {
println!("{}", first);
for line in lines {
println!("{}", line);
}
}
Err(e) => {
@ -57,9 +58,14 @@ fn cmd_start(name: &str) {
}
}
fn cmd_stop(name: &str) {
match ipc::send_cmd(&format!("stop {}", name)) {
Ok(_) => {}
fn cmd_stop(names: &[String]) {
let cmd = format!("stop {}", names.join(" "));
match ipc::send_cmd(&cmd) {
Ok(lines) => {
for line in lines {
println!("{}", line);
}
}
Err(e) => {
eprintln!("error: {}", e);
std::process::exit(1);
@ -67,11 +73,12 @@ fn cmd_stop(name: &str) {
}
}
fn cmd_restart(name: &str) {
match ipc::send_cmd(&format!("restart {}", name)) {
fn cmd_restart(names: &[String]) {
let cmd = format!("restart {}", names.join(" "));
match ipc::send_cmd(&cmd) {
Ok(lines) => {
if let Some(first) = lines.first() {
println!("{}", first);
for line in lines {
println!("{}", line);
}
}
Err(e) => {
@ -140,14 +147,18 @@ fn cmd_logs(name: &str, mut lines: Option<usize>, follow: bool) {
}
}
fn cmd_enable(name: &str, now: bool) {
fn cmd_enable(names: &[String], now: bool) {
let cmd = if now {
format!("enable {} now", name)
format!("enable {} now", names.join(" "))
} else {
format!("enable {}", name)
format!("enable {}", names.join(" "))
};
match ipc::send_cmd(&cmd) {
Ok(_) => println!("enabled {}", name),
Ok(lines) => {
for line in lines {
println!("{}", line);
}
}
Err(e) => {
eprintln!("error: {}", e);
std::process::exit(1);
@ -155,9 +166,14 @@ fn cmd_enable(name: &str, now: bool) {
}
}
fn cmd_disable(name: &str) {
match ipc::send_cmd(&format!("disable {}", name)) {
Ok(_) => println!("disabled {}", name),
fn cmd_disable(names: &[String]) {
let cmd = format!("disable {}", names.join(" "));
match ipc::send_cmd(&cmd) {
Ok(lines) => {
for line in lines {
println!("{}", line);
}
}
Err(e) => {
eprintln!("error: {}", e);
std::process::exit(1);