use crate::menus::cluster::Cluster;
use crate::double_column_menu::entry::Entry;
use ssh2::Session;
use std::{io::Read, process::Command, default::Default};
use regex::Regex;
use color_eyre::eyre::Result;
use serde::{Serialize, Deserialize};
use std::{fs, fs::OpenOptions, io::Write};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Spawner {
pub preset_name: String,
pub account: String,
pub partition: String,
pub time: String,
pub working_directory: String,
pub other_options: String,
}
impl Default for Spawner {
fn default() -> Spawner {
Spawner {
preset_name: String::from(""),
account: String::from(""),
partition: String::from(""),
time: String::from("01:00:00"),
working_directory: String::from(""),
other_options: String::from(""),
}
}
}
impl Entry for Spawner {
fn get_entry_name(&self) -> String {
self.preset_name.clone()
}
fn set_entry_name(&mut self, name: &str) {
self.preset_name = name.to_string();
}
fn get_value_from_index(&self, index: usize) -> String {
match index {
0 => self.preset_name.clone(),
1 => self.account.clone(),
2 => self.partition.clone(),
3 => self.time.clone(),
4 => self.working_directory.clone(),
5 => self.other_options.clone(),
_ => String::from(""),
}
}
fn set_value_from_index(&mut self, index: usize, value: &str) {
match index {
0 => self.preset_name = value.to_string(),
1 => self.account = value.to_string(),
2 => self.partition = value.to_string(),
3 => self.time = value.to_string(),
4 => self.working_directory = value.to_string(),
5 => self.other_options = value.to_string(),
_ => {},
}
}
fn get_entry_names(&self) -> Vec<String> {
vec![
"Preset Name: ".to_string(),
"Account: ".to_string(),
"Partition: ".to_string(),
"Max. Time: ".to_string(),
"Work. Dir.: ".to_string(),
"Other Options: ".to_string(),
]
}
fn get_entry_values(&self) -> Vec<String> {
vec![
self.preset_name.clone(),
self.account.clone(),
self.partition.clone(),
self.time.clone(),
self.working_directory.clone(),
self.other_options.clone(),
]
}
}
impl Spawner {
pub fn new(
preset_name: &str,
account: &str,
partition: &str,
time: &str,
working_directory: &str,
other_options: &str,
) -> Spawner {
Spawner {
preset_name: preset_name.to_string(),
account: account.to_string(),
partition: partition.to_string(),
time: time.to_string(),
working_directory: working_directory.to_string(),
other_options: other_options.to_string(),
}
}
pub fn spawn(&self, session: &mut Session, cluster: &Cluster) -> Result<()> {
let mut node_name = self.get_node_name(session)?;
if node_name.is_none() {
self.salloc(session, cluster)?;
node_name = self.get_node_name(session)?;
}
let node_name = node_name.unwrap();
self.add_cluster_to_ssh_config(&node_name, cluster)?;
let _ = self.clear_known_host(&node_name);
self.spawn_vscode(&self.preset_name, session)?;
Ok(())
}
pub fn get_spawn_command(&self) -> String {
let mut command = String::from("salloc");
if !self.preset_name.is_empty() {
command.push_str(" --job-name=");
command.push_str(&self.preset_name);
}
if !self.account.is_empty() {
command.push_str(" -A ");
command.push_str(&self.account);
}
if !self.partition.is_empty() {
command.push_str(" -p ");
command.push_str(&self.partition);
}
if !self.time.is_empty() {
command.push_str(" -t ");
command.push_str(&self.time);
}
if !self.other_options.is_empty() {
command.push_str(" ");
command.push_str(&self.other_options);
}
command.push_str(" --no-shell; exit");
command
}
pub fn spawn_vscode(&self, node_alias: &str, session: &mut Session) -> Result<()> {
let mut code_wd = self.working_directory.clone();
if self.working_directory.is_empty() {
let command = "echo $HOME";
let mut channel = session.channel_session()?;
channel.exec(&command)?;
let mut output = String::new();
channel.read_to_string(&mut output)?;
code_wd = output.trim().to_string();
}
let remote_argument = format!(
"vscode-remote://ssh-remote+cr-{}/{}", node_alias, code_wd);
Command::new("code")
.arg("--folder-uri").arg(remote_argument)
.output()
.expect("Failed to start Visual Studio Code");
Ok(())
}
pub fn format_config_entry(&self, node_name: &str, cluster: &Cluster) -> String {
let mut entry = String::new();
entry.push_str(&format!("# code-remote: start {}\n", self.preset_name));
entry.push_str(format!("Host cr-{}\n", self.preset_name).as_str());
entry.push_str(format!(" HostName {}\n", node_name).as_str());
entry.push_str(format!(" User {}\n", cluster.user).as_str());
if !cluster.identity_file.is_empty() {
entry.push_str(format!(" IdentityFile {}\n", cluster.identity_file).as_str());
}
entry.push_str(format!(" ProxyJump cr-{}\n", cluster.name).as_str());
entry.push_str(&format!("# code-remote: end {}", self.preset_name));
entry
}
pub fn add_cluster_to_ssh_config(&self, node_name: &str, cluster: &Cluster) -> Result<()> {
let home = std::env::var("HOME")?;
let config_file_path = format!("{}/.ssh/config", home);
let config_content = fs::read_to_string(&config_file_path)?;
let pattern = format!(r"(?ms)^# code-remote: start {}\n.*?# code-remote: end {}\s*"
, self.preset_name, self.preset_name);
let re = Regex::new(&pattern)?;
let modified_content = re.replace_all(&config_content, "").to_string();
fs::write(&config_file_path, modified_content)?;
let entry = self.format_config_entry(node_name, cluster);
let mut file = OpenOptions::new()
.write(true)
.read(true)
.append(true)
.create(true)
.open(&config_file_path)?;
writeln!(file, "{}", entry)?;
Ok(())
}
pub fn clear_known_host(&self, node_alias: &str) -> Result<()> {
let home_path = std::env::var("HOME")?;
let known_hosts_path = format!("{}/.ssh/known_hosts", home_path);
if std::path::Path::new(&known_hosts_path).exists() {
Command::new("ssh-keygen")
.args(&["-f", &known_hosts_path, "-R", &node_alias])
.output()?;
}
Ok(())
}
pub fn get_node_name(&self, session: &mut Session) -> Result<Option<String>> {
let command = format!("squeue -u $USER --name {} --noheader --format=%N", self.preset_name);
let mut channel = session.channel_session()?;
channel.exec(&command)?;
let mut output = String::new();
channel.read_to_string(&mut output)?;
if output.is_empty() {
return Ok(None);
}
Ok(Some(output.trim().to_string()))
}
pub fn salloc(&self, session: &mut Session, cluster: &Cluster) -> Result<()> {
let command = self.get_spawn_command();
Ok(cluster.execute_and_forward(session, &command)?)
}
}