1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! 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,
        }
    }
}