Add diagnostics reporting (#264)
* Add diagnostics reporting * Some tidying * Remove injected failure * Update URL * Fixups * Fix tests * Use triples instead of architecture
This commit is contained in:
parent
8de35b2477
commit
19dd7a13d4
33
Cargo.lock
generated
33
Cargo.lock
generated
|
@ -927,6 +927,7 @@ dependencies = [
|
||||||
"eyre",
|
"eyre",
|
||||||
"glob",
|
"glob",
|
||||||
"nix",
|
"nix",
|
||||||
|
"os-release",
|
||||||
"owo-colors",
|
"owo-colors",
|
||||||
"plist",
|
"plist",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
@ -935,6 +936,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
|
"strum",
|
||||||
"tar",
|
"tar",
|
||||||
"target-lexicon",
|
"target-lexicon",
|
||||||
"tempdir",
|
"tempdir",
|
||||||
|
@ -1004,6 +1006,15 @@ version = "1.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
|
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "os-release"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82f29ae2f71b53ec19cc23385f8e4f3d90975195aa3d09171ba3bef7159bec27"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "os_str_bytes"
|
name = "os_str_bytes"
|
||||||
version = "6.4.1"
|
version = "6.4.1"
|
||||||
|
@ -1541,6 +1552,28 @@ version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.24.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.24.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustversion",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "supports-color"
|
name = "supports-color"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
|
|
|
@ -15,8 +15,9 @@ build-inputs = ["darwin.apple_sdk.frameworks.Security"]
|
||||||
build-inputs = ["darwin.apple_sdk.frameworks.Security"]
|
build-inputs = ["darwin.apple_sdk.frameworks.Security"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["cli"]
|
default = ["cli", "diagnostics"]
|
||||||
cli = ["eyre", "color-eyre", "clap", "tracing-subscriber", "tracing-error", "atty"]
|
cli = ["eyre", "color-eyre", "clap", "tracing-subscriber", "tracing-error", "atty"]
|
||||||
|
diagnostics = ["os-release"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "nix-installer"
|
name = "nix-installer"
|
||||||
|
@ -53,6 +54,8 @@ rand = { version = "0.8.5", default-features = false, features = [ "std", "std_r
|
||||||
semver = { version = "1.0.14", default-features = false, features = ["serde", "std"] }
|
semver = { version = "1.0.14", default-features = false, features = ["serde", "std"] }
|
||||||
term = { version = "0.7.0", default-features = false }
|
term = { version = "0.7.0", default-features = false }
|
||||||
uuid = { version = "1.2.2", features = ["serde"] }
|
uuid = { version = "1.2.2", features = ["serde"] }
|
||||||
|
os-release = { version = "0.1.0", default-features = false, optional = true }
|
||||||
|
strum = { version = "0.24.1", features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
eyre = { version = "0.6.8", default-features = false, features = [ "track-caller" ] }
|
eyre = { version = "0.6.8", default-features = false, features = [ "track-caller" ] }
|
||||||
|
|
|
@ -133,6 +133,15 @@ impl Planner for MyPlanner {
|
||||||
|
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
async fn diagnostic_data(&self) -> Result<nix_installer::diagnostics::DiagnosticData, PlannerError> {
|
||||||
|
Ok(nix_installer::diagnostics::DiagnosticData::new(
|
||||||
|
self.common.diagnostic_endpoint.clone(),
|
||||||
|
self.typetag_name().into(),
|
||||||
|
self.configured_settings().await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# async fn custom_planner_install() -> color_eyre::Result<()> {
|
# async fn custom_planner_install() -> color_eyre::Result<()> {
|
||||||
|
@ -244,7 +253,7 @@ impl ActionDescription {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error occurring during an action
|
/// An error occurring during an action
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
|
||||||
pub enum ActionError {
|
pub enum ActionError {
|
||||||
/// A custom error
|
/// A custom error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
|
|
@ -47,6 +47,7 @@ pub struct Install {
|
||||||
global = true
|
global = true
|
||||||
)]
|
)]
|
||||||
pub explain: bool,
|
pub explain: bool,
|
||||||
|
|
||||||
#[clap(env = "NIX_INSTALLER_PLAN")]
|
#[clap(env = "NIX_INSTALLER_PLAN")]
|
||||||
pub plan: Option<PathBuf>,
|
pub plan: Option<PathBuf>,
|
||||||
|
|
||||||
|
@ -133,12 +134,12 @@ impl CommandExecute for Install {
|
||||||
let res = builtin_planner.plan().await;
|
let res = builtin_planner.plan().await;
|
||||||
match res {
|
match res {
|
||||||
Ok(plan) => plan,
|
Ok(plan) => plan,
|
||||||
Err(e) => {
|
Err(err) => {
|
||||||
if let Some(expected) = e.expected() {
|
if let Some(expected) = err.expected() {
|
||||||
eprintln!("{}", expected.red());
|
eprintln!("{}", expected.red());
|
||||||
return Ok(ExitCode::FAILURE);
|
return Ok(ExitCode::FAILURE);
|
||||||
}
|
}
|
||||||
return Err(e.into())
|
return Err(err.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -28,6 +28,7 @@ pub struct Uninstall {
|
||||||
global = true
|
global = true
|
||||||
)]
|
)]
|
||||||
pub no_confirm: bool,
|
pub no_confirm: bool,
|
||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
long,
|
long,
|
||||||
env = "NIX_INSTALLER_EXPLAIN",
|
env = "NIX_INSTALLER_EXPLAIN",
|
||||||
|
@ -36,6 +37,7 @@ pub struct Uninstall {
|
||||||
global = true
|
global = true
|
||||||
)]
|
)]
|
||||||
pub explain: bool,
|
pub explain: bool,
|
||||||
|
|
||||||
#[clap(default_value = RECEIPT_LOCATION)]
|
#[clap(default_value = RECEIPT_LOCATION)]
|
||||||
pub receipt: PathBuf,
|
pub receipt: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
156
src/diagnostics.rs
Normal file
156
src/diagnostics.rs
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
/*! Diagnostic reporting functionality
|
||||||
|
|
||||||
|
When enabled with the `diagnostics` feature (default) this module provides automated install success/failure reporting to an endpoint.
|
||||||
|
|
||||||
|
That endpoint can be a URL such as `https://our.project.org/nix-installer/diagnostics` or `file:///home/$USER/diagnostic.json` which receives a [`DiagnosticReport`] in JSON format.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use os_release::OsRelease;
|
||||||
|
use reqwest::Url;
|
||||||
|
|
||||||
|
/// The static of an action attempt
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
||||||
|
pub enum DiagnosticStatus {
|
||||||
|
Cancelled,
|
||||||
|
Success,
|
||||||
|
/// This includes the [`strum::IntoStaticStr`] representation of the error, we take special care not to include parameters of the error (which may include secrets)
|
||||||
|
Failure(String),
|
||||||
|
Pending,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The action attempted
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Copy)]
|
||||||
|
pub enum DiagnosticAction {
|
||||||
|
Install,
|
||||||
|
Uninstall,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A report sent to an endpoint
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
|
||||||
|
pub struct DiagnosticReport {
|
||||||
|
pub version: String,
|
||||||
|
pub planner: String,
|
||||||
|
pub configured_settings: Vec<String>,
|
||||||
|
pub os_name: String,
|
||||||
|
pub os_version: String,
|
||||||
|
pub triple: String,
|
||||||
|
pub action: DiagnosticAction,
|
||||||
|
pub status: DiagnosticStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A preparation of data to be sent to the `endpoint`.
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Default)]
|
||||||
|
pub struct DiagnosticData {
|
||||||
|
version: String,
|
||||||
|
planner: String,
|
||||||
|
configured_settings: Vec<String>,
|
||||||
|
os_name: String,
|
||||||
|
os_version: String,
|
||||||
|
triple: String,
|
||||||
|
endpoint: Option<Url>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiagnosticData {
|
||||||
|
pub fn new(endpoint: Option<Url>, planner: String, configured_settings: Vec<String>) -> Self {
|
||||||
|
let (os_name, os_version) = match OsRelease::new() {
|
||||||
|
Ok(os_release) => (os_release.name, os_release.version),
|
||||||
|
Err(_) => ("unknown".into(), "unknown".into()),
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
endpoint,
|
||||||
|
version: env!("CARGO_PKG_VERSION").into(),
|
||||||
|
planner,
|
||||||
|
configured_settings,
|
||||||
|
os_name,
|
||||||
|
os_version,
|
||||||
|
triple: target_lexicon::HOST.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn report(&self, action: DiagnosticAction, status: DiagnosticStatus) -> DiagnosticReport {
|
||||||
|
let Self {
|
||||||
|
version,
|
||||||
|
planner,
|
||||||
|
configured_settings,
|
||||||
|
os_name,
|
||||||
|
os_version,
|
||||||
|
triple,
|
||||||
|
endpoint: _,
|
||||||
|
} = self;
|
||||||
|
DiagnosticReport {
|
||||||
|
version: version.clone(),
|
||||||
|
planner: planner.clone(),
|
||||||
|
configured_settings: configured_settings.clone(),
|
||||||
|
os_name: os_name.clone(),
|
||||||
|
os_version: os_version.clone(),
|
||||||
|
triple: triple.clone(),
|
||||||
|
action,
|
||||||
|
status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
|
pub async fn send(
|
||||||
|
self,
|
||||||
|
action: DiagnosticAction,
|
||||||
|
status: DiagnosticStatus,
|
||||||
|
) -> Result<(), DiagnosticError> {
|
||||||
|
let serialized = serde_json::to_string_pretty(&self.report(action, status))?;
|
||||||
|
|
||||||
|
let endpoint = match self.endpoint {
|
||||||
|
Some(endpoint) => endpoint,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match endpoint.scheme() {
|
||||||
|
"https" | "http" => {
|
||||||
|
tracing::debug!("Sending diagnostic to `{endpoint}`");
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let res = client
|
||||||
|
.post(endpoint.clone())
|
||||||
|
.body(serialized)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.timeout(Duration::from_millis(3000))
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(_err) = res {
|
||||||
|
tracing::info!("Failed to send diagnostic to `{endpoint}`, continuing")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"file" => {
|
||||||
|
let path = endpoint.path();
|
||||||
|
tracing::debug!("Writing diagnostic to `{path}`");
|
||||||
|
let res = tokio::fs::write(path, serialized).await;
|
||||||
|
|
||||||
|
if let Err(_err) = res {
|
||||||
|
tracing::info!("Failed to send diagnostic to `{path}`, continuing")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => return Err(DiagnosticError::UnknownUrlScheme),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum DiagnosticError {
|
||||||
|
#[error("Unknown url scheme")]
|
||||||
|
UnknownUrlScheme,
|
||||||
|
#[error("Request error")]
|
||||||
|
Reqwest(
|
||||||
|
#[from]
|
||||||
|
#[source]
|
||||||
|
reqwest::Error,
|
||||||
|
),
|
||||||
|
#[error("Write path `{0}`")]
|
||||||
|
Write(std::path::PathBuf, #[source] std::io::Error),
|
||||||
|
#[error("Serializing receipt")]
|
||||||
|
Serializing(
|
||||||
|
#[from]
|
||||||
|
#[source]
|
||||||
|
serde_json::Error,
|
||||||
|
),
|
||||||
|
}
|
30
src/error.rs
30
src/error.rs
|
@ -3,7 +3,7 @@ use std::path::PathBuf;
|
||||||
use crate::{action::ActionError, planner::PlannerError, settings::InstallSettingsError};
|
use crate::{action::ActionError, planner::PlannerError, settings::InstallSettingsError};
|
||||||
|
|
||||||
/// An error occurring during a call defined in this crate
|
/// An error occurring during a call defined in this crate
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
|
||||||
pub enum NixInstallerError {
|
pub enum NixInstallerError {
|
||||||
/// An error originating from an [`Action`](crate::action::Action)
|
/// An error originating from an [`Action`](crate::action::Action)
|
||||||
#[error("Error executing action")]
|
#[error("Error executing action")]
|
||||||
|
@ -53,6 +53,15 @@ pub enum NixInstallerError {
|
||||||
#[source]
|
#[source]
|
||||||
InstallSettingsError,
|
InstallSettingsError,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
/// Diagnostic error
|
||||||
|
#[error("Diagnostic error")]
|
||||||
|
Diagnostic(
|
||||||
|
#[from]
|
||||||
|
#[source]
|
||||||
|
crate::diagnostics::DiagnosticError,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait HasExpectedErrors: std::error::Error + Sized + Send + Sync {
|
pub(crate) trait HasExpectedErrors: std::error::Error + Sized + Send + Sync {
|
||||||
|
@ -70,6 +79,25 @@ impl HasExpectedErrors for NixInstallerError {
|
||||||
NixInstallerError::SemVer(_) => None,
|
NixInstallerError::SemVer(_) => None,
|
||||||
NixInstallerError::Planner(planner_error) => planner_error.expected(),
|
NixInstallerError::Planner(planner_error) => planner_error.expected(),
|
||||||
NixInstallerError::InstallSettings(_) => None,
|
NixInstallerError::InstallSettings(_) => None,
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
NixInstallerError::Diagnostic(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #[cfg(feature = "diagnostics")]
|
||||||
|
// impl NixInstallerError {
|
||||||
|
// pub fn diagnostic_synopsis(&self) -> &'static str {
|
||||||
|
// match self {
|
||||||
|
// NixInstallerError::Action(inner) => inner.into(),
|
||||||
|
// NixInstallerError::Planner(inner) => inner.into(),
|
||||||
|
// NixInstallerError::RecordingReceipt(_, _)
|
||||||
|
// | NixInstallerError::CopyingSelf(_)
|
||||||
|
// | NixInstallerError::SerializingReceipt(_)
|
||||||
|
// | NixInstallerError::Cancelled
|
||||||
|
// | NixInstallerError::SemVer(_)
|
||||||
|
// | NixInstallerError::Diagnostic(_)
|
||||||
|
// | NixInstallerError::InstallSettings(_) => self.into(),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
|
@ -73,6 +73,8 @@ pub mod action;
|
||||||
mod channel_value;
|
mod channel_value;
|
||||||
#[cfg(feature = "cli")]
|
#[cfg(feature = "cli")]
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
pub mod diagnostics;
|
||||||
mod error;
|
mod error;
|
||||||
mod os;
|
mod os;
|
||||||
mod plan;
|
mod plan;
|
||||||
|
|
102
src/plan.rs
102
src/plan.rs
|
@ -24,17 +24,27 @@ pub struct InstallPlan {
|
||||||
pub(crate) actions: Vec<StatefulAction<Box<dyn Action>>>,
|
pub(crate) actions: Vec<StatefulAction<Box<dyn Action>>>,
|
||||||
|
|
||||||
pub(crate) planner: Box<dyn Planner>,
|
pub(crate) planner: Box<dyn Planner>,
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
pub(crate) diagnostic_data: Option<crate::diagnostics::DiagnosticData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InstallPlan {
|
impl InstallPlan {
|
||||||
pub async fn default() -> Result<Self, NixInstallerError> {
|
pub async fn default() -> Result<Self, NixInstallerError> {
|
||||||
let planner = BuiltinPlanner::default().await?.boxed();
|
let planner = BuiltinPlanner::default().await?;
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
let diagnostic_data = Some(planner.diagnostic_data().await?);
|
||||||
|
|
||||||
|
let planner = planner.boxed();
|
||||||
let actions = planner.plan().await?;
|
let actions = planner.plan().await?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
planner,
|
planner,
|
||||||
actions,
|
actions,
|
||||||
version: current_version()?,
|
version: current_version()?,
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
diagnostic_data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,11 +52,16 @@ impl InstallPlan {
|
||||||
where
|
where
|
||||||
P: Planner + 'static,
|
P: Planner + 'static,
|
||||||
{
|
{
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
let diagnostic_data = Some(planner.diagnostic_data().await?);
|
||||||
|
|
||||||
let actions = planner.plan().await?;
|
let actions = planner.plan().await?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
planner: planner.boxed(),
|
planner: planner.boxed(),
|
||||||
actions,
|
actions,
|
||||||
version: current_version()?,
|
version: current_version()?,
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
diagnostic_data,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
#[tracing::instrument(level = "debug", skip_all)]
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
|
@ -55,6 +70,7 @@ impl InstallPlan {
|
||||||
planner,
|
planner,
|
||||||
actions,
|
actions,
|
||||||
version,
|
version,
|
||||||
|
..
|
||||||
} = self;
|
} = self;
|
||||||
let buf = format!(
|
let buf = format!(
|
||||||
"\
|
"\
|
||||||
|
@ -107,11 +123,7 @@ impl InstallPlan {
|
||||||
&mut self,
|
&mut self,
|
||||||
cancel_channel: impl Into<Option<Receiver<()>>>,
|
cancel_channel: impl Into<Option<Receiver<()>>>,
|
||||||
) -> Result<(), NixInstallerError> {
|
) -> Result<(), NixInstallerError> {
|
||||||
let Self {
|
let Self { actions, .. } = self;
|
||||||
version: _,
|
|
||||||
actions,
|
|
||||||
planner: _,
|
|
||||||
} = self;
|
|
||||||
let mut cancel_channel = cancel_channel.into();
|
let mut cancel_channel = cancel_channel.into();
|
||||||
|
|
||||||
// This is **deliberately sequential**.
|
// This is **deliberately sequential**.
|
||||||
|
@ -125,6 +137,18 @@ impl InstallPlan {
|
||||||
if let Err(err) = write_receipt(self.clone()).await {
|
if let Err(err) = write_receipt(self.clone()).await {
|
||||||
tracing::error!("Error saving receipt: {:?}", err);
|
tracing::error!("Error saving receipt: {:?}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
if let Some(diagnostic_data) = &self.diagnostic_data {
|
||||||
|
diagnostic_data
|
||||||
|
.clone()
|
||||||
|
.send(
|
||||||
|
crate::diagnostics::DiagnosticAction::Install,
|
||||||
|
crate::diagnostics::DiagnosticStatus::Cancelled,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
return Err(NixInstallerError::Cancelled);
|
return Err(NixInstallerError::Cancelled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,11 +158,35 @@ impl InstallPlan {
|
||||||
if let Err(err) = write_receipt(self.clone()).await {
|
if let Err(err) = write_receipt(self.clone()).await {
|
||||||
tracing::error!("Error saving receipt: {:?}", err);
|
tracing::error!("Error saving receipt: {:?}", err);
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
if let Some(diagnostic_data) = &self.diagnostic_data {
|
||||||
|
diagnostic_data
|
||||||
|
.clone()
|
||||||
|
.send(
|
||||||
|
crate::diagnostics::DiagnosticAction::Install,
|
||||||
|
crate::diagnostics::DiagnosticStatus::Failure({
|
||||||
|
let x: &'static str = (&err).into();
|
||||||
|
x.to_string()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
return Err(NixInstallerError::Action(err));
|
return Err(NixInstallerError::Action(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
write_receipt(self.clone()).await?;
|
write_receipt(self.clone()).await?;
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
if let Some(diagnostic_data) = &self.diagnostic_data {
|
||||||
|
diagnostic_data
|
||||||
|
.clone()
|
||||||
|
.send(
|
||||||
|
crate::diagnostics::DiagnosticAction::Install,
|
||||||
|
crate::diagnostics::DiagnosticStatus::Success,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -149,6 +197,7 @@ impl InstallPlan {
|
||||||
version: _,
|
version: _,
|
||||||
planner,
|
planner,
|
||||||
actions,
|
actions,
|
||||||
|
..
|
||||||
} = self;
|
} = self;
|
||||||
let buf = format!(
|
let buf = format!(
|
||||||
"\
|
"\
|
||||||
|
@ -207,11 +256,7 @@ impl InstallPlan {
|
||||||
&mut self,
|
&mut self,
|
||||||
cancel_channel: impl Into<Option<Receiver<()>>>,
|
cancel_channel: impl Into<Option<Receiver<()>>>,
|
||||||
) -> Result<(), NixInstallerError> {
|
) -> Result<(), NixInstallerError> {
|
||||||
let Self {
|
let Self { actions, .. } = self;
|
||||||
version: _,
|
|
||||||
actions,
|
|
||||||
planner: _,
|
|
||||||
} = self;
|
|
||||||
let mut cancel_channel = cancel_channel.into();
|
let mut cancel_channel = cancel_channel.into();
|
||||||
|
|
||||||
// This is **deliberately sequential**.
|
// This is **deliberately sequential**.
|
||||||
|
@ -225,6 +270,17 @@ impl InstallPlan {
|
||||||
if let Err(err) = write_receipt(self.clone()).await {
|
if let Err(err) = write_receipt(self.clone()).await {
|
||||||
tracing::error!("Error saving receipt: {:?}", err);
|
tracing::error!("Error saving receipt: {:?}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
if let Some(diagnostic_data) = &self.diagnostic_data {
|
||||||
|
diagnostic_data
|
||||||
|
.clone()
|
||||||
|
.send(
|
||||||
|
crate::diagnostics::DiagnosticAction::Uninstall,
|
||||||
|
crate::diagnostics::DiagnosticStatus::Cancelled,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
return Err(NixInstallerError::Cancelled);
|
return Err(NixInstallerError::Cancelled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -234,10 +290,34 @@ impl InstallPlan {
|
||||||
if let Err(err) = write_receipt(self.clone()).await {
|
if let Err(err) = write_receipt(self.clone()).await {
|
||||||
tracing::error!("Error saving receipt: {:?}", err);
|
tracing::error!("Error saving receipt: {:?}", err);
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
if let Some(diagnostic_data) = &self.diagnostic_data {
|
||||||
|
diagnostic_data
|
||||||
|
.clone()
|
||||||
|
.send(
|
||||||
|
crate::diagnostics::DiagnosticAction::Uninstall,
|
||||||
|
crate::diagnostics::DiagnosticStatus::Failure({
|
||||||
|
let x: &'static str = (&err).into();
|
||||||
|
x.to_string()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
return Err(NixInstallerError::Action(err));
|
return Err(NixInstallerError::Action(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
if let Some(diagnostic_data) = &self.diagnostic_data {
|
||||||
|
diagnostic_data
|
||||||
|
.clone()
|
||||||
|
.send(
|
||||||
|
crate::diagnostics::DiagnosticAction::Uninstall,
|
||||||
|
crate::diagnostics::DiagnosticStatus::Success,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,15 @@ impl Planner for Linux {
|
||||||
|
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
async fn diagnostic_data(&self) -> Result<crate::diagnostics::DiagnosticData, PlannerError> {
|
||||||
|
Ok(crate::diagnostics::DiagnosticData::new(
|
||||||
|
self.settings.diagnostic_endpoint.clone(),
|
||||||
|
self.typetag_name().into(),
|
||||||
|
self.configured_settings().await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<BuiltinPlanner> for Linux {
|
impl Into<BuiltinPlanner> for Linux {
|
||||||
|
|
|
@ -169,6 +169,15 @@ impl Planner for Macos {
|
||||||
|
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
async fn diagnostic_data(&self) -> Result<crate::diagnostics::DiagnosticData, PlannerError> {
|
||||||
|
Ok(crate::diagnostics::DiagnosticData::new(
|
||||||
|
self.settings.diagnostic_endpoint.clone(),
|
||||||
|
self.typetag_name().into(),
|
||||||
|
self.configured_settings().await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<BuiltinPlanner> for Macos {
|
impl Into<BuiltinPlanner> for Macos {
|
||||||
|
|
|
@ -52,6 +52,15 @@ impl Planner for MyPlanner {
|
||||||
|
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
async fn diagnostic_data(&self) -> Result<nix_installer::diagnostics::DiagnosticData, PlannerError> {
|
||||||
|
Ok(nix_installer::diagnostics::DiagnosticData::new(
|
||||||
|
self.common.diagnostic_endpoint.clone(),
|
||||||
|
self.typetag_name().into(),
|
||||||
|
self.configured_settings().await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# async fn custom_planner_install() -> color_eyre::Result<()> {
|
# async fn custom_planner_install() -> color_eyre::Result<()> {
|
||||||
|
@ -101,6 +110,23 @@ pub trait Planner: std::fmt::Debug + Send + Sync + dyn_clone::DynClone {
|
||||||
async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError>;
|
async fn plan(&self) -> Result<Vec<StatefulAction<Box<dyn Action>>>, PlannerError>;
|
||||||
/// The settings being used by the planner
|
/// The settings being used by the planner
|
||||||
fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError>;
|
fn settings(&self) -> Result<HashMap<String, serde_json::Value>, InstallSettingsError>;
|
||||||
|
|
||||||
|
async fn configured_settings(&self) -> Result<Vec<String>, PlannerError>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let default = Self::default().await?.settings()?;
|
||||||
|
let configured = self.settings()?;
|
||||||
|
|
||||||
|
let mut keys: Vec<String> = Vec::new();
|
||||||
|
for (key, value) in configured.iter() {
|
||||||
|
if default.get(key) != Some(value) {
|
||||||
|
keys.push(key.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(keys)
|
||||||
|
}
|
||||||
|
|
||||||
/// A boxed, type erased planner
|
/// A boxed, type erased planner
|
||||||
fn boxed(self) -> Box<dyn Planner>
|
fn boxed(self) -> Box<dyn Planner>
|
||||||
where
|
where
|
||||||
|
@ -108,6 +134,9 @@ pub trait Planner: std::fmt::Debug + Send + Sync + dyn_clone::DynClone {
|
||||||
{
|
{
|
||||||
Box::new(self)
|
Box::new(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
async fn diagnostic_data(&self) -> Result<crate::diagnostics::DiagnosticData, PlannerError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
dyn_clone::clone_trait_object!(Planner);
|
dyn_clone::clone_trait_object!(Planner);
|
||||||
|
@ -171,6 +200,17 @@ impl BuiltinPlanner {
|
||||||
Ok(built)
|
Ok(built)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn configured_settings(&self) -> Result<Vec<String>, PlannerError> {
|
||||||
|
match self {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
BuiltinPlanner::Linux(inner) => inner.configured_settings().await,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
BuiltinPlanner::SteamDeck(inner) => inner.configured_settings().await,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
BuiltinPlanner::Macos(inner) => inner.configured_settings().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn plan(self) -> Result<InstallPlan, NixInstallerError> {
|
pub async fn plan(self) -> Result<InstallPlan, NixInstallerError> {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
@ -213,10 +253,24 @@ impl BuiltinPlanner {
|
||||||
BuiltinPlanner::Macos(i) => i.settings(),
|
BuiltinPlanner::Macos(i) => i.settings(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
pub async fn diagnostic_data(
|
||||||
|
&self,
|
||||||
|
) -> Result<crate::diagnostics::DiagnosticData, PlannerError> {
|
||||||
|
match self {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
BuiltinPlanner::Linux(i) => i.diagnostic_data().await,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
BuiltinPlanner::SteamDeck(i) => i.diagnostic_data().await,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
BuiltinPlanner::Macos(i) => i.diagnostic_data().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error originating from a [`Planner`]
|
/// An error originating from a [`Planner`]
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
|
||||||
pub enum PlannerError {
|
pub enum PlannerError {
|
||||||
/// `nix-installer` does not have a default planner for the target architecture right now
|
/// `nix-installer` does not have a default planner for the target architecture right now
|
||||||
#[error("`nix-installer` does not have a default planner for the `{0}` architecture right now, pass a specific archetype")]
|
#[error("`nix-installer` does not have a default planner for the `{0}` architecture right now, pass a specific archetype")]
|
||||||
|
|
|
@ -252,6 +252,15 @@ impl Planner for SteamDeck {
|
||||||
|
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
async fn diagnostic_data(&self) -> Result<crate::diagnostics::DiagnosticData, PlannerError> {
|
||||||
|
Ok(crate::diagnostics::DiagnosticData::new(
|
||||||
|
self.settings.diagnostic_endpoint.clone(),
|
||||||
|
self.typetag_name().into(),
|
||||||
|
self.configured_settings().await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<BuiltinPlanner> for SteamDeck {
|
impl Into<BuiltinPlanner> for SteamDeck {
|
||||||
|
|
|
@ -202,6 +202,31 @@ pub struct CommonSettings {
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
pub(crate) force: bool,
|
pub(crate) force: bool,
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
/// The URL or file path for an installation diagnostic to be sent
|
||||||
|
///
|
||||||
|
/// Sample of the data sent:
|
||||||
|
///
|
||||||
|
/// {
|
||||||
|
/// "version": "0.3.0",
|
||||||
|
/// "planner": "linux",
|
||||||
|
/// "configured-settings": [ "modify_profile" ],
|
||||||
|
/// "os-name": "Ubuntu",
|
||||||
|
/// "os-version": "22.04.1 LTS (Jammy Jellyfish)",
|
||||||
|
/// "triple": "x86_64-unknown-linux-gnu",
|
||||||
|
/// "action": "Install",
|
||||||
|
/// "status": "Success"
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// To disable diagnostic reporting, unset the default with `--diagnostic-endpoint=`
|
||||||
|
#[clap(
|
||||||
|
long,
|
||||||
|
env = "NIX_INSTALLER_DIAGNOSTIC_ENDPOINT",
|
||||||
|
global = true,
|
||||||
|
default_value = "https://install.determinate.systems/nix/diagnostic"
|
||||||
|
)]
|
||||||
|
pub diagnostic_endpoint: Option<Url>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommonSettings {
|
impl CommonSettings {
|
||||||
|
@ -217,19 +242,19 @@ impl CommonSettings {
|
||||||
(Architecture::X86_64, OperatingSystem::Linux) => {
|
(Architecture::X86_64, OperatingSystem::Linux) => {
|
||||||
url = NIX_X64_64_LINUX_URL;
|
url = NIX_X64_64_LINUX_URL;
|
||||||
nix_build_user_prefix = "nixbld";
|
nix_build_user_prefix = "nixbld";
|
||||||
nix_build_user_id_base = 3000;
|
nix_build_user_id_base = 30000;
|
||||||
},
|
},
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
(Architecture::X86_32(_), OperatingSystem::Linux) => {
|
(Architecture::X86_32(_), OperatingSystem::Linux) => {
|
||||||
url = NIX_I686_LINUX_URL;
|
url = NIX_I686_LINUX_URL;
|
||||||
nix_build_user_prefix = "nixbld";
|
nix_build_user_prefix = "nixbld";
|
||||||
nix_build_user_id_base = 3000;
|
nix_build_user_id_base = 30000;
|
||||||
},
|
},
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
(Architecture::Aarch64(_), OperatingSystem::Linux) => {
|
(Architecture::Aarch64(_), OperatingSystem::Linux) => {
|
||||||
url = NIX_AARCH64_LINUX_URL;
|
url = NIX_AARCH64_LINUX_URL;
|
||||||
nix_build_user_prefix = "nixbld";
|
nix_build_user_prefix = "nixbld";
|
||||||
nix_build_user_id_base = 3000;
|
nix_build_user_id_base = 30000;
|
||||||
},
|
},
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
(Architecture::X86_64, OperatingSystem::MacOSX { .. })
|
(Architecture::X86_64, OperatingSystem::MacOSX { .. })
|
||||||
|
@ -267,6 +292,10 @@ impl CommonSettings {
|
||||||
nix_package_url: url.parse()?,
|
nix_package_url: url.parse()?,
|
||||||
extra_conf: Default::default(),
|
extra_conf: Default::default(),
|
||||||
force: false,
|
force: false,
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
diagnostic_endpoint: Some(
|
||||||
|
"https://install.determinate.systems/diagnostics".try_into()?,
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,6 +312,8 @@ impl CommonSettings {
|
||||||
nix_package_url,
|
nix_package_url,
|
||||||
extra_conf,
|
extra_conf,
|
||||||
force,
|
force,
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
diagnostic_endpoint,
|
||||||
} = self;
|
} = self;
|
||||||
let mut map = HashMap::default();
|
let mut map = HashMap::default();
|
||||||
|
|
||||||
|
@ -326,6 +357,12 @@ impl CommonSettings {
|
||||||
map.insert("extra_conf".into(), serde_json::to_value(extra_conf)?);
|
map.insert("extra_conf".into(), serde_json::to_value(extra_conf)?);
|
||||||
map.insert("force".into(), serde_json::to_value(force)?);
|
map.insert("force".into(), serde_json::to_value(force)?);
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
map.insert(
|
||||||
|
"diagnostic_endpoint".into(),
|
||||||
|
serde_json::to_value(diagnostic_endpoint)?,
|
||||||
|
);
|
||||||
|
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -418,6 +455,13 @@ impl CommonSettings {
|
||||||
self.force = force;
|
self.force = force;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "diagnostics")]
|
||||||
|
/// The URL or file path for an [`DiagnosticReport`][crate::diagnostics::DiagnosticReport] to be sent
|
||||||
|
pub fn diagnostic_endpoint(&mut self, diagnostic_endpoint: Option<Url>) -> &mut Self {
|
||||||
|
self.diagnostic_endpoint = diagnostic_endpoint;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_with::serde_as]
|
#[serde_with::serde_as]
|
||||||
|
|
Loading…
Reference in a new issue