sopht 0.3.0

cute program for managing long running processes in a (slightly) more sophisticated way than tmux
Documentation
//! this module contains all the shared IPC logic agnostic to servers and
//! clients. in particular, it contains the [`Message`] type and its components
//! and the [`Connection`] type, which abstracts away sopht's messaging protocol
//!
use crate::{Command, Error, Message, Response, Result};
use directories::ProjectDirs;
use std::io::{BufRead, BufReader, ErrorKind, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;

/// structure containing the state of a connection to the sopht socket
pub struct Connection {
    /// the unix stream the connection is on
    stream: UnixStream,
    /// a second handle to the stream wrapped in a [`BufReader`] for the
    /// convenience method [`BufRead::read_until`]
    reader: BufReader<UnixStream>,
    /// internal buffer the connection uses to avoid allocating on every read
    buffer: Vec<u8>,
}

impl Connection {
    /// consumes a [`UnixStream`] and tries to create a [`Connection`]. this
    /// function calls [`try_clone`](UnixStream::try_clone) on the stream and,
    /// if this fails, returns a [`SocketError`](Error::SocketError). otherwise,
    /// the connection is successfully created
    pub fn new(stream: UnixStream) -> Result<Self> {
        let reader_stream = match stream.try_clone() {
            Ok(s) => s,
            Err(e) => return Err(Error::SocketError(e)),
        };

        let reader = BufReader::new(reader_stream);
        let buffer = Vec::new();

        Ok(Self {
            stream,
            reader,
            buffer,
        })
    }

    /// sends a [`Message`] over the connection. this function can fail if
    /// * the message fails to serialize
    ///   ([`SerializeError`](Error::SerializeError))
    /// * the connection is closed
    ///   ([`ConnectionClosed`](Error::ConnectionClosed))
    /// * there is some other internal error on the [`UnixStream`]
    ///   ([`SocketError`](Error::SocketError))
    pub fn send(&mut self, cmd: &Message) -> Result<()> {
        let data = match serde_json::to_vec(cmd) {
            Ok(d) => d,
            Err(e) => return Err(Error::SerializeError(e)),
        };

        let data = [data, vec![b'\n']].concat();

        match self.stream.write(&data) {
            Ok(n) => {
                if n == 0 {
                    return Err(Error::ConnectionClosed);
                }
            }
            Err(e) => return Err(Error::SocketError(e)),
        }

        match self.stream.flush() {
            Ok(_) => Ok(()),
            Err(e) => match e.kind() {
                ErrorKind::WriteZero => Err(Error::ConnectionClosed),
                _ => Err(Error::SocketError(e)),
            },
        }
    }

    /// attempts to receieve a [`Message`] over the connection. this function
    /// can fail if
    /// * the received message fails to deserialize
    ///   ([`SerializeError`](Error::SerializeError))
    /// * the connection is closed
    ///   ([`ConnectionClosed`](Error::ConnectionClosed))
    /// * there is some other internal error on the [`UnixStream`]
    ///   ([`SocketError`](Error::SocketError))
    pub fn recv(&mut self) -> Result<Message> {
        match self.reader.read_until(b'\n', &mut self.buffer) {
            Ok(n) => {
                if n == 0 {
                    return Err(Error::ConnectionClosed);
                };
            }
            Err(e) => {
                return Err(Error::SocketError(e));
            }
        }

        let cmd: Message = match serde_json::from_slice(&self.buffer) {
            Ok(cmd) => cmd,
            Err(e) => return Err(Error::DeserializeError(e)),
        };

        Ok(cmd)
    }

    /// helper function that internally calls [`recv`](Connection::recv), then
    /// attempts to pull a [`Command`] out of the [`Message`]. if the message is
    /// not a command, this function returns
    /// [`WrongMessageKind`](Error::WrongMessageKind)
    pub fn recv_command(&mut self) -> Result<Command> {
        let msg = self.recv()?;
        if let Message::Command(cmd) = msg {
            Ok(cmd)
        } else {
            Err(Error::WrongMessageKind)
        }
    }

    /// helper function that internally calls [`recv`](Connection::recv), then
    /// attempts to pull a [`Response`] out of the [`Message`]. if the message
    /// is not a command, this function returns
    /// [`WrongMessageKind`](Error::WrongMessageKind)
    pub fn recv_response(&mut self) -> Result<Response> {
        let msg = self.recv()?;
        if let Message::Response(cmd) = msg {
            Ok(cmd)
        } else {
            Err(Error::WrongMessageKind)
        }
    }
}

/// creates a [`UnixStream`] and attempts to build a [`Connection`]
/// out of it. if the stream cannot be created, this function returns
/// [`SocketError`](Error::SocketError)
pub fn create_client_connection() -> Result<Connection> {
    let fp = socket_file_path()?;

    let stream = match UnixStream::connect(fp) {
        Ok(stream) => stream,
        Err(e) => return Err(Error::SocketError(e)),
    };

    Connection::new(stream)
}

/// obtains the file path of the unix socket file on the local machine whether
/// it exists or not. this function can fail if the current user has no HOME
/// directory ([`NoHomeDir`](Error::NoHomeDir))
pub fn socket_file_path() -> Result<PathBuf> {
    let dirs = match ProjectDirs::from("", "", "sopht") {
        Some(d) => d,
        None => return Err(Error::NoHomeDir),
    };

    Ok(dirs.data_dir().join("sopht.sock"))
}