use anyhow::{Context, Result};
use clap::Parser;
use indicatif::ProgressBar;
use crate::{
client::GQLClient,
config::Configs,
controllers::terminal::{self, TerminalClient},
util::progress::{create_spinner, fail_spinner},
};
pub const SSH_CONNECTION_TIMEOUT_SECS: u64 = 30;
pub const SSH_MESSAGE_TIMEOUT_SECS: u64 = 10;
pub const SSH_MAX_CONNECT_ATTEMPTS: usize = 3;
pub const SSH_CONNECT_DELAY_SECS: u64 = 5;
pub const SSH_MAX_EMPTY_MESSAGES: usize = 100;
mod common;
mod platform;
use common::*;
use platform::*;
#[derive(Parser, Clone)]
pub struct Args {
#[clap(short, long)]
project: Option<String>,
#[clap(short, long)]
service: Option<String>,
#[clap(short, long)]
environment: Option<String>,
#[clap(short, long)]
#[arg(long = "deployment-instance", value_name = "deployment-instance-id")]
deployment_instance: Option<String>,
#[clap(long)]
session: bool,
#[clap(trailing_var_arg = true)]
command: Vec<String>,
}
pub async fn command(args: Args) -> Result<()> {
let configs = Configs::new()?;
let client = GQLClient::new_authorized(&configs)?;
let params = get_ssh_connect_params(args.clone(), &configs, &client).await?;
if args.session {
run_persistent_session(¶ms).await?;
return Ok(());
}
let running_command = !args.command.is_empty();
let mut spinner = create_spinner("Connecting to service...".to_string());
let mut terminal_client = create_client(¶ms, &mut spinner).await?;
if running_command {
execute_command(&mut terminal_client, args.command.join(" "), spinner).await?;
} else {
initialize_shell(&mut terminal_client, Some("bash".to_string()), &mut spinner).await?;
match run_interactive_session(terminal_client).await? {
SessionTermination::Complete => {}
term => {
eprintln!("{}", term.message());
std::process::exit(term.exit_code());
}
}
}
Ok(())
}
async fn run_persistent_session(params: &terminal::SSHConnectParams) -> Result<()> {
ensure_tmux_is_installed(params).await?;
loop {
let mut spinner = create_spinner("Connecting to service...".to_string());
let mut terminal_client = match create_client(params, &mut spinner).await {
Ok(tc) => tc,
Err(e) => {
fail_spinner(&mut spinner, format!("{}", e));
std::process::exit(1);
}
};
initialize_shell(&mut terminal_client, Some("bash".to_string()), &mut spinner).await?;
terminal_client
.send_data("exec tmux new-session -A -s railway\n")
.await?;
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
terminal_client
.send_data("tmux set -g mouse on; clear\n")
.await?;
send_window_size(&mut terminal_client).await?;
let termination = run_interactive_session(terminal_client).await?;
match termination {
SessionTermination::Complete => {
break;
}
SessionTermination::ConnectionReset => {
reset_terminal(true)?;
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
println!("Connection reset. Reconnecting...");
continue;
}
term => {
eprintln!("{}", term.message());
std::process::exit(term.exit_code());
}
};
}
reset_terminal(false)?;
Ok(())
}
async fn ensure_tmux_is_installed(params: &terminal::SSHConnectParams) -> Result<()> {
let command = "which tmux || (apt-get update && apt-get install -y tmux)";
let mut spinner = create_spinner("Installing tmux...".to_string());
let mut terminal_client = create_client(params, &mut spinner).await?;
let result =
execute_command_with_result(&mut terminal_client, command.to_string(), &mut spinner).await;
if let Err(err) = result {
fail_spinner(&mut spinner, format!("Error installing tmux: {}", err));
std::process::exit(1);
}
Ok(())
}
async fn create_client(
params: &terminal::SSHConnectParams,
spinner: &mut ProgressBar,
) -> Result<TerminalClient> {
let configs = Configs::new()?;
let token = configs
.get_railway_auth_token()
.context("No authentication token found. Please login first with 'railway login'")?;
let ws_url = format!("wss://{}", configs.get_relay_host_path());
let terminal_client = create_terminal_client(&ws_url, &token, params, spinner).await?;
Ok(terminal_client)
}