Skip to main content

horto_os_ui_shared/remote/runner/
reboot.rs

1//! Remote box reboot helpers.
2
3use super::super::process::{ProcessRunner, StdioMode};
4use super::super::ssh::SshSession;
5use super::options::{remote_progress, session_from, RemoteOptions};
6use crate::error::Result;
7
8/// Issue `sudo reboot` on the box (CLI: password on the terminal via Inherit).
9///
10/// # Errors
11///
12/// Returns [`crate::HortoError`] when SSH fails before reboot starts.
13pub fn remote_reboot(runner: &dyn ProcessRunner, opts: &RemoteOptions) -> Result<()> {
14    let session = session_from(opts)?;
15    finish_remote_reboot(runner, &session, "y", None)
16}
17
18/// Issue `sudo reboot` using a sudo password already collected by the UI.
19///
20/// Feeds `sudo -S` over captured SSH. Used by the TUI (no cooked TTY, no askpass).
21///
22/// # Errors
23///
24/// Returns [`crate::HortoError`] when SSH fails before reboot starts.
25pub fn remote_reboot_with_sudo_password(
26    runner: &dyn ProcessRunner,
27    opts: &RemoteOptions,
28    sudo_password: &str,
29) -> Result<()> {
30    let session = session_from(opts)?;
31    finish_remote_reboot(runner, &session, "y", Some(sudo_password))
32}
33
34/// Whether a reboot prompt answer means reboot now.
35#[must_use]
36pub fn wants_reboot_now(raw: &str) -> bool {
37    matches!(raw.trim().to_ascii_lowercase().as_str(), "y" | "yes")
38}
39
40/// Prompt on a local TTY, then `sudo reboot` on the box. Non-TTY prints a reminder.
41pub fn offer_remote_reboot(runner: &dyn ProcessRunner, session: &SshSession) -> Result<()> {
42    use std::io::{self, IsTerminal, Write};
43
44    if !io::stdin().is_terminal() {
45        eprintln!("Reboot recommended for hostname/network changes. On the box: sudo reboot");
46        return Ok(());
47    }
48    eprint!("Reboot the box now to apply hostname/network changes? [y/N]: ");
49    let _ = io::stderr().flush();
50    let mut line = String::new();
51    io::stdin()
52        .read_line(&mut line)
53        .map_err(|e| crate::error::HortoError::msg(format!("read reboot prompt: {e}")))?;
54    finish_remote_reboot(runner, session, &line, None)
55}
56
57pub fn finish_remote_reboot(
58    runner: &dyn ProcessRunner,
59    session: &SshSession,
60    answer: &str,
61    sudo_password: Option<&str>,
62) -> Result<()> {
63    if !wants_reboot_now(answer) {
64        eprintln!("Skipping reboot. Reboot the box later when convenient.");
65        return Ok(());
66    }
67    remote_progress(
68        &session.host.raw,
69        "reboot box (SSH + sudo; may ask password)",
70    );
71    eprintln!("Rebooting...");
72    let used_stdin = sudo_password.is_some();
73    let result = sudo_password.map_or_else(
74        || session.exec(runner, "sudo reboot", StdioMode::Inherit),
75        |pass| {
76            let mut feed = String::with_capacity(pass.len() + 1);
77            feed.push_str(pass);
78            feed.push('\n');
79            let out = session.exec_stdin_reboot(runner, "sudo -S reboot", feed.as_bytes());
80            feed.clear();
81            out
82        },
83    );
84    match result {
85        Ok(_) => Ok(()),
86        Err(e) => {
87            if used_stdin && !ssh_drop_after_reboot(&e) {
88                return Err(e);
89            }
90            // Host drop mid-session is expected once reboot starts.
91            eprintln!("reboot issued (SSH session closed is expected): {e}");
92            Ok(())
93        }
94    }
95}
96
97pub fn ssh_drop_after_reboot(err: &crate::error::HortoError) -> bool {
98    let s = err.to_string().to_ascii_lowercase();
99    s.contains("connection closed")
100        || s.contains("connection reset")
101        || s.contains("broken pipe")
102        || s.contains("exit 255")
103}