parent
bb671c5fd2
commit
3895ab12d6
8 changed files with 447 additions and 310 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -2293,9 +2293,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.19"
|
||||
version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53250a3b3fed8ff8fd988587d8925d26a83ac3845d9e03b220b37f34c2b8d6c2"
|
||||
checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"libc",
|
||||
|
@ -2313,9 +2313,9 @@ checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
|
|||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.7"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a460aeb8de6dcb0f381e1ee05f1cd56fcf5a5f6eb8187ff3d8f0b11078d38b7c"
|
||||
checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
|
||||
dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
|
80
crates/dtmm/src/log.rs
Normal file
80
crates/dtmm/src/log.rs
Normal file
|
@ -0,0 +1,80 @@
|
|||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::filter::FilterFn;
|
||||
use tracing_subscriber::fmt;
|
||||
use tracing_subscriber::fmt::format::debug_fn;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
// I currently cannot find a way to add a parameter to `dtmt_shared::create_tracing_subscriber`
|
||||
// that would allow me to pass an extra `Layer` to that function. So, for now,
|
||||
// its code has to be duplicated here.
|
||||
|
||||
pub struct ChannelWriter {
|
||||
tx: UnboundedSender<String>,
|
||||
}
|
||||
|
||||
impl ChannelWriter {
|
||||
pub fn new(tx: UnboundedSender<String>) -> Self {
|
||||
Self { tx }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Write for ChannelWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let tx = self.tx.clone();
|
||||
let string = String::from_utf8_lossy(buf).to_string();
|
||||
|
||||
// The `send` errors when the receiving end has closed.
|
||||
// But there's not much we can do at that point, so we just ignore it.
|
||||
let _ = tx.send(string);
|
||||
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_tracing_subscriber(tx: UnboundedSender<String>) {
|
||||
let env_layer =
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::try_new("info").unwrap());
|
||||
|
||||
let (dev_stdout_layer, prod_stdout_layer, filter_layer) = if cfg!(debug_assertions) {
|
||||
let fmt_layer = fmt::layer().pretty();
|
||||
(Some(fmt_layer), None, None)
|
||||
} else {
|
||||
// Creates a layer that
|
||||
// - only prints events that contain a message
|
||||
// - does not print fields
|
||||
// - does not print spans/targets
|
||||
// - only prints time, not date
|
||||
let fmt_layer = fmt::layer()
|
||||
.event_format(dtmt_shared::Formatter)
|
||||
.fmt_fields(debug_fn(dtmt_shared::format_field));
|
||||
|
||||
(
|
||||
None,
|
||||
Some(fmt_layer),
|
||||
Some(FilterFn::new(dtmt_shared::filter)),
|
||||
)
|
||||
};
|
||||
|
||||
let channel_layer = fmt::layer()
|
||||
// TODO: Re-enable and implement a formatter for the Druid widget
|
||||
.with_ansi(false)
|
||||
.event_format(dtmt_shared::Formatter)
|
||||
.fmt_fields(debug_fn(dtmt_shared::format_field))
|
||||
.with_writer(move || ChannelWriter::new(tx.clone()));
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(channel_layer)
|
||||
.with(filter_layer)
|
||||
.with(env_layer)
|
||||
.with(dev_stdout_layer)
|
||||
.with(prod_stdout_layer)
|
||||
.with(ErrorLayer::new(fmt::format::Pretty::default()))
|
||||
.init();
|
||||
}
|
|
@ -1,170 +1,35 @@
|
|||
#![recursion_limit = "256"]
|
||||
#![feature(let_chains)]
|
||||
|
||||
use std::fs;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::command;
|
||||
use clap::parser::ValueSource;
|
||||
use clap::value_parser;
|
||||
use clap::Arg;
|
||||
use color_eyre::eyre::Context;
|
||||
use color_eyre::Report;
|
||||
use color_eyre::Result;
|
||||
use color_eyre::{Report, Result};
|
||||
use druid::AppLauncher;
|
||||
use druid::ExtEventSink;
|
||||
use druid::SingleUse;
|
||||
use druid::Target;
|
||||
use engine::delete_mod;
|
||||
use engine::import_mod;
|
||||
use engine::reset_mod_deployment;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use state::ACTION_FINISH_ADD_MOD;
|
||||
use state::ACTION_FINISH_DELETE_SELECTED_MOD;
|
||||
use state::ACTION_FINISH_RESET_DEPLOYMENT;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::engine::deploy_mods;
|
||||
use crate::state::{AsyncAction, Delegate, State, ACTION_FINISH_DEPLOY};
|
||||
use crate::state::{Delegate, State};
|
||||
use crate::worker::work_thread;
|
||||
|
||||
mod controller;
|
||||
mod engine;
|
||||
mod log;
|
||||
mod main_window;
|
||||
mod state;
|
||||
mod theme;
|
||||
mod util;
|
||||
mod widget;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct Config {
|
||||
data_dir: Option<PathBuf>,
|
||||
game_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn work_thread(
|
||||
event_sink: Arc<RwLock<ExtEventSink>>,
|
||||
action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>,
|
||||
) -> Result<()> {
|
||||
let rt = Runtime::new()?;
|
||||
|
||||
rt.block_on(async {
|
||||
while let Some(action) = action_queue.write().await.recv().await {
|
||||
let event_sink = event_sink.clone();
|
||||
match action {
|
||||
AsyncAction::DeployMods(state) => tokio::spawn(async move {
|
||||
if let Err(err) = deploy_mods(state).await {
|
||||
tracing::error!("Failed to deploy mods: {:?}", err);
|
||||
}
|
||||
|
||||
event_sink
|
||||
.write()
|
||||
.await
|
||||
.submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto)
|
||||
.expect("failed to send command");
|
||||
}),
|
||||
AsyncAction::AddMod((state, info)) => tokio::spawn(async move {
|
||||
match import_mod(state, info).await {
|
||||
Ok(mod_info) => {
|
||||
event_sink
|
||||
.write()
|
||||
.await
|
||||
.submit_command(
|
||||
ACTION_FINISH_ADD_MOD,
|
||||
SingleUse::new(mod_info),
|
||||
Target::Auto,
|
||||
)
|
||||
.expect("failed to send command");
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to import mod: {:?}", err);
|
||||
}
|
||||
}
|
||||
}),
|
||||
AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move {
|
||||
if let Err(err) = delete_mod(state, &info).await {
|
||||
tracing::error!(
|
||||
"Failed to delete mod files. \
|
||||
You might want to clean up the data directory manually. \
|
||||
Reason: {:?}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
event_sink
|
||||
.write()
|
||||
.await
|
||||
.submit_command(
|
||||
ACTION_FINISH_DELETE_SELECTED_MOD,
|
||||
SingleUse::new(info),
|
||||
Target::Auto,
|
||||
)
|
||||
.expect("failed to send command");
|
||||
}),
|
||||
AsyncAction::ResetDeployment(state) => tokio::spawn(async move {
|
||||
if let Err(err) = reset_mod_deployment(state).await {
|
||||
tracing::error!("Failed to reset mod deployment: {:?}", err);
|
||||
}
|
||||
|
||||
event_sink
|
||||
.write()
|
||||
.await
|
||||
.submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto)
|
||||
.expect("failed to send command");
|
||||
}),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(arget_os = "windows"))]
|
||||
fn get_default_config_path() -> PathBuf {
|
||||
let config_dir = std::env::var("XDG_CONFIG_DIR").unwrap_or_else(|_| {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| {
|
||||
let user = std::env::var("USER").expect("user env variable not set");
|
||||
format!("/home/{user}")
|
||||
});
|
||||
format!("{home}/.config")
|
||||
});
|
||||
|
||||
PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_default_config_path() -> PathBuf {
|
||||
let config_dir = std::env::var("APPDATA").expect("appdata env var not set");
|
||||
PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg")
|
||||
}
|
||||
|
||||
#[cfg(not(arget_os = "windows"))]
|
||||
fn get_default_data_dir() -> PathBuf {
|
||||
let data_dir = std::env::var("XDG_DATA_DIR").unwrap_or_else(|_| {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| {
|
||||
let user = std::env::var("USER").expect("user env variable not set");
|
||||
format!("/home/{user}")
|
||||
});
|
||||
format!("{home}/.local/share")
|
||||
});
|
||||
|
||||
PathBuf::from(data_dir).join("dtmm")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_default_data_dir() -> PathBuf {
|
||||
let data_dir = std::env::var("APPDATA").expect("appdata env var not set");
|
||||
PathBuf::from(data_dir).join("dtmm")
|
||||
}
|
||||
mod worker;
|
||||
|
||||
#[tracing::instrument]
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
|
||||
let default_config_path = get_default_config_path();
|
||||
let default_config_path = util::get_default_config_path();
|
||||
|
||||
tracing::trace!(default_config_path = %default_config_path.display());
|
||||
|
||||
|
@ -185,78 +50,30 @@ fn main() -> Result<()> {
|
|||
)
|
||||
.get_matches();
|
||||
|
||||
dtmt_shared::create_tracing_subscriber();
|
||||
let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
log::create_tracing_subscriber(log_tx);
|
||||
|
||||
unsafe {
|
||||
oodle_sys::init(matches.get_one::<String>("oodle"));
|
||||
}
|
||||
|
||||
let config: Config = {
|
||||
let path = matches
|
||||
.get_one::<PathBuf>("config")
|
||||
.expect("argument missing despite default");
|
||||
match fs::read(path) {
|
||||
Ok(data) => {
|
||||
let data = String::from_utf8(data).wrap_err_with(|| {
|
||||
format!("config file {} contains invalid UTF-8", path.display())
|
||||
})?;
|
||||
serde_sjson::from_str(&data)
|
||||
.wrap_err_with(|| format!("invalid config file {}", path.display()))?
|
||||
}
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => {
|
||||
if matches.value_source("config") != Some(ValueSource::DefaultValue) {
|
||||
return Err(err).wrap_err_with(|| {
|
||||
format!("failed to read config file {}", path.display())
|
||||
})?;
|
||||
}
|
||||
|
||||
{
|
||||
let parent = default_config_path
|
||||
.parent()
|
||||
.expect("a file path always has a parent directory");
|
||||
fs::create_dir_all(parent).wrap_err_with(|| {
|
||||
format!("failed to create directories {}", parent.display())
|
||||
})?;
|
||||
}
|
||||
|
||||
let config = Config {
|
||||
data_dir: Some(get_default_data_dir()),
|
||||
game_dir: None,
|
||||
};
|
||||
|
||||
{
|
||||
let data = serde_sjson::to_string(&config)
|
||||
.wrap_err("failed to serialize default config value")?;
|
||||
fs::write(&default_config_path, data).wrap_err_with(|| {
|
||||
format!(
|
||||
"failed to write default config to {}",
|
||||
default_config_path.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err)
|
||||
.wrap_err_with(|| format!("failed to read config file {}", path.display()))?;
|
||||
}
|
||||
}
|
||||
};
|
||||
let config =
|
||||
util::read_config(&default_config_path, &matches).wrap_err("failed to read config file")?;
|
||||
|
||||
let initial_state = State::new(config);
|
||||
|
||||
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
|
||||
let delegate = Delegate::new(sender);
|
||||
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let delegate = Delegate::new(action_tx);
|
||||
|
||||
let launcher = AppLauncher::with_window(main_window::new()).delegate(delegate);
|
||||
|
||||
let event_sink = launcher.get_external_handle();
|
||||
std::thread::spawn(move || {
|
||||
let event_sink = Arc::new(RwLock::new(event_sink));
|
||||
let receiver = Arc::new(RwLock::new(receiver));
|
||||
let action_rx = Arc::new(RwLock::new(action_rx));
|
||||
let log_rx = Arc::new(RwLock::new(log_rx));
|
||||
loop {
|
||||
if let Err(err) = work_thread(event_sink.clone(), receiver.clone()) {
|
||||
if let Err(err) = work_thread(event_sink.clone(), action_rx.clone(), log_rx.clone()) {
|
||||
tracing::error!("Work thread failed, restarting: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use druid::im::Vector;
|
||||
use druid::widget::{
|
||||
Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split,
|
||||
TextBox, ViewSwitcher,
|
||||
Align, Button, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment, Maybe,
|
||||
Scroll, SizedBox, Split, TextBox, ViewSwitcher,
|
||||
};
|
||||
use druid::{
|
||||
lens, FileDialogOptions, FileSpec, Insets, LensExt, SingleUse, Widget, WidgetExt, WindowDesc,
|
||||
lens, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Insets, LensExt, SingleUse,
|
||||
Widget, WidgetExt, WindowDesc,
|
||||
};
|
||||
|
||||
use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_START_RESET_DEPLOYMENT};
|
||||
|
@ -261,9 +262,25 @@ fn build_main() -> impl Widget<State> {
|
|||
)
|
||||
}
|
||||
|
||||
fn build_log_view() -> impl Widget<State> {
|
||||
let font = FontDescriptor::new(FontFamily::MONOSPACE);
|
||||
let label = Label::raw()
|
||||
.with_font(font)
|
||||
.with_line_break_mode(LineBreaking::WordWrap)
|
||||
.lens(State::log);
|
||||
|
||||
SizedBox::new(label)
|
||||
.expand_width()
|
||||
.height(128.0)
|
||||
.scroll()
|
||||
.vertical()
|
||||
}
|
||||
|
||||
fn build_window() -> impl Widget<State> {
|
||||
// TODO: Add borders between the sections
|
||||
Flex::column()
|
||||
.must_fill_main_axis(true)
|
||||
.with_child(build_top_bar())
|
||||
.with_flex_child(build_main(), 1.0)
|
||||
.with_child(build_log_view())
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use druid::{
|
|||
use dtmt_shared::ModConfig;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::Config;
|
||||
use crate::util::Config;
|
||||
|
||||
pub(crate) const ACTION_SELECT_MOD: Selector<usize> = Selector::new("dtmm.action.select-mod");
|
||||
pub(crate) const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up");
|
||||
|
@ -33,6 +33,8 @@ pub(crate) const ACTION_ADD_MOD: Selector<FileInfo> = Selector::new("dtmm.action
|
|||
pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<ModInfo>> =
|
||||
Selector::new("dtmm.action.finish-add-mod");
|
||||
|
||||
pub(crate) const ACTION_LOG: Selector<SingleUse<String>> = Selector::new("dtmm.action.log");
|
||||
|
||||
#[derive(Copy, Clone, Data, Debug, PartialEq)]
|
||||
pub(crate) enum View {
|
||||
Mods,
|
||||
|
@ -154,6 +156,7 @@ pub(crate) struct State {
|
|||
game_dir: Arc<PathBuf>,
|
||||
data_dir: Arc<PathBuf>,
|
||||
ctx: Arc<sdk::Context>,
|
||||
log: Arc<String>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
|
@ -170,8 +173,9 @@ impl State {
|
|||
selected_mod_index: None,
|
||||
is_deployment_in_progress: false,
|
||||
is_reset_in_progress: false,
|
||||
game_dir: Arc::new(config.game_dir.unwrap_or_default()),
|
||||
data_dir: Arc::new(config.data_dir.unwrap_or_default()),
|
||||
game_dir: Arc::new(config.game_dir().cloned().unwrap_or_default()),
|
||||
data_dir: Arc::new(config.data_dir().cloned().unwrap_or_default()),
|
||||
log: Arc::new(String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -230,6 +234,11 @@ impl State {
|
|||
pub(crate) fn get_ctx(&self) -> Arc<sdk::Context> {
|
||||
self.ctx.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn add_log_line(&mut self, line: String) {
|
||||
let log = Arc::make_mut(&mut self.log);
|
||||
log.push_str(&line);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SelectedModLens;
|
||||
|
@ -454,6 +463,15 @@ impl AppDelegate<State> for Delegate {
|
|||
}
|
||||
Handled::Yes
|
||||
}
|
||||
cmd if cmd.is(ACTION_LOG) => {
|
||||
let line = cmd
|
||||
.get(ACTION_LOG)
|
||||
.expect("command type matched but didn't contain the expected value");
|
||||
if let Some(line) = line.take() {
|
||||
state.add_log_line(line);
|
||||
}
|
||||
Handled::Yes
|
||||
}
|
||||
cmd => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::warn!("Unknown command: {:?}", cmd);
|
||||
|
|
117
crates/dtmm/src/util.rs
Normal file
117
crates/dtmm/src/util.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
use std::fs;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use clap::{parser::ValueSource, ArgMatches};
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct Config {
|
||||
data_dir: Option<PathBuf>,
|
||||
game_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn game_dir(&self) -> Option<&PathBuf> {
|
||||
self.game_dir.as_ref()
|
||||
}
|
||||
|
||||
pub fn data_dir(&self) -> Option<&PathBuf> {
|
||||
self.data_dir.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(arget_os = "windows"))]
|
||||
pub fn get_default_config_path() -> PathBuf {
|
||||
let config_dir = std::env::var("XDG_CONFIG_DIR").unwrap_or_else(|_| {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| {
|
||||
let user = std::env::var("USER").expect("user env variable not set");
|
||||
format!("/home/{user}")
|
||||
});
|
||||
format!("{home}/.config")
|
||||
});
|
||||
|
||||
PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn get_default_config_path() -> PathBuf {
|
||||
let config_dir = std::env::var("APPDATA").expect("appdata env var not set");
|
||||
PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg")
|
||||
}
|
||||
|
||||
#[cfg(not(arget_os = "windows"))]
|
||||
pub fn get_default_data_dir() -> PathBuf {
|
||||
let data_dir = std::env::var("XDG_DATA_DIR").unwrap_or_else(|_| {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| {
|
||||
let user = std::env::var("USER").expect("user env variable not set");
|
||||
format!("/home/{user}")
|
||||
});
|
||||
format!("{home}/.local/share")
|
||||
});
|
||||
|
||||
PathBuf::from(data_dir).join("dtmm")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn get_default_data_dir() -> PathBuf {
|
||||
let data_dir = std::env::var("APPDATA").expect("appdata env var not set");
|
||||
PathBuf::from(data_dir).join("dtmm")
|
||||
}
|
||||
|
||||
pub(crate) fn read_config<P: AsRef<Path>>(
|
||||
default_config_path: P,
|
||||
matches: &ArgMatches,
|
||||
) -> Result<Config> {
|
||||
let path = matches
|
||||
.get_one::<PathBuf>("config")
|
||||
.expect("argument missing despite default");
|
||||
let default_config_path = default_config_path.as_ref();
|
||||
|
||||
match fs::read(path) {
|
||||
Ok(data) => {
|
||||
let data = String::from_utf8(data).wrap_err_with(|| {
|
||||
format!("config file {} contains invalid UTF-8", path.display())
|
||||
})?;
|
||||
serde_sjson::from_str(&data)
|
||||
.wrap_err_with(|| format!("invalid config file {}", path.display()))
|
||||
}
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => {
|
||||
if matches.value_source("config") != Some(ValueSource::DefaultValue) {
|
||||
return Err(err)
|
||||
.wrap_err_with(|| format!("failed to read config file {}", path.display()))?;
|
||||
}
|
||||
|
||||
{
|
||||
let parent = default_config_path
|
||||
.parent()
|
||||
.expect("a file path always has a parent directory");
|
||||
fs::create_dir_all(parent).wrap_err_with(|| {
|
||||
format!("failed to create directories {}", parent.display())
|
||||
})?;
|
||||
}
|
||||
|
||||
let config = Config {
|
||||
data_dir: Some(get_default_data_dir()),
|
||||
game_dir: None,
|
||||
};
|
||||
|
||||
{
|
||||
let data = serde_sjson::to_string(&config)
|
||||
.wrap_err("failed to serialize default config value")?;
|
||||
fs::write(default_config_path, data).wrap_err_with(|| {
|
||||
format!(
|
||||
"failed to write default config to {}",
|
||||
default_config_path.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
Err(err) => {
|
||||
Err(err).wrap_err_with(|| format!("failed to read config file {}", path.display()))
|
||||
}
|
||||
}
|
||||
}
|
114
crates/dtmm/src/worker.rs
Normal file
114
crates/dtmm/src/worker.rs
Normal file
|
@ -0,0 +1,114 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use color_eyre::Result;
|
||||
use druid::{ExtEventSink, SingleUse, Target};
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::engine::*;
|
||||
use crate::state::*;
|
||||
|
||||
async fn handle_action(
|
||||
event_sink: Arc<RwLock<ExtEventSink>>,
|
||||
action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>,
|
||||
) {
|
||||
while let Some(action) = action_queue.write().await.recv().await {
|
||||
let event_sink = event_sink.clone();
|
||||
match action {
|
||||
AsyncAction::DeployMods(state) => tokio::spawn(async move {
|
||||
if let Err(err) = deploy_mods(state).await {
|
||||
tracing::error!("Failed to deploy mods: {:?}", err);
|
||||
}
|
||||
|
||||
event_sink
|
||||
.write()
|
||||
.await
|
||||
.submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto)
|
||||
.expect("failed to send command");
|
||||
}),
|
||||
AsyncAction::AddMod((state, info)) => tokio::spawn(async move {
|
||||
match import_mod(state, info).await {
|
||||
Ok(mod_info) => {
|
||||
event_sink
|
||||
.write()
|
||||
.await
|
||||
.submit_command(
|
||||
ACTION_FINISH_ADD_MOD,
|
||||
SingleUse::new(mod_info),
|
||||
Target::Auto,
|
||||
)
|
||||
.expect("failed to send command");
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to import mod: {:?}", err);
|
||||
}
|
||||
}
|
||||
}),
|
||||
AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move {
|
||||
if let Err(err) = delete_mod(state, &info).await {
|
||||
tracing::error!(
|
||||
"Failed to delete mod files. \
|
||||
You might want to clean up the data directory manually. \
|
||||
Reason: {:?}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
event_sink
|
||||
.write()
|
||||
.await
|
||||
.submit_command(
|
||||
ACTION_FINISH_DELETE_SELECTED_MOD,
|
||||
SingleUse::new(info),
|
||||
Target::Auto,
|
||||
)
|
||||
.expect("failed to send command");
|
||||
}),
|
||||
AsyncAction::ResetDeployment(state) => tokio::spawn(async move {
|
||||
if let Err(err) = reset_mod_deployment(state).await {
|
||||
tracing::error!("Failed to reset mod deployment: {:?}", err);
|
||||
}
|
||||
|
||||
event_sink
|
||||
.write()
|
||||
.await
|
||||
.submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto)
|
||||
.expect("failed to send command");
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_log(
|
||||
event_sink: Arc<RwLock<ExtEventSink>>,
|
||||
log_queue: Arc<RwLock<UnboundedReceiver<String>>>,
|
||||
) {
|
||||
while let Some(line) = log_queue.write().await.recv().await {
|
||||
let event_sink = event_sink.clone();
|
||||
event_sink
|
||||
.write()
|
||||
.await
|
||||
.submit_command(ACTION_LOG, SingleUse::new(line), Target::Auto)
|
||||
.expect("failed to send command");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn work_thread(
|
||||
event_sink: Arc<RwLock<ExtEventSink>>,
|
||||
action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>,
|
||||
log_queue: Arc<RwLock<UnboundedReceiver<String>>>,
|
||||
) -> Result<()> {
|
||||
let rt = Runtime::new()?;
|
||||
|
||||
rt.block_on(async {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = handle_action(event_sink.clone(), action_queue.clone()) => {},
|
||||
_ = handle_log(event_sink.clone(), log_queue.clone()) => {},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,47 +1,43 @@
|
|||
// Rust Analyzer cannot properly determine that `cfg!(debug_assertions)` alone does not make code
|
||||
// unused. These sections should be small enough that no truly dead code slips in.
|
||||
use std::fmt::Result;
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod prod {
|
||||
use std::fmt::Result;
|
||||
use time::format_description::FormatItem;
|
||||
use time::macros::format_description;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::field::Field;
|
||||
use tracing::{Event, Metadata, Subscriber};
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::filter::FilterFn;
|
||||
use tracing_subscriber::fmt::format::{debug_fn, Writer};
|
||||
use tracing_subscriber::fmt::{self, FmtContext, FormatEvent, FormatFields};
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use time::format_description::FormatItem;
|
||||
use time::macros::format_description;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::field::Field;
|
||||
use tracing::{Event, Metadata, Subscriber};
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::filter::FilterFn;
|
||||
use tracing_subscriber::fmt::format::{debug_fn, Writer};
|
||||
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
pub const TIME_FORMAT: &[FormatItem] = format_description!("[hour]:[minute]:[second]");
|
||||
|
||||
const TIME_FORMAT: &[FormatItem] = format_description!("[hour]:[minute]:[second]");
|
||||
|
||||
fn format_field(w: &mut Writer<'_>, field: &Field, val: &dyn std::fmt::Debug) -> Result {
|
||||
pub fn format_field(w: &mut Writer<'_>, field: &Field, val: &dyn std::fmt::Debug) -> Result {
|
||||
if field.name() == "message" {
|
||||
write!(w, "{:?}", val)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn filter(metadata: &Metadata<'_>) -> bool {
|
||||
pub fn filter(metadata: &Metadata<'_>) -> bool {
|
||||
metadata
|
||||
.fields()
|
||||
.iter()
|
||||
.any(|field| field.name() == "message")
|
||||
}
|
||||
}
|
||||
|
||||
struct Formatter;
|
||||
pub struct Formatter;
|
||||
|
||||
impl<S, N> FormatEvent<S, N> for Formatter
|
||||
where
|
||||
impl<S, N> FormatEvent<S, N> for Formatter
|
||||
where
|
||||
S: Subscriber + for<'a> LookupSpan<'a>,
|
||||
N: for<'a> FormatFields<'a> + 'static,
|
||||
{
|
||||
{
|
||||
fn format_event(
|
||||
&self,
|
||||
ctx: &FmtContext<'_, S, N>,
|
||||
|
@ -59,55 +55,33 @@ mod prod {
|
|||
|
||||
writeln!(writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a subscriber that
|
||||
/// - only prints events that contain a message
|
||||
/// - does not print fields
|
||||
/// - does not print spans/targets
|
||||
/// - only prints time, not date
|
||||
pub fn create_tracing_subscriber() {
|
||||
let filter_layer = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::try_new("info").unwrap());
|
||||
pub fn create_tracing_subscriber() {
|
||||
let env_layer =
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::try_new("info").unwrap());
|
||||
|
||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||
let (dev_stdout_layer, prod_stdout_layer, filter_layer) = if cfg!(debug_assertions) {
|
||||
let fmt_layer = fmt::layer().pretty();
|
||||
(Some(fmt_layer), None, None)
|
||||
} else {
|
||||
// Creates a layer that
|
||||
// - only prints events that contain a message
|
||||
// - does not print fields
|
||||
// - does not print spans/targets
|
||||
// - only prints time, not date
|
||||
let fmt_layer = fmt::layer()
|
||||
.event_format(Formatter)
|
||||
.fmt_fields(debug_fn(format_field));
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(FilterFn::new(filter))
|
||||
.with(filter_layer)
|
||||
.with(fmt_layer)
|
||||
.with(ErrorLayer::new(
|
||||
tracing_subscriber::fmt::format::Pretty::default(),
|
||||
))
|
||||
.init();
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod dev {
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
pub fn create_tracing_subscriber() {
|
||||
let filter_layer = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::try_new("info").unwrap());
|
||||
let fmt_layer = tracing_subscriber::fmt::layer().pretty();
|
||||
(None, Some(fmt_layer), Some(FilterFn::new(filter)))
|
||||
};
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(filter_layer)
|
||||
.with(fmt_layer)
|
||||
.with(ErrorLayer::new(
|
||||
tracing_subscriber::fmt::format::Pretty::default(),
|
||||
))
|
||||
.with(env_layer)
|
||||
.with(dev_stdout_layer)
|
||||
.with(prod_stdout_layer)
|
||||
.with(ErrorLayer::new(fmt::format::Pretty::default()))
|
||||
.init();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub use dev::create_tracing_subscriber;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub use prod::create_tracing_subscriber;
|
||||
|
|
Loading…
Add table
Reference in a new issue