CLI Fixes & Tweaks (#2846)

Fixes scrolling on vscode using simple fix
 Adds scroll modifier, scroll at 5 lines per scroll while holding shift key.
 Adds error handling for manganis failure that still lets the build run.
 Revises TUI rendering code.
 Move TUI "info bars" to the bottom of the terminal.
 Revised logging system with tracing
 Working [c] clear keybind. This has been removed.
 Removal of [h] hide keybind text (it does nothing)
 Some opinionated cleanups and renames to make tui logic easier to understand.
 Rolling log file & maybe add some more "internal" logging. Closes CLI Rolling Log File #2764
 Removes log tabs. Closes CLI: Color-code logs and get rid of tabs #2857
 Combines info bars.
 Working and good text selection.
 Print launch URL in console.
 Handle log lines properly and add formatting.
 Move MessageSource to a more reasonable location.
 Add some background styling to powerline (info bar) - Tried this and it doesn't look the greatest.
 Log Filtering
 Final Cleaning & Changes - I could do this forever
 Test Linux

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Miles Murgaw 2024-09-13 04:34:19 -04:00 committed by GitHub
parent 87c2f64f13
commit 8d68886310
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1302 additions and 718 deletions

1
Cargo.lock generated
View file

@ -2520,6 +2520,7 @@ dependencies = [
"proc-macro2",
"ratatui",
"rayon",
"regex",
"reqwest",
"rsx-rosetta",
"rustls 0.23.12",

View file

@ -45,6 +45,7 @@ console = "0.15.8"
ctrlc = "3.2.3"
futures-channel = { workspace = true }
krates = { version = "0.17.0" }
regex = "1.10.6"
axum = { workspace = true, features = ["ws"] }
axum-server = { workspace = true, features = ["tls-rustls"] }

View file

@ -1,7 +1,6 @@
use crate::builder::{
BuildMessage, BuildRequest, MessageSource, MessageType, Stage, UpdateBuildProgress, UpdateStage,
};
use crate::builder::{BuildRequest, Stage, UpdateBuildProgress, UpdateStage};
use crate::Result;
use crate::TraceSrc;
use anyhow::Context;
use brotli::enc::BrotliEncoderParams;
use futures_channel::mpsc::UnboundedSender;
@ -13,19 +12,18 @@ use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use std::{ffi::OsString, path::PathBuf};
use std::{fs::File, io::Write};
use tracing::Level;
use walkdir::WalkDir;
/// The temp file name for passing manganis json from linker to current exec.
pub const MG_JSON_OUT: &str = "mg-out";
pub fn asset_manifest(build: &BuildRequest) -> AssetManifest {
pub fn asset_manifest(build: &BuildRequest) -> Option<AssetManifest> {
let file_path = build.target_out_dir().join(MG_JSON_OUT);
let read = fs::read_to_string(&file_path).unwrap();
let read = fs::read_to_string(&file_path).ok()?;
_ = fs::remove_file(file_path);
let json: Vec<String> = serde_json::from_str(&read).unwrap();
AssetManifest::load(json)
Some(AssetManifest::load(json))
}
/// Create a head file that contains all of the imports for assets that the user project uses
@ -58,17 +56,7 @@ pub(crate) fn process_assets(
match process_file(file_asset, &static_asset_output_dir) {
Ok(_) => {
// Update the progress
_ = progress.start_send(UpdateBuildProgress {
stage: Stage::OptimizingAssets,
update: UpdateStage::AddMessage(BuildMessage {
level: Level::INFO,
message: MessageType::Text(format!(
"Optimized static asset {}",
file_asset
)),
source: MessageSource::Build,
}),
});
tracing::info!(dx_src = ?TraceSrc::Build, "Optimized static asset {file_asset}");
let assets_finished =
assets_finished.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
_ = progress.start_send(UpdateBuildProgress {
@ -79,7 +67,7 @@ pub(crate) fn process_assets(
});
}
Err(err) => {
tracing::error!("Failed to copy static asset: {}", err);
tracing::error!(dx_src = ?TraceSrc::Build, "Failed to copy static asset: {}", err);
return Err(err);
}
}
@ -134,6 +122,7 @@ pub(crate) fn copy_dir_to(
copy_dir_to(entry_path.clone(), output_file_location, pre_compress)
{
tracing::error!(
dx_src = ?TraceSrc::Build,
"Failed to pre-compress directory {}: {}",
entry_path.display(),
err
@ -149,6 +138,7 @@ pub(crate) fn copy_dir_to(
if pre_compress {
if let Err(err) = pre_compress_file(&output_file_location) {
tracing::error!(
dx_src = ?TraceSrc::Build,
"Failed to pre-compress static assets {}: {}",
output_file_location.display(),
err
@ -206,7 +196,7 @@ pub(crate) fn pre_compress_folder(path: &Path, pre_compress: bool) -> std::io::R
if entry_path.is_file() {
if pre_compress {
if let Err(err) = pre_compress_file(entry_path) {
tracing::error!("Failed to pre-compress file {entry_path:?}: {err}");
tracing::error!(dx_src = ?TraceSrc::Build, "Failed to pre-compress file {entry_path:?}: {err}");
}
}
// If pre-compression isn't enabled, we should remove the old compressed file if it exists

View file

@ -12,6 +12,7 @@ use crate::builder::progress::UpdateBuildProgress;
use crate::builder::progress::UpdateStage;
use crate::link::LinkCommand;
use crate::Result;
use crate::TraceSrc;
use anyhow::Context;
use dioxus_cli_config::Platform;
use futures_channel::mpsc::UnboundedSender;
@ -19,6 +20,7 @@ use manganis_cli_support::AssetManifest;
use manganis_cli_support::ManganisSupportGuard;
use std::fs::create_dir_all;
use std::path::PathBuf;
use tracing::error;
impl BuildRequest {
/// Create a list of arguments for cargo builds
@ -101,7 +103,11 @@ impl BuildRequest {
&self,
mut progress: UnboundedSender<UpdateBuildProgress>,
) -> Result<BuildResult> {
tracing::info!("🚅 Running build [Desktop] command...");
tracing::info!(
dx_src = ?TraceSrc::Build,
"Running build [{}] command...",
self.target_platform,
);
// Set up runtime guards
let mut dioxus_version = crate::dx_build_info::PKG_VERSION.to_string();
@ -136,8 +142,9 @@ impl BuildRequest {
.context("Failed to post process build")?;
tracing::info!(
"🚩 Build completed: [{}]",
self.dioxus_crate.out_dir().display()
dx_src = ?TraceSrc::Build,
"Build completed: [{}]",
self.dioxus_crate.out_dir().display(),
);
_ = progress.start_send(UpdateBuildProgress {
@ -222,7 +229,10 @@ impl BuildRequest {
cargo_args,
Some(linker_args),
)?;
let assets = asset_manifest(&build);
let Some(assets) = asset_manifest(&build) else {
error!(dx_src = ?TraceSrc::Build, "the asset manifest was not provided by manganis and we were not able to collect assets");
return Err(anyhow::anyhow!("asset manifest was not provided by manganis"));
};
// Collect assets from the asset manifest the linker intercept created
process_assets(&build, &assets, &mut progress)?;
// Create the __assets_head.html file for bundling
@ -235,7 +245,7 @@ impl BuildRequest {
}
pub fn copy_assets_dir(&self) -> anyhow::Result<()> {
tracing::info!("Copying public assets to the output directory...");
tracing::info!(dx_src = ?TraceSrc::Build, "Copying public assets to the output directory...");
let out_dir = self.target_out_dir();
let asset_dir = self.dioxus_crate.asset_dir();

View file

@ -1,7 +1,7 @@
use crate::build::Build;
use crate::cli::serve::ServeArguments;
use crate::dioxus_crate::DioxusCrate;
use crate::Result;
use crate::{build::Build, TraceSrc};
use dioxus_cli_config::{Platform, RuntimeCLIArguments};
use futures_util::stream::select_all;
use futures_util::StreamExt;
@ -15,9 +15,7 @@ mod fullstack;
mod prepare_html;
mod progress;
mod web;
pub use progress::{
BuildMessage, MessageSource, MessageType, Stage, UpdateBuildProgress, UpdateStage,
};
pub use progress::{Stage, UpdateBuildProgress, UpdateStage};
/// The target platform for the build
/// This is very similar to the Platform enum, but we need to be able to differentiate between the
@ -167,13 +165,36 @@ impl BuildResult {
fullstack_address: Option<SocketAddr>,
workspace: &std::path::Path,
) -> std::io::Result<Option<Child>> {
if self.target_platform == TargetPlatform::Web {
return Ok(None);
}
if self.target_platform == TargetPlatform::Server {
tracing::trace!("Proxying fullstack server from port {fullstack_address:?}");
match self.target_platform {
TargetPlatform::Web => {
tracing::info!(dx_src = ?TraceSrc::Dev, "Serving web app on http://{} 🎉", serve.address.address());
return Ok(None);
}
TargetPlatform::Desktop => {
tracing::info!(dx_src = ?TraceSrc::Dev, "Launching desktop app at {} 🎉", self.executable.display());
}
TargetPlatform::Server => {
if let Some(fullstack_address) = fullstack_address {
tracing::info!(
dx_src = ?TraceSrc::Dev,
"Launching fullstack server on http://{:?} 🎉",
fullstack_address
);
}
}
TargetPlatform::Liveview => {
if let Some(fullstack_address) = fullstack_address {
tracing::info!(
dx_src = ?TraceSrc::Dev,
"Launching liveview server on http://{:?} 🎉",
fullstack_address
);
}
}
}
tracing::info!(dx_src = ?TraceSrc::Dev, "Press [o] to open the app manually.");
let arguments = RuntimeCLIArguments::new(serve.address.address(), fullstack_address);
let executable = self.executable.canonicalize()?;
let mut cmd = Command::new(executable);

View file

@ -1,14 +1,12 @@
//! Build the HTML file to load a web application. The index.html file may be created from scratch or modified from the `index.html` file in the crate root.
use super::{BuildRequest, UpdateBuildProgress};
use crate::builder::progress::MessageSource;
use crate::builder::Stage;
use crate::Result;
use crate::TraceSrc;
use futures_channel::mpsc::UnboundedSender;
use manganis_cli_support::AssetManifest;
use std::fmt::Write;
use std::path::{Path, PathBuf};
use tracing::Level;
const DEFAULT_HTML: &str = include_str!("../../assets/index.html");
const TOAST_HTML: &str = include_str!("../../assets/toast.html");
@ -17,12 +15,12 @@ impl BuildRequest {
pub(crate) fn prepare_html(
&self,
assets: Option<&AssetManifest>,
progress: &mut UnboundedSender<UpdateBuildProgress>,
_progress: &mut UnboundedSender<UpdateBuildProgress>,
) -> Result<String> {
let mut html = html_or_default(&self.dioxus_crate.crate_dir());
// Inject any resources from the config into the html
self.inject_resources(&mut html, assets, progress)?;
self.inject_resources(&mut html, assets)?;
// Inject loading scripts if they are not already present
self.inject_loading_scripts(&mut html);
@ -38,12 +36,7 @@ impl BuildRequest {
}
// Inject any resources from the config into the html
fn inject_resources(
&self,
html: &mut String,
assets: Option<&AssetManifest>,
progress: &mut UnboundedSender<UpdateBuildProgress>,
) -> Result<()> {
fn inject_resources(&self, html: &mut String, assets: Option<&AssetManifest>) -> Result<()> {
// Collect all resources into a list of styles and scripts
let resources = &self.dioxus_crate.dioxus_config.web.resource;
let mut style_list = resources.style.clone().unwrap_or_default();
@ -65,7 +58,7 @@ impl BuildRequest {
}
if !style_list.is_empty() {
self.send_resource_deprecation_warning(progress, style_list, ResourceType::Style);
self.send_resource_deprecation_warning(style_list, ResourceType::Style);
}
// Add all scripts to the head
@ -78,7 +71,7 @@ impl BuildRequest {
}
if !script_list.is_empty() {
self.send_resource_deprecation_warning(progress, script_list, ResourceType::Script);
self.send_resource_deprecation_warning(script_list, ResourceType::Script);
}
// Inject any resources from manganis into the head
@ -139,13 +132,8 @@ impl BuildRequest {
*html = html.replace("{app_name}", app_name);
}
fn send_resource_deprecation_warning(
&self,
progress: &mut UnboundedSender<UpdateBuildProgress>,
paths: Vec<PathBuf>,
variant: ResourceType,
) {
const RESOURCE_DEPRECATION_MESSAGE: &str = r#"The `web.resource` config has been deprecated in favor of head components and will be removed in a future release. Instead of including assets in the config, you can include assets with the `asset!` macro and add them to the head with `head::Link` and `Script` components."#;
fn send_resource_deprecation_warning(&self, paths: Vec<PathBuf>, variant: ResourceType) {
const RESOURCE_DEPRECATION_MESSAGE: &str = r#"The `web.resource` config has been deprecated in favor of head components and will be removed in a future release."#;
let replacement_components = paths
.iter()
@ -187,14 +175,7 @@ impl BuildRequest {
"{RESOURCE_DEPRECATION_MESSAGE}\nTo migrate to head components, remove `{section_name}` and include the following rsx in your root component:\n```rust\n{replacement_components}\n```"
);
_ = progress.unbounded_send(UpdateBuildProgress {
stage: Stage::OptimizingWasm,
update: super::UpdateStage::AddMessage(super::BuildMessage {
level: Level::WARN,
message: super::MessageType::Text(message),
source: MessageSource::Build,
}),
});
tracing::warn!(dx_src = ?TraceSrc::Build, "{}", message);
}
}

View file

@ -1,16 +1,15 @@
//! Report progress about the build to the user. We use channels to report progress back to the CLI.
use crate::TraceSrc;
use super::BuildRequest;
use anyhow::Context;
use cargo_metadata::{diagnostic::Diagnostic, Message};
use cargo_metadata::Message;
use futures_channel::mpsc::UnboundedSender;
use serde::Deserialize;
use std::fmt::Display;
use std::ops::Deref;
use std::path::PathBuf;
use std::process::Stdio;
use tokio::io::AsyncBufReadExt;
use tracing::Level;
use super::BuildRequest;
#[derive(Default, Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy)]
pub enum Stage {
@ -54,14 +53,6 @@ impl UpdateBuildProgress {
pub fn to_std_out(&self) {
match &self.update {
UpdateStage::Start => println!("--- {} ---", self.stage),
UpdateStage::AddMessage(message) => match &message.message {
MessageType::Cargo(message) => {
println!("{}", message.rendered.clone().unwrap_or_default());
}
MessageType::Text(message) => {
println!("{}", message);
}
},
UpdateStage::SetProgress(progress) => {
println!("Build progress {:0.0}%", progress * 100.0);
}
@ -75,70 +66,10 @@ impl UpdateBuildProgress {
#[derive(Debug, Clone, PartialEq)]
pub enum UpdateStage {
Start,
AddMessage(BuildMessage),
SetProgress(f64),
Failed(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct BuildMessage {
pub level: Level,
pub message: MessageType,
pub source: MessageSource,
}
#[derive(Debug, Clone, PartialEq)]
pub enum MessageType {
Cargo(Diagnostic),
Text(String),
}
/// Represents the source of where a message came from.
///
/// The CLI will render a prefix according to the message type
/// but this prefix, [`MessageSource::to_string()`] shouldn't be used if a strict message source is required.
#[derive(Debug, Clone, PartialEq)]
pub enum MessageSource {
/// Represents any message from the running application. Renders `[app]`
App,
/// Represents any generic message from the CLI. Renders `[dev]`
///
/// Usage of Tracing inside of the CLI will be routed to this type.
Dev,
/// Represents a message from the build process. Renders `[bld]`
///
/// This is anything emitted from a build process such as cargo and optimizations.
Build,
}
impl Display for MessageSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::App => write!(f, "app"),
Self::Dev => write!(f, "dev"),
Self::Build => write!(f, "bld"),
}
}
}
impl From<Diagnostic> for BuildMessage {
fn from(message: Diagnostic) -> Self {
Self {
level: match message.level {
cargo_metadata::diagnostic::DiagnosticLevel::Ice
| cargo_metadata::diagnostic::DiagnosticLevel::FailureNote
| cargo_metadata::diagnostic::DiagnosticLevel::Error => Level::ERROR,
cargo_metadata::diagnostic::DiagnosticLevel::Warning => Level::WARN,
cargo_metadata::diagnostic::DiagnosticLevel::Note => Level::INFO,
cargo_metadata::diagnostic::DiagnosticLevel::Help => Level::DEBUG,
_ => Level::DEBUG,
},
source: MessageSource::Build,
message: MessageType::Cargo(message),
}
}
}
pub(crate) async fn build_cargo(
crate_count: usize,
mut cmd: tokio::process::Command,
@ -183,10 +114,8 @@ pub(crate) async fn build_cargo(
match message {
Message::CompilerMessage(msg) => {
let message = msg.message;
_ = progress.start_send(UpdateBuildProgress {
stage: Stage::Compiling,
update: UpdateStage::AddMessage(message.clone().into()),
});
tracing::info!(dx_src = ?TraceSrc::Cargo, dx_no_fmt = true, "{}", message.to_string());
const WARNING_LEVELS: &[cargo_metadata::diagnostic::DiagnosticLevel] = &[
cargo_metadata::diagnostic::DiagnosticLevel::Help,
cargo_metadata::diagnostic::DiagnosticLevel::Note,
@ -230,14 +159,7 @@ pub(crate) async fn build_cargo(
}
}
Message::TextLine(line) => {
_ = progress.start_send(UpdateBuildProgress {
stage: Stage::Compiling,
update: UpdateStage::AddMessage(BuildMessage {
level: Level::DEBUG,
message: MessageType::Text(line),
source: MessageSource::Build,
}),
});
tracing::info!(dx_src = ?TraceSrc::Cargo, dx_no_fmt = true, "{}", line);
}
_ => {
// Unknown message

View file

@ -5,6 +5,7 @@ use crate::builder::progress::Stage;
use crate::builder::progress::UpdateBuildProgress;
use crate::builder::progress::UpdateStage;
use crate::error::{Error, Result};
use crate::TraceSrc;
use futures_channel::mpsc::UnboundedSender;
use manganis_cli_support::AssetManifest;
use std::path::Path;
@ -14,7 +15,7 @@ use wasm_bindgen_cli_support::Bindgen;
// Attempt to automatically recover from a bindgen failure by updating the wasm-bindgen version
async fn update_wasm_bindgen_version() -> Result<()> {
let cli_bindgen_version = wasm_bindgen_shared::version();
tracing::info!("Attempting to recover from bindgen failure by setting the wasm-bindgen version to {cli_bindgen_version}...");
tracing::info!(dx_src = ?TraceSrc::Build, "Attempting to recover from bindgen failure by setting the wasm-bindgen version to {cli_bindgen_version}...");
let output = Command::new("cargo")
.args([
@ -29,7 +30,7 @@ async fn update_wasm_bindgen_version() -> Result<()> {
let mut error_message = None;
if let Ok(output) = output {
if output.status.success() {
tracing::info!("Successfully updated wasm-bindgen to {cli_bindgen_version}");
tracing::info!(dx_src = ?TraceSrc::Dev, "Successfully updated wasm-bindgen to {cli_bindgen_version}");
return Ok(());
} else {
error_message = Some(output);
@ -37,7 +38,7 @@ async fn update_wasm_bindgen_version() -> Result<()> {
}
if let Some(output) = error_message {
tracing::error!("Failed to update wasm-bindgen: {:#?}", output);
tracing::error!(dx_src = ?TraceSrc::Dev, "Failed to update wasm-bindgen: {:#?}", output);
}
Err(Error::BuildFailed(format!("WASM bindgen build failed!\nThis is probably due to the Bindgen version, dioxus-cli is using `{cli_bindgen_version}` which is not compatible with your crate.\nPlease reinstall the dioxus cli to fix this issue.\nYou can reinstall the dioxus cli by running `cargo install dioxus-cli --force` and then rebuild your project")))
@ -57,7 +58,7 @@ pub(crate) async fn install_web_build_tooling(
stage: Stage::InstallingWasmTooling,
update: UpdateStage::Start,
});
tracing::info!("wasm32-unknown-unknown target not detected, installing..");
tracing::info!(dx_src = ?TraceSrc::Build, "`wasm32-unknown-unknown` target not detected, installing..");
let _ = Command::new("rustup")
.args(["target", "add", "wasm32-unknown-unknown"])
.output()
@ -70,7 +71,7 @@ pub(crate) async fn install_web_build_tooling(
impl BuildRequest {
async fn run_wasm_bindgen(&self, input_path: &Path, bindgen_outdir: &Path) -> Result<()> {
tracing::info!("Running wasm-bindgen");
tracing::info!(dx_src = ?TraceSrc::Build, "Running wasm-bindgen");
let input_path = input_path.to_path_buf();
let bindgen_outdir = bindgen_outdir.to_path_buf();
let keep_debug =
@ -99,7 +100,7 @@ impl BuildRequest {
// WASM bindgen requires the exact version of the bindgen schema to match the version the CLI was built with
// If we get an error, we can try to recover by pinning the user's wasm-bindgen version to the version we used
if let Err(err) = bindgen_result {
tracing::error!("Bindgen build failed: {:?}", err);
tracing::error!(dx_src = ?TraceSrc::Build, "Bindgen build failed: {:?}", err);
update_wasm_bindgen_version().await?;
run_wasm_bindgen();
}
@ -137,7 +138,7 @@ impl BuildRequest {
if self.build_arguments.release {
use dioxus_cli_config::WasmOptLevel;
tracing::info!("Running optimization with wasm-opt...");
tracing::info!(dx_src = ?TraceSrc::Build, "Running optimization with wasm-opt...");
let mut options = match self.dioxus_crate.dioxus_config.web.wasm_opt.level {
WasmOptLevel::Z => {
wasm_opt::OptimizationOptions::new_optimize_for_size_aggressively()
@ -162,6 +163,7 @@ impl BuildRequest {
.map_err(|err| Error::Other(anyhow::anyhow!(err)))?;
let new_size = wasm_file.metadata()?.len();
tracing::info!(
dx_src = ?TraceSrc::Build,
"wasm-opt reduced WASM size from {} to {} ({:2}%)",
old_size,
new_size,

View file

@ -1,4 +1,5 @@
use crate::build::TargetArgs;
use crate::TraceSrc;
use crate::{metadata::crate_root, CliSettings};
use super::*;
@ -26,6 +27,9 @@ pub enum Config {
/// Create a custom html file.
CustomHtml {},
/// Print the location of the CLI log file.
LogFile {},
/// Set CLI settings.
#[command(subcommand)]
Set(Setting),
@ -92,7 +96,7 @@ impl Config {
.replace("{{project-name}}", &name)
.replace("{{default-platform}}", &platform);
file.write_all(content.as_bytes())?;
tracing::info!("🚩 Init config file completed.");
tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 Init config file completed.");
}
Config::FormatPrint {} => {
println!(
@ -105,7 +109,11 @@ impl Config {
let mut file = File::create(html_path)?;
let content = include_str!("../../assets/index.html");
file.write_all(content.as_bytes())?;
tracing::info!("🚩 Create custom html file done.");
tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 Create custom html file done.");
}
Config::LogFile {} => {
let log_path = crate::tracer::log_path();
tracing::info!(dx_src = ?TraceSrc::Dev, "Log file is located at {}", log_path.display());
}
// Handle CLI settings.
Config::Set(setting) => {
@ -121,7 +129,7 @@ impl Config {
settings.wsl_file_poll_interval = Some(value)
}
})?;
tracing::info!("🚩 CLI setting `{setting}` has been set.");
tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 CLI setting `{setting}` has been set.");
}
}
Ok(())

View file

@ -1,4 +1,5 @@
use super::*;
use crate::TraceSrc;
use cargo_generate::{GenerateArgs, TemplatePath};
use cargo_metadata::Metadata;
use std::path::Path;
@ -134,9 +135,9 @@ pub fn post_create(path: &Path, metadata: Option<Metadata>) -> Result<()> {
let cmd = cmd.arg("fmt").current_dir(path);
let output = cmd.output().expect("failed to execute process");
if !output.status.success() {
tracing::error!("cargo fmt failed");
tracing::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
tracing::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
tracing::error!(dx_src = ?TraceSrc::Dev, "cargo fmt failed");
tracing::error!(dx_src = ?TraceSrc::Build, "stdout: {}", String::from_utf8_lossy(&output.stdout));
tracing::error!(dx_src = ?TraceSrc::Build, "stderr: {}", String::from_utf8_lossy(&output.stderr));
}
// 3. Format the `Cargo.toml` and `Dioxus.toml` files.
@ -166,7 +167,7 @@ pub fn post_create(path: &Path, metadata: Option<Metadata>) -> Result<()> {
let mut file = std::fs::File::create(readme_path)?;
file.write_all(new_readme.as_bytes())?;
tracing::info!("Generated project at {}", path.display());
tracing::info!(dx_src = ?TraceSrc::Dev, "Generated project at {}", path.display());
Ok(())
}

View file

@ -1,8 +1,4 @@
use crate::{
settings::{self},
tracer::CLILogControl,
DioxusCrate,
};
use crate::{settings, tracer::CLILogControl, DioxusCrate};
use anyhow::Context;
use build::Build;
use dioxus_cli_config::AddressArguments;

View file

@ -2,6 +2,8 @@ use std::process::exit;
use dioxus_rsx::{BodyNode, CallBody, TemplateBody};
use crate::TraceSrc;
use super::*;
/// Translate some source file into Dioxus code
@ -106,7 +108,7 @@ fn indent_and_write(raw: &str, idx: usize, out: &mut String) {
fn determine_input(file: Option<String>, raw: Option<String>) -> Result<String> {
// Make sure not both are specified
if file.is_some() && raw.is_some() {
tracing::error!("Only one of --file or --raw should be specified.");
tracing::error!(dx_src = ?TraceSrc::Dev, "Only one of --file or --raw should be specified.");
exit(0);
}

View file

@ -3,26 +3,22 @@
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
pub mod assets;
pub mod builder;
pub mod cli;
pub mod dioxus_crate;
pub mod dx_build_info;
pub mod error;
pub mod metadata;
pub mod serve;
pub mod settings;
pub mod tools;
pub mod tracer;
pub mod cli;
pub use cli::*;
pub mod error;
pub use error::*;
pub(crate) mod builder;
mod dioxus_crate;
pub use dioxus_crate::*;
mod settings;
pub(crate) use cli::*;
pub(crate) use dioxus_crate::*;
pub(crate) use error::*;
pub(crate) use settings::*;
pub(crate) mod metadata;
pub(crate) use tracer::{TraceMsg, TraceSrc};
use anyhow::Context;
use clap::Parser;

View file

@ -1,11 +1,14 @@
use std::future::{poll_fn, Future, IntoFuture};
use std::task::Poll;
use crate::builder::{Stage, TargetPlatform, UpdateBuildProgress, UpdateStage};
use crate::cli::serve::Serve;
use crate::dioxus_crate::DioxusCrate;
use crate::tracer::CLILogControl;
use crate::Result;
use crate::{
builder::{Stage, TargetPlatform, UpdateBuildProgress, UpdateStage},
TraceSrc,
};
use futures_util::FutureExt;
use tokio::task::yield_now;
@ -54,6 +57,8 @@ pub async fn serve_all(
dioxus_crate: DioxusCrate,
log_control: CLILogControl,
) -> Result<()> {
// Start the screen first so we collect build logs.
let mut screen = Output::start(&serve, log_control).expect("Failed to open terminal logger");
let mut builder = Builder::new(&dioxus_crate, &serve);
// Start the first build
@ -61,7 +66,6 @@ pub async fn serve_all(
let mut server = Server::start(&serve, &dioxus_crate);
let mut watcher = Watcher::start(&serve, &dioxus_crate);
let mut screen = Output::start(&serve, log_control).expect("Failed to open terminal logger");
let is_hot_reload = serve.server_arguments.hot_reload.unwrap_or(true);
@ -81,15 +85,21 @@ pub async fn serve_all(
}
let changed_files = watcher.dequeue_changed_files(&dioxus_crate);
let changed = changed_files.first().cloned();
// if change is hotreloadable, hotreload it
// and then send that update to all connected clients
if let Some(hr) = watcher.attempt_hot_reload(&dioxus_crate, changed_files) {
// Only send a hotreload message for templates and assets - otherwise we'll just get a full rebuild
if hr.templates.is_empty() && hr.assets.is_empty() {
if hr.templates.is_empty() && hr.assets.is_empty() && hr.unknown_files.is_empty() {
continue
}
if let Some(changed_path) = changed {
let path_relative = changed_path.strip_prefix(dioxus_crate.crate_dir()).map(|p| p.display().to_string()).unwrap_or_else(|_| changed_path.display().to_string());
tracing::info!(dx_src = ?TraceSrc::Dev, "Hotreloaded {}", path_relative);
}
server.send_hotreload(hr).await;
} else {
// If the change is not binary patchable, rebuild the project
@ -129,7 +139,7 @@ pub async fn serve_all(
match application {
Ok(BuilderUpdate::Progress { platform, update }) => {
let update_clone = update.clone();
screen.new_build_logs(platform, update_clone);
screen.new_build_progress(platform, update_clone);
server.update_build_status(screen.build_progress.progress(), update.stage.to_string()).await;
match update {
@ -151,7 +161,7 @@ pub async fn serve_all(
match child {
Ok(Some(child_proc)) => builder.children.push((build_result.target_platform, child_proc)),
Err(e) => {
tracing::error!("Failed to open build result: {e}");
tracing::error!(dx_src = ?TraceSrc::Build, "Failed to open build result: {e}");
break;
},
_ => {}
@ -176,11 +186,11 @@ pub async fn serve_all(
break;
}
else {
tracing::error!("Application exited with status: {status}");
tracing::error!(dx_src = ?TraceSrc::Dev, "Application exited with status: {status}");
}
},
Err(e) => {
tracing::error!("Application exited with error: {e}");
tracing::error!(dx_src = ?TraceSrc::Dev, "Application exited with error: {e}");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,487 @@
use super::{BuildProgress, TraceMsg, TraceSrc};
use ansi_to_tui::IntoText as _;
use dioxus_cli_config::Platform;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style, Stylize},
text::{Line, Span, Text},
widgets::{Block, Borders, Clear, List, ListState, Paragraph, Widget, Wrap},
Frame,
};
use regex::Regex;
use std::fmt::Write as _;
use std::rc::Rc;
use tracing::Level;
pub struct TuiLayout {
/// The entire TUI body.
_body: Rc<[Rect]>,
/// The console where build logs are displayed.
console: Rc<[Rect]>,
// The filter drawer if the drawer is open.
filter_drawer: Option<Rc<[Rect]>>,
// The border that separates the console and info bars.
border_sep: Rect,
// The status bar that displays build status, platform, versions, etc.
status_bar: Rc<[Rect]>,
// Misc
filter_list_state: ListState,
}
impl TuiLayout {
pub fn new(frame_size: Rect, filter_open: bool) -> Self {
// The full layout
let body = Layout::default()
.direction(Direction::Vertical)
.constraints([
// Footer Status
Constraint::Length(1),
// Border Separator
Constraint::Length(1),
// Console
Constraint::Fill(1),
// Padding
Constraint::Length(1),
])
.split(frame_size);
let mut console_constraints = vec![Constraint::Fill(1)];
if filter_open {
console_constraints.push(Constraint::Length(1));
console_constraints.push(Constraint::Length(25));
}
// Build the console, where logs go.
let console = Layout::default()
.direction(Direction::Horizontal)
.constraints(console_constraints)
.split(body[2]);
let filter_drawer = match filter_open {
false => None,
true => Some(
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(1),
Constraint::Fill(1),
Constraint::Length(1),
])
.split(console[2]),
),
};
// Build the status bar.
let status_bar = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Fill(1), Constraint::Fill(1)])
.split(body[0]);
// Specify borders
let border_sep_top = body[1];
Self {
_body: body,
console,
filter_drawer,
border_sep: border_sep_top,
status_bar,
filter_list_state: ListState::default(),
}
}
/// Render all decorations.
pub fn render_decor(&self, frame: &mut Frame, filter_open: bool) {
frame.render_widget(
Block::new()
.borders(Borders::TOP)
.border_style(Style::new().white()),
self.border_sep,
);
if filter_open {
frame.render_widget(
Block::new()
.borders(Borders::LEFT)
.border_style(Style::new().white()),
self.console[1],
);
}
}
/// Render the console and it's logs, returning the number of lines required to render the entire log output.
pub fn render_console(
&self,
frame: &mut Frame,
scroll_position: u16,
messages: &[TraceMsg],
enabled_filters: &[String],
) -> u16 {
const LEVEL_MAX: usize = "BUILD: ".len();
let mut out_text = Text::default();
// Assemble the messages
for msg in messages.iter() {
let mut sub_line_padding = 0;
let text = msg.content.trim_end().into_text().unwrap_or_default();
for (idx, line) in text.lines.into_iter().enumerate() {
// Don't add any formatting for cargo messages.
let out_line = if msg.source != TraceSrc::Cargo {
if idx == 0 {
match msg.source {
TraceSrc::Dev => {
let mut spans = vec![Span::from(" DEV: ").light_magenta()];
for span in line.spans {
spans.push(span);
}
spans
}
TraceSrc::Build => {
let mut spans = vec![Span::from("BUILD: ").light_blue()];
for span in line.spans {
spans.push(span);
}
spans
}
_ => {
// Build level tag: `INFO:``
// We don't subtract 1 here for `:` because we still want at least 1 padding.
let padding =
build_msg_padding(LEVEL_MAX - msg.level.to_string().len() - 2);
let level = format!("{padding}{}: ", msg.level);
sub_line_padding += level.len();
let level_span = Span::from(level);
let level_span = match msg.level {
Level::TRACE => level_span.black(),
Level::DEBUG => level_span.light_magenta(),
Level::INFO => level_span.light_green(),
Level::WARN => level_span.light_yellow(),
Level::ERROR => level_span.light_red(),
};
let mut out_line = vec![level_span];
for span in line.spans {
out_line.push(span);
}
out_line
}
}
} else {
// Not the first line. Append the padding and merge into list.
let padding = build_msg_padding(sub_line_padding);
let mut out_line = vec![Span::from(padding)];
for span in line.spans {
out_line.push(span);
}
out_line
}
} else {
line.spans
};
out_text.push_line(Line::from(out_line));
}
}
// Only show messages for filters that are enabled.
let mut included_line_ids = Vec::new();
for filter in enabled_filters {
let re = Regex::new(filter);
for (index, line) in out_text.lines.iter().enumerate() {
let line_str = line.to_string();
match re {
Ok(ref re) => {
// sort by provided regex
if re.is_match(&line_str) {
included_line_ids.push(index);
}
}
Err(_) => {
// default to basic string storing
if line_str.contains(filter) {
included_line_ids.push(index);
}
}
}
}
}
included_line_ids.sort_unstable();
included_line_ids.dedup();
let out_lines = out_text.lines;
let mut out_text = Text::default();
if enabled_filters.is_empty() {
for line in out_lines {
out_text.push_line(line.clone());
}
} else {
for id in included_line_ids {
if let Some(line) = out_lines.get(id) {
out_text.push_line(line.clone());
}
}
}
let (console_width, _console_height) = self.get_console_size();
let paragraph = Paragraph::new(out_text)
.left_aligned()
.wrap(Wrap { trim: false });
let num_lines_wrapping = paragraph.line_count(console_width) as u16;
paragraph
.scroll((scroll_position, 0))
.render(self.console[0], frame.buffer_mut());
num_lines_wrapping
}
/// Render the status bar.
pub fn render_status_bar(
&self,
frame: &mut Frame,
_platform: Platform,
build_progress: &BuildProgress,
more_modal_open: bool,
filter_menu_open: bool,
dx_version: &str,
) {
// left aligned text
let mut spans = vec![
Span::from("🧬 dx").white(),
Span::from(" ").white(),
Span::from(dx_version).white(),
Span::from(" | ").dark_gray(),
];
// If there is build progress, render the current status.
let is_build_progress = !build_progress.current_builds.is_empty();
if is_build_progress {
// If the build failed, show a failed status.
// Otherwise, render current status.
let build_failed = build_progress
.current_builds
.values()
.any(|b| b.failed.is_some());
if build_failed {
spans.push(Span::from("Build failed ❌").red());
} else {
// spans.push(Span::from("status: ").gray());
let build = build_progress
.current_builds
.values()
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap();
spans.extend_from_slice(&build.make_spans(Rect::new(
0,
0,
build.max_layout_size(),
1,
)));
}
}
// right aligned text
let more_span = Span::from("[/] more");
let more_span = match more_modal_open {
true => more_span.light_yellow(),
false => more_span.gray(),
};
let filter_span = Span::from("[f] filter");
let filter_span = match filter_menu_open {
true => filter_span.light_yellow(),
false => filter_span.gray(),
};
// Right-aligned text
let right_line = Line::from(vec![
Span::from("[o] open").gray(),
Span::from(" | ").gray(),
Span::from("[r] rebuild").gray(),
Span::from(" | ").gray(),
filter_span,
Span::from(" | ").dark_gray(),
more_span,
]);
frame.render_widget(
Paragraph::new(Line::from(spans)).left_aligned(),
self.status_bar[0],
);
// Render the info
frame.render_widget(
Paragraph::new(right_line).right_aligned(),
self.status_bar[1],
);
}
/// Renders the "more" modal to show extra info/keybinds accessible via the more keybind.
pub fn render_more_modal(&self, frame: &mut Frame) {
let modal = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Fill(1), Constraint::Length(5)])
.split(self.console[0])[1];
frame.render_widget(Clear, modal);
frame.render_widget(Block::default().borders(Borders::ALL), modal);
// Render under construction message
frame.render_widget(
Paragraph::new("Under construction, please check back at a later date!")
.alignment(Alignment::Center),
modal,
);
}
/// Render the filter drawer menu.
pub fn render_filter_menu(
&mut self,
frame: &mut Frame,
filters: &[(String, bool)],
selected_filter_index: usize,
search_mode: bool,
search_input: Option<&String>,
) {
let Some(ref filter_drawer) = self.filter_drawer else {
return;
};
// Vertical layout
let container = Layout::default()
.constraints([
Constraint::Length(4),
Constraint::Fill(1),
Constraint::Length(7),
])
.direction(Direction::Vertical)
.split(filter_drawer[1]);
// Render the search section.
let top_area = Layout::default()
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
])
.direction(Direction::Vertical)
.split(container[0]);
let search_title = Line::from("Search").gray();
let search_input_block = Block::new().bg(Color::White);
let search_text = match search_input {
Some(s) => s,
None => {
if search_mode {
"..."
} else {
"[enter] to type..."
}
}
};
let search_input = Paragraph::new(Line::from(search_text))
.fg(Color::Black)
.block(search_input_block);
frame.render_widget(search_title, top_area[1]);
frame.render_widget(search_input, top_area[2]);
// Render the filters
let list_area = container[1];
let mut list_items = Vec::new();
for (filter, enabled) in filters {
let filter = Span::from(filter);
let filter = match enabled {
true => filter.light_yellow(),
false => filter.dark_gray(),
};
list_items.push(filter);
}
list_items.reverse();
let list = List::new(list_items).highlight_symbol("» ");
self.filter_list_state.select(Some(selected_filter_index));
frame.render_stateful_widget(list, list_area, &mut self.filter_list_state);
// Render the keybind list at the bottom.
let keybinds = container[2];
let lines = vec![
Line::from(""),
Line::from("[↑] Up").white(),
Line::from("[↓] Down").white(),
Line::from("[←] Remove").white(),
Line::from("[→] Toggle").white(),
Line::from("[enter] Type / Submit").white(),
];
let text = Text::from(lines);
frame.render_widget(text, keybinds);
}
/// Returns the height of the console TUI area in number of lines.
pub fn get_console_size(&self) -> (u16, u16) {
(self.console[0].width, self.console[0].height)
}
/// Render the current scroll position at the top right corner of the frame
pub(crate) fn render_current_scroll(
&self,
scroll_position: u16,
lines: u16,
console_height: u16,
frame: &mut Frame<'_>,
) {
let mut row = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1)])
.split(self.console[0])[0];
// Hack: shove upwards the text to overlap with the border so text selection doesn't accidentally capture the number
row.y -= 1;
let max_scroll = lines.saturating_sub(console_height);
if max_scroll == 0 {
return;
}
let remaining_lines = max_scroll.saturating_sub(scroll_position);
if remaining_lines != 0 {
let text = vec![Span::from(format!(" {remaining_lines}")).dark_gray()];
frame.render_widget(
Paragraph::new(Line::from(text))
.alignment(Alignment::Right)
.block(Block::default()),
row,
);
}
}
}
/// Generate a string with a specified number of spaces.
fn build_msg_padding(padding_len: usize) -> String {
let mut padding = String::new();
for _ in 0..padding_len {
_ = write!(padding, " ");
}
padding
}

View file

@ -1,3 +1,4 @@
use crate::TraceSrc;
use crate::{Error, Result};
use dioxus_cli_config::WebProxyConfig;
@ -123,7 +124,7 @@ pub(crate) fn proxy_to(
}
fn handle_proxy_error(e: Error) -> axum::http::Response<axum::body::Body> {
tracing::error!("Proxy error: {}", e);
tracing::error!(dx_src = ?TraceSrc::Dev, "Proxy error: {}", e);
axum::http::Response::builder()
.status(axum::http::StatusCode::INTERNAL_SERVER_ERROR)
.body(axum::body::Body::from(format!(

View file

@ -1,5 +1,5 @@
use crate::dioxus_crate::DioxusCrate;
use crate::serve::{next_or_pending, Serve};
use crate::{dioxus_crate::DioxusCrate, TraceSrc};
use crate::{Error, Result};
use axum::extract::{Request, State};
use axum::middleware::{self, Next};
@ -7,7 +7,7 @@ use axum::{
body::Body,
extract::{
ws::{Message, WebSocket},
Extension, WebSocketUpgrade,
WebSocketUpgrade,
},
http::{
header::{HeaderName, HeaderValue, CACHE_CONTROL, EXPIRES, PRAGMA},
@ -15,7 +15,7 @@ use axum::{
},
response::IntoResponse,
routing::{get, get_service},
Router,
Extension, Router,
};
use axum_server::tls_rustls::RustlsConfig;
use dioxus_cli_config::{Platform, WebHttpsConfig};
@ -569,8 +569,12 @@ pub fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, St
match cmd {
Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => tracing::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
e => tracing::error!("an error occurred while generating mkcert certificates: {}", e.to_string()),
io::ErrorKind::NotFound => {
tracing::error!(dx_src = ?TraceSrc::Dev, "`mkcert` is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions.")
}
e => {
tracing::error!(dx_src = ?TraceSrc::Dev, "An error occurred while generating mkcert certificates: {}", e.to_string())
}
};
return Err("failed to generate mkcert certificates".into());
}

View file

@ -3,6 +3,7 @@ use std::{fs, path::PathBuf, time::Duration};
use super::hot_reloading_file_map::HotreloadError;
use crate::serve::hot_reloading_file_map::FileMap;
use crate::TraceSrc;
use crate::{cli::serve::Serve, dioxus_crate::DioxusCrate};
use dioxus_hot_reload::HotReloadMsg;
use dioxus_html::HtmlCtx;
@ -258,7 +259,7 @@ impl Watcher {
Err(HotreloadError::Parse) => {}
// Otherwise just log the error
Err(err) => {
tracing::error!("Error hotreloading file {rust_file:?}: {err}")
tracing::error!(dx_src = ?TraceSrc::Dev, "Error hotreloading file {rust_file:?}: {err}")
}
}
}

View file

@ -6,7 +6,7 @@ use std::{
};
use tracing::{debug, error, warn};
use crate::CrateConfigError;
use crate::{CrateConfigError, TraceSrc};
const GLOBAL_SETTINGS_FILE_NAME: &str = "dioxus/settings.toml";
@ -65,7 +65,7 @@ impl CliSettings {
/// This does not save to project-level settings.
pub fn save(self) -> Result<Self, CrateConfigError> {
let path = Self::get_settings_path().ok_or_else(|| {
error!("failed to get settings path");
error!(dx_src = ?TraceSrc::Dev, "failed to get settings path");
CrateConfigError::Io(Error::new(
ErrorKind::NotFound,
"failed to get settings path",
@ -73,7 +73,7 @@ impl CliSettings {
})?;
let data = toml::to_string_pretty(&self).map_err(|e| {
error!(?self, "failed to parse config into toml");
error!(dx_src = ?TraceSrc::Dev, ?self, "failed to parse config into toml");
CrateConfigError::Io(Error::new(ErrorKind::Other, e.to_string()))
})?;
@ -81,6 +81,7 @@ impl CliSettings {
let parent_path = path.parent().unwrap();
if let Err(e) = fs::create_dir_all(parent_path) {
error!(
dx_src = ?TraceSrc::Dev,
?data,
?path,
"failed to create directories for settings file"

View file

@ -1,39 +1,114 @@
//! CLI Tracing
//!
//! The CLI's tracing has internal and user-facing logs. User-facing logs are directly routed to the user in some form.
//! Internal logs are stored in a log file for consumption in bug reports and debugging.
//! We use tracing fields to determine whether a log is internal or external and additionally if the log should be
//! formatted or not.
//!
//! These two fields are
//! `dx_src` which tells the logger that this is a user-facing message and should be routed as so.
//! `dx_no_fmt`which tells the logger to avoid formatting the log and to print it as-is.
//!
//! 1. Build general filter
//! 2. Build file append layer for logging to a file. This file is reset on every CLI-run.
//! 3. Build CLI layer for routing tracing logs to the TUI.
//! 4. Build fmt layer for non-interactive logging with a custom writer that prevents output during interactive mode.
use crate::builder::TargetPlatform;
use console::strip_ansi_codes;
use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
use std::fmt::Display;
use std::{
env, io,
collections::HashMap,
env,
fmt::{Debug, Write as _},
fs,
io::{self, Write},
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
};
use tracing_subscriber::{prelude::*, EnvFilter};
use tracing::Level;
use tracing::{field::Visit, Subscriber};
use tracing_subscriber::{
filter::filter_fn, fmt::format, prelude::*, registry::LookupSpan, EnvFilter, Layer,
};
const LOG_ENV: &str = "DIOXUS_LOG";
const LOG_FILE_NAME: &str = "dx.log";
const DX_SRC_FLAG: &str = "dx_src";
const DX_NO_FMT_FLAG: &str = "dx_no_fmt";
pub fn log_path() -> PathBuf {
let tmp_dir = std::env::temp_dir();
tmp_dir.join(LOG_FILE_NAME)
}
/// Build tracing infrastructure.
pub fn build_tracing() -> CLILogControl {
// If {LOG_ENV} is set, default to env, otherwise filter to cli
// and manganis warnings and errors from other crates
let mut filter = EnvFilter::new("error,dx=info,dioxus-cli=info,manganis-cli-support=info");
if env::var(LOG_ENV).is_ok() {
filter = EnvFilter::from_env(LOG_ENV);
}
// Create writer controller and custom writer.
let (tui_tx, tui_rx) = unbounded();
let tui_enabled = Arc::new(AtomicBool::new(false));
let writer_control = CLILogControl {
tui_rx,
tui_enabled: tui_enabled.clone(),
// Log file
let log_path = log_path();
_ = std::fs::write(&log_path, "");
let file_append_layer = match FileAppendLayer::new(log_path) {
Ok(f) => Some(f),
Err(e) => {
tracing::error!(dx_src = ?TraceSrc::Dev, err = ?e, "failed to init log file");
None
}
};
let cli_writer = Mutex::new(CLIWriter::new(tui_enabled, tui_tx));
// Build tracing
// Create writer controller and custom writer.
let (output_tx, output_rx) = unbounded();
let output_enabled = Arc::new(AtomicBool::new(false));
let writer_control = CLILogControl {
output_rx,
output_enabled: output_enabled.clone(),
};
// Build CLI layer
let cli_layer = CLILayer::new(output_enabled.clone(), output_tx);
// Build fmt layer
let formatter = format::debug_fn(|writer, field, value| {
write!(writer, "{}", format_field(field.name(), value))
})
.delimited(" ");
// Format subscriber
let fmt_writer = Mutex::new(FmtLogWriter::new(output_enabled));
let fmt_layer = tracing_subscriber::fmt::layer()
.with_writer(cli_writer)
.with_filter(filter);
let sub = tracing_subscriber::registry().with(fmt_layer);
.fmt_fields(formatter)
.with_writer(fmt_writer)
.without_time()
.with_filter(filter_fn(|meta| {
// Filter any logs with "dx_no_fmt" or is not user facing (no dx_src)
let mut fields = meta.fields().iter();
let has_src_flag = fields.any(|f| f.name() == DX_SRC_FLAG);
if !has_src_flag {
return false;
}
let has_fmt_flag = fields.any(|f| f.name() == DX_NO_FMT_FLAG);
if has_fmt_flag {
return false;
}
true
}));
let sub = tracing_subscriber::registry()
.with(filter)
.with(file_append_layer)
.with(cli_layer)
.with(fmt_layer);
#[cfg(feature = "tokio-console")]
let sub = sub.with(console_subscriber::spawn());
@ -43,54 +118,297 @@ pub fn build_tracing() -> CLILogControl {
writer_control
}
/// Contains the sync primitives to control the CLIWriter.
pub struct CLILogControl {
pub tui_rx: UnboundedReceiver<String>,
pub tui_enabled: Arc<AtomicBool>,
/// A logging layer that appends to a file.
///
/// This layer returns on any error allowing the cli to continue work
/// despite failing to log to a file. This helps in case of permission errors and similar.
struct FileAppendLayer {
file_path: PathBuf,
buffer: Mutex<String>,
}
/// Represents the CLI's custom tracing writer for conditionally writing logs between outputs.
pub struct CLIWriter {
stdout: io::Stdout,
tui_tx: UnboundedSender<String>,
tui_enabled: Arc<AtomicBool>,
impl FileAppendLayer {
pub fn new(file_path: PathBuf) -> io::Result<Self> {
Ok(Self {
file_path,
buffer: Mutex::new(String::new()),
})
}
}
impl CLIWriter {
/// Create a new CLIWriter with required sync primitives for conditionally routing logs.
pub fn new(tui_enabled: Arc<AtomicBool>, tui_tx: UnboundedSender<String>) -> Self {
Self {
stdout: io::stdout(),
tui_tx,
tui_enabled,
impl<S> Layer<S> for FileAppendLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let mut visitor = CollectVisitor::new();
event.record(&mut visitor);
let new_line = if visitor.source == TraceSrc::Cargo
|| event.fields().any(|f| f.name() == DX_NO_FMT_FLAG)
{
visitor.message
} else {
let meta = event.metadata();
let level = meta.level();
let mut final_msg = String::new();
_ = write!(
final_msg,
"[{level}] {}: {} ",
meta.module_path().unwrap_or("dx"),
visitor.message
);
for (field, value) in visitor.fields.iter() {
_ = write!(final_msg, "{} ", format_field(field, value));
}
_ = writeln!(final_msg);
final_msg
};
// Append logs
let new_data = strip_ansi_codes(&new_line).to_string();
if let Ok(mut buf) = self.buffer.lock() {
*buf += &new_data;
// TODO: Make this efficient.
_ = fs::write(&self.file_path, buf.as_bytes());
}
}
}
// Implement a conditional writer so that logs are routed to the appropriate place.
impl io::Write for CLIWriter {
/// This is our "subscriber" (layer) that records structured data for the tui output.
struct CLILayer {
internal_output_enabled: Arc<AtomicBool>,
output_tx: UnboundedSender<TraceMsg>,
}
impl CLILayer {
pub fn new(
internal_output_enabled: Arc<AtomicBool>,
output_tx: UnboundedSender<TraceMsg>,
) -> Self {
Self {
internal_output_enabled,
output_tx,
}
}
}
impl<S> Layer<S> for CLILayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
// Subscribe to user
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
// We only care about user-facing logs.
let has_src_flag = event.fields().any(|f| f.name() == DX_SRC_FLAG);
if !has_src_flag {
return;
}
let mut visitor = CollectVisitor::new();
event.record(&mut visitor);
// If the TUI output is disabled we let fmt subscriber handle the logs
// EXCEPT for cargo logs which we just print.
if !self.internal_output_enabled.load(Ordering::SeqCst) {
if visitor.source == TraceSrc::Cargo
|| event.fields().any(|f| f.name() == DX_NO_FMT_FLAG)
{
println!("{}", visitor.message);
}
return;
}
let meta = event.metadata();
let level = meta.level();
let mut final_msg = String::new();
write!(final_msg, "{} ", visitor.message).unwrap();
for (field, value) in visitor.fields.iter() {
write!(final_msg, "{} ", format_field(field, value)).unwrap();
}
if visitor.source == TraceSrc::Unknown {
visitor.source = TraceSrc::Dev;
}
self.output_tx
.unbounded_send(TraceMsg::new(visitor.source, *level, final_msg))
.unwrap();
}
// TODO: support spans? structured tui log display?
}
/// A record visitor that collects dx-specific info and user-provided fields for logging consumption.
struct CollectVisitor {
message: String,
source: TraceSrc,
dx_user_msg: bool,
fields: HashMap<String, String>,
}
impl CollectVisitor {
pub fn new() -> Self {
Self {
message: String::new(),
source: TraceSrc::Unknown,
dx_user_msg: false,
fields: HashMap::new(),
}
}
}
impl Visit for CollectVisitor {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
let name = field.name();
let mut value_string = String::new();
write!(value_string, "{:?}", value).unwrap();
if name == "message" {
self.message = value_string;
return;
}
if name == DX_SRC_FLAG {
self.source = TraceSrc::from(value_string);
self.dx_user_msg = true;
return;
}
self.fields.insert(name.to_string(), value_string);
}
}
// Contains the sync primitives to control the CLIWriter.
pub struct CLILogControl {
pub output_rx: UnboundedReceiver<TraceMsg>,
pub output_enabled: Arc<AtomicBool>,
}
struct FmtLogWriter {
stdout: io::Stdout,
output_enabled: Arc<AtomicBool>,
}
impl FmtLogWriter {
pub fn new(output_enabled: Arc<AtomicBool>) -> Self {
Self {
stdout: io::stdout(),
output_enabled,
}
}
}
impl Write for FmtLogWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if self.tui_enabled.load(Ordering::SeqCst) {
let len = buf.len();
let as_string = String::from_utf8(buf.to_vec())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
self.tui_tx
.unbounded_send(as_string)
.map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, e))?;
Ok(len)
} else {
// Handle selection between TUI or Terminal output.
if !self.output_enabled.load(Ordering::SeqCst) {
self.stdout.write(buf)
} else {
Ok(buf.len())
}
}
fn flush(&mut self) -> io::Result<()> {
if !self.tui_enabled.load(Ordering::SeqCst) {
if !self.output_enabled.load(Ordering::SeqCst) {
self.stdout.flush()
} else {
Ok(())
}
}
}
/// Formats a tracing field and value, removing any internal fields from the final output.
fn format_field(field_name: &str, value: &dyn Debug) -> String {
let mut out = String::new();
match field_name {
DX_SRC_FLAG => write!(out, ""),
DX_NO_FMT_FLAG => write!(out, ""),
"message" => write!(out, "{:?}", value),
_ => write!(out, "{}={:?}", field_name, value),
}
.unwrap();
out
}
#[derive(Clone, PartialEq)]
pub struct TraceMsg {
pub source: TraceSrc,
pub level: Level,
pub content: String,
}
impl TraceMsg {
pub fn new(source: TraceSrc, level: Level, content: String) -> Self {
Self {
source,
level,
content,
}
}
}
#[derive(Clone, PartialEq)]
pub enum TraceSrc {
App(TargetPlatform),
Dev,
Build,
/// Provides no formatting.
Cargo,
/// Avoid using this
Unknown,
}
impl std::fmt::Debug for TraceSrc {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let as_string = self.to_string();
write!(f, "{as_string}")
}
}
impl From<String> for TraceSrc {
fn from(value: String) -> Self {
match value.as_str() {
"dev" => Self::Dev,
"build" => Self::Build,
"cargo" => Self::Cargo,
"web" => Self::App(TargetPlatform::Web),
"desktop" => Self::App(TargetPlatform::Desktop),
"server" => Self::App(TargetPlatform::Server),
"liveview" => Self::App(TargetPlatform::Liveview),
_ => Self::Unknown,
}
}
}
impl Display for TraceSrc {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::App(platform) => match platform {
TargetPlatform::Web => write!(f, "web"),
TargetPlatform::Desktop => write!(f, "desktop"),
TargetPlatform::Server => write!(f, "server"),
TargetPlatform::Liveview => write!(f, "server"),
},
Self::Dev => write!(f, "dev"),
Self::Build => write!(f, "build"),
Self::Cargo => write!(f, "cargo"),
Self::Unknown => write!(f, "n/a"),
}
}
}

View file

@ -232,7 +232,8 @@ pub(crate) fn invalidate_browser_asset_cache() {
for x in 0..links.length() {
use wasm_bindgen::JsCast;
let link: web_sys::Element = links.get(x).unwrap().unchecked_into();
let href = link.get_attribute("href").unwrap();
_ = link.set_attribute("href", &format!("{}?{}", href, noise));
if let Some(href) = link.get_attribute("href") {
_ = link.set_attribute("href", &format!("{}?{}", href, noise));
}
}
}