feat(process manager): launch games with log files

This commit is contained in:
DecDuck
2024-12-15 17:29:21 +11:00
parent 269dcbb6f3
commit 3f71149289
11 changed files with 191 additions and 31 deletions

View File

@@ -18,7 +18,11 @@
<div class="w-full min-h-screen mx-auto bg-zinc-900 px-5 py-6">
<!-- game toolbar -->
<div>
<GameStatusButton @install="() => installFlow()" :status="status" />
<GameStatusButton
@install="() => installFlow()"
@play="() => play()"
:status="status"
/>
</div>
</div>
</div>
@@ -168,8 +172,8 @@
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-600">
There are no supported versions to install. Please contact your
server admin or try again later.
There are no supported versions to install. Please
contact your server admin or try again later.
</h3>
</div>
</div>
@@ -369,4 +373,13 @@ async function install() {
installError.value = (error as string).toString();
}
}
async function play() {
try {
await invoke("launch_game", { gameId: game.value.id });
} catch (e) {
game.value.mName = e as string;
console.error(e);
}
}
</script>

View File

@@ -34,9 +34,11 @@ pub enum DatabaseGameStatus {
},
SetupRequired {
version_name: String,
install_dir: String,
},
Installed {
version_name: String,
install_dir: String,
},
Updating {
version_name: String,
@@ -100,11 +102,14 @@ impl DatabaseImpls for DatabaseInterface {
let data_root_dir = DATA_ROOT_DIR.lock().unwrap();
let db_path = data_root_dir.join("drop.db");
let games_base_dir = data_root_dir.join("games");
let logs_root_dir = data_root_dir.join("logs");
debug!("Creating data directory at {:?}", data_root_dir);
create_dir_all(data_root_dir.clone()).unwrap();
debug!("Creating games directory");
create_dir_all(games_base_dir.clone()).unwrap();
debug!("Creating logs directory");
create_dir_all(logs_root_dir.clone()).unwrap();
#[allow(clippy::let_and_return)]
let exists = fs::exists(db_path.clone()).unwrap();

View File

@@ -4,9 +4,9 @@ use crate::downloads::manifest::{DropDownloadContext, DropManifest};
use crate::downloads::progress_object::ProgressHandle;
use crate::remote::RemoteAccessError;
use crate::DB;
use core::time;
use log::{debug, error, info};
use rayon::ThreadPoolBuilder;
use core::time;
use std::fmt::{Display, Formatter};
use std::fs::{create_dir_all, File};
use std::io;
@@ -28,7 +28,7 @@ pub struct GameDownloadAgent {
pub id: String,
pub version: String,
pub control_flag: DownloadThreadControl,
pub target_download_dir: usize,
pub base_dir: String,
contexts: Mutex<Vec<DropDownloadContext>>,
pub manifest: Mutex<Option<DropManifest>>,
pub progress: Arc<ProgressObject>,
@@ -72,12 +72,20 @@ impl GameDownloadAgent {
) -> Self {
// Don't run by default
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
let db_lock = DB.borrow_data().unwrap();
let base_dir = db_lock.games.install_dirs[target_download_dir].clone();
drop(db_lock);
let base_dir_path = Path::new(&base_dir);
let data_base_dir_path = base_dir_path.join(id.clone());
Self {
id,
version,
control_flag,
manifest: Mutex::new(None),
target_download_dir,
base_dir: data_base_dir_path.to_str().unwrap().to_owned(),
contexts: Mutex::new(Vec::new()),
progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
sender,
@@ -104,7 +112,11 @@ impl GameDownloadAgent {
let timer = Instant::now();
self.run().map_err(|_| GameDownloadError::DownloadError)?;
info!("{} took {}ms to download", self.id, timer.elapsed().as_millis());
info!(
"{} took {}ms to download",
self.id,
timer.elapsed().as_millis()
);
Ok(())
}
@@ -187,18 +199,12 @@ impl GameDownloadAgent {
}
pub fn generate_contexts(&self) -> Result<(), GameDownloadError> {
let db_lock = DB.borrow_data().unwrap();
let data_base_dir = db_lock.games.install_dirs[self.target_download_dir].clone();
drop(db_lock);
let manifest = self.manifest.lock().unwrap().clone().unwrap();
let game_id = self.id.clone();
let data_base_dir_path = Path::new(&data_base_dir);
let mut contexts = Vec::new();
let base_path = data_base_dir_path.join(game_id.clone()).clone();
create_dir_all(base_path.clone()).unwrap();
let base_path = Path::new(&self.base_dir);
create_dir_all(base_path).unwrap();
for (raw_path, chunk) in manifest {
let path = base_path.join(Path::new(&raw_path));
@@ -219,6 +225,7 @@ impl GameDownloadAgent {
path: path.clone(),
checksum: chunk.checksums[index].clone(),
length: *length,
permissions: chunk.permissions,
});
running_offset += *length as u64;
}

View File

@@ -6,8 +6,11 @@ use crate::DB;
use log::warn;
use md5::{Context, Digest};
use reqwest::blocking::Response;
use tauri::utils::acl::Permission;
use std::fs::{set_permissions, Permissions};
use std::io::Read;
use std::os::unix::fs::PermissionsExt;
use std::{
fs::{File, OpenOptions},
io::{self, BufWriter, Seek, SeekFrom, Write},
@@ -157,7 +160,7 @@ pub fn download_game_chunk(
));
}
let mut destination = DropWriter::new(ctx.path);
let mut destination = DropWriter::new(ctx.path.clone());
if ctx.offset != 0 {
destination
@@ -185,6 +188,13 @@ pub fn download_game_chunk(
return Ok(false);
};
// If we complete the file, set the permissions (if on Linux)
#[cfg(unix)]
{
let permissions = Permissions::from_mode(ctx.permissions);
set_permissions(ctx.path, permissions).unwrap();
}
/*
let checksum = pipeline
.finish()

View File

@@ -220,9 +220,12 @@ impl DownloadManagerBuilder {
info!("Popping consumed data");
let download_agent = self.remove_and_cleanup_game(&game_id);
if let Err(error) =
on_game_complete(game_id, download_agent.version.clone(), &self.app_handle)
{
if let Err(error) = on_game_complete(
game_id,
download_agent.version.clone(),
download_agent.base_dir.clone(),
&self.app_handle,
) {
self.sender
.send(DownloadManagerSignal::Error(
GameDownloadError::Communication(error),

View File

@@ -6,7 +6,7 @@ pub type DropManifest = HashMap<String, DropChunk>;
#[derive(Serialize, Deserialize, Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DropChunk {
pub permissions: usize,
pub permissions: u32,
pub ids: Vec<String>,
pub checksums: Vec<String>,
pub lengths: Vec<usize>,
@@ -23,4 +23,5 @@ pub struct DropDownloadContext {
pub path: PathBuf,
pub checksum: String,
pub length: usize,
pub permissions: u32,
}

View File

@@ -26,6 +26,7 @@ use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Root};
use log4rs::encode::pattern::PatternEncoder;
use log4rs::Config;
use process::process_commands::launch_game;
use process::process_manager::ProcessManager;
use remote::{gen_drop_url, use_remote};
use serde::{Deserialize, Serialize};
@@ -67,7 +68,7 @@ pub struct AppState {
#[serde(skip_serializing)]
download_manager: Arc<DownloadManager>,
#[serde(skip_serializing)]
process_manager: Arc<ProcessManager>,
process_manager: Arc<Mutex<ProcessManager>>,
}
#[tauri::command]
@@ -104,7 +105,7 @@ fn setup(handle: AppHandle) -> AppState {
let games = HashMap::new();
let download_manager = Arc::new(DownloadManagerBuilder::build(handle));
let process_manager = Arc::new(ProcessManager::new());
let process_manager = Arc::new(Mutex::new(ProcessManager::new()));
debug!("Checking if database is set up");
let is_set_up = DB.database_is_set_up();
@@ -168,6 +169,8 @@ pub fn run() {
move_game_in_queue,
pause_game_downloads,
resume_game_downloads,
// Processes
launch_game,
])
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())

View File

@@ -225,15 +225,12 @@ fn fetch_game_verion_options_logic<'a>(
let data = response.json::<Vec<GameVersionOption>>()?;
let state_lock = state.lock().unwrap();
let process_manager_lock = state_lock.process_manager.lock().unwrap();
let data = data
.into_iter()
.filter(|v| {
state_lock
.process_manager
.valid_platform(&v.platform)
.unwrap()
})
.filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap())
.collect::<Vec<GameVersionOption>>();
drop(process_manager_lock);
drop(state_lock);
Ok(data)
@@ -250,6 +247,7 @@ pub fn fetch_game_verion_options<'a>(
pub fn on_game_complete(
game_id: String,
version_name: String,
install_dir: String,
app_handle: &AppHandle,
) -> Result<(), RemoteAccessError> {
// Fetch game version information from remote
@@ -284,9 +282,15 @@ pub fn on_game_complete(
DB.save().unwrap();
let status = if data.setup_command.is_empty() {
DatabaseGameStatus::Installed { version_name }
DatabaseGameStatus::Installed {
version_name,
install_dir,
}
} else {
DatabaseGameStatus::SetupRequired { version_name }
DatabaseGameStatus::SetupRequired {
version_name,
install_dir,
}
};
let mut db_handle = DB.borrow_data_mut().unwrap();

View File

@@ -1 +1,2 @@
pub mod process_manager;
pub mod process_commands;

View File

@@ -0,0 +1,16 @@
use std::sync::Mutex;
use crate::AppState;
#[tauri::command]
pub fn launch_game(game_id: String, state: tauri::State<'_, Mutex<AppState>>) -> Result<(), String> {
let state_lock = state.lock().unwrap();
let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
process_manager_lock.launch_game(game_id)?;
drop(process_manager_lock);
drop(state_lock);
Ok(())
}

View File

@@ -1,22 +1,62 @@
use std::{collections::HashMap, sync::LazyLock};
use std::{
collections::HashMap,
fs::{File, OpenOptions},
path::PathBuf,
process::{Child, Command},
sync::LazyLock,
};
use log::info;
use serde::{Deserialize, Serialize};
use crate::{
db::{DatabaseGameStatus, DATA_ROOT_DIR},
DB,
};
pub struct ProcessManager {
current_platform: Platform,
log_output_dir: PathBuf,
processes: HashMap<String, Child>,
}
impl ProcessManager {
pub fn new() -> Self {
let root_dir_lock = DATA_ROOT_DIR.lock().unwrap();
let log_output_dir = root_dir_lock.join("logs");
drop(root_dir_lock);
ProcessManager {
current_platform: if cfg!(windows) {
Platform::Windows
} else {
Platform::Linux
},
processes: HashMap::new(),
log_output_dir,
}
}
fn process_command(&self, raw_command: String) -> (String, Vec<String>) {
let command_components = raw_command.split(" ").collect::<Vec<&str>>();
let root = match self.current_platform {
Platform::Windows => command_components[0].to_string(),
Platform::Linux => {
let mut root = command_components[0].to_string();
if !root.starts_with("./") {
root = format!("{}{}", "./", root);
}
root
}
};
let args = command_components[1..]
.into_iter()
.map(|v| v.to_string())
.collect();
(root, args)
}
pub fn valid_platform(&self, platform: &Platform) -> Result<bool, String> {
let current = &self.current_platform;
let valid_platforms = PROCESS_COMPATABILITY_MATRIX
@@ -25,6 +65,63 @@ impl ProcessManager {
Ok(valid_platforms.contains(platform))
}
pub fn launch_game(&mut self, game_id: String) -> Result<(), String> {
if self.processes.contains_key(&game_id) {
return Err("Game or setup is already running.".to_owned());
}
let db_lock = DB.borrow_data().unwrap();
let game_status = db_lock
.games
.games_statuses
.get(&game_id)
.ok_or("Game not installed")?;
let DatabaseGameStatus::Installed {
version_name,
install_dir,
} = game_status
else {
return Err("Game not installed.".to_owned());
};
let game_version = db_lock
.games
.game_versions
.get(&game_id)
.ok_or("Invalid game ID".to_owned())?
.get(version_name)
.ok_or("Invalid version name".to_owned())?;
let (command, args) = self.process_command(game_version.launch_command.clone());
info!("launching process {} in {}", command, install_dir);
let current_time = chrono::offset::Local::now();
let log_file = OpenOptions::new()
.write(true)
.append(true)
.read(true)
.create(true)
.open(self.log_output_dir.join(format!(
"{}-{}.log",
game_id,
current_time.to_rfc3339()
)))
.map_err(|v| v.to_string())?;
let launch_process = Command::new(command)
.current_dir(install_dir)
.stdout(log_file)
.args(args)
.spawn()
.map_err(|v| v.to_string())?;
self.processes.insert(game_id, launch_process);
Ok(())
}
}
#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone)]