add basic crash analytics and very light telemetry to dioxus-cli (#4224)

* wip

* wip: basic telemetry

* fix merge

* keep errors for telemetry

* fix upload when queued events are empty

* add per session id and keep track of build timing

* keep track of what commands are run

* capture panics

* keep track of panics

* better path stripping

* better tracing error telemetry

* only sync every week and check settings before sending any telemetry

* keep track of the args passed to commands

* anonymize commands

* more error tolerant

* wip

* wip

* wip

* merge cnflict

* other merg conflict

* migrate everything from telemetry to logging

* update docs

* wip... about to merge trace controller

* lil bit more refactor to clean up tracer

* more performant log writer

* cleanup ci detecting

* wire up to posthog

* re-uploads work

* yoink out the anon from each file

* cleanup sig

* lil bit cleaner

* dont write empty files

* wip log handler

* fmt

* cleanup logging of backtraces

* add proper backtrace support

* use stable reporter

* adhere to posthog format

* fix stacktraces

* use anyhow for capturing backtraces

* I think we're done?

* cleanup logging

* properly collect!

* move out keycode handling

* she works

* typo

* add extra fields

* fix cargo doc

---------

Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
This commit is contained in:
Jonathan KelleyandEvan Almloff authored and GitHub committed 2025-08-08 19:00:00 -07:00
1 parent a610aaefc3
commit e43bf05b69
30 files changed
+1748 -441

No files matched your search

+6
View File
@@ -26,6 +26,7 @@ dioxus-dx-wire-format = { workspace = true }
wasm-split-cli = { workspace = true }
depinfo = { workspace = true }
subsecond-types = { workspace = true }
dioxus-cli-telemetry = { workspace = true }
clap = { workspace = true, features = ["derive", "cargo"] }
convert_case = { workspace = true }
@@ -154,6 +155,10 @@ self_update = { version = "0.42.0", features = [
] }
self-replace = "1.5.0"
cargo-config2 = { workspace = true }
posthog-rs = "0.3.5"
serde_json5 = "0.2.1"
fs2 = "0.4.3"
sentry-backtrace = "0.42.0"
[build-dependencies]
built = { version = "0.8.0", features = ["git2"] }
@@ -162,6 +167,7 @@ built = { version = "0.8.0", features = ["git2"] }
default = []
tokio-console = ["dep:console-subscriber", "tokio/tracing"]
no-downloads = []
disable-telemetry = []
[[bin]]
path = "src/main.rs"
-1
View File
@@ -424,7 +424,6 @@ impl AppBuilder {
}
}
tracing::error!(?err, json = ?StructuredOutput::Error { message: err.to_string() });
return Err(err);
}
BuilderUpdate::StdoutReceived { .. } => {}
+4
View File
@@ -38,10 +38,13 @@ pub enum BuilderUpdate {
message: Diagnostic,
},
/// The build completed successfully and the artifacts are ready. The artifacts are dependent on
/// the build mode (fat vs thin vs base).
BuildReady {
bundle: BuildArtifacts,
},
/// The build failed. This might be because of a compilation error, or an error internal to DX.
BuildFailed {
err: Error,
},
@@ -62,6 +65,7 @@ pub enum BuilderUpdate {
msg: String,
},
/// The running app (DUT) has exited and is no longer running.
ProcessExited {
status: ExitStatus,
},
+1 -1
View File
@@ -1943,7 +1943,7 @@ impl BuildRequest {
}
}
LinkerFlavor::Unsupported => {
tracing::error!("Unsupported platform for fat linking");
tracing::error!("Unsupported platform for fat linking: {}", self.triple);
}
};
}
+11 -4
View File
@@ -1,7 +1,4 @@
use crate::BuildMode;
use crate::{cli::*, AppBuilder, BuildRequest, Workspace};
use super::target::TargetArgs;
use crate::{cli::*, Anonymized, AppBuilder, BuildMode, BuildRequest, TargetArgs, Workspace};
/// Build the Rust Dioxus app and all of its assets.
///
@@ -28,6 +25,16 @@ pub struct BuildArgs {
pub(crate) build_arguments: TargetArgs,
}
impl Anonymized for BuildArgs {
fn anonymized(&self) -> Value {
json! {{
"fullstack": self.fullstack,
"ssg": self.ssg,
"build_arguments": self.build_arguments.anonymized(),
}}
}
}
pub struct BuildTargets {
pub client: BuildRequest,
pub server: Option<BuildRequest>,
+7 -8
View File
@@ -21,9 +21,6 @@ pub(crate) enum Config {
/// Create a custom html file.
CustomHtml {},
/// Print the location of the CLI log file.
LogFile {},
/// Set CLI settings.
#[command(subcommand)]
Set(Setting),
@@ -39,6 +36,8 @@ pub(crate) enum Setting {
AlwaysOnTop { value: BoolValue },
/// Set the interval that file changes are polled on WSL for hot reloading.
WSLFilePollInterval { value: u16 },
/// Disable the built-in telemetry for the CLI
DisableTelemetry { value: BoolValue },
}
impl Display for Setting {
@@ -48,13 +47,14 @@ impl Display for Setting {
Self::AlwaysOpenBrowser { value: _ } => write!(f, "always-open-browser"),
Self::AlwaysOnTop { value: _ } => write!(f, "always-on-top"),
Self::WSLFilePollInterval { value: _ } => write!(f, "wsl-file-poll-interval"),
Self::DisableTelemetry { value: _ } => write!(f, "disable-telemetry"),
}
}
}
// Clap complains if we use a bool directly and I can't find much info about it.
// "Argument 'value` is positional and it must take a value but action is SetTrue"
#[derive(Debug, Clone, Copy, Deserialize, clap::ValueEnum)]
#[derive(Debug, Clone, Copy, serde::Serialize, Deserialize, clap::ValueEnum)]
pub(crate) enum BoolValue {
True,
False,
@@ -98,10 +98,6 @@ impl Config {
file.write_all(content.as_bytes())?;
tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 Create custom html file done.");
}
Config::LogFile {} => {
let log_path = crate::logging::FileAppendLayer::log_path();
tracing::info!(dx_src = ?TraceSrc::Dev, "Log file is located at {}", log_path.display());
}
// Handle CLI settings.
Config::Set(setting) => {
CliSettings::modify_settings(|settings| match setting {
@@ -115,6 +111,9 @@ impl Config {
Setting::WSLFilePollInterval { value } => {
settings.wsl_file_poll_interval = Some(value)
}
Setting::DisableTelemetry { value } => {
settings.disable_telemetry = Some(value.into());
}
})?;
tracing::info!(dx_src = ?TraceSrc::Dev, "🚩 CLI setting `{setting}` has been set.");
}
+10 -25
View File
@@ -10,48 +10,48 @@ pub(crate) static DEFAULT_TEMPLATE: &str = "gh:dioxuslabs/dioxus-template";
#[clap(name = "new")]
pub struct Create {
/// Create a new Dioxus project at PATH
path: PathBuf,
pub path: PathBuf,
/// Project name. Defaults to directory name
#[arg(short, long)]
name: Option<String>,
pub name: Option<String>,
/// Template path
#[clap(short, long)]
template: Option<String>,
pub template: Option<String>,
/// Branch to select when using `template` from a git repository.
/// Mutually exclusive with: `--revision`, `--tag`.
#[clap(long, conflicts_with_all(["revision", "tag"]))]
branch: Option<String>,
pub branch: Option<String>,
/// A commit hash to select when using `template` from a git repository.
/// Mutually exclusive with: `--branch`, `--tag`.
#[clap(long, conflicts_with_all(["branch", "tag"]))]
revision: Option<String>,
pub revision: Option<String>,
/// Tag to select when using `template` from a git repository.
/// Mutually exclusive with: `--branch`, `--revision`.
#[clap(long, conflicts_with_all(["branch", "revision"]))]
tag: Option<String>,
pub tag: Option<String>,
/// Specify a sub-template within the template repository to be used as the actual template
#[clap(long)]
subtemplate: Option<String>,
pub subtemplate: Option<String>,
/// Pass `<option>=<value>` for the used template (e.g., `foo=bar`)
#[clap(short, long)]
option: Vec<String>,
pub option: Vec<String>,
/// Skip user interaction by using the default values for the used template.
/// Default values can be overridden with `--option`
#[clap(short, long)]
yes: bool,
pub yes: bool,
/// Specify the VCS used to initialize the generated template.
/// Options: `git`, `none`.
#[arg(long, value_parser)]
vcs: Option<Vcs>,
pub vcs: Option<Vcs>,
}
impl Create {
@@ -99,7 +99,6 @@ impl Create {
..Default::default()
};
restore_cursor_on_sigint();
tracing::debug!(dx_src = ?TraceSrc::Dev, "Creating new project with args: {args:#?}");
let path = cargo_generate::generate(args)?;
@@ -126,20 +125,6 @@ pub(crate) fn resolve_template_and_branch(
};
}
/// Prevent hidden cursor if Ctrl+C is pressed when interacting
/// with cargo-generate's prompts.
///
/// See <https://github.com/DioxusLabs/dioxus/pull/2603>.
pub(crate) fn restore_cursor_on_sigint() {
ctrlc::set_handler(move || {
if let Err(err) = console::Term::stdout().show_cursor() {
eprintln!("Error showing the cursor again: {err}");
}
std::process::exit(1); // Ideally should mimic the INT signal.
})
.expect("ctrlc::set_handler");
}
/// Extracts the last directory name from the `path`.
pub(crate) fn name_from_path(path: &Path) -> Result<String> {
use path_absolutize::Absolutize;
+11 -11
View File
@@ -6,48 +6,48 @@ use cargo_generate::{GenerateArgs, TemplatePath, Vcs};
pub struct Init {
/// Create a new Dioxus project at PATH
#[arg(default_value = ".")]
path: PathBuf,
pub path: PathBuf,
/// Project name. Defaults to directory name
#[arg(short, long)]
name: Option<String>,
pub name: Option<String>,
/// Template path
#[clap(short, long)]
template: Option<String>,
pub template: Option<String>,
/// Branch to select when using `template` from a git repository.
/// Mutually exclusive with: `--revision`, `--tag`.
#[clap(long, conflicts_with_all(["revision", "tag"]))]
branch: Option<String>,
pub branch: Option<String>,
/// A commit hash to select when using `template` from a git repository.
/// Mutually exclusive with: `--branch`, `--tag`.
#[clap(long, conflicts_with_all(["branch", "tag"]))]
revision: Option<String>,
pub revision: Option<String>,
/// Tag to select when using `template` from a git repository.
/// Mutually exclusive with: `--branch`, `--revision`.
#[clap(long, conflicts_with_all(["branch", "revision"]))]
tag: Option<String>,
pub tag: Option<String>,
/// Specify a sub-template within the template repository to be used as the actual template
#[clap(long)]
subtemplate: Option<String>,
pub subtemplate: Option<String>,
/// Pass `<option>=<value>` for the used template (e.g., `foo=bar`)
#[clap(short, long)]
option: Vec<String>,
pub option: Vec<String>,
/// Skip user interaction by using the default values for the used template.
/// Default values can be overridden with `--option`
#[clap(short, long)]
yes: bool,
pub yes: bool,
/// Specify the VCS used to initialize the generated template.
/// Options: `git`, `none`.
#[arg(long, value_parser)]
vcs: Option<Vcs>,
pub vcs: Option<Vcs>,
}
impl Init {
@@ -85,7 +85,7 @@ impl Init {
},
..Default::default()
};
create::restore_cursor_on_sigint();
let path = cargo_generate::generate(args)?;
_ = create::post_create(&path, &self.vcs.unwrap_or(Vcs::Git));
Ok(StructuredOutput::Success)
+3 -3
View File
@@ -111,9 +111,9 @@ impl LinkAction {
Ok(())
}
pub(crate) async fn run_link(self) {
pub(crate) fn run_link(self) {
let link_err_file = self.link_err_file.clone();
let res = self.run_link_inner().await;
let res = self.run_link_inner();
if let Err(err) = res {
// If we failed to run the linker, we need to write the error to the file
@@ -127,7 +127,7 @@ impl LinkAction {
///
/// The file will be given by the dx-magic-link-arg env var itself, so we use
/// it both for determining if we should act as a linker and the for the file name itself.
async fn run_link_inner(self) -> Result<()> {
fn run_link_inner(self) -> Result<()> {
let mut args: Vec<_> = std::env::args().collect();
if args.is_empty() {
return Ok(());
+2 -20
View File
@@ -22,11 +22,13 @@ pub(crate) use target::*;
pub(crate) use verbosity::*;
use crate::platform_override::CommandWithPlatformOverrides;
use crate::Anonymized;
use crate::{error::Result, Error, StructuredOutput};
use clap::builder::styling::{AnsiColor, Effects, Style, Styles};
use clap::{Parser, Subcommand};
use html_parser::Dom;
use serde::Deserialize;
use serde_json::{json, Value};
use std::sync::LazyLock;
use std::{
fmt::Display,
@@ -113,26 +115,6 @@ pub enum BuildTools {
BuildAssets(build_assets::BuildAssets),
}
impl Display for Commands {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Commands::Build(_) => write!(f, "build"),
Commands::Translate(_) => write!(f, "translate"),
Commands::Serve(_) => write!(f, "serve"),
Commands::New(_) => write!(f, "create"),
Commands::Init(_) => write!(f, "init"),
Commands::Config(_) => write!(f, "config"),
Commands::Autoformat(_) => write!(f, "fmt"),
Commands::Check(_) => write!(f, "check"),
Commands::Bundle(_) => write!(f, "bundle"),
Commands::Run(_) => write!(f, "run"),
Commands::SelfUpdate(_) => write!(f, "self-update"),
Commands::Tools(_) => write!(f, "tools"),
Commands::Doctor(_) => write!(f, "doctor"),
}
}
}
pub(crate) static VERSION: LazyLock<String> = LazyLock::new(|| {
format!(
"{} ({})",
+15
View File
@@ -1,6 +1,8 @@
#![allow(dead_code)]
use crate::Anonymized;
use clap::parser::ValueSource;
use clap::{ArgMatches, Args, CommandFactory, FromArgMatches, Parser, Subcommand};
use serde_json::{json, Value};
/// Wraps a component with the subcommands `@server` and `@client` which will let you override the
/// base arguments for the client and server instances.
@@ -30,6 +32,19 @@ impl<T> CommandWithPlatformOverrides<T> {
}
}
impl<T> Anonymized for CommandWithPlatformOverrides<T>
where
T: Anonymized,
{
fn anonymized(&self) -> Value {
json!({
"shared": self.shared.anonymized(),
"server": self.server.as_ref().map(|s| s.anonymized()),
"client": self.client.as_ref().map(|c| c.anonymized()),
})
}
}
impl<T: CommandFactory + Args> Parser for CommandWithPlatformOverrides<T> {}
impl<T: CommandFactory + Args> CommandFactory for CommandWithPlatformOverrides<T> {
+36 -83
View File
@@ -1,9 +1,5 @@
use super::*;
use crate::{AddressArguments, BuildArgs, TraceController};
use futures_util::FutureExt;
use std::sync::OnceLock;
use std::{backtrace::Backtrace, panic::AssertUnwindSafe};
use crate::{AddressArguments, Anonymized, BuildArgs, TraceController};
/// Serve the project
///
/// `dx serve` takes cargo args by default with additional renderer args like `--web`, `--webview`, and `--native`:
@@ -99,91 +95,20 @@ impl ServeArgs {
/// higher log levels
///
/// We also set up proper panic handling since the TUI has a tendency to corrupt the terminal.
pub(crate) async fn serve(self) -> Result<StructuredOutput> {
pub(crate) async fn serve(self, tracer: &TraceController) -> Result<StructuredOutput> {
if std::env::var("RUST_BACKTRACE").is_err() {
std::env::set_var("RUST_BACKTRACE", "1");
}
struct SavedLocation {
file: String,
line: u32,
column: u32,
}
static BACKTRACE: OnceLock<(Backtrace, Option<SavedLocation>)> = OnceLock::new();
// We *don't* want printing here, since it'll break the tui and log ordering.
//
// We *will* re-emit the panic after we've drained the tracer, so our panic hook will simply capture the panic
// and save it.
std::panic::set_hook(Box::new(move |panic_info| {
_ = BACKTRACE.set((
Backtrace::capture(),
panic_info.location().map(|l| SavedLocation {
file: l.file().to_string(),
line: l.line(),
column: l.column(),
}),
));
}));
let interactive = self.is_interactive_tty();
// Redirect all logging the cli logger - if there's any pending after a panic, we flush it
let mut tracer = TraceController::redirect(interactive);
let res = AssertUnwindSafe(crate::serve::serve_all(self, &mut tracer))
.catch_unwind()
.await;
// Kill the screen so we don't ruin the terminal
_ = crate::serve::Output::remote_shutdown(interactive);
// And drain the tracer as regular messages. All messages will be logged (including traces)
// and then we can print the panic message
if !matches!(res, Ok(Ok(_))) {
tracer.shutdown_panic();
let is_interactive_tty = self.is_interactive_tty();
if is_interactive_tty {
tracer.redirect_to_tui();
}
match res {
Ok(Ok(_res)) => Ok(StructuredOutput::Success),
Ok(Err(e)) => Err(e),
Err(panic_err) => {
// And then print the panic itself.
let as_str = if let Some(p) = panic_err.downcast_ref::<String>() {
p.as_ref()
} else if let Some(p) = panic_err.downcast_ref::<&str>() {
p
} else {
"<unknown panic>"
};
// Attempt to emulate the default panic hook
let message = BACKTRACE
.get()
.map(|(back, location)| {
let location_display = location
.as_ref()
.map(|l| format!("{}:{}:{}", l.file, l.line, l.column))
.unwrap_or_else(|| "<unknown>".to_string());
let mut backtrace_display = back.to_string();
// split at the line that ends with ___rust_try for short backtraces
if std::env::var("RUST_BACKTRACE") == Ok("1".to_string()) {
backtrace_display = backtrace_display
.split(" ___rust_try\n")
.next()
.map(|f| format!("{f} ___rust_try"))
.unwrap_or_default();
}
format!("dx serve panicked at {location_display}\n{as_str}\n{backtrace_display} ___rust_try")
})
.unwrap_or_else(|| format!("dx serve panicked: {as_str}"));
Err(anyhow::anyhow!(message))
}
}
crate::serve::serve_all(self, tracer)
.await
.map(|_| StructuredOutput::Success)
}
/// Check if the server is running in interactive mode. This involves checking the terminal as well
@@ -192,3 +117,31 @@ impl ServeArgs {
std::io::stdout().is_terminal() && self.interactive.unwrap_or(true)
}
}
impl Anonymized for ServeArgs {
fn anonymized(&self) -> Value {
json! {{
"address": self.address.anonymized(),
"open": self.open,
"hot_reload": self.hot_reload,
"always_on_top": self.always_on_top,
"cross_origin_policy": self.cross_origin_policy,
"wsl_file_poll_interval": self.wsl_file_poll_interval,
"interactive": self.interactive,
"hot_patch": self.hot_patch,
"watch": self.watch,
"force_sequential": self.force_sequential,
"exit_on_error": self.exit_on_error,
"platform_args": self.platform_args.anonymized(),
}}
}
}
impl Anonymized for PlatformServeArgs {
fn anonymized(&self) -> Value {
json! {{
"targets": self.targets.anonymized(),
"args": !self.args.is_empty(),
}}
}
}
+28
View File
@@ -118,3 +118,31 @@ pub(crate) struct TargetArgs {
#[clap(long, help_heading = HELP_HEADING)]
pub(crate) base_path: Option<String>,
}
impl Anonymized for TargetArgs {
fn anonymized(&self) -> Value {
json! {{
"target_alias": self.target_alias,
"renderer": self.renderer,
"bundle": self.bundle,
"platform": self.platform,
"release": self.release,
"package": self.package,
"bin": self.bin,
"example": self.example.is_some(),
"profile": self.profile.is_some(),
"features": !self.features.is_empty(),
"no_default_features": self.no_default_features,
"all_features": self.all_features,
"target": self.target.as_ref().map(|t| t.to_string()),
"skip_assets": self.skip_assets,
"inject_loading_scripts": self.inject_loading_scripts,
"wasm_split": self.wasm_split,
"debug_symbols": self.debug_symbols,
"device": self.device,
"base_path": self.base_path.is_some(),
"cargo_args": self.cargo_args.is_some(),
"rustc_args": self.rustc_args.is_some(),
}}
}
}
+6 -1
View File
@@ -1,6 +1,7 @@
use clap::Parser;
use std::path::PathBuf;
#[derive(Parser, Clone, Copy, Debug, Default)]
#[derive(Parser, Clone, Debug, Default)]
pub struct Verbosity {
/// Use verbose output [default: false]
#[clap(long, global = true)]
@@ -14,6 +15,10 @@ pub struct Verbosity {
#[clap(long, global = true)]
pub(crate) json_output: bool,
/// Write *all* logs to a file
#[clap(long, global = true, help_heading = "Logging Options")]
pub(crate) log_to_file: Option<PathBuf>,
/// Assert that `Cargo.lock` will remain unchanged
#[clap(long, global = true, help_heading = "Manifest Options")]
pub(crate) locked: bool,
+1 -1
View File
@@ -288,7 +288,7 @@ pub struct CustomSignCommandSettings {
pub args: Vec<String>,
}
#[derive(Clone, Copy, Debug, clap::ValueEnum)]
#[derive(Clone, Copy, Debug, clap::ValueEnum, Serialize)]
pub(crate) enum PackageType {
/// The macOS application bundle (.app).
#[clap(name = "macos")]
+9
View File
@@ -11,3 +11,12 @@ pub(crate) struct AddressArguments {
#[clap(long)]
pub(crate) addr: Option<std::net::IpAddr>,
}
impl crate::Anonymized for AddressArguments {
fn anonymized(&self) -> serde_json::Value {
serde_json::json!({
"port": self.port,
"addr": self.addr.map(|addr| if addr.is_loopback() { "loopback" } else if addr.is_unspecified() { "unspecified" } else { "other" }),
})
}
}
-5
View File
@@ -8,11 +8,6 @@ pub(crate) fn should_force_entropy() -> bool {
std::env::var("DIOXUS_FORCE_ENTRY").is_ok()
}
/// Should the CLI not download any additional tools?
pub(crate) fn no_downloads() -> bool {
std::env::var("NO_DOWNLOADS").is_ok()
}
/// Should we test the installs?
#[allow(dead_code)] // -> used in tests only
pub(crate) fn test_installs() -> bool {
+1030 -171
View File
File diff suppressed because it is too large. Load diff
+27 -36
View File
@@ -40,50 +40,41 @@ pub(crate) use workspace::*;
async fn main() {
// The CLI uses dx as a rustcwrapper in some instances (like binary patching)
if rustcwrapper::is_wrapping_rustc() {
return rustcwrapper::run_rustc().await;
return rustcwrapper::run_rustc();
}
// If we're being ran as a linker (likely from ourselves), we want to act as a linker instead.
if let Some(link_args) = link::LinkAction::from_env() {
return link_args.run_link().await;
return link_args.run_link();
}
let args = TraceController::initialize();
let result = match args.action {
Commands::Translate(opts) => opts.translate(),
Commands::New(opts) => opts.create().await,
Commands::Init(opts) => opts.init().await,
Commands::Config(opts) => opts.config().await,
Commands::Autoformat(opts) => opts.autoformat().await,
Commands::Check(opts) => opts.check().await,
Commands::Build(opts) => opts.build().await,
Commands::Serve(opts) => opts.serve().await,
Commands::Bundle(opts) => opts.bundle().await,
Commands::Run(opts) => opts.run().await,
Commands::SelfUpdate(opts) => opts.self_update().await,
Commands::Tools(BuildTools::BuildAssets(opts)) => opts.run().await,
Commands::Doctor(opts) => opts.doctor().await,
};
// Provide a structured output for third party tools that can consume the output of the CLI
match result {
Ok(output) => {
tracing::debug!(json = ?output);
// Run under the tracing collector so we can capture errors/panics.
let result = TraceController::main(|args, tracer| async move {
match args {
Commands::Serve(opts) => opts.serve(&tracer).await,
Commands::Translate(opts) => opts.translate(),
Commands::New(opts) => opts.create().await,
Commands::Init(opts) => opts.init().await,
Commands::Config(opts) => opts.config().await,
Commands::Autoformat(opts) => opts.autoformat().await,
Commands::Check(opts) => opts.check().await,
Commands::Build(opts) => opts.build().await,
Commands::Bundle(opts) => opts.bundle().await,
Commands::Run(opts) => opts.run().await,
Commands::SelfUpdate(opts) => opts.self_update().await,
Commands::Tools(BuildTools::BuildAssets(opts)) => opts.run().await,
Commands::Doctor(opts) => opts.doctor().await,
}
Err(err) => {
tracing::error!(
json = ?StructuredOutput::Error {
message: format!("{err:?}"),
},
);
eprintln!(
"{ERROR_STYLE}Failed{ERROR_STYLE:#}: {}",
crate::error::log_stacktrace(&err, 1),
ERROR_STYLE = crate::styles::ERROR_STYLE,
);
});
// Print the structured output in JSON format for third-party tools to consume.
// Make sure we do this as the last step so you can always `tail -1` it
match result.await {
StructuredOutput::Error { message } => {
tracing::error!(json = ?StructuredOutput::Error { message });
std::process::exit(1);
}
};
output => tracing::debug!(json = ?output),
}
}
+2 -3
View File
@@ -65,13 +65,12 @@ fn has_linking_args() -> bool {
/// Run rustc directly, but output the result to a file.
///
/// <https://doc.rust-lang.org/cargo/reference/config.html#buildrustc>
pub async fn run_rustc() {
pub fn run_rustc() {
// If we are being asked to link, delegate to the linker action.
if has_linking_args() {
return crate::link::LinkAction::from_env()
.expect("Linker action not found")
.run_link()
.await;
.run_link();
}
let var_file: PathBuf = std::env::var(DX_RUSTC_WRAPPER_ENV_VAR)
+1 -1
View File
@@ -39,7 +39,7 @@ pub(crate) use update::*;
/// - I'd love to be able to configure the CLI while it's running so we can change settings on the fly.
/// - I want us to be able to detect a `server_fn` in the project and then upgrade from a static server
/// to a dynamic one on the fly.
pub(crate) async fn serve_all(args: ServeArgs, tracer: &mut TraceController) -> Result<()> {
pub(crate) async fn serve_all(args: ServeArgs, tracer: &TraceController) -> Result<()> {
// Load the args into a plan, resolving all tooling, build dirs, arguments, decoding the multi-target, etc
let exit_on_error = args.exit_on_error;
let mut builder = AppServer::new(args).await?;
+72 -23
View File
@@ -1,7 +1,9 @@
use crate::Result;
use crate::{
serve::{ansi_buffer::ansi_string_to_line, ServeUpdate, WebServer},
BuildId, BuildStage, BuilderUpdate, BundleFormat, TraceContent, TraceMsg, TraceSrc,
};
use anyhow::{anyhow, bail, Context};
use cargo_metadata::diagnostic::Diagnostic;
use crossterm::{
cursor::{Hide, Show},
@@ -104,7 +106,7 @@ impl Output {
/// Call the startup functions that might mess with the terminal settings.
/// This is meant to be paired with "shutdown" to restore the terminal to its original state.
fn startup(&mut self) -> io::Result<()> {
fn startup(&mut self) -> Result<()> {
if self.interactive {
// Check if writing the terminal is going to block infinitely.
// If it does, we should disable interactive mode. This ensures we work with programs like `bg`
@@ -138,7 +140,7 @@ impl Output {
///
/// This lets us check if writing to tty is going to block forever and then recover, allowing
/// interopability with programs like `bg`.
fn enable_raw_mode() -> io::Result<()> {
fn enable_raw_mode() -> Result<()> {
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
@@ -152,7 +154,7 @@ impl Output {
use std::io::IsTerminal;
if !stdout().is_terminal() {
return io::Result::Err(io::Error::other("Not a terminal"));
bail!("Cannot enable raw mode on a non-terminal output");
}
enable_raw_mode()?;
@@ -164,7 +166,7 @@ impl Output {
Ok(())
}
pub(crate) fn remote_shutdown(interactive: bool) -> io::Result<()> {
pub(crate) fn remote_shutdown(interactive: bool) -> Result<()> {
if interactive && crossterm::terminal::is_raw_mode_enabled().unwrap_or(true) {
stdout()
.execute(Show)?
@@ -213,23 +215,42 @@ impl Output {
}
/// Handle an input event, returning `true` if the event should cause the program to restart.
fn handle_input(&mut self, input: Event) -> io::Result<Option<ServeUpdate>> {
// handle ctrlc
if let Event::Key(key) = input {
if let KeyCode::Char('c') = key.code {
if key.modifiers.contains(KeyModifiers::CONTROL) {
return Ok(Some(ServeUpdate::Exit { error: None }));
}
}
}
fn handle_input(&mut self, input: Event) -> Result<Option<ServeUpdate>> {
match input {
Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_keypress(key),
_ => Ok(Some(ServeUpdate::Redraw)),
}
}
fn handle_keypress(&mut self, key: KeyEvent) -> io::Result<Option<ServeUpdate>> {
fn handle_keypress(&mut self, key: KeyEvent) -> Result<Option<ServeUpdate>> {
// Some dev helpers for testing panic propagation and error handling. Remove this eventually.
if cfg!(debug_assertions) && std::env::var("DEBUG_PANICS").is_ok() {
match key.code {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
return Ok(Some(ServeUpdate::Exit { error: None }))
}
KeyCode::Char('Z') => panic!("z pressed so we panic -> {}", uuid::Uuid::new_v4()),
KeyCode::Char('X') => bail!("x pressed so we bail -> {}", uuid::Uuid::new_v4()),
KeyCode::Char('E') => {
Err(anyhow!(
"E pressed so we bail with context -> {}",
uuid::Uuid::new_v4()
))
.context("With a message")
.context("With a context")?;
}
KeyCode::Char('C') => {
return Ok(Some(ServeUpdate::Exit {
error: Some(anyhow!(
"C pressed, so exiting with safe error -> {}",
uuid::Uuid::new_v4()
)),
}));
}
_ => {}
}
}
match key.code {
KeyCode::Char('r') => return Ok(Some(ServeUpdate::RequestRebuild)),
KeyCode::Char('o') => return Ok(Some(ServeUpdate::OpenApp)),
@@ -255,7 +276,6 @@ impl Output {
id: BuildId::CLIENT,
}));
}
KeyCode::Char('c') => {
stdout()
.execute(Clear(ClearType::All))?
@@ -394,7 +414,9 @@ impl Output {
};
// First, dequeue any logs that have built up from event handling
_ = self.drain_logs(term);
while let Some(log) = self.pending_logs.pop_back() {
_ = self.render_log(term, log);
}
// Then, draw the frame, passing along all the state of the TUI so we can render it properly
_ = term.draw(|frame| {
@@ -844,16 +866,13 @@ impl Output {
/// TODO(jon): we could look into implementing scroll regions ourselves, but I think insert_before will
/// land in a reasonable amount of time.
#[deny(clippy::manual_saturating_arithmetic)]
fn drain_logs(
fn render_log(
&mut self,
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
) -> io::Result<()> {
log: TraceMsg,
) -> Result<()> {
use unicode_segmentation::UnicodeSegmentation;
let Some(log) = self.pending_logs.pop_back() else {
return Ok(());
};
// Only show debug logs if verbose is enabled
if log.level == Level::DEBUG && !self.verbose {
return Ok(());
@@ -1029,3 +1048,33 @@ impl Output {
lines
}
}
impl std::ops::Drop for Output {
fn drop(&mut self) {
if !self.interactive {
return;
}
// Get a handle to the terminal with a different lifetime so we can continue to call &self methods
let owned_term = self.term.clone();
let mut term = owned_term.borrow_mut();
let Some(term) = term.as_mut() else {
return;
};
// First, dequeue any logs that have built up from event handling
while let Some(log) = self.pending_logs.pop_back() {
_ = self.render_log(term, log);
}
// And then lets move the cursor to the bottom of the terminal
let frame_rect = term.get_frame().area();
_ = term.set_cursor_position(Position {
x: 0,
y: frame_rect.y + frame_rect.height,
});
// Tear down the TUI if it's active at this point.
_ = crate::serve::Output::remote_shutdown(self.interactive);
}
}
+3 -3
View File
@@ -143,15 +143,15 @@ pub(crate) fn proxy_to(
|| uri.path().starts_with("/public/")
|| uri.path().starts_with("/wasm/")
{
tracing::trace!(dx_src = ?TraceSrc::Dev, "[{}] {}", res.status().as_u16(), uri);
tracing::trace!(dx_src = ?TraceSrc::App(crate::BundleFormat::Server), "[{}] {}", res.status().as_u16(), uri);
} else {
tracing::info!(dx_src = ?TraceSrc::Dev, "[{}] {}", res.status().as_u16(), uri);
tracing::info!(dx_src = ?TraceSrc::App(crate::BundleFormat::Server), "[{}] {}", res.status().as_u16(), uri);
}
Ok(res.into_response())
}
Err(err) => {
tracing::error!(dx_src = ?TraceSrc::Dev, "[{}] {}", err.status().as_u16(), uri);
tracing::error!(dx_src = ?TraceSrc::App(crate::BundleFormat::Server), "[{}] {}", err.status().as_u16(), uri);
Err(err)
}
}
+1 -1
View File
@@ -115,7 +115,7 @@ impl AppServer {
let open_browser = args
.open
.unwrap_or_else(|| workspace.settings.always_open_browser.unwrap_or(true))
.unwrap_or_else(|| workspace.settings.always_open_browser.unwrap_or(false))
&& interactive;
let wsl_file_poll_interval = args
+66 -34
View File
@@ -5,8 +5,6 @@ use std::sync::LazyLock;
use std::{fs, path::PathBuf, sync::Arc};
use tracing::{error, trace, warn};
const GLOBAL_SETTINGS_FILE_NAME: &str = "dioxus/settings.toml";
/// Describes cli settings from project or global level.
/// The order of priority goes:
/// 1. CLI Flags/Arguments
@@ -29,6 +27,8 @@ pub(crate) struct CliSettings {
pub(crate) no_downloads: Option<bool>,
/// Ignore updates for this version
pub(crate) ignore_version_update: Option<String>,
/// Disable telemetry
pub(crate) disable_telemetry: Option<bool>,
}
impl CliSettings {
@@ -43,38 +43,37 @@ impl CliSettings {
CliSettings::from_global().unwrap_or_default()
}
/// Get the path to the settings toml file.
pub(crate) fn get_settings_path() -> PathBuf {
crate::Workspace::global_settings_file()
}
/// Get the current settings structure from global.
pub(crate) fn from_global() -> Option<Self> {
let Some(path) = dirs::data_local_dir() else {
warn!("failed to get local data directory, some config keys may be missing");
return None;
};
let settings = crate::Workspace::global_settings_file();
let path = path.join(GLOBAL_SETTINGS_FILE_NAME);
let Some(data) = fs::read_to_string(path).ok() else {
// We use a debug here because we expect the file to not exist.
trace!("failed to read `{}` config file", GLOBAL_SETTINGS_FILE_NAME);
if !settings.exists() {
trace!("global settings file does not exist, returning None");
return None;
};
let data = toml::from_str::<CliSettings>(&data).ok();
if data.is_none() {
warn!(
"failed to parse `{}` config file",
GLOBAL_SETTINGS_FILE_NAME
);
}
data
let Some(data) = fs::read_to_string(&settings).ok() else {
warn!("failed to read global settings file");
return None;
};
let Some(data) = serde_json5::from_str::<CliSettings>(&data).ok() else {
warn!("failed to parse global settings file");
return None;
};
Some(data)
}
/// Save the current structure to the global settings toml.
/// This does not save to project-level settings.
pub(crate) fn save(&self) -> Result<()> {
let path = Self::get_settings_path().ok_or_else(|| {
error!(dx_src = ?TraceSrc::Dev, "failed to get settings path");
anyhow::anyhow!("failed to get settings path")
})?;
let path = Self::get_settings_path();
let data = toml::to_string_pretty(&self).map_err(|e| {
error!(dx_src = ?TraceSrc::Dev, ?self, "failed to parse config into toml");
@@ -103,16 +102,6 @@ impl CliSettings {
Ok(())
}
/// Get the path to the settings toml file.
pub(crate) fn get_settings_path() -> Option<PathBuf> {
let Some(path) = dirs::data_local_dir() else {
warn!("failed to get local data directory, some config keys may be missing");
return None;
};
Some(path.join(GLOBAL_SETTINGS_FILE_NAME))
}
/// Modify the settings toml file - doesn't change the settings for this session
pub(crate) fn modify_settings(with: impl FnOnce(&mut CliSettings)) -> Result<()> {
let mut _settings = CliSettings::load();
@@ -129,12 +118,55 @@ impl CliSettings {
return true;
}
if crate::devcfg::no_downloads() {
if std::env::var("NO_DOWNLOADS").is_ok() {
return true;
}
CliSettings::load().no_downloads.unwrap_or_default()
}
/// Check if telemetry is disabled
pub(crate) fn telemetry_disabled() -> bool {
use std::env::var;
static TELEMETRY_DISABLED: LazyLock<bool> = LazyLock::new(|| {
if cfg!(feature = "disable-telemetry") {
return true;
}
if matches!(var("DX_TELEMETRY_ENABLED"), Ok(val) if val.eq_ignore_ascii_case("false") || val == "0")
{
return true;
}
if matches!(var("TELEMETRY"), Ok(val) if val.eq_ignore_ascii_case("false") || val == "0")
{
return true;
}
CliSettings::load().disable_telemetry.unwrap_or_default()
});
*TELEMETRY_DISABLED
}
pub(crate) fn is_ci() -> bool {
static CI: LazyLock<bool> = LazyLock::new(|| {
if matches!(std::env::var("CI"), Ok(val) if val.eq_ignore_ascii_case("true") || val == "1")
{
return true;
}
if matches!(std::env::var("DX_CI"), Ok(val) if val.eq_ignore_ascii_case("true") || val == "1")
{
return true;
}
false
});
*CI
}
}
fn default_wsl_file_poll_interval() -> Option<u16> {
+4
View File
@@ -534,6 +534,10 @@ impl Workspace {
})
.to_path_buf()
}
pub(crate) fn global_settings_file() -> PathBuf {
Self::dioxus_data_dir().join("settings.json")
}
}
impl std::fmt::Debug for Workspace {