1use crate::client::run_client;
2use crate::commands::{Commands, ServiceAction};
3pub use crate::config::{ClientConfig, ClientOpts};
4use crate::ping;
5use crate::service::{create_service_manager, ServiceStatus};
6use crate::shell::get_cache_dir;
7use anyhow::{bail, Context, Result};
8use clap::Parser;
9use cloudpub_common::logging::{init_log, LogZone, WorkerGuard};
10use cloudpub_common::protocol::message::Message;
11use cloudpub_common::protocol::{
12 ConnectState, EndpointClear, EndpointList, EndpointRemove, EndpointStart, EndpointStartAll,
13 EndpointStop, ErrorKind, PerformUpgrade, Stop,
14};
15use cloudpub_common::{LONG_VERSION, VERSION};
16
17use futures::future::FutureExt;
18use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
19use parking_lot::RwLock;
20use std::collections::HashMap;
21use std::io::{self, IsTerminal, Write};
22use std::path::PathBuf;
23use std::sync::Arc;
24use tokio::sync::mpsc;
25use tracing::{debug, error, warn};
26
27const CONFIG_FILE: &str = "client.toml";
28
29#[derive(Parser, Debug)]
30#[command(about, version(VERSION), long_version(LONG_VERSION))]
31pub struct Cli {
32 #[clap(subcommand)]
33 pub command: Commands,
34 #[clap(short, long, default_value = "debug", help = "Log level")]
35 pub log_level: String,
36 #[clap(short, long, default_value = "false", help = "Ouput log to console")]
37 pub verbose: bool,
38 #[clap(short, long, help = "Path to the config file")]
39 pub conf: Option<String>,
40 #[clap(short, long, default_value = "false", help = "Read-only config mode")]
41 pub readonly: bool,
42}
43
44fn handle_service_command(action: &ServiceAction, config: &ClientConfig) -> Result<()> {
45 let config_path = match action {
47 ServiceAction::Install { conf: Some(path) } => {
48 PathBuf::from(path)
50 }
51 _ => {
52 config.get_config_path().to_owned()
54 }
55 };
56
57 let actual_config = if config.token.is_some() {
59 debug!("Service config has token: {:?}", config_path);
60 config_path.clone()
61 } else {
62 #[cfg(any(target_os = "linux", target_os = "macos"))]
63 {
64 use crate::service::ServiceConfig;
65 if let Some(sudo_config) = ServiceConfig::get_sudo_user_config_path() {
67 sudo_config.clone()
68 } else {
69 config_path.clone()
70 }
71 }
72 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
73 config_path.clone()
74 };
75
76 if actual_config.as_os_str().is_empty() || !actual_config.exists() {
77 bail!(crate::t!("error-config-not-found", "path" => format!("{:?}", config_path)));
78 }
79
80 let cfg = ClientConfig::from_file(&actual_config, true)
81 .context(crate::t!("error-config-load-failed", "path" => format!("{:?}", actual_config)))?;
82
83 if cfg.token.is_none() {
84 bail!(crate::t!("error-config-token-missing"));
85 }
86 let service_manager = create_service_manager(actual_config);
88
89 match action {
90 ServiceAction::Install { .. } => {
91 service_manager.install()?;
92 println!("{}", crate::t!("service-installed"));
93 }
94 ServiceAction::Uninstall => {
95 service_manager.uninstall()?;
96 println!("{}", crate::t!("service-uninstalled"));
97 }
98 ServiceAction::Start => {
99 service_manager.start()?;
100 println!("{}", crate::t!("service-started"));
101 }
102 ServiceAction::Stop => {
103 service_manager.stop()?;
104 println!("{}", crate::t!("service-stopped-service"));
105 }
106 ServiceAction::Status => {
107 let status = service_manager.status()?;
108 match status {
109 ServiceStatus::Running => println!("{}", crate::t!("service-running")),
110 ServiceStatus::Stopped => println!("{}", crate::t!("service-stopped-status")),
111 ServiceStatus::NotInstalled => println!("{}", crate::t!("service-not-installed")),
112 ServiceStatus::Unknown => println!("{}", crate::t!("service-status-unknown")),
113 }
114 }
115 }
116
117 Ok(())
118}
119
120pub fn init(args: &Cli) -> Result<(WorkerGuard, Arc<RwLock<ClientConfig>>)> {
121 if let Err(err) = fdlimit::raise_fd_limit() {
123 warn!("Failed to raise file descriptor limit: {}", err);
124 }
125
126 let log_dir = crate::report::log_dir()?;
129 std::fs::create_dir_all(&log_dir).context("Can't create log dir")?;
130
131 let log_file = log_dir.join("client.log");
132
133 let guard = init_log(
134 &args.log_level,
135 &log_file,
136 args.verbose,
137 LogZone::Local,
138 10 * 1024 * 1024,
139 2,
140 )
141 .context("Failed to initialize logging")?;
142
143 let config = if let Some(path) = args.conf.as_ref() {
144 ClientConfig::from_file(&path.into(), args.readonly)?
145 } else {
146 ClientConfig::load(CONFIG_FILE, true, args.readonly)?
147 };
148 let config = Arc::new(RwLock::new(config));
149 Ok((guard, config))
150}
151
152#[tokio::main]
153pub async fn cli_main(cli: Cli, config: Arc<RwLock<ClientConfig>>) -> Result<()> {
154 ctrlc::set_handler(move || {
155 std::process::exit(1);
156 })
157 .context("Error setting Ctrl-C handler")?;
158
159 let (command_tx, command_rx) = mpsc::channel(1024);
160 main_loop(cli, config, command_tx, command_rx).await
161}
162
163fn make_spinner(msg: String) -> ProgressBar {
164 let spinner = ProgressBar::new_spinner();
165 let style = ProgressStyle::default_spinner()
166 .template("{spinner} {msg}")
167 .unwrap();
168 #[cfg(target_os = "windows")]
169 let style = style.tick_chars("-\\|/ ");
170 spinner.set_style(style);
171 spinner.set_message(msg);
172 #[cfg(unix)]
173 spinner.enable_steady_tick(std::time::Duration::from_millis(100));
174 spinner
175}
176
177pub async fn main_loop(
178 mut cli: Cli,
179 config: Arc<RwLock<ClientConfig>>,
180 command_tx: mpsc::Sender<Message>,
181 command_rx: mpsc::Receiver<Message>,
182) -> Result<()> {
183 let mut opts = ClientOpts::default();
184 let write_stdout = |res: String| {
185 println!("{}", res);
186 };
187
188 let write_stderr = |res: String| {
189 eprintln!("{}", res);
190 };
191
192 let (result_tx, mut result_rx) = mpsc::channel(1024);
193
194 let mut pings = 1;
195
196 opts.secondary = match &mut cli.command {
197 Commands::Set(set_args) => {
198 config.write().set(&set_args.key, &set_args.value)?;
199 return Ok(());
200 }
201 Commands::Get(get_args) => {
202 let value = config.read().get(&get_args.key)?;
203 write_stdout(value);
204 return Ok(());
205 }
206 Commands::Options => {
207 let options = config.read().get_all_options();
208 let mut output = String::new();
209 for (key, value) in options {
210 output.push_str(&format!("{}: {}\n", key, value));
211 }
212 write_stdout(output.trim_end().to_string());
213 return Ok(());
214 }
215 Commands::Purge => {
216 let cache_dir = get_cache_dir("")?;
217 debug!(
218 "{}",
219 crate::t!("purge-cache-dir", "path" => cache_dir.to_str().unwrap())
220 );
221 std::fs::remove_dir_all(&cache_dir).ok();
222 return Ok(());
223 }
224 Commands::Login(args) => {
225 let email = match &args.email {
226 Some(email) => email.clone(),
227 None => {
228 print!("{}", crate::t!("enter-email"));
230 std::io::stdout().flush().ok();
231 let mut email = String::new();
232 std::io::stdin().read_line(&mut email)?;
233 email.trim().to_string()
234 }
235 };
236
237 let password = match &args.password {
238 Some(pwd) => pwd.clone(),
239 None => {
240 if let Ok(pwd) = std::env::var("PASSWORD") {
242 pwd
243 } else {
244 print!("{}", crate::t!("enter-password"));
246 std::io::stdout().flush().ok();
247 rpassword::read_password().unwrap_or_default()
248 }
249 }
250 };
251 opts.credentials = Some((email, password));
252 true
253 }
254 Commands::Report(args) => {
255 crate::report::send_report(config.clone(), args.message.clone()).await?;
256 write_stdout(crate::t!("report-sent"));
257 return Ok(());
258 }
259 Commands::Logout => {
260 config.write().token = None;
261 config
262 .write()
263 .save()
264 .context("Failed to save config after logout")?;
265 write_stderr(crate::t!("session-terminated"));
266 return Ok(());
267 }
268 Commands::Register(publish_args) => {
269 config.read().validate()?;
270 publish_args.parse()?;
271 true
272 }
273 Commands::Publish(publish_args) => {
274 config.read().validate()?;
275 publish_args.parse()?;
276 false
277 }
278
279 Commands::Ping(_) => {
280 opts.transient = true;
281 true
282 }
283
284 Commands::Run { run_as_service } => {
285 opts.is_service = *run_as_service;
286 false
287 }
288
289 Commands::Unpublish(_)
290 | Commands::Start(_)
291 | Commands::Stop(_)
292 | Commands::Break
293 | Commands::Ls
294 | Commands::Clean
295 | Commands::Upgrade => {
296 config.read().validate()?;
297 true
298 }
299 Commands::Service { action } => {
300 return handle_service_command(action, &config.read());
301 }
302 };
303
304 debug!("Config: {:?}", config);
305
306 tokio::spawn(async move {
307 if let Err(err) = run_client(config.clone(), opts, command_rx, result_tx)
308 .boxed()
309 .await
310 {
311 error!("Error running client: {:?}", err);
312 }
313 });
314
315 let mut current_spinner = None;
316 let multi_progress = MultiProgress::new();
317 let mut progress_bars: HashMap<String, ProgressBar> = HashMap::new();
318
319 loop {
320 match result_rx
321 .recv()
322 .await
323 .context("Failed to receive message")?
324 {
325 Message::Error(err) => {
326 let kind: ErrorKind = err.kind.try_into().unwrap_or(ErrorKind::Fatal);
327 if kind.is_fatal() {
328 command_tx.send(Message::Stop(Stop {})).await.ok();
329 bail!("{}", err.message);
330 } else {
331 write_stderr(err.message.to_string());
332 }
333 }
334
335 Message::UpgradeAvailable(info) => match cli.command {
336 Commands::Upgrade => {
337 command_tx
338 .send(Message::PerformUpgrade(PerformUpgrade {
339 version: info.version.clone(),
340 }))
341 .await
342 .context("Failed to send upgrade message")?;
343 }
344 Commands::Run { .. } | Commands::Publish(_) => {
345 write_stderr(crate::t!("upgrade-available", "version" => info.version.clone()));
346 }
347 _ => {}
348 },
349
350 Message::EndpointAck(endpoint) => {
351 if endpoint.status == Some("online".to_string()) {
352 match cli.command {
353 Commands::Ping(ref args) => {
354 current_spinner = Some(make_spinner(crate::t!("measuring-speed")));
355 let stats = ping::ping_test(endpoint, args.bare).await?;
356 current_spinner.take();
357 if args.bare {
358 write_stdout(stats.to_string());
359 } else {
360 write_stdout(stats);
361 }
362 pings -= 1;
363 if pings == 0 {
364 break;
365 }
366 }
367 Commands::Register(_) => {
368 write_stdout(
369 crate::t!("service-registered", "endpoint" => endpoint.to_string()),
370 );
371 break;
372 }
373 Commands::Publish(_) | Commands::Run { .. } => {
374 if endpoint.error.is_empty() {
375 write_stdout(
376 crate::t!("service-published", "endpoint" => endpoint.to_string()),
377 )
378 } else {
379 write_stdout(
380 crate::t!("service-error", "endpoint" => endpoint.to_string()),
381 )
382 }
383 }
384 _ => {}
385 }
386 }
387 }
388
389 Message::EndpointStopAck(ep) => {
390 write_stdout(crate::t!("service-stopped", "guid" => ep.guid));
391 if matches!(cli.command, Commands::Unpublish(_)) {
392 break;
393 }
394 }
395
396 Message::EndpointRemoveAck(ep) => {
397 write_stdout(crate::t!("service-removed", "guid" => ep.guid));
398 if matches!(cli.command, Commands::Unpublish(_)) {
399 break;
400 }
401 }
402
403 Message::ConnectState(st) => match st.try_into().unwrap_or(ConnectState::Connecting) {
404 ConnectState::Connecting => {
405 current_spinner = Some(make_spinner(crate::t!("connecting")));
406 }
407
408 ConnectState::Connected => {
409 if let Some(spinner) = current_spinner.take() {
410 spinner.finish_and_clear();
411 }
412
413 match cli.command {
414 Commands::Ls => {
415 command_tx
416 .send(Message::EndpointList(EndpointList {}))
417 .await?;
418 }
419 Commands::Clean => {
420 command_tx
421 .send(Message::EndpointClear(EndpointClear {}))
422 .await?;
423 }
424 Commands::Run { .. } => {
425 command_tx
426 .send(Message::EndpointStartAll(EndpointStartAll {}))
427 .await?;
428 }
429 Commands::Publish(ref endpoint) => {
430 command_tx
431 .send(Message::EndpointStart(endpoint.parse()?))
432 .await?;
433 }
434 Commands::Register(ref endpoint) => {
435 command_tx
436 .send(Message::EndpointStart(endpoint.parse()?))
437 .await?;
438 }
439 Commands::Unpublish(ref args) => {
440 command_tx
441 .send(Message::EndpointRemove(EndpointRemove {
442 guid: args.guid.clone(),
443 }))
444 .await?;
445 }
446 Commands::Start(ref args) => {
447 command_tx
448 .send(Message::EndpointGuidStart(EndpointStart {
449 guid: args.guid.clone(),
450 }))
451 .await?;
452 }
453 Commands::Stop(ref args) => {
454 command_tx
455 .send(Message::EndpointStop(EndpointStop {
456 guid: args.guid.clone(),
457 }))
458 .await?;
459 }
460 Commands::Ping(ref args) => {
461 pings = args.num.unwrap_or(1);
462 for _i in 0..pings {
463 ping::publish(command_tx.clone()).await?;
464 }
465 }
466 Commands::Login(_) => {
467 write_stdout(crate::t!("client-authorized"));
468 break;
469 }
470 _ => {}
471 }
472 }
473 ConnectState::Disconnected => {
474 if let Some(spinner) = current_spinner.take() {
475 spinner.finish_and_clear();
476 }
477 }
478 },
479
480 Message::Progress(info) => {
481 let progress_guid = if info.guid.is_empty() {
482 "default"
483 } else {
484 &info.guid
485 };
486
487 if info.current == 0 {
488 let bar = multi_progress.add(ProgressBar::new(info.total as u64));
490 bar.set_message(info.message.clone());
491 bar.set_style(ProgressStyle::default_bar().template(&info.template)?);
492 progress_bars.insert(progress_guid.to_string(), bar);
493 } else if info.current >= info.total {
494 if let Some(progress_bar) = progress_bars.remove(progress_guid) {
496 progress_bar.finish_and_clear();
497 }
498 } else if let Some(bar) = progress_bars.get(progress_guid) {
499 bar.set_position(info.current as u64);
501 if !info.message.is_empty() {
502 bar.set_message(info.message.clone());
503 }
504 }
505 }
506
507 Message::EndpointListAck(list) => {
508 if list.endpoints.is_empty() {
509 write_stdout(crate::t!("no-registered-services"));
510 } else {
511 let mut output = String::new();
512 let use_colors = io::stderr().is_terminal();
513
514 for ep in &list.endpoints {
515 let status = ep.status.as_deref().unwrap_or("unknown");
516 let colored_status = if use_colors {
517 match status {
518 "online" => format!("\x1b[32m{}\x1b[0m ", status), "offline" => format!("\x1b[31m{}\x1b[0m", status), "starting" => format!("\x1b[33m{}\x1b[0m", status), "stopping" => format!("\x1b[33m{}\x1b[0m", status), "error" => format!("\x1b[31m{}\x1b[0m ", status), _ => status.to_string(),
524 }
525 } else {
526 status.to_string()
527 };
528
529 output.push_str(&format!("{} {} {}\n", colored_status, ep.guid, ep));
530 }
531 write_stdout(output);
532 }
533 if !matches!(cli.command, Commands::Run { .. }) {
534 break;
535 }
536 }
537
538 Message::EndpointClearAck(_) => {
539 write_stdout(crate::t!("all-services-removed"));
540 if !matches!(cli.command, Commands::Run { .. }) {
541 break;
542 }
543 }
544
545 other => {
546 debug!("Unhandled message: {:?}", other);
547 }
548 }
549 }
550
551 command_tx.send(Message::Stop(Stop {})).await.ok();
552
553 Ok(())
554}