Skip to main content

cloudpub_client/
base.rs

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    // Determine config path to use for service
46    let config_path = match action {
47        ServiceAction::Install { conf: Some(path) } => {
48            // Use provided config path
49            PathBuf::from(path)
50        }
51        _ => {
52            // Use default config path
53            config.get_config_path().to_owned()
54        }
55    };
56
57    // Check if the provided config path exists
58    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            // On Unix, check if running under sudo and use original user's config if available
66            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    // Create the appropriate service manager for the current platform
87    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    // Raise `nofile` limit on linux and mac
122    if let Err(err) = fdlimit::raise_fd_limit() {
123        warn!("Failed to raise file descriptor limit: {}", err);
124    }
125
126    // Create log directory. `clo report` packs whatever is in here, so both
127    // sides have to agree on the path - CLOUDPUB-577.
128    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                    // Prompt the user for email
229                    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                    // Try to read from environment first
241                    if let Ok(pwd) = std::env::var("PASSWORD") {
242                        pwd
243                    } else {
244                        // If not in environment, prompt the user
245                        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                    // Create a new progress bar for this GUID
489                    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                    // Progress completed, remove and finish the progress bar
495                    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                    // Update existing progress bar
500                    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), // Green
519                                "offline" => format!("\x1b[31m{}\x1b[0m", status), // Red
520                                "starting" => format!("\x1b[33m{}\x1b[0m", status), // Yellow
521                                "stopping" => format!("\x1b[33m{}\x1b[0m", status), // Yellow
522                                "error" => format!("\x1b[31m{}\x1b[0m   ", status), // Red
523                                _ => 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}