From 9d5d5b2c34ca4b604540bbda7bd4de546c6bc2df Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Wed, 10 Jul 2024 11:58:56 -0700 Subject: [PATCH] feat: add sidekick show-term command --- Cargo.lock | 2 +- cli/Cargo.toml | 2 +- cli/src/commands/sidekick/mod.rs | 208 ++---------------------- cli/src/commands/sidekick/show_term.rs | 30 ++++ cli/src/util/mod.rs | 1 + cli/src/util/show_term.rs | 211 +++++++++++++++++++++++++ 6 files changed, 254 insertions(+), 200 deletions(-) create mode 100644 cli/src/commands/sidekick/show_term.rs create mode 100644 cli/src/util/show_term.rs diff --git a/Cargo.lock b/Cargo.lock index b074f6c8..f63a4af3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1911,7 +1911,7 @@ dependencies = [ [[package]] name = "rivet-cli" -version = "1.3.2" +version = "1.4.0" dependencies = [ "anyhow", "assert_cmd", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f50768e8..922c32a9 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rivet-cli" -version = "1.3.2" +version = "1.4.0" authors = ["Rivet Gaming, Inc. "] edition = "2018" license = "MIT" diff --git a/cli/src/commands/sidekick/mod.rs b/cli/src/commands/sidekick/mod.rs index 4c83848f..1d3f2275 100644 --- a/cli/src/commands/sidekick/mod.rs +++ b/cli/src/commands/sidekick/mod.rs @@ -3,7 +3,6 @@ use console::Term; use global_error::prelude::*; use serde::Serialize; use serde_json::{json, Value}; -use std::process::Command; use crate::util::{ global_config, @@ -20,6 +19,7 @@ pub mod get_logs_link; pub mod get_namespace_dev_token; pub mod get_namespace_pub_token; pub mod get_versions_link; +pub mod show_term; pub mod unlink; pub mod util; pub mod wait_for_login; @@ -32,6 +32,11 @@ pub trait SideKickHandler: Serialize { #[derive(Parser)] pub enum SubCommand { + /// Run an arbritrary command in a terminal window. Primarily used for showing logs from + /// arbirtrary commands. + /// + /// Prefer using the `--show-terminal` flag for Rivet-specific commands. + ShowTerm(show_term::Opts), /// Get the link for the user to sign in GetLink(get_link::Opts), /// Long poll the server to check if the user has signed in @@ -76,52 +81,6 @@ pub enum PreExecuteHandled { No, } -struct Terminal { - /// The name of the terminal emulator command - name: &'static str, - /// The flag to pass the command to the terminal emulator - prompt_str: &'static [&'static str], -} - -/// Terminals that don't work (note, more work might make them work): -/// -/// - guake (runs the whole window, doesn't handle closing) -/// - upterm (doesn't have an arg to pass a command it) -/// - x-terminal-emulator -/// - tilda (doesn't show automatically) -/// - terminator (issues running the command) -/// - xfce4-terminal (issues running the command) -const TERMINALS: [Terminal; 7] = [ - Terminal { - name: "kitty", - prompt_str: &["-e"], - }, - Terminal { - name: "konsole", - prompt_str: &["-e"], - }, - Terminal { - name: "gnome-terminal", - prompt_str: &["--"], - }, - Terminal { - name: "st", - prompt_str: &["-e"], - }, - Terminal { - name: "tilix", - prompt_str: &["-e"], - }, - Terminal { - name: "urxvt", - prompt_str: &["-e"], - }, - Terminal { - name: "xterm", - prompt_str: &["-e"], - }, -]; - impl SubCommand { /// These commands run before a token is required, so they don't have access /// to ctx @@ -132,6 +91,7 @@ impl SubCommand { ) -> GlobalResult { let mut handled = PreExecuteHandled::Yes; let response = match self { + SubCommand::ShowTerm(opts) => serialize_output(opts.execute().await), SubCommand::GetLink(opts) => serialize_output(opts.execute().await), SubCommand::WaitForLogin(opts) => serialize_output(opts.execute().await), SubCommand::CheckLoginState => serialize_output(self.validate_token(&token)), @@ -183,7 +143,8 @@ impl SubCommand { .await?; let response = match self { - SubCommand::GetLink(_) + SubCommand::ShowTerm(_) + | SubCommand::GetLink(_) | SubCommand::CheckLoginState | SubCommand::WaitForLogin(_) | SubCommand::GenerateConfig(_) @@ -262,157 +223,8 @@ impl SubCommand { // Add the binary path back as the first argument args.insert(0, binary_path.to_str().unwrap().to_string()); - #[cfg(target_os = "windows")] - Command::new("cmd.exe") - .arg("/C") - .args(args) - .spawn() - .expect("cmd.exe failed to start"); - - #[cfg(target_os = "macos")] - { - // This script will run from home, so we need top change to the - // project directory before running the command. - let current_dir = std::env::current_dir()? - .into_os_string() - .into_string() - .unwrap(); - - // Create the content for the script - let script_path = format!("{}/script.command", current_dir); - let command_to_run = format!( - "cd \"{}\" && {} && rm \"{}\"", - current_dir, - args.join(" "), - script_path - ); - - // Write the script content to the script file - std::fs::write(&script_path, format!("#!/bin/bash\n{}", command_to_run))?; - std::fs::set_permissions( - &script_path, - std::os::unix::fs::PermissionsExt::from_mode(0o755), - )?; - - // Use `open` to run the script - std::process::Command::new("open") - .arg(&script_path) - .spawn() - .expect("Failed to open script"); - } - - #[cfg(target_os = "linux")] - { - // TODO(forest): For Linux, the code is trying to find an - // available terminal emulator from a predefined list and - // then run the command in it. However, the way to run a - // command in a terminal emulator can vary between different - // emulators. The -e flag used here might not work for all - // of them. - let mut command = None; - - for terminal in TERMINALS { - if which::which(terminal.name).is_ok() { - command = Some(terminal); - break; - } - } - - match command { - Some(terminal) => { - // See if they have bash installed. If not, fallback to sh - let shell = if which::which("bash").is_ok() { - "bash" - } else { - "sh" - }; - - // Insert the flag --inside-terminal right after `sidekick` - // in the args. The only args before it are the binary path - // to the binary and `sidekick` itself, so it can go at the - // 2nd index. - args.insert(2, "--inside-terminal".to_string()); - - // Add a "press any key to continue" message to the end of - // the arguments to be run - args.append( - vec![ - "&&", - "read", - "-n", - "1", - "-s", - "-r", - "-p", - "\"Press any key to continue\"", - ] - .iter() - .map(|x| x.to_string()) - .collect::>() - .as_mut(), - ); - - args = vec![args.join(" ")]; - - Command::new(terminal.name) - // This is the flag to run a command in the - // terminal. Most will use -e, but some might use - // something different. - .args(terminal.prompt_str) - // We pass everything to a shell manually so that we can - // pass an entire string of the rest of the commands. - // This is more consistant across terminals on linux. - .arg(shell) - .arg("-c") - .args(&args) - .spawn() - .expect("Terminal emulator failed to start"); - } - None => { - panic!("No terminal emulator found"); - } - } - } + crate::util::show_term::show_term(&args).await?; Ok(()) } } - -#[cfg(test)] -mod tests { - use std::fs; - use std::path::Path; - use std::process::Command; - - use super::TERMINALS; - - #[test] - #[ignore] - /// This test makes sure that the configuration to run a command in each - /// terminal works. It shouldn't run in CI, since it would be difficult to - /// configure. It can be run locally if each terminal in the const is - /// installed. - fn test_terminals() { - for terminal in TERMINALS { - let file_name = format!("{}.txt", terminal.name); - - let mut args = Vec::new(); - - args.push(format!("touch {}", file_name)); - - let output = Command::new(terminal.name) - .args(terminal.prompt_str) - .args(&args) - .output() - .expect("Failed to execute command"); - - assert!(output.status.success(), "Command failed: {}", terminal.name); - - let file_path = Path::new(&file_name); - assert!(file_path.exists(), "File does not exist: {}", file_name); - - // Clean up the file - fs::remove_file(file_path).expect("Failed to remove file"); - } - } -} diff --git a/cli/src/commands/sidekick/show_term.rs b/cli/src/commands/sidekick/show_term.rs new file mode 100644 index 00000000..a10d7b4c --- /dev/null +++ b/cli/src/commands/sidekick/show_term.rs @@ -0,0 +1,30 @@ +use clap::Parser; +use global_error::prelude::*; +use serde::Serialize; + +use crate::util::{show_term, struct_fmt}; + +use super::SideKickHandler; + +#[derive(Parser)] +pub struct Opts { + #[clap(index = 1, multiple_values = true)] + args: Vec, +} + +#[derive(Serialize)] +pub struct Output { + pid: u32, +} + +impl SideKickHandler for Output {} + +impl Opts { + pub async fn execute(&self) -> GlobalResult { + let cmd = show_term::show_term(&self.args).await?; + + let output = Output { pid: cmd.id() }; + struct_fmt::print_opt(None, &output)?; + Ok(output) + } +} diff --git a/cli/src/util/mod.rs b/cli/src/util/mod.rs index 8e1569b2..6018862c 100644 --- a/cli/src/util/mod.rs +++ b/cli/src/util/mod.rs @@ -3,6 +3,7 @@ pub mod cmd; pub mod download; pub mod global_config; pub mod lz4; +pub mod show_term; pub mod os; pub mod paths; pub mod struct_fmt; diff --git a/cli/src/util/show_term.rs b/cli/src/util/show_term.rs new file mode 100644 index 00000000..12544479 --- /dev/null +++ b/cli/src/util/show_term.rs @@ -0,0 +1,211 @@ +use global_error::prelude::*; +use std::process::{Child, Command}; + +#[cfg(target_os = "linux")] +struct Terminal { + /// The name of the terminal emulator command + name: &'static str, + /// The flag to pass the command to the terminal emulator + prompt_str: &'static [&'static str], +} + +/// Terminals that don't work (note, more work might make them work): +/// +/// - guake (runs the whole window, doesn't handle closing) +/// - upterm (doesn't have an arg to pass a command it) +/// - x-terminal-emulator +/// - tilda (doesn't show automatically) +/// - terminator (issues running the command) +/// - xfce4-terminal (issues running the command) +#[cfg(target_os = "linux")] +const TERMINALS: [Terminal; 7] = [ + Terminal { + name: "kitty", + prompt_str: &["-e"], + }, + Terminal { + name: "konsole", + prompt_str: &["-e"], + }, + Terminal { + name: "gnome-terminal", + prompt_str: &["--"], + }, + Terminal { + name: "st", + prompt_str: &["-e"], + }, + Terminal { + name: "tilix", + prompt_str: &["-e"], + }, + Terminal { + name: "urxvt", + prompt_str: &["-e"], + }, + Terminal { + name: "xterm", + prompt_str: &["-e"], + }, +]; + +pub async fn show_term(args: &[String]) -> GlobalResult { + #[cfg(target_os = "windows")] + let child: Child = Command::new("cmd.exe") + .arg("/C") + .args(args) + .spawn() + .expect("cmd.exe failed to start"); + + #[cfg(target_os = "macos")] + let child: Child = { + // This script will run from home, so we need top change to the + // project directory before running the command. + let current_dir = std::env::current_dir()? + .into_os_string() + .into_string() + .unwrap(); + + // Create script tempfile + let script_temp_file = tempfile::Builder::new() + .prefix("rivet_term_") + .suffix(".command") + .tempfile()?; + let script_path = script_temp_file.path().to_path_buf(); + script_temp_file.keep()?; + + // Write the script content to the script file + let command_to_run = format!( + "cd \"{}\" && {} && rm \"{}\"", + current_dir, + args.join(" "), + script_path.display() + ); + std::fs::write(&script_path, format!("#!/bin/bash\n{}", command_to_run))?; + std::fs::set_permissions( + &script_path, + std::os::unix::fs::PermissionsExt::from_mode(0o755), + )?; + + // Use `open` to run the script + Command::new("open") + .arg(&script_path) + .spawn() + .expect("Failed to open script") + }; + + #[cfg(target_os = "linux")] + let child: Child = { + // TODO(forest): For Linux, the code is trying to find an + // available terminal emulator from a predefined list and + // then run the command in it. However, the way to run a + // command in a terminal emulator can vary between different + // emulators. The -e flag used here might not work for all + // of them. + let mut command = None; + + for terminal in TERMINALS { + if which::which(terminal.name).is_ok() { + command = Some(terminal); + break; + } + } + + match command { + Some(terminal) => { + // See if they have bash installed. If not, fallback to sh + let shell = if which::which("bash").is_ok() { + "bash" + } else { + "sh" + }; + + // Insert the flag --inside-terminal right after `sidekick` + // in the args. The only args before it are the binary path + // to the binary and `sidekick` itself, so it can go at the + // 2nd index. + args.insert(2, "--inside-terminal".to_string()); + + // Add a "press any key to continue" message to the end of + // the arguments to be run + args.append( + vec![ + "&&", + "read", + "-n", + "1", + "-s", + "-r", + "-p", + "\"Press any key to continue\"", + ] + .iter() + .map(|x| x.to_string()) + .collect::>() + .as_mut(), + ); + + args = vec![args.join(" ")]; + + Command::new(terminal.name) + // This is the flag to run a command in the + // terminal. Most will use -e, but some might use + // something different. + .args(terminal.prompt_str) + // We pass everything to a shell manually so that we can + // pass an entire string of the rest of the commands. + // This is more consistant across terminals on linux. + .arg(shell) + .arg("-c") + .args(&args) + .spawn() + .expect("Terminal emulator failed to start"); + } + None => { + panic!("No terminal emulator found"); + } + } + }; + + Ok(child) +} + +#[cfg(target_os = "linux")] +#[cfg(test)] +mod tests { + use std::fs; + use std::path::Path; + use std::process::Command; + + use super::TERMINALS; + + #[test] + #[ignore] + /// This test makes sure that the configuration to run a command in each + /// terminal works. It shouldn't run in CI, since it would be difficult to + /// configure. It can be run locally if each terminal in the const is + /// installed. + fn test_terminals() { + for terminal in TERMINALS { + let file_name = format!("{}.txt", terminal.name); + + let mut args = Vec::new(); + + args.push(format!("touch {}", file_name)); + + let output = Command::new(terminal.name) + .args(terminal.prompt_str) + .args(&args) + .output() + .expect("Failed to execute command"); + + assert!(output.status.success(), "Command failed: {}", terminal.name); + + let file_path = Path::new(&file_name); + assert!(file_path.exists(), "File does not exist: {}", file_name); + + // Clean up the file + fs::remove_file(file_path).expect("Failed to remove file"); + } + } +}