sopht 0.3.0

cute program for managing long running processes in a (slightly) more sophisticated way than tmux
Documentation
//! sopht is a program for managing long running processes in a (slightly) more
//! sophisticated way than something like [tmux](https://github.com/tmux/tmux).
//! this library currently implements the IPC protocol sopht uses to pass
//! commands and responses between clients and a server, as well as the
//! machinery required for starting and managing child processes
//!
pub mod ipc;
pub mod manager;
pub mod process;
pub use ipc::{create_client_connection, socket_file_path, Connection};
pub use manager::{
    ChangeRestartPolicyArgs, ExitStatus, ProcessStatus, RestartPolicy,
    SendArgs, StartArgs, State,
};
pub use process::{Process, PID};
use serde::{Deserialize, Serialize};
use std::io;

/// specialized [`Result`](std::result::Result) type for return values from
/// sopht functions
pub type Result<T> = std::result::Result<T, Error>;

/// union of all errors resulting from sopht functions. this type implements
/// `Display` and `Error`, and is the error type of [`sopht::Result`](Result).
#[derive(Debug)]
pub enum Error {
    /// something went wrong with a socket. wraps [`io::Error`]
    SocketError(io::Error),
    /// failure to serialize data. wraps [`serde_json::Error`]
    SerializeError(serde_json::Error),
    /// failure to deserialize data. wraps [`serde_json::Error`]
    DeserializeError(serde_json::Error),
    /// the current user has no home directory, which is needed for the sopht
    /// socket
    NoHomeDir,
    /// the connection betwixt server and client was closed
    ConnectionClosed,
    /// the wrong kind of message was received
    WrongMessageKind,
    /// the supplied command string for starting a process is empty
    EmptyProgram,
    /// encountered an error while interacting with a child process
    ProcessError(io::Error),
    /// the PID supplied is invalid
    InvalidPID,
    /// the process did not stay alive for long enough to be considered started
    StayAliveError,
    /// error creating sopht directories
    DirectoryCreationError(io::Error),
}

/// represents a command to be executed by a server. this should never be
/// received by a client.
#[derive(PartialEq, Serialize, Deserialize, Debug)]
pub enum Command {
    /// greets the client. kinda like a ping command, but friendlier
    Hello,
    /// shuts down the server gracefully
    Shutdown,
    /// starts a process given a command string
    Start(StartArgs),
    /// requests the output of a process given its PID
    Output(PID),
    /// requests the status of all processes
    Status(Option<usize>),
    /// stops a process given its PID
    Stop(PID),
    /// stops, then starts a process again given its PID
    Restart(PID),
    /// send output to a process given its PID and a string
    Send(SendArgs),
    /// change the restart policy of a process given its PID and the new policy
    ChangeRestartPolicy(ChangeRestartPolicyArgs),
}

/// represents a response from the server to the client. this should never
/// be received by a server. additionally, this structure is subject to change
/// in the future as responses get more detailed
#[derive(Serialize, Deserialize, Debug)]
pub struct Response {
    /// whether or not the command succeeded
    pub success: bool,
    /// the message the command returned
    pub message: String,
    /// the server may optionally decide to send a list of process statuses to
    /// display to the user. this is not always the entire status!
    pub status: Option<Vec<ProcessStatus>>,
}

/// union of all different message types. currently the only two message types
/// are `Command` and `Response`. the `Command` variant should only ever be
/// sent by clients and received by servers, and the `Response` variant should
/// only ever be received by clients and sent by servers
#[derive(Serialize, Deserialize)]
pub enum Message {
    Command(Command),
    Response(Response),
}

/// attempts to create the local directories required for sopht
pub fn create_local_dirs() -> Result<()> {
    let fp = socket_file_path()?;
    match std::fs::create_dir_all(fp.parent().unwrap()) {
        Ok(_) => Ok(()),
        Err(e) => Err(Error::DirectoryCreationError(e)),
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::SocketError(e) => write!(f, "{}", e),
            Error::SerializeError(e) => write!(f, "{}", e),
            Error::DeserializeError(e) => {
                write!(f, "{}", e)
            }
            Error::NoHomeDir => write!(f, "no home directory found for user"),
            Error::ConnectionClosed => write!(f, "connection closed"),
            Error::WrongMessageKind => write!(f, "got wrong kind of message"),
            Error::EmptyProgram => write!(f, "empty program string"),
            Error::ProcessError(e) => write!(f, "{}", e),
            Error::InvalidPID => write!(f, "invalid PID"),
            Error::StayAliveError => {
                write!(f, "process did not stay alive long enough")
            }
            Error::DirectoryCreationError(e) => write!(f, "{}", e),
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Error::SocketError(e) => Some(e),
            Error::SerializeError(e) => Some(e),
            Error::DeserializeError(e) => Some(e),
            Error::ProcessError(e) => Some(e),
            Error::DirectoryCreationError(e) => Some(e),
            _ => None,
        }
    }
}