cobble-core 1.2.0

Library for managing, installing and launching Minecraft instances and more.
Documentation
use super::GameProcess;
use crate::error::{LaunchError, LaunchResult};
use async_trait::async_trait;
use fork::{fork, Fork};
use ipc_channel::ipc::{bytes_channel, IpcBytesReceiver, IpcBytesSender, TryRecvError};
use nix::sys::signal::{kill, Signal};
use nix::sys::wait::waitpid;
use nix::unistd::Pid;
use parking_lot::Mutex;
use std::process::{exit, Command};
use std::sync::Arc;
use std::thread::sleep;
use std::time::Duration;
use tokio::task;

/// Handle to the game process.
#[cfg_attr(doc_cfg, doc(cfg(unix)))]
#[derive(Clone, Debug)]
pub struct GameProcessHandle {
    pid: Arc<Mutex<Option<Pid>>>,
    stop: Arc<Mutex<Option<IpcBytesSender>>>,
    stopped: Arc<Mutex<Option<IpcBytesReceiver>>>,
}

#[async_trait]
impl GameProcess for GameProcessHandle {
    /// Launches the game by forking the process.
    /// Communication to the forked process works through IPC channels.
    /// The handle should at some point be waited to avoid creating zombie processes.
    fn launch(mut command: Command) -> LaunchResult<Self> {
        let (stop_sender, stop_receiver) = bytes_channel()?;
        let (stopped_sender, stopped_receiver) = bytes_channel()?;

        match fork() {
            Ok(Fork::Child) => {
                let mut child = match command.spawn() {
                    Ok(child) => child,
                    Err(err) => {
                        error!("{}", err);
                        exit(1);
                    }
                };
                let pid = Pid::from_raw(child.id() as i32);

                loop {
                    // Check if game is running
                    match child.try_wait() {
                        Ok(Some(status)) => {
                            // Game exited
                            match status.success() {
                                true => debug!("Game exited successfully"),
                                false => warn!("Game exited with status '{:?}'", status.code()),
                            }

                            // Notify parent that game has exited
                            if let Err(err) = stopped_sender.send(&[]) {
                                error!("{}", err);
                                exit(1);
                            }

                            exit(0);
                        }
                        Err(err) => {
                            error!("Could not check if game is still running: {}", err);
                            exit(1);
                        }
                        _ => {}
                    }

                    // Check if game was stopped
                    match stop_receiver.try_recv() {
                        Ok(_) => {
                            // Stopped
                            let signal = Signal::SIGINT;
                            if let Err(err) = kill(pid, signal) {
                                error!("{}", err);
                                exit(1);
                            }
                        }
                        Err(TryRecvError::Empty) => {}
                        Err(err) => {
                            error!("{}", err);
                            exit(1);
                        }
                    }

                    sleep(Duration::from_millis(500));
                }
            }
            Ok(Fork::Parent(pid)) => {
                debug!("Child process forked with PID {}", pid);
                Ok(Self {
                    pid: Arc::new(Mutex::new(Some(Pid::from_raw(pid)))),
                    stop: Arc::new(Mutex::new(Some(stop_sender))),
                    stopped: Arc::new(Mutex::new(Some(stopped_receiver))),
                })
            }
            Err(err) => {
                error!("{}", err);
                Err(LaunchError::ProcessForking)
            }
        }
    }

    /// Sends a shutdown signal to the forked process.
    /// After this call the process should be waited to reap the zombie process.
    async fn stop(&self) -> LaunchResult<()> {
        let sender = self.stop.clone();
        task::spawn_blocking(move || {
            let sender = sender.lock();
            if let Some(sender) = sender.as_ref() {
                sender.send(&[])?;
            }
            Ok(())
        })
        .await
        .unwrap()
    }

    /// Waits for the process to exit.
    /// This allows the OS to reap the created zombie process.
    async fn wait(&self) -> LaunchResult<()> {
        let handle = Self {
            pid: self.pid.clone(),
            stop: self.stop.clone(),
            stopped: self.stopped.clone(),
        };

        task::spawn_blocking(move || {
            // Wait for stopped
            loop {
                if handle.is_stopped_blocking()? {
                    break;
                }

                // Sleep
                sleep(Duration::from_millis(500));
            }

            // Process exited
            let mut pid = handle.pid.lock();
            let mut stop = handle.stop.lock();
            let mut stopped = handle.stopped.lock();

            if let Some(pid) = pid.as_ref() {
                waitpid(Some(*pid), None).map_err(|err| {
                    error!("{}", err);
                    LaunchError::WaitPid
                })?;
            }

            *pid = None;
            *stop = None;
            *stopped = None;

            Ok(())
        })
        .await
        .unwrap()
    }

    /// Checks whether the game is still running.
    async fn is_stopped(&self) -> LaunchResult<bool> {
        let receiver = self.stopped.clone();
        task::spawn_blocking(move || {
            let receiver = receiver.lock();

            match receiver.as_ref() {
                Some(receiver) => match receiver.try_recv() {
                    Ok(_) => Ok(true),
                    Err(TryRecvError::Empty) => Ok(false),
                    Err(TryRecvError::IpcError(err)) => Err(LaunchError::Ipc(err)),
                },
                None => Ok(true),
            }
        })
        .await
        .unwrap()
    }

    /// Checks whether the game is still running.
    fn is_stopped_blocking(&self) -> LaunchResult<bool> {
        let receiver = self.stopped.lock();

        match receiver.as_ref() {
            Some(receiver) => match receiver.try_recv() {
                Ok(_) => Ok(true),
                Err(TryRecvError::Empty) => Ok(false),
                Err(TryRecvError::IpcError(err)) => Err(LaunchError::Ipc(err)),
            },
            None => Ok(true),
        }
    }
}