on main help sign in
Merge pull request #3 from robcholz/consistency
fc9edbbFinn Sheng
docs/tasks.md+3−2
@@ -47,8 +47,9 @@
47 2. [x] Remove old cli.
47 2. [x] Remove old cli.
48 3. [x] add an actual config file.
48 3. [x] add an actual config file.
49 4. [x] set up the cli.
49 4. [x] set up the cli.
50−9. [ ] fix ui overlap, and consistency issue.
50+9. [x] fix ui overlap, and consistency issue.
51+10. [ ] `clean_cache` to cleanup the cache.
51−10. [ ] intensive integration test.
52+11. [ ] intensive integration test.
52
53
53## Publish
54## Publish
54
55
src/bin/vibebox-cli.rs+68−13
@@ -11,7 +11,9 @@ use color_eyre::Result;
11use dialoguer::Confirm;
11use dialoguer::Confirm;
12use time::OffsetDateTime;
12use time::OffsetDateTime;
13use time::format_description::well_known::Rfc3339;
13use time::format_description::well_known::Rfc3339;
14−use tracing_subscriber::EnvFilter;
14+use tracing_subscriber::filter::LevelFilter;
15+use tracing_subscriber::registry::Registry;
16+use tracing_subscriber::{EnvFilter, fmt, prelude::*, reload};
15
17
16use vibebox::tui::{AppState, VmInfo};
18use vibebox::tui::{AppState, VmInfo};
17use vibebox::{
19use vibebox::{
@@ -39,11 +41,11 @@ enum Command {
39}
41}
40
42
41fn main() -> Result<()> {
43fn main() -> Result<()> {
42− init_tracing();
43 color_eyre::install()?;
44 color_eyre::install()?;
45+ let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
46+ let stderr_handle = init_tracing(&cwd);
44
47
45 let cli = Cli::parse();
48 let cli = Cli::parse();
46− let cwd = env::current_dir().map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
47 tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
49 tracing::info!(cwd = %cwd.display(), "starting vibebox cli");
48 if let Some(command) = cli.command {
50 if let Some(command) = cli.command {
49 return handle_command(command, &cwd, cli.config.as_deref());
51 return handle_command(command, &cwd, cli.config.as_deref());
@@ -79,12 +81,13 @@ fn main() -> Result<()> {
79 mounts: config.box_cfg.mounts.clone(),
81 mounts: config.box_cfg.mounts.clone(),
80 };
82 };
81
83
84+ let auto_shutdown_ms = config.supervisor.auto_shutdown_ms;
82 let vm_info = VmInfo {
85 let vm_info = VmInfo {
83− version: env!("CARGO_PKG_VERSION").to_string(),
84 max_memory_mb: vm_args.ram_bytes / (1024 * 1024),
86 max_memory_mb: vm_args.ram_bytes / (1024 * 1024),
85 cpu_cores: vm_args.cpu_count,
87 cpu_cores: vm_args.cpu_count,
88+ system_name: "Debian".to_string(), // TODO: read system name from the VM.
89+ auto_shutdown_ms,
86 };
90 };
87− let auto_shutdown_ms = config.supervisor.auto_shutdown_ms;
88 if let Ok(manager) = SessionManager::new() {
91 if let Ok(manager) = SessionManager::new() {
89 if let Err(err) = manager.update_global_sessions(&cwd) {
92 if let Err(err) = manager.update_global_sessions(&cwd) {
90 tracing::warn!(error = %err, "failed to update a global session list");
93 tracing::warn!(error = %err, "failed to update a global session list");
@@ -104,6 +107,9 @@ fn main() -> Result<()> {
104 writeln!(stdout)?;
107 writeln!(stdout)?;
105 stdout.flush()?;
108 stdout.flush()?;
106 }
109 }
110+ if let Some(handle) = stderr_handle {
111+ let _ = handle.modify(|filter| *filter = LevelFilter::INFO);
112+ }
107
113
108 tracing::info!(auto_shutdown_ms, "auto shutdown config");
114 tracing::info!(auto_shutdown_ms, "auto shutdown config");
109 let manager_conn =
115 let manager_conn =
@@ -248,13 +254,62 @@ fn format_last_active(value: Option<&str>) -> String {
248 format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
254 format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
249}
255}
250
256
257+type StderrHandle = reload::Handle<LevelFilter, Registry>;
258+
251−fn init_tracing() {
259+fn init_tracing(cwd: &Path) -> Option<StderrHandle> {
252 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
260 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
261+ let file_filter = filter.clone();
262+ let stderr_is_tty = std::io::stderr().is_terminal();
253− let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err();
263+ let ansi = stderr_is_tty && env::var("VIBEBOX_LOG_NO_COLOR").is_err();
264+ let file = instance::ensure_instance_dir(cwd)
265+ .ok()
266+ .and_then(|instance_dir| {
267+ let log_path = instance_dir.join("cli.log");
268+ std::fs::OpenOptions::new()
269+ .create(true)
270+ .write(true)
271+ .truncate(true)
272+ .open(log_path)
273+ .ok()
274+ });
275+
276+ if stderr_is_tty {
277+ let (stderr_filter, handle) = reload::Layer::new(LevelFilter::OFF);
278+ let stderr_layer = fmt::layer()
279+ .with_target(false)
280+ .with_ansi(ansi)
281+ .without_time()
282+ .with_writer(std::io::stderr)
283+ .with_filter(stderr_filter);
254− let _ = tracing_subscriber::fmt()
284+ let subscriber = tracing_subscriber::registry().with(stderr_layer);
255− .with_env_filter(filter)
285+ if let Some(file) = file {
286+ let file_layer = fmt::layer()
287+ .with_target(false)
288+ .with_ansi(false)
289+ .with_writer(file)
290+ .with_filter(file_filter);
291+ let _ = subscriber.with(file_layer).try_init();
292+ } else {
293+ let _ = subscriber.try_init();
294+ }
295+ Some(handle)
296+ } else {
297+ let stderr_layer = fmt::layer()
256− .with_target(false)
298+ .with_target(false)
257− .with_ansi(ansi)
299+ .with_ansi(ansi)
258− .with_writer(std::io::stderr)
300+ .with_writer(std::io::stderr)
259− .try_init();
301+ .with_filter(filter);
302+ let subscriber = tracing_subscriber::registry().with(stderr_layer);
303+ if let Some(file) = file {
304+ let file_layer = fmt::layer()
305+ .with_target(false)
306+ .with_ansi(false)
307+ .with_writer(file)
308+ .with_filter(file_filter);
309+ let _ = subscriber.with(file_layer).try_init();
310+ } else {
311+ let _ = subscriber.try_init();
312+ }
313+ None
314+ }
260}
315}
src/bin/vibebox-supervisor.rs+21−7
@@ -5,6 +5,9 @@ use std::{
5
5
6use color_eyre::Result;
6use color_eyre::Result;
7use tracing_subscriber::EnvFilter;
7use tracing_subscriber::EnvFilter;
8+use tracing_subscriber::filter::LevelFilter;
9+use tracing_subscriber::fmt;
10+use tracing_subscriber::prelude::*;
8
11
9use vibebox::{config, instance, vm, vm_manager};
12use vibebox::{config, instance, vm, vm_manager};
10
13
@@ -40,11 +43,22 @@ fn main() -> Result<()> {
40
43
41fn init_tracing() {
44fn init_tracing() {
42 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
45 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
46+ let stderr_is_tty = std::io::stderr().is_terminal();
43− let ansi = std::io::stderr().is_terminal() && env::var("VIBEBOX_LOG_NO_COLOR").is_err();
47+ let ansi = stderr_is_tty && env::var("VIBEBOX_LOG_NO_COLOR").is_err();
48+ if stderr_is_tty {
49+ let stderr_layer = fmt::layer()
50+ .with_target(false)
51+ .with_ansi(ansi)
52+ .without_time()
53+ .with_writer(io::stderr)
54+ .with_filter(LevelFilter::INFO);
44− let _ = tracing_subscriber::fmt()
55+ let _ = tracing_subscriber::registry().with(stderr_layer).try_init();
45− .with_env_filter(filter)
56+ } else {
46− .with_target(false)
57+ let stderr_layer = fmt::layer()
47− .with_ansi(ansi)
58+ .with_target(false)
48− .with_writer(io::stderr)
59+ .with_ansi(ansi)
49− .try_init();
60+ .with_writer(io::stderr)
61+ .with_filter(filter);
62+ let _ = tracing_subscriber::registry().with(stderr_layer).try_init();
63+ }
50}
64}
src/config.rs+1−1
@@ -279,6 +279,6 @@ fn validate_or_exit(config: &Config) {
279}
279}
280
280
281fn die(message: &str) -> ! {
281fn die(message: &str) -> ! {
282− eprintln!("[vibebox] {message}");
282+ tracing::error!("{message}");
283 std::process::exit(1);
283 std::process::exit(1);
284}
284}
src/instance.rs+62−30
@@ -26,7 +26,7 @@ use crate::{
26};
26};
27
27
28const SSH_KEY_NAME: &str = "ssh_key";
28const SSH_KEY_NAME: &str = "ssh_key";
29−pub(crate) const SERIAL_LOG_NAME: &str = "serial.log";
29+pub(crate) const VM_ROOT_LOG_NAME: &str = "vm_root.log";
30pub(crate) const DEFAULT_SSH_USER: &str = "vibecoder";
30pub(crate) const DEFAULT_SSH_USER: &str = "vibecoder";
31const SSH_CONNECT_RETRIES: usize = 30;
31const SSH_CONNECT_RETRIES: usize = 30;
32const SSH_CONNECT_DELAY_MS: u64 = 500;
32const SSH_CONNECT_DELAY_MS: u64 = 500;
@@ -72,7 +72,6 @@ pub fn run_with_ssh(manager_conn: UnixStream) -> Result<(), Box<dyn std::error::
72 tracing::debug!(ssh_user = %ssh_user, "loaded instance config");
72 tracing::debug!(ssh_user = %ssh_user, "loaded instance config");
73
73
74 let _manager_conn = manager_conn;
74 let _manager_conn = manager_conn;
75− tracing::debug!("waiting for vm ipv4");
76 wait_for_vm_ipv4(&instance_dir, Duration::from_secs(120))?;
75 wait_for_vm_ipv4(&instance_dir, Duration::from_secs(120))?;
77
76
78 let ip = load_or_create_instance_config(&instance_dir)?
77 let ip = load_or_create_instance_config(&instance_dir)?
@@ -118,7 +117,7 @@ pub(crate) fn ensure_ssh_keypair(
118 "vibebox",
117 "vibebox",
119 ])
118 ])
120 .stdin(Stdio::null())
119 .stdin(Stdio::null())
121− .stdout(Stdio::null())
120+ .stdout(Stdio::inherit())
122 .stderr(Stdio::inherit())
121 .stderr(Stdio::inherit())
123 .status()?;
122 .status()?;
124
123
@@ -246,6 +245,9 @@ fn wait_for_vm_ipv4(
246 timeout: Duration,
245 timeout: Duration,
247) -> Result<(), Box<dyn std::error::Error>> {
246) -> Result<(), Box<dyn std::error::Error>> {
248 let start = Instant::now();
247 let start = Instant::now();
248+ let mut next_log_at = start + Duration::from_secs(10);
249+ tracing::info!("waiting for vm ipv4");
250+ let mut once_hint = false;
249 loop {
251 loop {
250 let config = load_or_create_instance_config(instance_dir)?;
252 let config = load_or_create_instance_config(instance_dir)?;
251 if config.vm_ipv4.is_some() {
253 if config.vm_ipv4.is_some() {
@@ -254,8 +256,16 @@ fn wait_for_vm_ipv4(
254 if start.elapsed() > timeout {
256 if start.elapsed() > timeout {
255 return Err("Timed out waiting for VM IPv4".into());
257 return Err("Timed out waiting for VM IPv4".into());
256 }
258 }
259+ if Instant::now() >= next_log_at {
257− if start.elapsed().as_secs() % 5 == 0 {
260+ let waited = start.elapsed();
261+ if waited.as_secs() > 15 && !once_hint {
262+ tracing::info!(
263+ "if vibebox is just initialized in this directory, it might take up to 1 minutes depending on your machine, and then you can enjoy the speed vibecoding! go pack!"
264+ );
265+ once_hint = true;
266+ }
258− tracing::debug!("still waiting for vm ipv4");
267+ tracing::info!("still waiting for vm ipv4, {}s elapsed", waited.as_secs(),);
268+ next_log_at += Duration::from_secs(10);
259 }
269 }
260 thread::sleep(Duration::from_millis(200));
270 thread::sleep(Duration::from_millis(200));
261 }
271 }
@@ -270,9 +280,13 @@ fn run_ssh_session(
270 loop {
280 loop {
271 attempts += 1;
281 attempts += 1;
272 if !ssh_port_open(&ip) {
282 if !ssh_port_open(&ip) {
273− tracing::debug!(attempts, "ssh port not open yet");
283+ tracing::debug!(attempts, "ssh port doesn't open yet");
274− eprintln!(
284+ tracing::info!(
285+ attempts,
286+ ip = %ip,
287+ "waiting for ssh port ({}/{})",
288+ attempts,
275− "[vibebox] waiting for ssh port on {ip} (attempt {attempts}/{SSH_CONNECT_RETRIES})"
289+ SSH_CONNECT_RETRIES
276 );
290 );
277 if attempts >= SSH_CONNECT_RETRIES {
291 if attempts >= SSH_CONNECT_RETRIES {
278 return Err(
292 return Err(
@@ -283,10 +297,14 @@ fn run_ssh_session(
283 continue;
297 continue;
284 }
298 }
285
299
286− eprintln!(
300+ tracing::info!(
301+ attempts,
302+ user = %ssh_user,
303+ ip = %ip,
304+ "starting ssh ({}/{})",
305+ attempts,
287− "[vibebox] starting ssh to {ssh_user}@{ip} (attempt {attempts}/{SSH_CONNECT_RETRIES})"
306+ SSH_CONNECT_RETRIES
288 );
307 );
289− tracing::info!(attempts, user = %ssh_user, ip = %ip, "starting ssh");
290 let status = Command::new("ssh")
308 let status = Command::new("ssh")
291 .args([
309 .args([
292 "-i",
310 "-i",
@@ -319,21 +337,22 @@ fn run_ssh_session(
319
337
320 match status {
338 match status {
321 Ok(status) if status.success() => {
339 Ok(status) if status.success() => {
322− eprintln!("[vibebox] ssh exited with status: {status}");
340+ tracing::info!(status = %status, "ssh exited");
323 break;
341 break;
324 }
342 }
325 Ok(status) if status.code() == Some(255) => {
343 Ok(status) if status.code() == Some(255) => {
326− eprintln!("[vibebox] ssh connection failed: {status}");
344+ tracing::warn!(status = %status, "ssh connection failed");
327 if attempts >= SSH_CONNECT_RETRIES {
345 if attempts >= SSH_CONNECT_RETRIES {
328 return Err(format!("ssh failed after {SSH_CONNECT_RETRIES} attempts").into());
346 return Err(format!("ssh failed after {SSH_CONNECT_RETRIES} attempts").into());
329 }
347 }
330 thread::sleep(Duration::from_millis(500));
348 thread::sleep(Duration::from_millis(500));
331 }
349 }
332 Ok(status) => {
350 Ok(status) => {
333− eprintln!("[vibebox] ssh exited with status: {status}");
351+ tracing::info!(status = %status, "ssh exited");
334 break;
352 break;
335 }
353 }
336 Err(err) => {
354 Err(err) => {
355+ tracing::error!(error = %err, "failed to start ssh");
337 return Err(format!("failed to start ssh: {err}").into());
356 return Err(format!("failed to start ssh: {err}").into());
338 }
357 }
339 }
358 }
@@ -404,7 +423,7 @@ fn spawn_ssh_io(
404) -> vm::IoContext {
423) -> vm::IoContext {
405 let io_control = vm::IoControl::new();
424 let io_control = vm::IoControl::new();
406
425
407− let log_path = instance_dir.join(SERIAL_LOG_NAME);
426+ let log_path = instance_dir.join(VM_ROOT_LOG_NAME);
408 let log_file = fs::OpenOptions::new()
427 let log_file = fs::OpenOptions::new()
409 .create(true)
428 .create(true)
410 .write(true)
429 .write(true)
@@ -458,7 +477,7 @@ fn spawn_ssh_io(
458 if cfg.vm_ipv4.as_deref() != Some(ip.as_str()) {
477 if cfg.vm_ipv4.as_deref() != Some(ip.as_str()) {
459 cfg.vm_ipv4 = Some(ip.clone());
478 cfg.vm_ipv4 = Some(ip.clone());
460 let _ = write_instance_config(&instance_path, &cfg);
479 let _ = write_instance_config(&instance_path, &cfg);
461− eprintln!("[vibebox] detected vm IPv4: {ip}");
480+ tracing::info!(ip = %ip, "detected vm ipv4");
462 }
481 }
463 }
482 }
464 }
483 }
@@ -466,7 +485,7 @@ fn spawn_ssh_io(
466
485
467 if cleaned.contains("VIBEBOX_SSH_READY") {
486 if cleaned.contains("VIBEBOX_SSH_READY") {
468 ssh_ready_for_output.store(true, Ordering::SeqCst);
487 ssh_ready_for_output.store(true, Ordering::SeqCst);
469− eprintln!("[vibebox] sshd ready");
488+ tracing::info!("sshd ready");
470 }
489 }
471 }
490 }
472 };
491 };
@@ -517,7 +536,7 @@ fn spawn_ssh_io(
517 break;
536 break;
518 }
537 }
519
538
520− eprintln!("[vibebox] starting ssh to {ssh_user}@{ip}");
539+ tracing::info!(user = %ssh_user, ip = %ip, "starting ssh");
521 io_control_for_thread.request_terminal_restore();
540 io_control_for_thread.request_terminal_restore();
522 io_control_for_thread.set_forward_output(false);
541 io_control_for_thread.set_forward_output(false);
523 io_control_for_thread.set_forward_input(false);
542 io_control_for_thread.set_forward_input(false);
@@ -527,13 +546,19 @@ fn spawn_ssh_io(
527 loop {
546 loop {
528 attempts += 1;
547 attempts += 1;
529 if !ssh_port_open(&ip) {
548 if !ssh_port_open(&ip) {
530− eprintln!(
549+ tracing::info!(
550+ attempts,
551+ ip = %ip,
552+ "waiting for ssh port ({}/{})",
553+ attempts,
531− "[vibebox] waiting for ssh port on {ip} (attempt {attempts}/{SSH_CONNECT_RETRIES})"
554+ SSH_CONNECT_RETRIES
532 );
555 );
533 if attempts >= SSH_CONNECT_RETRIES {
556 if attempts >= SSH_CONNECT_RETRIES {
534 ssh_connected_for_thread.store(false, Ordering::SeqCst);
557 ssh_connected_for_thread.store(false, Ordering::SeqCst);
535− eprintln!(
558+ tracing::warn!(
559+ attempts,
536− "[vibebox] ssh port not ready after {SSH_CONNECT_RETRIES} attempts"
560+ "ssh port not ready after {} attempts",
561+ SSH_CONNECT_RETRIES
537 );
562 );
538 break;
563 break;
539 }
564 }
@@ -541,8 +566,13 @@ fn spawn_ssh_io(
541 continue;
566 continue;
542 }
567 }
543
568
544− eprintln!(
569+ tracing::info!(
570+ attempts,
571+ user = %ssh_user,
572+ ip = %ip,
573+ "starting ssh ({}/{})",
574+ attempts,
545− "[vibebox] starting ssh to {ssh_user}@{ip} (attempt {attempts}/{SSH_CONNECT_RETRIES})"
575+ SSH_CONNECT_RETRIES
546 );
576 );
547 let status = Command::new("ssh")
577 let status = Command::new("ssh")
548 .args([
578 .args([
@@ -577,18 +607,20 @@ fn spawn_ssh_io(
577 match status {
607 match status {
578 Ok(status) if status.success() => {
608 Ok(status) if status.success() => {
579 ssh_connected_for_thread.store(false, Ordering::SeqCst);
609 ssh_connected_for_thread.store(false, Ordering::SeqCst);
580− eprintln!("[vibebox] ssh exited with status: {status}");
610+ tracing::info!(status = %status, "ssh exited");
581 if let Some(tx) = input_tx_holder_for_thread.lock().unwrap().clone() {
611 if let Some(tx) = input_tx_holder_for_thread.lock().unwrap().clone() {
582 let _ = tx.send(VmInput::Bytes(b"systemctl poweroff\n".to_vec()));
612 let _ = tx.send(VmInput::Bytes(b"systemctl poweroff\n".to_vec()));
583 }
613 }
584 break;
614 break;
585 }
615 }
586 Ok(status) if status.code() == Some(255) => {
616 Ok(status) if status.code() == Some(255) => {
587− eprintln!("[vibebox] ssh connection failed: {status}");
617+ tracing::warn!(status = %status, "ssh connection failed");
588 if attempts >= SSH_CONNECT_RETRIES {
618 if attempts >= SSH_CONNECT_RETRIES {
589 ssh_connected_for_thread.store(false, Ordering::SeqCst);
619 ssh_connected_for_thread.store(false, Ordering::SeqCst);
590− eprintln!(
620+ tracing::warn!(
621+ attempts,
591− "[vibebox] ssh failed after {SSH_CONNECT_RETRIES} attempts"
622+ "ssh failed after {} attempts",
623+ SSH_CONNECT_RETRIES
592 );
624 );
593 break;
625 break;
594 }
626 }
@@ -597,7 +629,7 @@ fn spawn_ssh_io(
597 }
629 }
598 Ok(status) => {
630 Ok(status) => {
599 ssh_connected_for_thread.store(false, Ordering::SeqCst);
631 ssh_connected_for_thread.store(false, Ordering::SeqCst);
600− eprintln!("[vibebox] ssh exited with status: {status}");
632+ tracing::info!(status = %status, "ssh exited");
601 if let Some(tx) = input_tx_holder_for_thread.lock().unwrap().clone() {
633 if let Some(tx) = input_tx_holder_for_thread.lock().unwrap().clone() {
602 let _ = tx.send(VmInput::Bytes(b"systemctl poweroff\n".to_vec()));
634 let _ = tx.send(VmInput::Bytes(b"systemctl poweroff\n".to_vec()));
603 }
635 }
@@ -605,7 +637,7 @@ fn spawn_ssh_io(
605 }
637 }
606 Err(err) => {
638 Err(err) => {
607 ssh_connected_for_thread.store(false, Ordering::SeqCst);
639 ssh_connected_for_thread.store(false, Ordering::SeqCst);
608− eprintln!("[vibebox] failed to start ssh: {err}");
640+ tracing::error!(error = %err, "failed to start ssh");
609 break;
641 break;
610 }
642 }
611 }
643 }
src/tui.rs+21−6
@@ -34,12 +34,14 @@ const ASCII_BANNER: [&str; 7] = [
34 " ╚═══╝ ╚═╝╚═════╝ ╚══════╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝",
34 " ╚═══╝ ╚═╝╚═════╝ ╚══════╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝",
35 "",
35 "",
36];
36];
37+const INFO_LINE_COUNT: u16 = 5;
37
38
38#[derive(Debug, Clone)]
39#[derive(Debug, Clone)]
39pub struct VmInfo {
40pub struct VmInfo {
40− pub version: String,
41 pub max_memory_mb: u64,
41 pub max_memory_mb: u64,
42 pub cpu_cores: usize,
42 pub cpu_cores: usize,
43+ pub system_name: String,
44+ pub auto_shutdown_ms: u64,
43}
45}
44
46
45#[derive(Debug)]
47#[derive(Debug)]
@@ -155,10 +157,14 @@ fn compute_page_layout(app: &AppState, width: u16) -> PageLayout {
155fn header_height() -> u16 {
157fn header_height() -> u16 {
156 let banner_height = ASCII_BANNER.len() as u16;
158 let banner_height = ASCII_BANNER.len() as u16;
157 let welcome_height = 1;
159 let welcome_height = 1;
158− let info_height = 4;
160+ let info_height = info_block_height();
159 welcome_height + banner_height + info_height
161 welcome_height + banner_height + info_height
160}
162}
161
163
164+fn info_block_height() -> u16 {
165+ INFO_LINE_COUNT.saturating_add(1)
166+}
167+
162pub fn render_tui_once(app: &mut AppState) -> Result<()> {
168pub fn render_tui_once(app: &mut AppState) -> Result<()> {
163 let (width, _) = crossterm::terminal::size()?;
169 let (width, _) = crossterm::terminal::size()?;
164 if width == 0 {
170 if width == 0 {
@@ -532,13 +538,15 @@ fn render_header(buffer: &mut Buffer, area: Rect, app: &AppState) {
532 .constraints([
538 .constraints([
533 Constraint::Length(1),
539 Constraint::Length(1),
534 Constraint::Length(ASCII_BANNER.len() as u16),
540 Constraint::Length(ASCII_BANNER.len() as u16),
535− Constraint::Length(4),
541+ Constraint::Length(info_block_height()),
536 ])
542 ])
537 .split(area);
543 .split(area);
538
544
545+ let version = env!("CARGO_PKG_VERSION");
546+
539 let welcome = Line::from(vec![
547 let welcome = Line::from(vec![
540 Span::raw("Welcome to Vibebox v"),
548 Span::raw("Welcome to Vibebox v"),
541− Span::styled(&app.vm_info.version, Style::default().fg(Color::Yellow)),
549+ Span::styled(version, Style::default().fg(Color::Yellow)),
542 ]);
550 ]);
543
551
544 Paragraph::new(welcome).render(header_chunks[0], buffer);
552 Paragraph::new(welcome).render(header_chunks[0], buffer);
@@ -557,8 +565,8 @@ fn render_header(buffer: &mut Buffer, area: Rect, app: &AppState) {
557 Span::styled(app.cwd.to_string_lossy(), Style::default().fg(Color::Cyan)),
565 Span::styled(app.cwd.to_string_lossy(), Style::default().fg(Color::Cyan)),
558 ]),
566 ]),
559 Line::from(vec![
567 Line::from(vec![
560− Span::raw("VM Version: "),
568+ Span::raw("System: "),
561− Span::styled(&app.vm_info.version, Style::default().fg(Color::Green)),
569+ Span::styled(&app.vm_info.system_name, Style::default().fg(Color::Green)),
562 ]),
570 ]),
563 Line::from(vec![
571 Line::from(vec![
564 Span::raw("CPU / Memory: "),
572 Span::raw("CPU / Memory: "),
@@ -570,6 +578,13 @@ fn render_header(buffer: &mut Buffer, area: Rect, app: &AppState) {
570 Style::default().fg(Color::Green),
578 Style::default().fg(Color::Green),
571 ),
579 ),
572 ]),
580 ]),
581+ Line::from(vec![
582+ Span::raw("Auto Shutdown: "),
583+ Span::styled(
584+ format!("{} ms", app.vm_info.auto_shutdown_ms),
585+ Style::default().fg(Color::Green),
586+ ),
587+ ]),
573 ];
588 ];
574
589
575 Paragraph::new(info_lines)
590 Paragraph::new(info_lines)
src/vm.rs+10−10
@@ -429,7 +429,7 @@ fn ensure_base_image(
429 if !base_compressed.exists()
429 if !base_compressed.exists()
430 || std::fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
430 || std::fs::metadata(base_compressed).map(|m| m.len())? < DEBIAN_COMPRESSED_SIZE_BYTES
431 {
431 {
432− println!("Downloading base image...");
432+ tracing::info!("downloading base image");
433 let status = Command::new("curl")
433 let status = Command::new("curl")
434 .args([
434 .args([
435 "--continue-at",
435 "--continue-at",
@@ -470,7 +470,7 @@ fn ensure_base_image(
470 }
470 }
471 }
471 }
472
472
473− println!("Decompressing base image...");
473+ tracing::info!("decompressing base image");
474 let status = Command::new("tar")
474 let status = Command::new("tar")
475 .args([
475 .args([
476 "-xOf",
476 "-xOf",
@@ -499,7 +499,7 @@ fn ensure_default_image(
499
499
500 ensure_base_image(base_raw, base_compressed)?;
500 ensure_base_image(base_raw, base_compressed)?;
501
501
502− println!("Configuring base image...");
502+ tracing::info!("configuring base image");
503 fs::copy(base_raw, default_raw)?;
503 fs::copy(base_raw, default_raw)?;
504
504
505 let provision_command = script_command_from_content(PROVISION_SCRIPT_NAME, PROVISION_SCRIPT)?;
505 let provision_command = script_command_from_content(PROVISION_SCRIPT_NAME, PROVISION_SCRIPT)?;
@@ -522,7 +522,7 @@ fn ensure_instance_disk(
522 return Ok(());
522 return Ok(());
523 }
523 }
524
524
525− println!("Creating instance disk from {}...", template_raw.display());
525+ tracing::info!(path = %template_raw.display(), "creating instance disk");
526 std::fs::create_dir_all(instance_raw.parent().unwrap())?;
526 std::fs::create_dir_all(instance_raw.parent().unwrap())?;
527 fs::copy(template_raw, instance_raw)?;
527 fs::copy(template_raw, instance_raw)?;
528 Ok(())
528 Ok(())
@@ -961,7 +961,7 @@ fn spawn_login_actions_thread(
961 let command = match script_command_from_path(&path, index) {
961 let command = match script_command_from_path(&path, index) {
962 Ok(command) => command,
962 Ok(command) => command,
963 Err(err) => {
963 Err(err) => {
964− eprintln!("{err}");
964+ tracing::error!(error = %err, "failed to build login script command");
965 return;
965 return;
966 }
966 }
967 };
967 };
@@ -1042,7 +1042,7 @@ where
1042 return Err("Timed out waiting for VM to start".into());
1042 return Err("Timed out waiting for VM to start".into());
1043 }
1043 }
1044
1044
1045− println!("VM booting...");
1045+ tracing::info!("vm booting");
1046
1046
1047 let output_monitor = Arc::new(OutputMonitor::default());
1047 let output_monitor = Arc::new(OutputMonitor::default());
1048 let io_ctx = io_handler(output_monitor.clone(), we_read_from, we_write_to);
1048 let io_ctx = io_handler(output_monitor.clone(), we_read_from, we_write_to);
@@ -1111,7 +1111,7 @@ where
1111 unsafe {
1111 unsafe {
1112 if vm.canRequestStop() {
1112 if vm.canRequestStop() {
1113 if let Err(err) = vm.requestStopWithError() {
1113 if let Err(err) = vm.requestStopWithError() {
1114− eprintln!("Failed to request VM stop: {:?}", err);
1114+ tracing::error!(error = ?err, "failed to request VM stop");
1115 }
1115 }
1116 } else if vm.canStop() {
1116 } else if vm.canStop() {
1117 let handler = RcBlock::new(|_error: *mut NSError| {});
1117 let handler = RcBlock::new(|_error: *mut NSError| {});
@@ -1249,15 +1249,15 @@ pub fn ensure_signed() {
1249 match status {
1249 match status {
1250 Ok(s) if s.success() => {
1250 Ok(s) if s.success() => {
1251 let err = Command::new(&exe).args(std::env::args_os().skip(1)).exec();
1251 let err = Command::new(&exe).args(std::env::args_os().skip(1)).exec();
1252− eprintln!("failed to re-exec after signing: {err}");
1252+ tracing::error!(error = %err, "failed to re-exec after signing");
1253 std::process::exit(1);
1253 std::process::exit(1);
1254 }
1254 }
1255 Ok(s) => {
1255 Ok(s) => {
1256− eprintln!("codesign failed with status: {s}");
1256+ tracing::error!(status = %s, "codesign failed");
1257 std::process::exit(1);
1257 std::process::exit(1);
1258 }
1258 }
1259 Err(e) => {
1259 Err(e) => {
1260− eprintln!("failed to run codesign: {e}");
1260+ tracing::error!(error = %e, "failed to run codesign");
1261 std::process::exit(1);
1261 std::process::exit(1);
1262 }
1262 }
1263 }
1263 }
src/vm_manager.rs+2−2
@@ -17,7 +17,7 @@ use std::{
17
17
18use crate::{
18use crate::{
19 config::CONFIG_PATH_ENV,
19 config::CONFIG_PATH_ENV,
20− instance::SERIAL_LOG_NAME,
20+ instance::VM_ROOT_LOG_NAME,
21 instance::{
21 instance::{
22 DEFAULT_SSH_USER, InstanceConfig, build_ssh_login_actions, ensure_instance_dir,
22 DEFAULT_SSH_USER, InstanceConfig, build_ssh_login_actions, ensure_instance_dir,
23 ensure_ssh_keypair, extract_ipv4, load_or_create_instance_config, write_instance_config,
23 ensure_ssh_keypair, extract_ipv4, load_or_create_instance_config, write_instance_config,
@@ -451,7 +451,7 @@ fn spawn_manager_io(
451 vm_output_fd: std::os::unix::io::OwnedFd,
451 vm_output_fd: std::os::unix::io::OwnedFd,
452 vm_input_fd: std::os::unix::io::OwnedFd,
452 vm_input_fd: std::os::unix::io::OwnedFd,
453) -> vm::IoContext {
453) -> vm::IoContext {
454− let log_path = instance_dir.join(SERIAL_LOG_NAME);
454+ let log_path = instance_dir.join(VM_ROOT_LOG_NAME);
455 let log_file = fs::OpenOptions::new()
455 let log_file = fs::OpenOptions::new()
456 .create(true)
456 .create(true)
457 .write(true)
457 .write(true)