oc2_hlapi_v2 0.2.0

A client for the new version of OpenComputers II's HLAPI
Documentation
use std::fs::OpenOptions;
use std::{cell::Cell, os::fd::AsFd, time::Duration};
use std::{
    fmt::Debug,
    fs::{self, File, read_dir},
    io::{self, Read, Write},
    os::unix::net::UnixStream,
    path::{Path, PathBuf},
};

use nix::{
    fcntl::{FcntlArg, OFlag, fcntl},
    poll::{PollFd, PollFlags, PollTimeout, poll},
};
use thiserror::Error;

use crate::Stream;
use crate::transports::BusdStream;
use crate::{Transport, codecs::BaseStreams};

/// A file that can have timeouts set for read operations.
///
/// **This type is not meant for general-purpose use!**
/// It is exposed only because it is the returned stream type of the [`LinuxVirtio`] transport.
#[derive(Debug)]
pub struct TimeoutFile {
    file: File,
    timeout: Cell<Option<Duration>>,
}

impl std::ops::DerefMut for TimeoutFile {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.file
    }
}

impl std::ops::Deref for TimeoutFile {
    type Target = File;

    fn deref(&self) -> &Self::Target {
        &self.file
    }
}

impl From<File> for TimeoutFile {
    fn from(file: File) -> Self {
        Self {
            file,
            timeout: Cell::new(None),
        }
    }
}

impl Read for TimeoutFile {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let timeout = self.timeout.get();
        if let Some(timeout) = timeout {
            let pollfd = PollFd::new(self.as_fd(), PollFlags::POLLIN);
            let timeout = PollTimeout::try_from(timeout).unwrap_or(PollTimeout::MAX);
            if poll(&mut [pollfd], timeout)? == 0 {
                return Err(io::Error::new(
                    io::ErrorKind::TimedOut,
                    "File read timed out",
                ));
            }
        }
        self.file.read(buf)
    }
}

impl Write for TimeoutFile {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.file.write(buf)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.file.flush()
    }
}

impl Stream for TimeoutFile {
    fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
        let mut flags = OFlag::from_bits_retain(fcntl(&**self, FcntlArg::F_GETFL)?);

        if nonblocking {
            flags |= OFlag::O_NONBLOCK;
        } else {
            flags &= !OFlag::O_NONBLOCK;
        }

        fcntl(&**self, FcntlArg::F_SETFL(flags))?;

        Ok(())
    }

    fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
        if timeout == Some(Duration::ZERO) {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "zero timeout passed to set_read_timeout",
            ));
        }
        self.timeout.set(timeout);
        Ok(())
    }
}

/// The Linux virtio transport
///
/// This transport connects directly to the virtio console ports that the HLAPI is provided on,
/// using Linux device files. Other programs cannot access the bus if this transport is used,
/// however latency will be lower.
///
/// To use this transport, the bus multiplexer daemon *cannot* be running! In addition, by default
/// your program must run as `root`, as the device files can normally only be accessed by `root`.
#[derive(Clone, Debug)]
pub struct LinuxVirtio {
    rpc: PathBuf,
    blob: PathBuf,
    event: PathBuf,
}

#[derive(Debug, Error)]
/// Possible errors returned by [`LinuxVirtio::new`]
pub enum NewVirtioError {
    /// I/O error
    #[error("I/O error: {0}")]
    Io(#[from] io::Error),
    /// Missing virtio ports
    #[error("Some or all virtio ports not found")]
    NoPorts,
}

impl LinuxVirtio {
    /// Creates a new virtio transport.
    ///
    /// Doing so requires scanning `/sys/class/virtio-ports` to know the correct device paths to use,
    /// so this function is thus fallible.
    pub fn new() -> Result<Self, NewVirtioError> {
        let mut rpc = None;
        let mut blob = None;
        let mut event = None;
        for port in read_dir("/sys/class/virtio-ports")? {
            let port = port?;
            let name = fs::read_to_string(port.path().join("name"))?
                .trim_end()
                .to_owned();
            if name == "oc2.rpc.0" {
                rpc = Some(Path::new("/dev/").join(port.file_name()));
            } else if name == "oc2.blob.0" {
                blob = Some(Path::new("/dev/").join(port.file_name()));
            } else if name == "oc2.event.0" {
                event = Some(Path::new("/dev/").join(port.file_name()));
            }
        }
        if let Some(rpc) = rpc
            && let Some(blob) = blob
            && let Some(event) = event
        {
            Ok(Self { rpc, blob, event })
        } else {
            Err(NewVirtioError::NoPorts)
        }
    }
}

impl Transport for LinuxVirtio {
    type Streams = BaseStreams<TimeoutFile>;
    type Error = io::Error;
    fn open_streams(&mut self) -> Result<Self::Streams, Self::Error> {
        let mut open_opts = OpenOptions::new();
        open_opts.read(true);
        open_opts.write(true);
        // These two might not matter for device files? Still, setting them in case...
        open_opts.append(true);
        open_opts.truncate(false);
        Ok(BaseStreams {
            rpc: open_opts.open(&self.rpc)?.into(),
            blob: open_opts.open(&self.blob)?.into(),
            event: open_opts.open(&self.event)?.into(),
        })
    }
}

/// The Unix socket transport for the OC2 bus multiplexer daemon client
///
/// This is the *only* transport the daemon natively provides, other transports would require a
/// custom daemon or a proxy between the Unix socket and said other transport.
///
/// To use this transport with the HLAPI client, it must first be wrapped with a [`BusdClient`]
/// to handle the connection multiplexing.
///
/// [`BusdClient`]: crate::transports::BusdClient
#[derive(Clone, Debug)]
pub struct BusdUnixSocket {
    path: PathBuf,
}

impl BusdUnixSocket {
    /// The default path to the bus daemon's socket
    pub const DEFAULT_PATH: &str = "/run/oc2/bus";

    #[must_use]
    /// Create a new transport, autodetecting the path.
    ///
    /// If `$OC2_BUS_SOCKET` is set, use that, otherwise use the default path.
    pub fn new() -> Self {
        if let Ok(path) = std::env::var("OC2_BUS_SOCKET") {
            Self::new_with_path(path)
        } else {
            Self::new_with_path(Self::DEFAULT_PATH)
        }
    }

    #[must_use]
    /// Create a new transport, manually specifying the path.
    pub fn new_with_path<P: Into<PathBuf>>(path: P) -> Self {
        Self { path: path.into() }
    }
}

impl Default for BusdUnixSocket {
    fn default() -> Self {
        Self::new()
    }
}

impl Transport for BusdUnixSocket {
    type Streams = BusdStream<UnixStream>;
    type Error = io::Error;
    fn open_streams(&mut self) -> Result<Self::Streams, Self::Error> {
        Ok(BusdStream(UnixStream::connect(&self.path)?))
    }
}