feat(dtmm): Implement log view

Ref: #7.
This commit is contained in:
Lucas Schwiderski 2023-02-27 16:32:29 +01:00
parent bb671c5fd2
commit 3895ab12d6
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8
8 changed files with 447 additions and 310 deletions

8
Cargo.lock generated
View file

@ -2293,9 +2293,9 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.19" version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53250a3b3fed8ff8fd988587d8925d26a83ac3845d9e03b220b37f34c2b8d6c2" checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
dependencies = [ dependencies = [
"itoa", "itoa",
"libc", "libc",
@ -2313,9 +2313,9 @@ checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.7" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a460aeb8de6dcb0f381e1ee05f1cd56fcf5a5f6eb8187ff3d8f0b11078d38b7c" checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
dependencies = [ dependencies = [
"time-core", "time-core",
] ]

80
crates/dtmm/src/log.rs Normal file
View 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();
}

View file

@ -1,170 +1,35 @@
#![recursion_limit = "256"] #![recursion_limit = "256"]
#![feature(let_chains)] #![feature(let_chains)]
use std::fs;
use std::io::ErrorKind;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use clap::command; use clap::command;
use clap::parser::ValueSource;
use clap::value_parser; use clap::value_parser;
use clap::Arg; use clap::Arg;
use color_eyre::eyre::Context; use color_eyre::eyre::Context;
use color_eyre::Report; use color_eyre::{Report, Result};
use color_eyre::Result;
use druid::AppLauncher; 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 tokio::sync::RwLock;
use crate::engine::deploy_mods; use crate::state::{Delegate, State};
use crate::state::{AsyncAction, Delegate, State, ACTION_FINISH_DEPLOY}; use crate::worker::work_thread;
mod controller; mod controller;
mod engine; mod engine;
mod log;
mod main_window; mod main_window;
mod state; mod state;
mod theme; mod theme;
mod util;
mod widget; mod widget;
mod worker;
#[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")
}
#[tracing::instrument] #[tracing::instrument]
fn main() -> Result<()> { fn main() -> Result<()> {
color_eyre::install()?; 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()); tracing::trace!(default_config_path = %default_config_path.display());
@ -185,78 +50,30 @@ fn main() -> Result<()> {
) )
.get_matches(); .get_matches();
dtmt_shared::create_tracing_subscriber(); let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel();
log::create_tracing_subscriber(log_tx);
unsafe { unsafe {
oodle_sys::init(matches.get_one::<String>("oodle")); oodle_sys::init(matches.get_one::<String>("oodle"));
} }
let config: Config = { let config =
let path = matches util::read_config(&default_config_path, &matches).wrap_err("failed to read config file")?;
.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 initial_state = State::new(config); let initial_state = State::new(config);
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
let delegate = Delegate::new(sender); let delegate = Delegate::new(action_tx);
let launcher = AppLauncher::with_window(main_window::new()).delegate(delegate); let launcher = AppLauncher::with_window(main_window::new()).delegate(delegate);
let event_sink = launcher.get_external_handle(); let event_sink = launcher.get_external_handle();
std::thread::spawn(move || { std::thread::spawn(move || {
let event_sink = Arc::new(RwLock::new(event_sink)); 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 { 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); tracing::error!("Work thread failed, restarting: {:?}", err);
} }
} }

View file

@ -1,10 +1,11 @@
use druid::im::Vector; use druid::im::Vector;
use druid::widget::{ use druid::widget::{
Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split, Align, Button, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment, Maybe,
TextBox, ViewSwitcher, Scroll, SizedBox, Split, TextBox, ViewSwitcher,
}; };
use druid::{ 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}; 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> { fn build_window() -> impl Widget<State> {
// TODO: Add borders between the sections
Flex::column() Flex::column()
.must_fill_main_axis(true) .must_fill_main_axis(true)
.with_child(build_top_bar()) .with_child(build_top_bar())
.with_flex_child(build_main(), 1.0) .with_flex_child(build_main(), 1.0)
.with_child(build_log_view())
} }

View file

@ -10,7 +10,7 @@ use druid::{
use dtmt_shared::ModConfig; use dtmt_shared::ModConfig;
use tokio::sync::mpsc::UnboundedSender; 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_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"); 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>> = pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<ModInfo>> =
Selector::new("dtmm.action.finish-add-mod"); 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)] #[derive(Copy, Clone, Data, Debug, PartialEq)]
pub(crate) enum View { pub(crate) enum View {
Mods, Mods,
@ -154,6 +156,7 @@ pub(crate) struct State {
game_dir: Arc<PathBuf>, game_dir: Arc<PathBuf>,
data_dir: Arc<PathBuf>, data_dir: Arc<PathBuf>,
ctx: Arc<sdk::Context>, ctx: Arc<sdk::Context>,
log: Arc<String>,
} }
impl State { impl State {
@ -170,8 +173,9 @@ impl State {
selected_mod_index: None, selected_mod_index: None,
is_deployment_in_progress: false, is_deployment_in_progress: false,
is_reset_in_progress: false, is_reset_in_progress: false,
game_dir: Arc::new(config.game_dir.unwrap_or_default()), game_dir: Arc::new(config.game_dir().cloned().unwrap_or_default()),
data_dir: Arc::new(config.data_dir.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> { pub(crate) fn get_ctx(&self) -> Arc<sdk::Context> {
self.ctx.clone() 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; pub(crate) struct SelectedModLens;
@ -454,6 +463,15 @@ impl AppDelegate<State> for Delegate {
} }
Handled::Yes 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 => { cmd => {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
tracing::warn!("Unknown command: {:?}", cmd); tracing::warn!("Unknown command: {:?}", cmd);

117
crates/dtmm/src/util.rs Normal file
View 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
View 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(())
}

View file

@ -1,8 +1,3 @@
// 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.
#[allow(dead_code)]
mod prod {
use std::fmt::Result; use std::fmt::Result;
use time::format_description::FormatItem; use time::format_description::FormatItem;
@ -13,14 +8,15 @@ mod prod {
use tracing_error::ErrorLayer; use tracing_error::ErrorLayer;
use tracing_subscriber::filter::FilterFn; use tracing_subscriber::filter::FilterFn;
use tracing_subscriber::fmt::format::{debug_fn, Writer}; use tracing_subscriber::fmt::format::{debug_fn, Writer};
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields}; use tracing_subscriber::fmt::{self, FmtContext, FormatEvent, FormatFields};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::prelude::*; use tracing_subscriber::prelude::*;
use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
const TIME_FORMAT: &[FormatItem] = format_description!("[hour]:[minute]:[second]"); pub 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" { if field.name() == "message" {
write!(w, "{:?}", val) write!(w, "{:?}", val)
} else { } else {
@ -28,14 +24,14 @@ mod prod {
} }
} }
fn filter(metadata: &Metadata<'_>) -> bool { pub fn filter(metadata: &Metadata<'_>) -> bool {
metadata metadata
.fields() .fields()
.iter() .iter()
.any(|field| field.name() == "message") .any(|field| field.name() == "message")
} }
struct Formatter; pub struct Formatter;
impl<S, N> FormatEvent<S, N> for Formatter impl<S, N> FormatEvent<S, N> for Formatter
where where
@ -61,53 +57,31 @@ mod prod {
} }
} }
/// 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() { pub fn create_tracing_subscriber() {
let filter_layer = EnvFilter::try_from_default_env() let env_layer =
.unwrap_or_else(|_| EnvFilter::try_new("info").unwrap()); 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) .event_format(Formatter)
.fmt_fields(debug_fn(format_field)); .fmt_fields(debug_fn(format_field));
tracing_subscriber::registry() (None, Some(fmt_layer), Some(FilterFn::new(filter)))
.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();
tracing_subscriber::registry() tracing_subscriber::registry()
.with(filter_layer) .with(filter_layer)
.with(fmt_layer) .with(env_layer)
.with(ErrorLayer::new( .with(dev_stdout_layer)
tracing_subscriber::fmt::format::Pretty::default(), .with(prod_stdout_layer)
)) .with(ErrorLayer::new(fmt::format::Pretty::default()))
.init(); .init();
} }
}
#[cfg(debug_assertions)]
pub use dev::create_tracing_subscriber;
#[cfg(not(debug_assertions))]
pub use prod::create_tracing_subscriber;