Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ tempfile = "3.20.0"
kdl = "6.5.0"
libc = "0.2.62"
cbindgen = "0.29.2"
arc-swap = "1.8.0"

[workspace.lints.rust]
rust_2018_idioms = { level = "warn", priority = -1 }
Expand Down
1 change: 1 addition & 0 deletions moss/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ triggers = { path = "../crates/triggers" }
tui = { path = "../crates/tui" }
vfs = { path = "../crates/vfs" }

arc-swap.workspace = true
blsforme.workspace = true
bytes.workspace = true
camino.workspace = true
Expand Down
21 changes: 20 additions & 1 deletion moss/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ use clap_complete::{
shells::{Bash, Fish, Zsh},
};
use clap_mangen::Man;
use moss::{Installation, installation};
use moss::{
Installation, installation,
output::{self, DefaultOutput, TracingOutput},
};
use thiserror::Error;
use tracing_common::{self, logging::LogConfig, logging::init_log_with_config};
use tui::Styled;
Expand Down Expand Up @@ -101,6 +104,14 @@ fn command() -> Command {
.value_name("DIR")
.hide(true),
)
.arg(
Arg::new("silent")
.short('s')
.long("silent")
.global(true)
.help("Suppress all output")
.action(ArgAction::SetTrue),
)
.arg_required_else_help(true)
.subcommand(boot::command())
.subcommand(cache::command())
Expand Down Expand Up @@ -159,11 +170,19 @@ pub fn process() -> Result<(), Error> {
let matches = command().get_matches_from(args);

let show_version = matches.get_one::<bool>("version").is_some_and(|v| *v);
let silent = matches.get_one::<bool>("silent").is_some_and(|v| *v);

if show_version {
println!("moss {}", tools_buildinfo::get_full_version());
}

if silent {
// We still output logging, that's controlled w/ a separate flag
output::install_emitter(TracingOutput::default());
} else {
output::install_emitter(DefaultOutput::default());
}

if let Some(log_config) = matches.get_one::<LogConfig>("log") {
init_log_with_config(log_config.clone());
}
Expand Down
1 change: 1 addition & 0 deletions moss/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod db;
pub mod dependency;
pub mod environment;
pub mod installation;
pub mod output;
pub mod package;
pub mod registry;
pub mod repository;
Expand Down
89 changes: 89 additions & 0 deletions moss/src/output.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: Copyright © 2020-2026 Serpent OS Developers
//
// SPDX-License-Identifier: MPL-2.0

use std::sync::{Arc, OnceLock};

use crate::repository;

pub use self::tracing::TracingOutput;
pub use self::tui::TuiOutput;

mod tracing;
mod tui;

/// Default emitter used if [`install_emitter`] isn't called with a
/// custom [`Emit`] implementation.
pub type DefaultOutput = Chain<TracingOutput, TuiOutput>;

/// Global emitter either defaulted or installed via [`install_emitter`]
static EMITTER: OnceLock<Arc<dyn Emitter>> = OnceLock::new();

/// Install a global emitter that can be used with [`emit`]. If not called,
/// [`DefaultOutput`] is used.
///
/// This can only be called once. Future calls have no effect.
pub fn install_emitter(emitter: impl Emitter + 'static) {
let _ = EMITTER.set(Arc::new(emitter));
}

/// Get access to the global emitter
pub fn emitter() -> &'static dyn Emitter {
EMITTER.get_or_init(|| Arc::new(DefaultOutput::default())).as_ref()
}

/// Emit an event for output
#[macro_export]
macro_rules! emit {
($($tt:tt)*) => {
$crate::output::Event::emit(($($tt)*), $crate::output::emitter());
};
}

/// Defines how events are emitted to some output
pub trait Emitter: Send + Sync {
fn emit(&self, _event: &InternalEvent) {}
}

/// An emittable event
pub trait Event: Sized {
fn emit(self, _emitter: &dyn Emitter) {}
}

/// An internal `moss` library event
pub enum InternalEvent {
RepositoryManager(repository::manager::OutputEvent),
}

pub trait EmitExt: Emitter + Sized {
fn chain<U>(self, other: U) -> Chain<Self, U>
where
U: Emitter + Sized,
{
Chain { a: self, b: other }
}
}

/// Do nothing with / suppress all output
#[derive(Debug, Clone, Copy)]
pub struct NoopOutput;

impl Emitter for NoopOutput {}

/// Chains multiple emitters together
#[derive(Clone, Default)]
pub struct Chain<A, B> {
a: A,
b: B,
}

impl<A, B> Emitter for Chain<A, B>
where
A: Emitter,
B: Emitter,
{
fn emit(&self, event: &InternalEvent) {
self.a.emit(event);
self.b.emit(event);
}
}
51 changes: 51 additions & 0 deletions moss/src/output/tracing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use tracing::info;

use crate::{output, repository};

/// Tracing output
#[derive(Debug, Clone, Default)]
pub struct TracingOutput {
_tracing: TracingState,
}

impl output::Emitter for TracingOutput {
fn emit(&self, event: &output::InternalEvent) {
match event {
output::InternalEvent::RepositoryManager(event) => match event {
repository::manager::OutputEvent::RefreshStarted { num_to_refresh } => {
info!(
target: "repository_manager",
num_repositories = %num_to_refresh,
"Refreshing repositories"
);
}
repository::manager::OutputEvent::RefreshRepoStarted(id) => {
info!(
target: "repository_manager",
repo_id = %id,
"Refreshing repository"
);
}
repository::manager::OutputEvent::RefreshRepoFinished(id) => {
info!(
target: "repository_manager",
repo_id = %id,
"Repository refreshed"
);
}
repository::manager::OutputEvent::RefreshFinished { elapsed } => {
info!(
target: "repository_manager",
elapsed_seconds = %elapsed.as_secs_f32(),
"All repositories refreshed"
);
}
},
}
}
}

#[derive(Debug, Clone, Default)]
struct TracingState {
// spans: Arc<ArcSwap<HashMap<TypeId, tracing::Span>>>,
}
95 changes: 95 additions & 0 deletions moss/src/output/tui.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use std::{collections::HashMap, fmt, sync::Arc, time::Duration};

use arc_swap::ArcSwap;
use tui::{MultiProgress, ProgressBar, ProgressStyle, Styled};

use crate::{output, repository};

/// Textual output to stdout / stderr
#[derive(Debug, Clone, Default)]
pub struct TuiOutput {
progress: ProgressState,
}

impl output::Emitter for TuiOutput {
fn emit(&self, event: &output::InternalEvent) {
match event {
output::InternalEvent::RepositoryManager(event) => self.emit_repository_manager(event),
}
}
}

impl TuiOutput {
fn emit_repository_manager(&self, event: &repository::manager::OutputEvent) {
match event {
repository::manager::OutputEvent::RefreshStarted { .. } => {
self.progress.multi_start();
}
repository::manager::OutputEvent::RefreshRepoStarted(id) => {
let id = id.to_string();
let pb = self.progress.multi_add_pb(
&id,
ProgressBar::new_spinner()
.with_style(
ProgressStyle::with_template(" {spinner} {wide_msg}")
.unwrap()
.tick_chars("--=≡■≡=--"),
)
.with_message(format!("{} {id}", "Refreshing".blue())),
);
pb.enable_steady_tick(Duration::from_millis(150));
}
repository::manager::OutputEvent::RefreshRepoFinished(id) => {
let id = id.to_string();
self.progress
.multi_pb_println(&id, format_args!("{} {id}", "Refreshed".green()));
self.progress.multi_remove_pb(&id);
}
repository::manager::OutputEvent::RefreshFinished { .. } => {
self.progress.multi_finish();
}
}
}
}

#[derive(Debug, Clone, Default)]
struct ProgressState {
// ArcSwap provides lock-free safe usage in sync & async environments
mpb: Arc<ArcSwap<MultiProgress>>,
pbs: Arc<ArcSwap<HashMap<String, ProgressBar>>>,
}

impl ProgressState {
fn multi_start(&self) {
self.mpb.store(Arc::new(MultiProgress::new()));
self.pbs.store(Arc::new(HashMap::new()));
}

fn multi_add_pb(&self, id: &str, pb: ProgressBar) -> ProgressBar {
let pb = self.mpb.load().add(pb);
self.pbs.rcu(|pbs| {
let mut pbs = (**pbs).clone();
pbs.insert(id.to_owned(), pb.clone());
Arc::new(pbs)
});
pb
}

fn multi_pb_println(&self, id: &str, args: fmt::Arguments<'_>) {
let pbs = self.pbs.load();
pbs.get(id).expect("pb exists").suspend(|| println!("{args}"));
}

fn multi_remove_pb(&self, id: &str) {
self.pbs.rcu(|pbs| {
let mut pbs = (**pbs).clone();
pbs.remove(id);
Arc::new(pbs)
});
}

fn multi_finish(&self) {
self.mpb.store(Arc::new(MultiProgress::new()));
self.pbs.store(Arc::new(HashMap::new()));
}
}
Loading