mcserver 0.1.10

A command line interface which simplifies minecraft server management with zellij and mcrcon
use crate::{
    error::{Error, Result},
    server::save_last_used_now,
    session,
};
use std::{
    collections::{HashMap, HashSet},
    ffi::OsStr,
    fmt::Display,
    io::{self, Read, Write},
    path::Path,
    process::{Command, Stdio},
    thread,
    time::Duration,
};

pub const BASE_COMMAND: &str = "zellij";
pub const SUFFIX: &str = ".mcserver";

pub fn get_name(server: impl Display) -> String {
    format!("{server}{SUFFIX}")
}

fn get_server_sessions_raw_string() -> Result<Option<String>> {
    let output = Command::new(BASE_COMMAND).arg("list-sessions").output()?;

    match output.status.code() {
        Some(0) => Ok(Some(String::from_utf8_lossy(&output.stdout).to_string())),
        Some(1) => Ok(None), // no sessions
        _ => Err(Error::CommandFailure {
            code: output.status.code(),
            stderr: Some(output.stderr),
        }),
    }
}

fn session_has_exited(session_line: impl AsRef<str>) -> bool {
    let session_line = session_line.as_ref();
    let bracket_pos = match session_line.rfind('(') {
        Some(pos) => pos,
        None => return false,
    };

    session_line[bracket_pos..].contains("EXITED") // if there is no "EXITED", still alive
}

fn session_is_alive(session_line: impl AsRef<str>) -> bool {
    !session_has_exited(session_line)
}

fn session_line_to_server(session_line: impl AsRef<str>) -> Option<String> {
    let session_line = session_line.as_ref();
    let session_name = match session_line.rfind("[Created") {
        Some(pos) => &session_line[7..=pos - 5],
        None => return None, // unexpected error
    };

    session_name.strip_suffix(session::SUFFIX).map(String::from)
}

pub fn get_alive_server_sessions() -> Result<HashSet<String>> {
    Ok(get_server_sessions_raw_string()?
        .map(|server_sessions| {
            server_sessions
                .lines()
                .filter(|sl| session_is_alive(sl))
                .filter_map(session_line_to_server)
                .collect()
        })
        .unwrap_or_default())
}

pub fn get_dead_server_sessions() -> Result<HashSet<String>> {
    Ok(get_server_sessions_raw_string()?
        .map(|server_sessions| {
            server_sessions
                .lines()
                .filter(|sl| session_has_exited(sl))
                .filter_map(session_line_to_server)
                .collect()
        })
        .unwrap_or_default())
}

pub fn get_server_sessions_to_living() -> Result<HashMap<String, bool>> {
    Ok(get_server_sessions_raw_string()?
        .map(|ss| {
            ss.lines()
                .map(|s| (s, session_is_alive(s)))
                .filter_map(|(session, living)| {
                    session_line_to_server(session).map(|server| (server, living))
                })
                .collect()
        })
        .unwrap_or_default())
}

pub fn attach(server: impl AsRef<str>) -> Result<()> {
    let server = server.as_ref();
    let mut child = Command::new(BASE_COMMAND)
        .arg("attach")
        .arg(get_name(server))
        .stderr(Stdio::piped())
        .spawn()?;

    let status = child.wait()?;

    if status.success() {
        save_last_used_now(server)
    } else {
        let mut buf = Vec::new();
        child
            .stderr
            .take()
            .ok_or(io::Error::new(
                io::ErrorKind::BrokenPipe,
                "Failed to take stderr pipe",
            ))?
            .read_to_end(&mut buf)?;

        Err(Error::CommandFailure {
            code: status.code(),
            stderr: Some(buf),
        })
    }
}

pub fn new_session<S, I>(session: S, initial_command: Option<I>) -> Result<()>
where
    S: AsRef<OsStr>,
    I: AsRef<OsStr>,
{
    Command::new(BASE_COMMAND)
        .arg("delete-session")
        .arg(&session)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()?;

    let mut command = Command::new(BASE_COMMAND);
    command.arg("--session").arg(&session);
    let mut child = command.spawn()?;

    thread::sleep(Duration::from_millis(300));

    if let Some(command) = initial_command {
        write_line(&session, command)?;
    }

    child.wait()?;

    Ok(())
}

pub fn new_server(
    server: impl Display + AsRef<Path>,
    initial_command: Option<impl AsRef<OsStr>>,
) -> Result<()> {
    save_last_used_now(&server)?;
    let session_name = get_name(&server);
    new_session(session_name, initial_command)?;
    save_last_used_now(&server)
}

pub fn delete_server_session(server: impl Display, force: bool) -> Result<()> {
    let mut command = Command::new(BASE_COMMAND);
    command.arg("delete-session");
    command.arg(format!("{server}{SUFFIX}"));

    if force {
        command.arg("--force");
    }

    command.status()?;
    Ok(())
}

pub fn delete_all() -> Result<()> {
    for session in get_dead_server_sessions()? {
        delete_server_session(session, false)?;
    }

    Ok(())
}

pub fn delete_all_confirmed() -> Result<()> {
    loop {
        print!("Delete all sessions? (y/n): ");
        io::stdout().flush()?;

        let mut confirmation = String::new();
        io::stdin().read_line(&mut confirmation)?;

        match confirmation.trim_end().to_lowercase().as_str() {
            "y" | "yes" => break delete_all()?,
            "n" | "no" => {
                println!("Operation canceled");
                break;
            }
            _ => {}
        };
    }

    Ok(())
}

fn session_write(
    session: impl AsRef<OsStr>,
    mode: &'static str,
    chars: impl AsRef<OsStr>,
) -> Result<()> {
    let status = Command::new(BASE_COMMAND)
        .arg("--session")
        .arg(session)
        .arg("action")
        .arg(mode)
        .arg(chars)
        .spawn()?
        .wait()?;

    if !status.success() {
        return Err(Error::CommandFailure {
            code: status.code(),
            stderr: None,
        });
    }

    Ok(())
}

pub fn write_chars(session: impl AsRef<OsStr>, chars: impl AsRef<OsStr>) -> Result<()> {
    session_write(session, "write-chars", chars)
}

pub fn write_line(session: impl AsRef<OsStr>, chars: impl AsRef<OsStr>) -> Result<()> {
    write_chars(&session, chars)?;
    session_write(&session, "write", "13")?; // 13 is for carriage return
    Ok(())
}