Shell-Benzeri Child Process'i Rust altinda kullanirken problem

Rust ile basit bir Minecraft sunucu launcher yaziyorum: launcher.rs · GitHub

Kodun tumu (gist'in aynisi)
use std::process::{Command, exit};
use std::path::Path;
use std::thread;
use std::fs;
use log::{info, debug, warn, error, LevelFilter};
use std::sync::{Arc, Mutex};
use std::io::BufReader;
use std::io::BufRead;
use std::sync::mpsc::Sender;
use std::io::Write;


#[derive(PartialEq)]
pub enum ServerState {
    RUNNING,
    STOPPED,
    CRASHED,
}


pub struct ServerLauncher {
    jarfile: String,
    java_path: String,
    server_dir: String,
    server_args: Vec<String>,
    server_name: String,
    memory: i32,
    pub state: ServerState,
    process: Option<Arc<Mutex<std::process::Child>>>,
    log_stream_sender: Arc<Sender<String>>,
}

impl ServerLauncher {
    pub fn new(jarfile: String, java_path: String, server_dir: String, server_args: Vec<String>, server_name: String, memory: i32, log_stream_sender: Arc<Sender<String>>) -> ServerLauncher {
        ServerLauncher {
            jarfile,
            java_path,
            server_dir,
            server_args,
            server_name,
            memory,
            state: ServerState::STOPPED,
            process: None,
            log_stream_sender,
        }
    }

    pub fn launch(&mut self) {
        info!("Launching {}...", self.server_name);

        let mut cmd = Command::new(&self.java_path);

        self.check_server_dir();
        cmd.current_dir(&self.server_dir);

        cmd.arg(format!("-Xmx{}M", self.memory));
        cmd.arg("-jar");
        cmd.arg(&self.jarfile);
        for arg in &self.server_args {
            cmd.arg(arg);
        }
        cmd.arg("-nogui");
        cmd.stdin(std::process::Stdio::piped());
        cmd.stdout(std::process::Stdio::piped());
        cmd.stderr(std::process::Stdio::piped());

        debug!("Generated command: {:?}", cmd);

        self.state = ServerState::RUNNING;

        let process = cmd.spawn().expect("Failed to launch server");
        info!("{} launched with PID {}", self.server_name, process.id());

        let process = Arc::new(Mutex::new(process));
        self.process = Some(Arc::clone(&process));

        let process_clone = Arc::clone(&process);
        let server_name = self.server_name.clone();
        let sender_clone = self.log_stream_sender.clone();
        thread::spawn(move || {
            let output = process_clone.lock().unwrap().stdout.take().expect("Failed to capture stdout");
            let reader = BufReader::new(output);
            for line in reader.lines() {
                let line = line.expect("Failed to read line");
                sender_clone.send(line).expect("Failed to send line to GUI");
            }
        });

        let process_clone = Arc::clone(&process);
        let server_name = self.server_name.clone();
        thread::spawn(move || {
            let output = process_clone.lock().unwrap().wait().expect("Failed to wait on child");

            if output.success() {
                info!("{} has stopped with code 0.", server_name);
            } else {
                warn!("{} has crashed with code {}.", server_name, output.code().expect("Failed to get the exit code"));
            }
        });
    }

    pub fn stop(&mut self) {
        info!("Stopping {}...", self.server_name);

        if let Some(process) = &self.process {
            process.lock().unwrap().kill().expect("Failed to kill server");
            self.state = ServerState::STOPPED;
        } else {
            error!("Failed to stop server: no process found");
        }
    }

    pub fn send_command(&mut self, command: String) {
        if let Some(process) = &self.process {
            let mut stdin: std::process::ChildStdin = process.lock().unwrap().stdin.take().expect("Failed to capture stdin");
            stdin.write_all(command.as_bytes()).expect("Failed to write to stdin");
        } else {
            error!("Failed to send command to server: no process found");
        }
    }

    fn check_server_dir(&self) {
        if Path::new(&self.server_dir).exists() {
            info!("Server directory {} exists.", self.server_dir);
        } else {
            info!("Server directory {} does not exist. Creating the folder.", self.server_dir);
            fs::create_dir_all(&self.server_dir).expect("Failed to create server directory");
        }
    }
}

Sorun yasadigim yer surasi:

    pub fn send_command(&mut self, command: String) {
        if let Some(process) = &self.process {
            let mut stdin: std::process::ChildStdin = process.lock().unwrap().stdin.take().expect("Failed to capture stdin");
            stdin.write_all(command.as_bytes()).expect("Failed to write to stdin");
        } else {
            error!("Failed to send command to server: no process found");
        }
    }

Minecraft sunucusu (su an kullandigim papermc) process’i konsoldan komut alabiliyor. Basitce yapmak istedigim sey bir insanin konsola komut yazip enter’a basmasini Rust’ta cagirdigim child-process ile yapmak.

Python’da benzer seyi basitce yapmistim birkac gun once. Onu da birakayim gist ile: launcher.py · GitHub

Sorun ise send_command cagirildiginda tum program donup kaliyor. Ilk basta write_all, process’in stdin’i okumasini bekledigi icin boyle oluyor diye dusunuyordum ama alakasi yok. Loglardan yuklendigini anliyorum sunucunun, ki o durumda da stdin’i okuyor oluyor.

Aslinda zorlansam da cok karsilasiyorum boyle seylerle, bir sekilde hallediyorum hepsini. Bu problemi de hallederim diye dusunuyorum. Ama buraya katki saglamanin tek yolunun soru cevaplamak olmadigini dusundum. (Kalitesiz soru cevaplamak da pek eglenceli degil acikcasi zaten)

Kolay gelsin, iyi forumlar

3 Beğeni

Ben direk “Failed to capture stdin” panigi aliyorum. Acaba donup kalma stack unwind’in uzun surmesi filan mi (Child’i durdurmaya calisiyorsa, veya durmasini bekliyorsa)?

Neyse, sebebi de take’in Option’in icini bosaltmasi. Tekrar kullanmak istiyorsak stdin’i iceri geri koymak lazim. Veya:

    pub fn send_command(&mut self, command: String) {
        if let Some(process) = &self.process {
//            let mut stdin: std::process::ChildStdin = process.lock().unwrap().stdin.take().expect("Failed to capture stdin");
//            stdin.write_all(command.as_bytes()).expect("Failed to write to stdin");
            process.lock().unwrap().stdin.as_mut().map_or_else(|| {
                panic!("Failed to capture stdin"); // Eski koda benzesin diye
            }, |stdin| {
                stdin.write_all(command.as_bytes()).expect("Failed to write to stdin");
            });
        } else {
            error!("Failed to send command to server: no process found");
        }
    }

main de soyle bu arada:

fn main() {
    let (sender, receiver) = std::sync::mpsc::channel();
    thread::spawn(move|| {
        loop {
            let val = receiver.recv().unwrap();
            println!("R: {:?}", val);
        }
    });

    let mut sl = ServerLauncher::new(String::from(""), String::from("cat"), String::from("/tmp/foobar"), vec![], String::from("server_name"), 32, Arc::new(sender));
    sl.launch();
    sl.send_command(String::from("ohai\n"));
    sl.send_command(String::from("ohai2\n"));

    std::thread::sleep(std::time::Duration::from_secs(60));
}

Bu arada Some(process) check’lerini elimine etmek ve kodu daha temiz bir hale getirmek icin typestate pattern’ini kullanmayi dusunebilirsin. (Bir ismi daha vardi fakat su an aklima gelmiyor)

Basitce, sinifi ikiye Option noktasindan ikiye ayiriyoruz:

ServerLauncher → ServerLauncher, LaunchedServer

Ilkine process’in None oldugu fonksiyonlari ve Child’i calistirmak icin gereken seyleri koyuyoruz. Ikincisinde process opsiyonel degil.
Bi toplantiya firlamam lazim, detayli yazamiyorum, ama sey dusun:

ServerLauncher { jar, args, …, new(…), launch(…), check_server_dir(…) }
LaunchedServer { process: Child, stop(mut self), send_command(&mut self, command: AsRef), … }

2 Beğeni

Tum projeyi Github’a atip paylasayim, denemesi daha kolay olur: GitHub - reo6/rsxn: Tiny Minecraft server launcher

Once de atardim aslinda da ilk commit’leri olabildigi kadar gec yapmaya calisiyordum, erken donemde kod surekli degisiyor, her birine commit zor oluyo.

Benim de aklima ilk o geldi, cogunlukla internetteki ornek kodlar ve kutuphane, devamli calisan shell-like process’ler ile degil de; input alip, isleyip aninda stdout’a veren basit programlarla calisabilmeleri yonelimiyle yazilmis gibi duruyorlar.

Builder pattern, belki?

Soyle tekrar yazmayi denedim:

use std::process::{Command, Child};
use std::path::Path;
use std::fs;
use std::io::Write;
use std::sync::{Arc, Mutex};
use std::thread;
use std::io::BufReader;
use std::io::BufRead;
use std::sync::mpsc::Sender;

pub struct ServerLauncher {
    jarfile: String,
    java_path: String,
    server_dir: String,
    server_args: Vec<String>,
    server_name: String,
    memory: i32,
    log_stream_sender: Arc<Sender<String>>,
}

impl ServerLauncher {
    pub fn new(jarfile: String, java_path: String, server_dir: String, server_args: Vec<String>, server_name: String, memory: i32, log_stream_sender: Arc<Sender<String>>) -> ServerLauncher {
        ServerLauncher {
            jarfile,
            java_path,
            server_dir,
            server_args,
            server_name,
            memory,
            log_stream_sender,
        }
    }

    pub fn launch(self) -> LaunchedServer {
        self.check_server_dir();
        let mut cmd = Command::new(&self.java_path);
        cmd.current_dir(&self.server_dir);
        cmd.arg(format!("-Xmx{}M", self.memory));
        cmd.arg("-jar");
        cmd.arg(&self.jarfile);
        for arg in &self.server_args {
            cmd.arg(arg);
        }
        cmd.arg("-nogui");
        cmd.stdin(std::process::Stdio::piped());
        cmd.stdout(std::process::Stdio::piped());
        cmd.stderr(std::process::Stdio::piped());

        let process = cmd.spawn().expect("Failed to launch server");
        let process = Arc::new(Mutex::new(process));

        LaunchedServer {
            process,
            server_name: self.server_name,
            log_stream_sender: self.log_stream_sender,
        }
    }

    fn check_server_dir(&self) {
        if Path::new(&self.server_dir).exists() {
            println!("Server directory {} exists.", self.server_dir);
        } else {
            println!("Server directory {} does not exist. Creating the folder.", self.server_dir);
            fs::create_dir_all(&self.server_dir).expect("Failed to create server directory");
        }
    }
}

pub struct LaunchedServer {
    process: Arc<Mutex<Child>>,
    server_name: String,
    log_stream_sender: Arc<Sender<String>>,
}

impl LaunchedServer {
    pub fn stop(self) {
        self.process.lock().unwrap().kill().expect("Failed to kill server");
    }

    pub fn send_command(&mut self, command: String) {
        self.process.lock().unwrap().stdin.as_mut().map_or_else(|| {
            panic!("Failed to capture stdin");
        }, |stdin| {
            stdin.write_all(command.as_bytes()).expect("Failed to write to stdin");
        });
    }
}

Bu sistemin cikarttigi ilk sorun GUI kisimlariyla uyumluluk, kod boyle olursa surayi nasil yapmam gerek mesela?:

pub struct RsxnGUI {
    command_input: String,
    log_stream_receiver: Receiver<String>,
    logs: Vec<String>,
    launcher: Arc<Mutex<ServerLauncher>>, // Burasi
}

Bir de LauncherState’i ne yapacagimi da bilemedim, ikisine de (ServerLauncher ve LaunchedServer) mi eklemem lazim?


Buna stdin flush’i ekleyip oyle de denedim, ama yok, yine freeze oluyor.

Birkac tane thread var, deadlock falan mi oluyor acaba thread’ler Mutex’ten dolayi?

Evet, builder bunu iceriyor. Tam ayni degiller ama. Builder’in en guzel ornegi launcher.rs’te (cmd.*)

Simdi arastirinca State Design Pattern cikti ama aciklamalari cok abstract, cok enterprise coder odakli. Sade ornekle baska bir isim veren bir blog post okumustum zamaninda da, bulamayacagim galiba.

Ama evet, tam olarak ServerLauncher / LaunchedServer seklinde.

State’i GUI’de gosterip kontrol etmek istedigin icin, bu ayrimi yapmanin avantaji kayboluyor sanirim, eski koda donmek lazim.

Veya belki soyle bir sey olabilir:

enum ServerState {
    NotLauncher(ServerLauncher),
    Launched(LaunchedServer),
}

Ama muhtemelen eskisinden de karmasik olur boyle.

Ornek kod her zaman “Failed to capture stdin” veriyor, nedenini bulamadim daha.

Bu arada:

commit 5ffa7652f7e57595289d98a937432ea6cee914ba
Author: aib <aibok42@gmail.com>
Date:   Tue Apr 2 13:14:50 2024 +0200

    Add a test program mocking a server to be launched

diff --git a/test_program/.gitignore b/test_program/.gitignore
new file mode 100644
index 0000000..8e9e794
--- /dev/null
+++ b/test_program/.gitignore
@@ -0,0 +1,2 @@
+*.class
+*.jar
diff --git a/test_program/Makefile b/test_program/Makefile
new file mode 100644
index 0000000..951b632
--- /dev/null
+++ b/test_program/Makefile
@@ -0,0 +1,12 @@
+.PHONY: all
+all: TestProgram.jar
+
+TestProgram.jar: TestProgram.class
+	jar cfe TestProgram.jar TestProgram TestProgram.class
+
+%.class: %.java
+	javac $<
+
+.PHONY: clean
+clean:
+	$(RM) *.class TestProgram.jar
diff --git a/test_program/TestProgram.java b/test_program/TestProgram.java
new file mode 100644
index 0000000..5c2ef87
--- /dev/null
+++ b/test_program/TestProgram.java
@@ -0,0 +1,19 @@
+public class TestProgram {
+	public static void main(String[] args) {
+		System.out.print("Test program args: ");
+		for (String arg: args) {
+			System.out.print(arg + " ");
+		}
+		System.out.println();
+
+		try (var reader = new java.io.BufferedReader(new java.io.InputStreamReader(System.in))) {
+			String line;
+			while ((line = reader.readLine()) != null) {
+				System.out.println(line);
+			}
+		} catch (Exception e) {
+			System.err.println("Error reading from stdin: " + e.getMessage());
+			e.printStackTrace();
+		}
+	}
+}

Sorunu da buldum: Child in std::process - Rust
Ama niye sende deadlock ediyor bilmiyorum.

1 Beğeni

Donma olayini hallettim:

Sunucunun kapanmasini bekleyen thread wait kullandigi icin mutex’i lockluyormus. try_wait kullandim, su an kilitlemiyor.

Test icin Java kodu yazmak cok iyi fikirmis. O test koduyla neden failed to capture stdin aldiginizi anlamadim ama, ben test icin PaperMC jar dosyalarini kullaniyorum. (Butun minecraft sunuculari ayni minecraft.jar tabanli bu arada. Onlarin koduyla test kodunun farki ne acaba…)

Su anda donma sorunu yok ama sunucu girdigim input’u almiyor gibi gozukuyor:

rsxn

Normalde minecraft sunucusuna boyle komutlar yazinca “oyuncu bulunamadi” falan gibi seyler yaziyor stdout’ta. Bu input’u alamiyor gibi.

EDIT: onu da cozdum, sonunda \n eklemeyi unutmusum :man_facepalming: : Fix send_command to append newline character · reo6/rsxn@bd84480 · GitHub

Su anda launcher’da sorun kalmadi!

UI icin egui kullaniyordum, onda hala ufak tefek sorunlar var, hallederim hepsini yavas yavas.

Yine sorun olursa bu basligi kullanirim.

Yardim icin tesekkurler :heart:

3 Beğeni