feat: run game stdout/stderr reading in parallel threads

This commit is contained in:
Nikita Podvirnyi 2024-07-21 08:59:06 +02:00
parent 048fad0e35
commit a67dae54f5
No known key found for this signature in database
GPG key ID: 859D416E5142AFF3
6 changed files with 537 additions and 384 deletions

View file

@ -1,6 +1,9 @@
use std::io::{Read, Write};
use std::process::{Command, Stdio};
use std::path::PathBuf;
use std::fs::File;
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicUsize, Ordering};
use anime_game_core::prelude::*;
use anime_game_core::genshin::telemetry;
@ -309,83 +312,108 @@ pub fn run() -> anyhow::Result<()> {
.spawn()?;
// Create new game.log file to log all the game output
let mut game_output = std::fs::File::create(consts::launcher_dir()?.join("game.log"))?;
let mut written = 0;
let game_output = Arc::new(Mutex::new(
File::create(consts::launcher_dir()?.join("game.log"))?
));
let written = Arc::new(AtomicUsize::new(0));
let mut stdout_join = None;
let mut stderr_join = None;
// Log process output while it's running
while child.try_wait()?.is_none() {
// Check if we've written less than a limit amount of data
if written < *consts::GAME_LOG_FILE_LIMIT {
// Redirect stdout to the game.log file
if let Some(stdout) = &mut child.stdout {
let mut buf = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
let game_output = game_output.clone();
let written = written.clone();
stdout.read_to_end(&mut buf)?;
stdout_join = Some(std::thread::spawn(move || -> std::io::Result<()> {
let mut buf = [0; 1024];
if !buf.is_empty() {
for line in buf.split(|c| c == &b'\n') {
while let Ok(read) = stdout.read(&mut buf) {
if read == 0 {
break;
}
let Ok(mut game_output) = game_output.lock() else {
break;
};
for line in buf[..read].split(|c| c == &b'\n') {
game_output.write_all(b" [stdout] ")?;
game_output.write_all(line)?;
game_output.write_all(b"\n")?;
written += line.len() + 14;
written.fetch_add(line.len() + 14, Ordering::Relaxed);
}
// buf can contain more data than the limit
if written > *consts::GAME_LOG_FILE_LIMIT {
if written.load(Ordering::Relaxed) > *consts::GAME_LOG_FILE_LIMIT {
break;
}
}
// Flush written lines
game_output.flush()?;
}
Ok(())
}));
}
// Redirect stderr to the game.log file
if let Some(stderr) = &mut child.stderr {
let mut buf = Vec::new();
if let Some(mut stderr) = child.stderr.take() {
let game_output = game_output.clone();
let written = written.clone();
stderr.read_to_end(&mut buf)?;
stderr_join = Some(std::thread::spawn(move || -> std::io::Result<()> {
let mut buf = [0; 1024];
if !buf.is_empty() {
for line in buf.split(|c| c == &b'\n') {
while let Ok(read) = stderr.read(&mut buf) {
if read == 0 {
break;
}
let Ok(mut game_output) = game_output.lock() else {
break;
};
for line in buf[..read].split(|c| c == &b'\n') {
game_output.write_all(b"[!] [stderr] ")?;
game_output.write_all(line)?;
game_output.write_all(b"\n")?;
written += line.len() + 14;
written.fetch_add(line.len() + 14, Ordering::Relaxed);
}
// buf can contain more data than the limit
if written > *consts::GAME_LOG_FILE_LIMIT {
if written.load(Ordering::Relaxed) > *consts::GAME_LOG_FILE_LIMIT {
break;
}
}
// Flush written lines
game_output.flush()?;
}
Ok(())
}));
}
// Drop stdio bufs if the limit was reached
if written >= *consts::GAME_LOG_FILE_LIMIT {
drop(child.stdout.take());
drop(child.stderr.take());
}
}
// Update discord RPC until the game process is closed
while child.try_wait()?.is_none() {
std::thread::sleep(std::time::Duration::from_secs(3));
#[cfg(feature = "discord-rpc")]
if let Some(rpc) = &rpc {
rpc.update(RpcUpdates::Update)?;
}
std::thread::sleep(std::time::Duration::from_secs(3));
}
// Flush and close the game log file
game_output.flush()?;
if let Ok(mut file) = game_output.lock() {
file.flush()?;
}
drop(game_output);
if let Some(join) = stdout_join {
join.join().map_err(|err| anyhow::anyhow!("Failed to join stdout reader thread: {err:?}"))??;
}
if let Some(join) = stderr_join {
join.join().map_err(|err| anyhow::anyhow!("Failed to join stderr reader thread: {err:?}"))??;
}
// Workaround for fast process closing (is it still a thing?)
loop {
std::thread::sleep(std::time::Duration::from_secs(3));

View file

@ -262,83 +262,108 @@ pub fn run() -> anyhow::Result<()> {
.spawn()?;
// Create new game.log file to log all the game output
let mut game_output = std::fs::File::create(consts::launcher_dir()?.join("game.log"))?;
let mut written = 0;
let game_output = Arc::new(Mutex::new(
File::create(consts::launcher_dir()?.join("game.log"))?
));
let written = Arc::new(AtomicUsize::new(0));
let mut stdout_join = None;
let mut stderr_join = None;
// Log process output while it's running
while child.try_wait()?.is_none() {
// Check if we've written less than a limit amount of data
if written < *consts::GAME_LOG_FILE_LIMIT {
// Redirect stdout to the game.log file
if let Some(stdout) = &mut child.stdout {
let mut buf = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
let game_output = game_output.clone();
let written = written.clone();
stdout.read_to_end(&mut buf)?;
stdout_join = Some(std::thread::spawn(move || -> std::io::Result<()> {
let mut buf = [0; 1024];
if !buf.is_empty() {
for line in buf.split(|c| c == &b'\n') {
while let Ok(read) = stdout.read(&mut buf) {
if read == 0 {
break;
}
let Ok(mut game_output) = game_output.lock() else {
break;
};
for line in buf[..read].split(|c| c == &b'\n') {
game_output.write_all(b" [stdout] ")?;
game_output.write_all(line)?;
game_output.write_all(b"\n")?;
written += line.len() + 14;
written.fetch_add(line.len() + 14, Ordering::Relaxed);
}
// buf can contain more data than the limit
if written > *consts::GAME_LOG_FILE_LIMIT {
if written.load(Ordering::Relaxed) > *consts::GAME_LOG_FILE_LIMIT {
break;
}
}
// Flush written lines
game_output.flush()?;
}
Ok(())
}));
}
// Redirect stderr to the game.log file
if let Some(stderr) = &mut child.stderr {
let mut buf = Vec::new();
if let Some(mut stderr) = child.stderr.take() {
let game_output = game_output.clone();
let written = written.clone();
stderr.read_to_end(&mut buf)?;
stderr_join = Some(std::thread::spawn(move || -> std::io::Result<()> {
let mut buf = [0; 1024];
if !buf.is_empty() {
for line in buf.split(|c| c == &b'\n') {
while let Ok(read) = stderr.read(&mut buf) {
if read == 0 {
break;
}
let Ok(mut game_output) = game_output.lock() else {
break;
};
for line in buf[..read].split(|c| c == &b'\n') {
game_output.write_all(b"[!] [stderr] ")?;
game_output.write_all(line)?;
game_output.write_all(b"\n")?;
written += line.len() + 14;
written.fetch_add(line.len() + 14, Ordering::Relaxed);
}
// buf can contain more data than the limit
if written > *consts::GAME_LOG_FILE_LIMIT {
if written.load(Ordering::Relaxed) > *consts::GAME_LOG_FILE_LIMIT {
break;
}
}
// Flush written lines
game_output.flush()?;
}
Ok(())
}));
}
// Drop stdio bufs if the limit was reached
if written >= *consts::GAME_LOG_FILE_LIMIT {
drop(child.stdout.take());
drop(child.stderr.take());
}
}
// Update discord RPC until the game process is closed
while child.try_wait()?.is_none() {
std::thread::sleep(std::time::Duration::from_secs(3));
#[cfg(feature = "discord-rpc")]
if let Some(rpc) = &rpc {
rpc.update(RpcUpdates::Update)?;
}
std::thread::sleep(std::time::Duration::from_secs(3));
}
// Flush and close the game log file
game_output.flush()?;
if let Ok(mut file) = game_output.lock() {
file.flush()?;
}
drop(game_output);
if let Some(join) = stdout_join {
join.join().map_err(|err| anyhow::anyhow!("Failed to join stdout reader thread: {err:?}"))??;
}
if let Some(join) = stderr_join {
join.join().map_err(|err| anyhow::anyhow!("Failed to join stderr reader thread: {err:?}"))??;
}
// Workaround for fast process closing (is it still a thing?)
loop {
std::thread::sleep(std::time::Duration::from_secs(3));

View file

@ -252,83 +252,108 @@ pub fn run() -> anyhow::Result<()> {
.spawn()?;
// Create new game.log file to log all the game output
let mut game_output = std::fs::File::create(consts::launcher_dir()?.join("game.log"))?;
let mut written = 0;
let game_output = Arc::new(Mutex::new(
File::create(consts::launcher_dir()?.join("game.log"))?
));
let written = Arc::new(AtomicUsize::new(0));
let mut stdout_join = None;
let mut stderr_join = None;
// Log process output while it's running
while child.try_wait()?.is_none() {
// Check if we've written less than a limit amount of data
if written < *consts::GAME_LOG_FILE_LIMIT {
// Redirect stdout to the game.log file
if let Some(stdout) = &mut child.stdout {
let mut buf = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
let game_output = game_output.clone();
let written = written.clone();
stdout.read_to_end(&mut buf)?;
stdout_join = Some(std::thread::spawn(move || -> std::io::Result<()> {
let mut buf = [0; 1024];
if !buf.is_empty() {
for line in buf.split(|c| c == &b'\n') {
while let Ok(read) = stdout.read(&mut buf) {
if read == 0 {
break;
}
let Ok(mut game_output) = game_output.lock() else {
break;
};
for line in buf[..read].split(|c| c == &b'\n') {
game_output.write_all(b" [stdout] ")?;
game_output.write_all(line)?;
game_output.write_all(b"\n")?;
written += line.len() + 14;
written.fetch_add(line.len() + 14, Ordering::Relaxed);
}
// buf can contain more data than the limit
if written > *consts::GAME_LOG_FILE_LIMIT {
if written.load(Ordering::Relaxed) > *consts::GAME_LOG_FILE_LIMIT {
break;
}
}
// Flush written lines
game_output.flush()?;
}
Ok(())
}));
}
// Redirect stderr to the game.log file
if let Some(stderr) = &mut child.stderr {
let mut buf = Vec::new();
if let Some(mut stderr) = child.stderr.take() {
let game_output = game_output.clone();
let written = written.clone();
stderr.read_to_end(&mut buf)?;
stderr_join = Some(std::thread::spawn(move || -> std::io::Result<()> {
let mut buf = [0; 1024];
if !buf.is_empty() {
for line in buf.split(|c| c == &b'\n') {
while let Ok(read) = stderr.read(&mut buf) {
if read == 0 {
break;
}
let Ok(mut game_output) = game_output.lock() else {
break;
};
for line in buf[..read].split(|c| c == &b'\n') {
game_output.write_all(b"[!] [stderr] ")?;
game_output.write_all(line)?;
game_output.write_all(b"\n")?;
written += line.len() + 14;
written.fetch_add(line.len() + 14, Ordering::Relaxed);
}
// buf can contain more data than the limit
if written > *consts::GAME_LOG_FILE_LIMIT {
if written.load(Ordering::Relaxed) > *consts::GAME_LOG_FILE_LIMIT {
break;
}
}
// Flush written lines
game_output.flush()?;
}
Ok(())
}));
}
// Drop stdio bufs if the limit was reached
if written >= *consts::GAME_LOG_FILE_LIMIT {
drop(child.stdout.take());
drop(child.stderr.take());
}
}
// Update discord RPC until the game process is closed
while child.try_wait()?.is_none() {
std::thread::sleep(std::time::Duration::from_secs(3));
#[cfg(feature = "discord-rpc")]
if let Some(rpc) = &rpc {
rpc.update(RpcUpdates::Update)?;
}
std::thread::sleep(std::time::Duration::from_secs(3));
}
// Flush and close the game log file
game_output.flush()?;
if let Ok(mut file) = game_output.lock() {
file.flush()?;
}
drop(game_output);
if let Some(join) = stdout_join {
join.join().map_err(|err| anyhow::anyhow!("Failed to join stdout reader thread: {err:?}"))??;
}
if let Some(join) = stderr_join {
join.join().map_err(|err| anyhow::anyhow!("Failed to join stderr reader thread: {err:?}"))??;
}
// Workaround for fast process closing (is it still a thing?)
loop {
std::thread::sleep(std::time::Duration::from_secs(3));

View file

@ -263,83 +263,108 @@ pub fn run() -> anyhow::Result<()> {
.spawn()?;
// Create new game.log file to log all the game output
let mut game_output = std::fs::File::create(consts::launcher_dir()?.join("game.log"))?;
let mut written = 0;
let game_output = Arc::new(Mutex::new(
File::create(consts::launcher_dir()?.join("game.log"))?
));
let written = Arc::new(AtomicUsize::new(0));
let mut stdout_join = None;
let mut stderr_join = None;
// Log process output while it's running
while child.try_wait()?.is_none() {
// Check if we've written less than a limit amount of data
if written < *consts::GAME_LOG_FILE_LIMIT {
// Redirect stdout to the game.log file
if let Some(stdout) = &mut child.stdout {
let mut buf = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
let game_output = game_output.clone();
let written = written.clone();
stdout.read_to_end(&mut buf)?;
stdout_join = Some(std::thread::spawn(move || -> std::io::Result<()> {
let mut buf = [0; 1024];
if !buf.is_empty() {
for line in buf.split(|c| c == &b'\n') {
while let Ok(read) = stdout.read(&mut buf) {
if read == 0 {
break;
}
let Ok(mut game_output) = game_output.lock() else {
break;
};
for line in buf[..read].split(|c| c == &b'\n') {
game_output.write_all(b" [stdout] ")?;
game_output.write_all(line)?;
game_output.write_all(b"\n")?;
written += line.len() + 14;
written.fetch_add(line.len() + 14, Ordering::Relaxed);
}
// buf can contain more data than the limit
if written > *consts::GAME_LOG_FILE_LIMIT {
if written.load(Ordering::Relaxed) > *consts::GAME_LOG_FILE_LIMIT {
break;
}
}
// Flush written lines
game_output.flush()?;
}
Ok(())
}));
}
// Redirect stderr to the game.log file
if let Some(stderr) = &mut child.stderr {
let mut buf = Vec::new();
if let Some(mut stderr) = child.stderr.take() {
let game_output = game_output.clone();
let written = written.clone();
stderr.read_to_end(&mut buf)?;
stderr_join = Some(std::thread::spawn(move || -> std::io::Result<()> {
let mut buf = [0; 1024];
if !buf.is_empty() {
for line in buf.split(|c| c == &b'\n') {
while let Ok(read) = stderr.read(&mut buf) {
if read == 0 {
break;
}
let Ok(mut game_output) = game_output.lock() else {
break;
};
for line in buf[..read].split(|c| c == &b'\n') {
game_output.write_all(b"[!] [stderr] ")?;
game_output.write_all(line)?;
game_output.write_all(b"\n")?;
written += line.len() + 14;
written.fetch_add(line.len() + 14, Ordering::Relaxed);
}
// buf can contain more data than the limit
if written > *consts::GAME_LOG_FILE_LIMIT {
if written.load(Ordering::Relaxed) > *consts::GAME_LOG_FILE_LIMIT {
break;
}
}
// Flush written lines
game_output.flush()?;
}
Ok(())
}));
}
// Drop stdio bufs if the limit was reached
if written >= *consts::GAME_LOG_FILE_LIMIT {
drop(child.stdout.take());
drop(child.stderr.take());
}
}
// Update discord RPC until the game process is closed
while child.try_wait()?.is_none() {
std::thread::sleep(std::time::Duration::from_secs(3));
#[cfg(feature = "discord-rpc")]
if let Some(rpc) = &rpc {
rpc.update(RpcUpdates::Update)?;
}
std::thread::sleep(std::time::Duration::from_secs(3));
}
// Flush and close the game log file
game_output.flush()?;
if let Ok(mut file) = game_output.lock() {
file.flush()?;
}
drop(game_output);
if let Some(join) = stdout_join {
join.join().map_err(|err| anyhow::anyhow!("Failed to join stdout reader thread: {err:?}"))??;
}
if let Some(join) = stderr_join {
join.join().map_err(|err| anyhow::anyhow!("Failed to join stderr reader thread: {err:?}"))??;
}
// Workaround for fast process closing (is it still a thing?)
loop {
std::thread::sleep(std::time::Duration::from_secs(3));

View file

@ -243,83 +243,108 @@ pub fn run() -> anyhow::Result<()> {
.spawn()?;
// Create new game.log file to log all the game output
let mut game_output = std::fs::File::create(consts::launcher_dir()?.join("game.log"))?;
let mut written = 0;
let game_output = Arc::new(Mutex::new(
File::create(consts::launcher_dir()?.join("game.log"))?
));
let written = Arc::new(AtomicUsize::new(0));
let mut stdout_join = None;
let mut stderr_join = None;
// Log process output while it's running
while child.try_wait()?.is_none() {
// Check if we've written less than a limit amount of data
if written < *consts::GAME_LOG_FILE_LIMIT {
// Redirect stdout to the game.log file
if let Some(stdout) = &mut child.stdout {
let mut buf = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
let game_output = game_output.clone();
let written = written.clone();
stdout.read_to_end(&mut buf)?;
stdout_join = Some(std::thread::spawn(move || -> std::io::Result<()> {
let mut buf = [0; 1024];
if !buf.is_empty() {
for line in buf.split(|c| c == &b'\n') {
while let Ok(read) = stdout.read(&mut buf) {
if read == 0 {
break;
}
let Ok(mut game_output) = game_output.lock() else {
break;
};
for line in buf[..read].split(|c| c == &b'\n') {
game_output.write_all(b" [stdout] ")?;
game_output.write_all(line)?;
game_output.write_all(b"\n")?;
written += line.len() + 14;
written.fetch_add(line.len() + 14, Ordering::Relaxed);
}
// buf can contain more data than the limit
if written > *consts::GAME_LOG_FILE_LIMIT {
if written.load(Ordering::Relaxed) > *consts::GAME_LOG_FILE_LIMIT {
break;
}
}
// Flush written lines
game_output.flush()?;
}
Ok(())
}));
}
// Redirect stderr to the game.log file
if let Some(stderr) = &mut child.stderr {
let mut buf = Vec::new();
if let Some(mut stderr) = child.stderr.take() {
let game_output = game_output.clone();
let written = written.clone();
stderr.read_to_end(&mut buf)?;
stderr_join = Some(std::thread::spawn(move || -> std::io::Result<()> {
let mut buf = [0; 1024];
if !buf.is_empty() {
for line in buf.split(|c| c == &b'\n') {
while let Ok(read) = stderr.read(&mut buf) {
if read == 0 {
break;
}
let Ok(mut game_output) = game_output.lock() else {
break;
};
for line in buf[..read].split(|c| c == &b'\n') {
game_output.write_all(b"[!] [stderr] ")?;
game_output.write_all(line)?;
game_output.write_all(b"\n")?;
written += line.len() + 14;
written.fetch_add(line.len() + 14, Ordering::Relaxed);
}
// buf can contain more data than the limit
if written > *consts::GAME_LOG_FILE_LIMIT {
if written.load(Ordering::Relaxed) > *consts::GAME_LOG_FILE_LIMIT {
break;
}
}
// Flush written lines
game_output.flush()?;
}
Ok(())
}));
}
// Drop stdio bufs if the limit was reached
if written >= *consts::GAME_LOG_FILE_LIMIT {
drop(child.stdout.take());
drop(child.stderr.take());
}
}
// Update discord RPC until the game process is closed
while child.try_wait()?.is_none() {
std::thread::sleep(std::time::Duration::from_secs(3));
#[cfg(feature = "discord-rpc")]
if let Some(rpc) = &rpc {
rpc.update(RpcUpdates::Update)?;
}
std::thread::sleep(std::time::Duration::from_secs(3));
}
// Flush and close the game log file
game_output.flush()?;
if let Ok(mut file) = game_output.lock() {
file.flush()?;
}
drop(game_output);
if let Some(join) = stdout_join {
join.join().map_err(|err| anyhow::anyhow!("Failed to join stdout reader thread: {err:?}"))??;
}
if let Some(join) = stderr_join {
join.join().map_err(|err| anyhow::anyhow!("Failed to join stderr reader thread: {err:?}"))??;
}
// Workaround for fast process closing (is it still a thing?)
loop {
std::thread::sleep(std::time::Duration::from_secs(3));

View file

@ -261,83 +261,108 @@ pub fn run() -> anyhow::Result<()> {
.spawn()?;
// Create new game.log file to log all the game output
let mut game_output = std::fs::File::create(consts::launcher_dir()?.join("game.log"))?;
let mut written = 0;
let game_output = Arc::new(Mutex::new(
File::create(consts::launcher_dir()?.join("game.log"))?
));
let written = Arc::new(AtomicUsize::new(0));
let mut stdout_join = None;
let mut stderr_join = None;
// Log process output while it's running
while child.try_wait()?.is_none() {
// Check if we've written less than a limit amount of data
if written < *consts::GAME_LOG_FILE_LIMIT {
// Redirect stdout to the game.log file
if let Some(stdout) = &mut child.stdout {
let mut buf = Vec::new();
if let Some(mut stdout) = child.stdout.take() {
let game_output = game_output.clone();
let written = written.clone();
stdout.read_to_end(&mut buf)?;
stdout_join = Some(std::thread::spawn(move || -> std::io::Result<()> {
let mut buf = [0; 1024];
if !buf.is_empty() {
for line in buf.split(|c| c == &b'\n') {
while let Ok(read) = stdout.read(&mut buf) {
if read == 0 {
break;
}
let Ok(mut game_output) = game_output.lock() else {
break;
};
for line in buf[..read].split(|c| c == &b'\n') {
game_output.write_all(b" [stdout] ")?;
game_output.write_all(line)?;
game_output.write_all(b"\n")?;
written += line.len() + 14;
written.fetch_add(line.len() + 14, Ordering::Relaxed);
}
// buf can contain more data than the limit
if written > *consts::GAME_LOG_FILE_LIMIT {
if written.load(Ordering::Relaxed) > *consts::GAME_LOG_FILE_LIMIT {
break;
}
}
// Flush written lines
game_output.flush()?;
}
Ok(())
}));
}
// Redirect stderr to the game.log file
if let Some(stderr) = &mut child.stderr {
let mut buf = Vec::new();
if let Some(mut stderr) = child.stderr.take() {
let game_output = game_output.clone();
let written = written.clone();
stderr.read_to_end(&mut buf)?;
stderr_join = Some(std::thread::spawn(move || -> std::io::Result<()> {
let mut buf = [0; 1024];
if !buf.is_empty() {
for line in buf.split(|c| c == &b'\n') {
while let Ok(read) = stderr.read(&mut buf) {
if read == 0 {
break;
}
let Ok(mut game_output) = game_output.lock() else {
break;
};
for line in buf[..read].split(|c| c == &b'\n') {
game_output.write_all(b"[!] [stderr] ")?;
game_output.write_all(line)?;
game_output.write_all(b"\n")?;
written += line.len() + 14;
written.fetch_add(line.len() + 14, Ordering::Relaxed);
}
// buf can contain more data than the limit
if written > *consts::GAME_LOG_FILE_LIMIT {
if written.load(Ordering::Relaxed) > *consts::GAME_LOG_FILE_LIMIT {
break;
}
}
// Flush written lines
game_output.flush()?;
}
Ok(())
}));
}
// Drop stdio bufs if the limit was reached
if written >= *consts::GAME_LOG_FILE_LIMIT {
drop(child.stdout.take());
drop(child.stderr.take());
}
}
// Update discord RPC until the game process is closed
while child.try_wait()?.is_none() {
std::thread::sleep(std::time::Duration::from_secs(3));
#[cfg(feature = "discord-rpc")]
if let Some(rpc) = &rpc {
rpc.update(RpcUpdates::Update)?;
}
std::thread::sleep(std::time::Duration::from_secs(3));
}
// Flush and close the game log file
game_output.flush()?;
if let Ok(mut file) = game_output.lock() {
file.flush()?;
}
drop(game_output);
if let Some(join) = stdout_join {
join.join().map_err(|err| anyhow::anyhow!("Failed to join stdout reader thread: {err:?}"))??;
}
if let Some(join) = stderr_join {
join.join().map_err(|err| anyhow::anyhow!("Failed to join stderr reader thread: {err:?}"))??;
}
// Workaround for fast process closing (is it still a thing?)
loop {
std::thread::sleep(std::time::Duration::from_secs(3));