diff --git a/src/bin/vibebox.rs b/src/bin/vibebox.rs index 14a1107..e3558b0 100644 --- a/src/bin/vibebox.rs +++ b/src/bin/vibebox.rs @@ -10,15 +10,15 @@ use std::{ use clap::Parser; use color_eyre::Result; use dialoguer::Confirm; -use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; +use time::format_description::well_known::Rfc3339; use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::registry::Registry; -use tracing_subscriber::{fmt, prelude::*, reload, EnvFilter}; +use tracing_subscriber::{EnvFilter, fmt, prelude::*, reload}; use vibebox::tui::{AppState, VmInfo}; use vibebox::{ - commands, config, explain, instance, session_manager, tui, vm, vm_manager, SessionManager, + SessionManager, commands, config, explain, instance, session_manager, tui, vm, vm_manager, }; #[derive(Debug, Parser)] diff --git a/src/instance.rs b/src/instance.rs index 21c2122..a6882f6 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1,28 +1,23 @@ use std::{ env, fs, - io::{self, Write}, + io::{self}, net::{SocketAddr, TcpStream}, - os::unix::{fs::PermissionsExt, io::OwnedFd, net::UnixStream}, + os::unix::{fs::PermissionsExt, net::UnixStream}, path::{Path, PathBuf}, process::{Command, Stdio}, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, - }, + sync::{Arc, Mutex}, thread, time::{Duration, Instant}, }; use serde::{Deserialize, Serialize}; -use std::sync::mpsc::Sender; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use uuid::Uuid; use crate::{ commands, session_manager::{INSTANCE_DIR_NAME, INSTANCE_FILENAME}, - tui::AppState, - vm::{self, LoginAction, VmInput}, + vm::{self, LoginAction}, }; const SSH_KEY_NAME: &str = "ssh_key"; @@ -422,243 +417,3 @@ pub(crate) fn build_ssh_login_actions( vec![LoginAction::Send(setup)] } - -fn spawn_ssh_io( - app: Arc>, - config: Arc>, - instance_dir: PathBuf, - ssh_key: PathBuf, - output_monitor: Arc, - vm_output_fd: OwnedFd, - vm_input_fd: OwnedFd, -) -> vm::IoContext { - let io_control = vm::IoControl::new(); - - let log_path = instance_dir.join(VM_ROOT_LOG_NAME); - let log_file = fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(&log_path) - .ok() - .map(|file| Arc::new(Mutex::new(file))); - - let ssh_connected = Arc::new(AtomicBool::new(false)); - let ssh_started = Arc::new(AtomicBool::new(false)); - let ssh_ready = Arc::new(AtomicBool::new(false)); - let input_tx_holder: Arc>>> = Arc::new(Mutex::new(None)); - - let instance_path = instance_dir.join(INSTANCE_FILENAME); - let config_for_output = config.clone(); - let log_for_output = log_file.clone(); - let ssh_connected_for_output = ssh_connected.clone(); - let ssh_ready_for_output = ssh_ready.clone(); - - let mut line_buf = String::new(); - - let handlers = commands::build_handlers(app.clone(), io_control.clone()); - let on_line = move |line: &str| handlers.handle(line); - - let on_output = move |bytes: &[u8]| { - if ssh_connected_for_output.load(Ordering::SeqCst) { - if let Some(log) = &log_for_output { - if let Ok(mut file) = log.lock() { - let _ = file.write_all(bytes); - } - } - } - - let text = String::from_utf8_lossy(bytes); - line_buf.push_str(&text); - - while let Some(pos) = line_buf.find('\n') { - let mut line = line_buf[..pos].to_string(); - if line.ends_with('\r') { - line.pop(); - } - line_buf.drain(..=pos); - - let cleaned = line.trim_start_matches(|c: char| c == '\r' || c == ' '); - - if let Some(pos) = cleaned.find("VIBEBOX_IPV4=") { - let ip_raw = &cleaned[(pos + "VIBEBOX_IPV4=".len())..]; - let ip = extract_ipv4(ip_raw).unwrap_or_default(); - if !ip.is_empty() { - if let Ok(mut cfg) = config_for_output.lock() { - if cfg.vm_ipv4.as_deref() != Some(ip.as_str()) { - cfg.vm_ipv4 = Some(ip.clone()); - let _ = write_instance_config(&instance_path, &cfg); - tracing::info!(ip = %ip, "detected vm ipv4"); - } - } - } - } - - if cleaned.contains("VIBEBOX_SSH_READY") { - ssh_ready_for_output.store(true, Ordering::SeqCst); - tracing::info!("sshd ready"); - } - } - }; - - let io_ctx = vm::spawn_vm_io_with_hooks( - output_monitor, - vm_output_fd, - vm_input_fd, - io_control.clone(), - on_line, - on_output, - ); - - *input_tx_holder.lock().unwrap() = Some(io_ctx.input_tx.clone()); - - let ssh_ready_for_thread = ssh_ready.clone(); - let ssh_started_for_thread = ssh_started.clone(); - let ssh_connected_for_thread = ssh_connected.clone(); - let io_control_for_thread = io_control.clone(); - let ssh_key_for_thread = ssh_key.clone(); - let config_for_thread = config.clone(); - let input_tx_holder_for_thread = input_tx_holder.clone(); - - thread::spawn(move || { - loop { - if ssh_started_for_thread.load(Ordering::SeqCst) { - break; - } - if !ssh_ready_for_thread.load(Ordering::SeqCst) { - thread::sleep(std::time::Duration::from_millis(200)); - continue; - } - - let ip = config_for_thread - .lock() - .ok() - .and_then(|cfg| cfg.vm_ipv4.clone()); - let ssh_user = config_for_thread - .lock() - .map(|cfg| cfg.ssh_user.clone()) - .unwrap_or_else(|_| DEFAULT_SSH_USER.to_string()); - - if let Some(ip) = ip { - if ssh_started_for_thread - .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) - .is_err() - { - break; - } - - tracing::info!(user = %ssh_user, ip = %ip, "starting ssh"); - io_control_for_thread.request_terminal_restore(); - io_control_for_thread.set_forward_output(false); - io_control_for_thread.set_forward_input(false); - ssh_connected_for_thread.store(true, Ordering::SeqCst); - - let mut attempts = 0usize; - loop { - attempts += 1; - if !ssh_port_open(&ip) { - tracing::info!( - attempts, - ip = %ip, - "waiting for ssh port ({}/{})", - attempts, - SSH_CONNECT_RETRIES - ); - if attempts >= SSH_CONNECT_RETRIES { - ssh_connected_for_thread.store(false, Ordering::SeqCst); - tracing::warn!( - attempts, - "ssh port not ready after {} attempts", - SSH_CONNECT_RETRIES - ); - break; - } - thread::sleep(std::time::Duration::from_millis(SSH_CONNECT_DELAY_MS)); - continue; - } - - tracing::info!( - attempts, - user = %ssh_user, - ip = %ip, - "starting ssh ({}/{})", - attempts, - SSH_CONNECT_RETRIES - ); - let status = Command::new("ssh") - .args([ - "-i", - ssh_key_for_thread.to_str().unwrap_or(".vibebox/ssh_key"), - "-o", - "IdentitiesOnly=yes", - "-o", - "StrictHostKeyChecking=no", - "-o", - "UserKnownHostsFile=/dev/null", - "-o", - "GlobalKnownHostsFile=/dev/null", - "-o", - "PasswordAuthentication=no", - "-o", - "BatchMode=yes", - "-o", - "LogLevel=ERROR", - "-o", - "ConnectTimeout=5", - ]) - .env_remove("LC_CTYPE") - .env_remove("LC_ALL") - // .env_remove("LANG") - .arg(format!("{ssh_user}@{ip}")) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .status(); - - match status { - Ok(status) if status.success() => { - ssh_connected_for_thread.store(false, Ordering::SeqCst); - tracing::info!(status = %status, "ssh exited"); - if let Some(tx) = input_tx_holder_for_thread.lock().unwrap().clone() { - let _ = tx.send(VmInput::Bytes(b"systemctl poweroff\n".to_vec())); - } - break; - } - Ok(status) if status.code() == Some(255) => { - tracing::warn!(status = %status, "ssh connection failed"); - if attempts >= SSH_CONNECT_RETRIES { - ssh_connected_for_thread.store(false, Ordering::SeqCst); - tracing::warn!( - attempts, - "ssh failed after {} attempts", - SSH_CONNECT_RETRIES - ); - break; - } - thread::sleep(std::time::Duration::from_millis(500)); - continue; - } - Ok(status) => { - ssh_connected_for_thread.store(false, Ordering::SeqCst); - tracing::info!(status = %status, "ssh exited"); - if let Some(tx) = input_tx_holder_for_thread.lock().unwrap().clone() { - let _ = tx.send(VmInput::Bytes(b"systemctl poweroff\n".to_vec())); - } - break; - } - Err(err) => { - ssh_connected_for_thread.store(false, Ordering::SeqCst); - tracing::error!(error = %err, "failed to start ssh"); - break; - } - } - } - break; - } - - thread::sleep(std::time::Duration::from_millis(200)); - } - }); - - io_ctx -} diff --git a/src/vm.rs b/src/vm.rs index 17dc880..65b1e20 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -82,7 +82,6 @@ const BASE_DISK_RAW_NAME: &str = "disk.raw"; pub(crate) enum LoginAction { Expect { text: String, timeout: Duration }, Send(String), - Script { path: PathBuf, index: usize }, } use LoginAction::*; @@ -281,16 +280,6 @@ where ) } -fn script_command_from_path( - path: &Path, - index: usize, -) -> Result> { - let script = fs::read_to_string(path) - .map_err(|err| format!("Failed to read script {}: {err}", path.display()))?; - let label = format!("{}_{}", index, path.file_name().unwrap().display()); - script_command_from_content(&label, &script) -} - pub(crate) fn script_command_from_content( label: &str, script: &str, @@ -1014,18 +1003,6 @@ fn spawn_login_actions_thread( text.push('\n'); // Type the newline so the command is actually submitted. input_tx.send(VmInput::Bytes(text.into_bytes())).unwrap(); } - Script { path, index } => { - let command = match script_command_from_path(&path, index) { - Ok(command) => command, - Err(err) => { - tracing::error!(error = %err, "failed to build login script command"); - return; - } - }; - let mut text = command; - text.push('\n'); - input_tx.send(VmInput::Bytes(text.into_bytes())).unwrap(); - } } } })