use anyhow::{bail, Context, Result};
use clap::{Args as ClapArgs, Subcommand};
use console::style;
use std::path::PathBuf;
use stout_state::{InstalledPackages, Paths};
#[derive(ClapArgs)]
pub struct Args {
#[command(subcommand)]
pub command: Option<ServiceCommand>,
}
#[derive(Subcommand)]
pub enum ServiceCommand {
List,
Start {
service: String,
},
Stop {
service: String,
},
Restart {
service: String,
},
Run {
service: String,
},
Info {
service: String,
},
Cleanup,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[allow(dead_code)]
enum ServiceStatus {
Running,
Stopped,
Error,
Unknown,
}
impl std::fmt::Display for ServiceStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ServiceStatus::Running => write!(f, "started"),
ServiceStatus::Stopped => write!(f, "stopped"),
ServiceStatus::Error => write!(f, "error"),
ServiceStatus::Unknown => write!(f, "unknown"),
}
}
}
pub async fn run(args: Args) -> Result<()> {
let command = args.command.unwrap_or(ServiceCommand::List);
match command {
ServiceCommand::List => list_services().await,
ServiceCommand::Start { service } => start_service(&service).await,
ServiceCommand::Stop { service } => stop_service(&service).await,
ServiceCommand::Restart { service } => restart_service(&service).await,
ServiceCommand::Run { service } => run_service(&service).await,
ServiceCommand::Info { service } => info_service(&service).await,
ServiceCommand::Cleanup => cleanup_services().await,
}
}
async fn list_services() -> Result<()> {
let paths = Paths::default();
let installed = InstalledPackages::load(&paths)?;
println!("{} Managed services:", style("==>").blue().bold());
let mut found_services = false;
for name in installed.names() {
let pkg = installed
.get(name)
.with_context(|| format!("package '{}' is in installed list but not found", name))?;
let install_path = paths.cellar.join(name).join(&pkg.version);
let service_files = find_service_files(&install_path);
if !service_files.is_empty() {
found_services = true;
let status = get_service_status(name);
let status_style = match status {
ServiceStatus::Running => style("started").green(),
ServiceStatus::Stopped => style("stopped").dim(),
ServiceStatus::Error => style("error").red(),
ServiceStatus::Unknown => style("unknown").yellow(),
};
println!(
" {} {} ({}) - {}",
style(if status == ServiceStatus::Running {
"●"
} else {
"○"
})
.dim(),
name,
&pkg.version,
status_style
);
}
}
if !found_services {
println!(" {}", style("No services available").dim());
}
Ok(())
}
async fn start_service(name: &str) -> Result<()> {
let paths = Paths::default();
let installed = InstalledPackages::load(&paths)?;
let pkg = installed
.get(name)
.ok_or_else(|| anyhow::anyhow!("Formula '{}' is not installed", name))?;
let install_path = paths.cellar.join(name).join(&pkg.version);
let service_files = find_service_files(&install_path);
if service_files.is_empty() {
bail!("Formula '{}' does not have a service to start", name);
}
println!(
"{} Starting {}...",
style("==>").blue().bold(),
style(name).cyan()
);
#[cfg(target_os = "macos")]
{
for plist in service_files {
let output = std::process::Command::new("launchctl")
.args(["load", "-w"])
.arg(&plist)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Failed to start service: {}", stderr);
}
}
}
#[cfg(target_os = "linux")]
{
println!(
"{}",
style("Service management on Linux requires systemd setup").yellow()
);
}
println!("{} Successfully started {}", style("✓").green(), name);
Ok(())
}
async fn stop_service(name: &str) -> Result<()> {
let paths = Paths::default();
let installed = InstalledPackages::load(&paths)?;
let pkg = installed
.get(name)
.ok_or_else(|| anyhow::anyhow!("Formula '{}' is not installed", name))?;
let install_path = paths.cellar.join(name).join(&pkg.version);
let service_files = find_service_files(&install_path);
if service_files.is_empty() {
bail!("Formula '{}' does not have a service to stop", name);
}
println!(
"{} Stopping {}...",
style("==>").blue().bold(),
style(name).cyan()
);
#[cfg(target_os = "macos")]
{
for plist in service_files {
let output = std::process::Command::new("launchctl")
.args(["unload", "-w"])
.arg(&plist)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!("{} {}", style("Warning:").yellow(), stderr);
}
}
}
#[cfg(target_os = "linux")]
{
println!(
"{}",
style("Service management on Linux requires systemd setup").yellow()
);
}
println!("{} Successfully stopped {}", style("✓").green(), name);
Ok(())
}
async fn restart_service(name: &str) -> Result<()> {
stop_service(name).await?;
start_service(name).await
}
async fn run_service(name: &str) -> Result<()> {
let paths = Paths::default();
let installed = InstalledPackages::load(&paths)?;
let pkg = installed
.get(name)
.ok_or_else(|| anyhow::anyhow!("Formula '{}' is not installed", name))?;
let install_path = paths.cellar.join(name).join(&pkg.version);
let service_files = find_service_files(&install_path);
if service_files.is_empty() {
bail!("Formula '{}' does not have a service", name);
}
println!(
"{} Running {} in foreground (Ctrl+C to stop)...",
style("==>").blue().bold(),
style(name).cyan()
);
#[cfg(target_os = "macos")]
{
for plist in &service_files {
let output = std::process::Command::new("launchctl")
.args(["start"])
.arg(plist)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!("{} {}", style("Warning:").yellow(), stderr);
}
}
}
Ok(())
}
async fn info_service(name: &str) -> Result<()> {
let paths = Paths::default();
let installed = InstalledPackages::load(&paths)?;
let pkg = installed
.get(name)
.ok_or_else(|| anyhow::anyhow!("Formula '{}' is not installed", name))?;
let install_path = paths.cellar.join(name).join(&pkg.version);
let service_files = find_service_files(&install_path);
println!(
"{} Service info for {}:",
style("==>").blue().bold(),
style(name).cyan()
);
println!(" {}: {}", style("Version").dim(), pkg.version);
println!(" {}: {}", style("Status").dim(), get_service_status(name));
if service_files.is_empty() {
println!(" {}: none", style("Service files").dim());
} else {
println!(" {}:", style("Service files").dim());
for file in &service_files {
println!(" {}", file.display());
}
}
Ok(())
}
async fn cleanup_services() -> Result<()> {
println!(
"{} Cleaning up unused services...",
style("==>").blue().bold()
);
println!("{}", style("No unused services found.").dim());
Ok(())
}
pub fn find_service_files(install_path: &std::path::Path) -> Vec<PathBuf> {
let mut files = Vec::new();
let _homebrew_dir = install_path.join("homebrew.mxcl.*.plist");
if let Ok(entries) = glob_simple(install_path, "*.plist") {
files.extend(entries);
}
let share_dir = install_path.join("share");
if share_dir.exists() {
if let Ok(entries) = glob_simple(&share_dir, "*.plist") {
files.extend(entries);
}
}
files
}
pub fn stop_package_service(name: &str, install_path: &std::path::Path) -> bool {
let service_files = find_service_files(install_path);
if service_files.is_empty() {
return false;
}
#[cfg(target_os = "macos")]
{
for plist in &service_files {
let output = std::process::Command::new("launchctl")
.args(["unload", "-w"])
.arg(plist)
.output();
match output {
Ok(o) if o.status.success() => {}
Ok(o) => {
let stderr = String::from_utf8_lossy(&o.stderr);
eprintln!(" {} {}", style("Warning:").yellow(), stderr.trim());
}
Err(e) => {
eprintln!(
" {} Failed to stop service for {}: {}",
style("Warning:").yellow(),
name,
e
);
}
}
}
println!(" {} Stopped service for {}", style("✓").green(), name);
true
}
#[cfg(not(target_os = "macos"))]
{
let _ = (name, service_files);
false
}
}
fn glob_simple(dir: &std::path::Path, pattern: &str) -> Result<Vec<PathBuf>> {
let mut results = Vec::new();
if !dir.exists() {
return Ok(results);
}
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let name = entry.file_name().to_string_lossy().to_string();
if let Some(suffix) = pattern.strip_prefix('*') {
if name.ends_with(suffix) {
results.push(entry.path());
}
} else if name == pattern {
results.push(entry.path());
}
}
Ok(results)
}
fn get_service_status(name: &str) -> ServiceStatus {
#[cfg(target_os = "macos")]
{
let output = std::process::Command::new("launchctl")
.args(["list"])
.output();
if let Ok(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.contains(name) {
return ServiceStatus::Running;
}
}
ServiceStatus::Stopped
}
#[cfg(not(target_os = "macos"))]
{
let _ = name;
ServiceStatus::Unknown
}
}