xianyun_cli/commands/
registry.rs1use std::io::Write;
6
7use clap::{Args, Subcommand, ValueEnum};
8use thiserror::Error;
9use xianyun_deploy::registrycache::{self, RegistryCacheError, State};
10
11#[derive(Args, Debug)]
12pub struct RegistryArgs {
13 #[command(subcommand)]
14 pub command: RegistryCommands,
15}
16
17#[derive(Subcommand, Debug)]
18pub enum RegistryCommands {
19 Status(RegistryStatusArgs),
21
22 Start(RegistryStartArgs),
24
25 Stop(RegistryStopArgs),
27}
28
29#[derive(Copy, Clone, Debug, ValueEnum, Default, PartialEq, Eq)]
30pub enum OutputFormat {
31 #[default]
32 Text,
33 Json,
34}
35
36#[derive(Args, Debug)]
37pub struct RegistryStatusArgs {
38 #[arg(short = 'o', long = "output", default_value = "text")]
40 pub output: OutputFormat,
41}
42
43#[derive(Args, Debug)]
44pub struct RegistryStartArgs {}
45
46#[derive(Args, Debug)]
47pub struct RegistryStopArgs {}
48
49#[derive(Debug, Error)]
52pub enum RegistryStatusError {
53 #[error("unsupported output format {0:?}: expected text or json")]
55 UnsupportedOutputFormat(String),
56}
57
58pub fn run(args: &RegistryArgs) -> anyhow::Result<()> {
59 let mut stdout = std::io::stdout();
60
61 match &args.command {
62 RegistryCommands::Status(status_args) => {
63 let format = match status_args.output {
64 OutputFormat::Text => "text",
65 OutputFormat::Json => "json",
66 };
67 run_status(&mut stdout, format)
68 }
69 RegistryCommands::Start(_) => run_start_cmd(&mut stdout),
70 RegistryCommands::Stop(_) => run_stop(&mut stdout),
71 }
72}
73
74pub fn run_start_cmd(stdout: &mut impl Write) -> anyhow::Result<()> {
85 let state = registrycache::status()
86 .map_err(|err| anyhow::anyhow!("reading registry cache status: {err}"))?;
87
88 if state.running {
89 writeln!(stdout, "registry cache is already running")?;
90 return Ok(());
91 }
92
93 match registrycache::start() {
94 Ok(()) => {
95 writeln!(stdout, "registry cache started")?;
96 Ok(())
97 }
98 Err(RegistryCacheError::NoPersistedConfig) => {
99 writeln!(stdout, "{}", RegistryCacheError::NoPersistedConfig)?;
100 Err(anyhow::anyhow!(""))
104 }
105 Err(err) => Err(anyhow::anyhow!("starting registry cache: {err}")),
106 }
107}
108
109pub fn run_status(stdout: &mut impl Write, format: &str) -> anyhow::Result<()> {
117 if format != "text" && format != "json" {
118 return Err(RegistryStatusError::UnsupportedOutputFormat(format.to_string()).into());
119 }
120
121 let state = registrycache::status()
122 .map_err(|err| anyhow::anyhow!("reading registry cache status: {err}"))?;
123
124 if format == "json" {
125 write_status_json(stdout, &state)
126 } else {
127 write_status_text(stdout, &state);
128 Ok(())
129 }
130}
131
132#[derive(serde::Serialize)]
135struct StatusOutput {
136 running: bool,
137 pid: i32,
138 port: String,
139 refs: Vec<String>,
140 upstreams: Vec<String>,
141 data_bytes: i64,
142 config_path: String,
143 data_dir: String,
144 log_path: String,
145}
146
147fn write_status_json(stdout: &mut impl Write, state: &State) -> anyhow::Result<()> {
148 let out = StatusOutput {
149 running: state.running,
150 pid: state.pid,
151 port: state.port.clone(),
152 refs: state.refs.clone(),
153 upstreams: state.upstreams.clone(),
154 data_bytes: i64::try_from(state.data_bytes).unwrap_or(i64::MAX),
155 config_path: state.config_path.to_string_lossy().into_owned(),
156 data_dir: state.data_dir.to_string_lossy().into_owned(),
157 log_path: state.log_path.to_string_lossy().into_owned(),
158 };
159
160 let body = serde_json::to_string_pretty(&out)
161 .map_err(|err| anyhow::anyhow!("encoding registry cache status: {err}"))?;
162 writeln!(stdout, "{body}")?;
163
164 Ok(())
165}
166
167fn write_status_text(stdout: &mut impl Write, state: &State) {
168 let status = if state.running {
169 format!("Running (pid={}, port={})", state.pid, state.port)
170 } else {
171 "Stopped".to_string()
172 };
173
174 let _ = writeln!(stdout, "Status: {status}");
175 let _ = writeln!(stdout, "Refs: {}", join_or_dash(&state.refs));
176 let _ = writeln!(stdout, "Upstreams: {}", join_or_dash(&state.upstreams));
177 let _ = writeln!(stdout, "Cache size: {}", humanize_bytes(state.data_bytes));
178 let _ = writeln!(stdout, "Config: {}", state.config_path.display());
179 let _ = writeln!(stdout, "Data dir: {}", state.data_dir.display());
180 let _ = writeln!(stdout, "Log: {}", state.log_path.display());
181}
182
183fn join_or_dash(items: &[String]) -> String {
184 if items.is_empty() {
185 "(none)".to_string()
186 } else {
187 items.join(", ")
188 }
189}
190
191fn humanize_bytes(n: u64) -> String {
192 const KB: u64 = 1024;
193 const MB: u64 = KB * 1024;
194 const GB: u64 = MB * 1024;
195
196 if n < KB {
197 format!("{n} B")
198 } else if n < MB {
199 format!("{:.1} KiB", n as f64 / KB as f64)
200 } else if n < GB {
201 format!("{:.1} MiB", n as f64 / MB as f64)
202 } else {
203 format!("{:.2} GiB", n as f64 / GB as f64)
204 }
205}
206
207pub fn run_stop(stdout: &mut impl Write) -> anyhow::Result<()> {
214 let state = registrycache::status()
215 .map_err(|err| anyhow::anyhow!("reading registry cache status: {err}"))?;
216
217 if !state.running {
218 writeln!(stdout, "registry cache is not running")?;
219 return Ok(());
220 }
221
222 if !state.refs.is_empty() {
223 writeln!(
224 stdout,
225 "registry cache is in use by {} profile(s): {:?}\n\
226 release refs first (e.g. `caproni destroy` in each profile) before stopping",
227 state.refs.len(),
228 state.refs
229 )?;
230 return Err(anyhow::anyhow!(""));
232 }
233
234 registrycache::stop().map_err(|err| anyhow::anyhow!("stopping registry cache: {err}"))?;
235
236 writeln!(stdout, "registry cache stopped")?;
237
238 Ok(())
239}