use std::{io::Write, path::PathBuf};
use anyhow::{bail, Context, Error};
use clap::Parser;
use futures::TryStreamExt;
use launcho::{
get_config_path, guarantee_launcho_directory, make_authenticated_client, ClientResponse,
GetAuthConfigMode,
};
#[derive(Debug, Parser)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(subcommand)]
action: Action,
}
#[derive(Debug, clap::Subcommand)]
enum Action {
Server {
#[clap(short, long, value_parser)]
config: Option<String>,
},
PrintAuth,
#[clap(subcommand, alias = "t")]
Target(TargetAction),
#[clap(subcommand, aliases = &["r", "resources"])]
Resource(ResourceAction),
#[clap(subcommand, aliases = &["s", "secrets"])]
Secret(SecretAction),
Status {
#[clap(long, action)]
ipvs: bool,
},
Logs {
process: String,
},
RestartProcess {
process: String,
},
#[clap(subcommand)]
Uncommon(UncommonAction),
Version,
}
#[derive(Debug, clap::Subcommand)]
enum TargetAction {
Get,
Set { file: String },
Edit,
}
#[derive(Debug, clap::Subcommand)]
enum ResourceAction {
Up {
file: String,
#[clap(short, long)]
name: Option<String>,
},
Down {
id: String,
file: String,
},
Rm {
ids: Vec<String>,
},
Ls,
}
#[derive(Debug, clap::Subcommand)]
enum SecretAction {
Get { names: Vec<String> },
Set { name: String, value: String },
Rm { names: Vec<String> },
Ls,
}
#[derive(Debug, clap::Subcommand)]
enum UncommonAction {
ClearLaunchRateLimits,
Ping,
}
fn handle_error_response(response: ClientResponse) -> ClientResponse {
match response {
ClientResponse::Error { message } => {
eprintln!("Error: {}", message);
std::process::exit(1);
}
_ => response,
}
}
fn handle_success_or_error(response: ClientResponse) {
let response = handle_error_response(response);
match &response {
ClientResponse::Success { message: None } => println!("Success"),
ClientResponse::Success {
message: Some(message),
} => println!("Success: {}", message),
_ => panic!("Unexpected response: {:?}", response),
}
}
fn progress_bar(prefix: &str, bytes: f64, full_size: f64) {
eprint!(
"\r{} {:.2}% ({:.2} / {:.2} MiB)",
prefix,
100.0 * bytes / full_size,
bytes / (1024.0 * 1024.0),
full_size / (1024.0 * 1024.0)
);
}
async fn main_result() -> Result<(), Error> {
dotenv::dotenv().ok();
let args: Args = Args::parse();
Ok(match args.action {
Action::Version => {
println!("launcho {}", env!("CARGO_PKG_VERSION"));
}
Action::PrintAuth => {
let mut auth_config = launcho::get_auth_config(GetAuthConfigMode::ServerFailIfNotExists)?;
auth_config.host = Some("change-me-to-point-to-the-server.example.com:12888".to_string());
auth_config.private = None;
println!("# Paste this into ~/.launcho/launcho-client-auth.yaml on the client machine");
print!("{}", serde_yaml::to_string(&auth_config)?);
}
Action::Server {
config: maybe_config_path,
} => {
#[cfg(not(target_os = "linux"))]
{
std::mem::drop(maybe_config_path); eprintln!("launcho server only works on Linux");
std::process::exit(1);
}
#[cfg(target_os = "linux")]
{
let using_default_config_path = maybe_config_path.is_none();
let config_path = match maybe_config_path {
Some(config_path) => PathBuf::from(config_path),
None => get_config_path()?,
};
let config_string_result = std::fs::read_to_string(&config_path);
let config_string = match (using_default_config_path, config_string_result) {
(_, Ok(config_string)) => config_string,
(true, Err(e)) if e.kind() == std::io::ErrorKind::NotFound => {
launcho::server::log_event(launcho::LogEvent::Warning {
msg: format!("Config file not found at {:?} -- writing default config", config_path),
});
guarantee_launcho_directory()?;
let default_config = include_str!("../default-config.yaml");
std::fs::write(&config_path, &default_config)?;
default_config.to_string()
}
(_, Err(e)) => bail!("Failed to read config file at {:?}: {}", config_path, e),
};
let server_config = serde_yaml::from_str(&config_string)?;
launcho::server::server_main(server_config).await?
}
}
Action::Target(TargetAction::Get) => {
let response =
handle_error_response(launcho::send_request(launcho::ClientRequest::GetTarget).await?);
match response {
ClientResponse::Target { target } => print!("{}", target),
_ => panic!("Unexpected response: {:?}", response),
}
}
Action::Target(TargetAction::Set { file }) => {
handle_success_or_error(
launcho::send_request(launcho::ClientRequest::SetTarget {
target: std::fs::read_to_string(&file)?,
})
.await?,
);
}
Action::Target(TargetAction::Edit) => {
let response =
handle_error_response(launcho::send_request(launcho::ClientRequest::GetTarget).await?);
let target = match response {
ClientResponse::Target { target } => target,
_ => panic!("Unexpected response: {:?}", response),
};
let new_target = edit::edit(&target)?;
if new_target == target {
println!("No changes -- not updating");
} else {
handle_success_or_error(
launcho::send_request(launcho::ClientRequest::SetTarget { target: new_target })
.await?,
);
}
}
Action::Secret(SecretAction::Get { names }) => {
let response = handle_error_response(
launcho::send_request(launcho::ClientRequest::GetSecrets { names }).await?,
);
match response {
ClientResponse::Secrets { secrets } =>
for (name, secret) in secrets {
print!("{}: ", name);
match secret {
Some(secret) => println!("{:?}", secret),
None => println!("(not found)"),
}
},
_ => panic!("Unexpected response: {:?}", response),
}
}
Action::Secret(SecretAction::Set { name, value }) => {
handle_success_or_error(
launcho::send_request(launcho::ClientRequest::SetSecret { name, value }).await?,
);
}
Action::Secret(SecretAction::Rm { names }) => {
let response = handle_error_response(
launcho::send_request(launcho::ClientRequest::DeleteSecrets { names }).await?,
);
match response {
ClientResponse::Success {
message: Some(message),
} => println!("{}", message.trim()),
ClientResponse::Success { message: None } =>
println!("Unexpected lack of message from server, but operation succeeded"),
_ => panic!("Unexpected response"),
}
}
Action::Secret(SecretAction::Ls) => {
let response = handle_error_response(
launcho::send_request(launcho::ClientRequest::ListSecrets).await?,
);
match response {
ClientResponse::SecretList { secrets } =>
for name in secrets {
println!("{}", name);
},
_ => panic!("Unexpected response: {:?}", response),
}
}
Action::Status { ipvs } => {
println!("Events:");
let response =
handle_error_response(launcho::send_request(launcho::ClientRequest::Status).await?);
match response {
ClientResponse::Status {
status,
events,
ipvs_state,
} => {
for event in events {
println!(" {:?}", event);
}
if ipvs {
println!("IPVS:");
println!("{:#?}", ipvs_state);
}
println!("{}", status)
}
_ => panic!("Unexpected response: {:?}", response),
}
}
Action::Logs { process } => {
let response = handle_error_response(
launcho::send_request(launcho::ClientRequest::GetLogs { name: process }).await?,
);
match response {
ClientResponse::Logs { name, output } => {
println!("Process: {}", name);
println!("{}", output);
}
_ => panic!("Unexpected response: {:?}", response),
}
}
Action::RestartProcess { process } => {
handle_success_or_error(
launcho::send_request(launcho::ClientRequest::Restart { name: process }).await?,
);
}
Action::Resource(ResourceAction::Up { name, file }) => {
let full_size = std::fs::metadata(&file)?.len();
let mut bytes_written = 0;
let reader =
tokio_util::io::ReaderStream::new(tokio::fs::File::open(&file).await?).map_ok(move |x| {
bytes_written += x.len();
progress_bar("Uploading:", bytes_written as f64, full_size as f64);
x
});
let (client, host, port) = make_authenticated_client()?;
let response = client
.post(format!("https://launcho:{}/upload", port))
.body(reqwest::Body::wrap_stream(reader))
.query(&[("name", name.unwrap_or(file))])
.header("Content-Length", full_size as u64)
.send()
.await
.with_context(|| format!("Upload to {} failed", host))?
.text()
.await?;
eprintln!(" Done.");
match serde_json::from_str(&response)? {
ClientResponse::Success { message: None } =>
eprintln!("Success, but no resource ID returned, for some reason"),
ClientResponse::Success {
message: Some(message),
} => println!("{}", message),
_ => panic!("Unexpected response: {:?}", response),
}
()
}
Action::Resource(ResourceAction::Down { id, file }) => {
let (client, host, port) = make_authenticated_client()?;
let response = client
.get(format!("https://launcho:{}/download", port))
.query(&[("id", id)])
.send()
.await
.with_context(|| format!("Download from {} failed", host))?;
if !response.status().is_success() {
bail!("Download failed: {}", response.text().await?);
}
let full_size = response
.headers()
.get("content-length")
.and_then(|x| x.to_str().ok())
.and_then(|x| x.parse::<u64>().ok())
.unwrap_or(1);
let mut stream = response.bytes_stream();
let mut file = std::fs::File::create(file)?;
let mut bytes_written = 0;
while let Some(chunk) = stream.try_next().await? {
bytes_written += chunk.len();
progress_bar("Downloading:", bytes_written as f64, full_size as f64);
file.write_all(&chunk)?;
}
eprintln!(" Done.");
}
Action::Resource(ResourceAction::Rm { ids }) => {
handle_success_or_error(
launcho::send_request(launcho::ClientRequest::DeleteResources { ids }).await?,
);
}
Action::Resource(ResourceAction::Ls) => {
let response = handle_error_response(
launcho::send_request(launcho::ClientRequest::ListResources).await?,
);
match response {
ClientResponse::ResourceList { resources } => {
println!("Found {} resources", resources.len());
for resource in resources {
println!("{} [{:9} bytes] {}", resource.id, resource.size, resource.name);
}
}
_ => panic!("Unexpected response: {:?}", response),
}
}
Action::Uncommon(UncommonAction::ClearLaunchRateLimits) => {
handle_success_or_error(
launcho::send_request(launcho::ClientRequest::ClearLaunchRateLimits).await?,
);
}
Action::Uncommon(UncommonAction::Ping) => {
let pong = launcho::send_request(launcho::ClientRequest::Ping).await?;
println!("{:#?}", pong);
}
})
}
#[tokio::main]
async fn main() {
match main_result().await {
Ok(()) => {}
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
}