cobble_core/minecraft/launch/detached_launch/
unix.rs

1use super::GameProcess;
2use crate::error::{LaunchError, LaunchResult};
3use async_trait::async_trait;
4use fork::{fork, Fork};
5use ipc_channel::ipc::{bytes_channel, IpcBytesReceiver, IpcBytesSender, TryRecvError};
6use nix::sys::signal::{kill, Signal};
7use nix::sys::wait::waitpid;
8use nix::unistd::Pid;
9use parking_lot::Mutex;
10use std::process::{exit, Command};
11use std::sync::Arc;
12use std::thread::sleep;
13use std::time::Duration;
14use tokio::task;
15
16/// Handle to the game process.
17#[cfg_attr(doc_cfg, doc(cfg(unix)))]
18#[derive(Clone, Debug)]
19pub struct GameProcessHandle {
20    pid: Arc<Mutex<Option<Pid>>>,
21    stop: Arc<Mutex<Option<IpcBytesSender>>>,
22    stopped: Arc<Mutex<Option<IpcBytesReceiver>>>,
23}
24
25#[async_trait]
26impl GameProcess for GameProcessHandle {
27    /// Launches the game by forking the process.
28    /// Communication to the forked process works through IPC channels.
29    /// The handle should at some point be waited to avoid creating zombie processes.
30    fn launch(mut command: Command) -> LaunchResult<Self> {
31        let (stop_sender, stop_receiver) = bytes_channel()?;
32        let (stopped_sender, stopped_receiver) = bytes_channel()?;
33
34        match fork() {
35            Ok(Fork::Child) => {
36                let mut child = match command.spawn() {
37                    Ok(child) => child,
38                    Err(err) => {
39                        error!("{}", err);
40                        exit(1);
41                    }
42                };
43                let pid = Pid::from_raw(child.id() as i32);
44
45                loop {
46                    // Check if game is running
47                    match child.try_wait() {
48                        Ok(Some(status)) => {
49                            // Game exited
50                            match status.success() {
51                                true => debug!("Game exited successfully"),
52                                false => warn!("Game exited with status '{:?}'", status.code()),
53                            }
54
55                            // Notify parent that game has exited
56                            if let Err(err) = stopped_sender.send(&[]) {
57                                error!("{}", err);
58                                exit(1);
59                            }
60
61                            exit(0);
62                        }
63                        Err(err) => {
64                            error!("Could not check if game is still running: {}", err);
65                            exit(1);
66                        }
67                        _ => {}
68                    }
69
70                    // Check if game was stopped
71                    match stop_receiver.try_recv() {
72                        Ok(_) => {
73                            // Stopped
74                            let signal = Signal::SIGINT;
75                            if let Err(err) = kill(pid, signal) {
76                                error!("{}", err);
77                                exit(1);
78                            }
79                        }
80                        Err(TryRecvError::Empty) => {}
81                        Err(err) => {
82                            error!("{}", err);
83                            exit(1);
84                        }
85                    }
86
87                    sleep(Duration::from_millis(500));
88                }
89            }
90            Ok(Fork::Parent(pid)) => {
91                debug!("Child process forked with PID {}", pid);
92                Ok(Self {
93                    pid: Arc::new(Mutex::new(Some(Pid::from_raw(pid)))),
94                    stop: Arc::new(Mutex::new(Some(stop_sender))),
95                    stopped: Arc::new(Mutex::new(Some(stopped_receiver))),
96                })
97            }
98            Err(err) => {
99                error!("{}", err);
100                Err(LaunchError::ProcessForking)
101            }
102        }
103    }
104
105    /// Sends a shutdown signal to the forked process.
106    /// After this call the process should be waited to reap the zombie process.
107    async fn stop(&self) -> LaunchResult<()> {
108        let sender = self.stop.clone();
109        task::spawn_blocking(move || {
110            let sender = sender.lock();
111            if let Some(sender) = sender.as_ref() {
112                sender.send(&[])?;
113            }
114            Ok(())
115        })
116        .await
117        .unwrap()
118    }
119
120    /// Waits for the process to exit.
121    /// This allows the OS to reap the created zombie process.
122    async fn wait(&self) -> LaunchResult<()> {
123        let handle = Self {
124            pid: self.pid.clone(),
125            stop: self.stop.clone(),
126            stopped: self.stopped.clone(),
127        };
128
129        task::spawn_blocking(move || {
130            // Wait for stopped
131            loop {
132                if handle.is_stopped_blocking()? {
133                    break;
134                }
135
136                // Sleep
137                sleep(Duration::from_millis(500));
138            }
139
140            // Process exited
141            let mut pid = handle.pid.lock();
142            let mut stop = handle.stop.lock();
143            let mut stopped = handle.stopped.lock();
144
145            if let Some(pid) = pid.as_ref() {
146                waitpid(Some(*pid), None).map_err(|err| {
147                    error!("{}", err);
148                    LaunchError::WaitPid
149                })?;
150            }
151
152            *pid = None;
153            *stop = None;
154            *stopped = None;
155
156            Ok(())
157        })
158        .await
159        .unwrap()
160    }
161
162    /// Checks whether the game is still running.
163    async fn is_stopped(&self) -> LaunchResult<bool> {
164        let receiver = self.stopped.clone();
165        task::spawn_blocking(move || {
166            let receiver = receiver.lock();
167
168            match receiver.as_ref() {
169                Some(receiver) => match receiver.try_recv() {
170                    Ok(_) => Ok(true),
171                    Err(TryRecvError::Empty) => Ok(false),
172                    Err(TryRecvError::IpcError(err)) => Err(LaunchError::Ipc(err)),
173                },
174                None => Ok(true),
175            }
176        })
177        .await
178        .unwrap()
179    }
180
181    /// Checks whether the game is still running.
182    fn is_stopped_blocking(&self) -> LaunchResult<bool> {
183        let receiver = self.stopped.lock();
184
185        match receiver.as_ref() {
186            Some(receiver) => match receiver.try_recv() {
187                Ok(_) => Ok(true),
188                Err(TryRecvError::Empty) => Ok(false),
189                Err(TryRecvError::IpcError(err)) => Err(LaunchError::Ipc(err)),
190            },
191            None => Ok(true),
192        }
193    }
194}