Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
232e127
start creating mock GUI with egui
sermuns Apr 28, 2026
e9ffd22
only use glow backend, add rfd
sermuns Apr 29, 2026
50743d0
simplify structure, add input file pick
sermuns Apr 29, 2026
c547dd2
start adding hashing, overall app state
sermuns Apr 29, 2026
c0b79ba
create `sections.rs`, create functions per section. add (shitty) targ…
sermuns Apr 30, 2026
95bb4d0
start creating "begin write" section
sermuns Apr 30, 2026
9ad3e08
REMOVEME: remove all CI
sermuns Apr 30, 2026
05c9644
Revert "REMOVEME: remove all CI"
sermuns Apr 30, 2026
b97c443
WIP: use local flavor of tokio runtime (PROBABLY NOT THE SOLUTION) an…
sermuns Apr 30, 2026
149f799
go back to multithreaded tokio runtime
sermuns May 4, 2026
2da6c03
somewhat working PoC, but insanely hacky. Spawning a thread just for …
sermuns May 5, 2026
aac7fff
revert changes to other code
sermuns May 6, 2026
c351ee3
WIP: rework using syncified code. remove `sections.rs`, just have the…
sermuns May 6, 2026
c479b7d
rename methods, simplify
sermuns May 6, 2026
90d673c
WIP: using orchestrator in main thread. THIS WONT WORK actually, need…
sermuns May 6, 2026
32a9573
rename Orchestrator to Facade
ifd3f May 7, 2026
737253b
Split Facade into multiple traits
ifd3f May 7, 2026
108a108
Move write_verify and hash into submodule of facade
ifd3f May 7, 2026
c3da6d7
now split Orchestrator into workflow-specific traits
ifd3f May 7, 2026
e766ab7
split into workflow and worker error
ifd3f May 7, 2026
531c636
fix all the errors
ifd3f May 7, 2026
60012b0
Rename and document stuff
ifd3f May 7, 2026
a137cac
Merge branch 'refactor/facade-split' into gui
ifd3f May 7, 2026
9daebf8
export `ui::simple_ui::facade_ext::FacadeExt` publicly
sermuns May 7, 2026
a755a0f
WIP: try to correctly implement worker thread
sermuns May 7, 2026
8f4bc95
remove generics, simplifying impl blocks
sermuns May 7, 2026
cb020bc
somewhat working, need to clean this shit code up
sermuns May 7, 2026
ce7f480
very shitty but working abort worker threada
sermuns May 7, 2026
47fa7eb
add TODO
sermuns May 7, 2026
d86aa2c
move sections into own module (again). add back confirm write button
sermuns May 7, 2026
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
4,852 changes: 4,105 additions & 747 deletions Cargo.lock

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is one chunky boi

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ clap = { version = "4.5.49", features = ["derive", "cargo", "wrap_help"] }
crossterm = { version = "0.27.0", features = ["event-stream"] }
derive_more = "0.99.20"
digest = "0.10.7"
eframe = { version = "0.34", optional = true, default-features = false, features = ["accesskit", "default_fonts", "wayland", "web_screen_reader", "x11", "glow", "persistence"] }
egui = { version = "0.34", optional = true }
flate2 = "1.1.4"
format-bytes = "0.3.0"
futures = { version = "0.3.31", default-features = false, features = ["std"] }
Expand All @@ -40,6 +42,7 @@ lz4_flex = { version = "0.11.6", default-features = false, features = [
# aliased to appease cargo-machete because the crate's namespace is md5 rather than md_5
md5 = { package = "md-5", version = "0.10.6", default-features = false }
process_path = "0.1.4"
rfd = "0.17"
ratatui = { version = "0.26.3", default-features = false, features = [
"crossterm",
] }
Expand All @@ -65,6 +68,9 @@ tracing-unwrap = "1.0.1"
which = "6.0.3"
xz2 = { version = "0.1.7", features = ["static"] }

[features]
gui = ["dep:egui", "dep:eframe"]

[dev-dependencies]
approx = "0.5.1"
assert_matches = "1.5.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use bytes::Bytes;
use crate::{compression::CompressionFormat, hash::HashAlg, util::candidate::Candidates};

/// Result of analyzing an input file for its properties.
pub struct InputAnalysis {
pub struct FileAnalysis {
pub compression: Candidates<CompressionCandidate>,
pub hash_file: Candidates<HashFile>,
}
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use tracing::{debug, trace};
use tracing_unwrap::ResultExt;

use super::{
DaemonError, HerdHandle, HerderFacade, StartWriterError,
DaemonError, HerdHandle, LegacyFacade, StartWriterError,
client::{HerderClient, HerderClientFactory, LazyHerderClient, RawHerderClient},
};
use crate::{
Expand All @@ -16,11 +16,11 @@ use crate::{
ipc_common::read_msg_async,
};

/// Make the actual prod-used [HerderFacade].
/// Make the actual prod-used [LegacyFacade].
///
/// Doing it this way with a function is so that we can hide all of those ugly
/// ugly ugly type signatures under a nice `impl HerderFacade + 'static`!
pub fn make_herder_facade_impl(log_path: &str) -> impl HerderFacade + 'static {
/// ugly ugly type signatures under a nice `impl LegacyFacade + 'static`!
pub fn make_legacy_facade_impl(log_path: &str) -> impl LegacyFacade + 'static {
let event_demux = Arc::new(std::sync::Mutex::new(EventDemuxMap::new()));

/// Simple implementor of [HerderClientFactory].
Expand Down Expand Up @@ -61,7 +61,7 @@ pub fn make_herder_facade_impl(log_path: &str) -> impl HerderFacade + 'static {
}
}

/// Implementation of the actual [HerderFacade] used by Caligula.
/// Implementation of the actual [LegacyFacade] used by Caligula.
struct HerderFacadeImpl<Std, Esc> {
event_demux: Arc<std::sync::Mutex<EventDemuxMap<u64, TopLevelHerdEvent>>>,
next_writer_id: u64,
Expand All @@ -70,7 +70,7 @@ struct HerderFacadeImpl<Std, Esc> {
escalated_daemon: Esc,
}

impl<Std, Esc> HerderFacade for HerderFacadeImpl<Std, Esc>
impl<Std, Esc> LegacyFacade for HerderFacadeImpl<Std, Esc>
where
Std: HerderClient,
Esc: HerderClient,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
//! WARNING: HERE THERE BE DRAGONS
//!
//! The good parts of this submodule will get assimilated into orchestrator once
//! I get back to working on the stdiomux branch. Don't rely on this module
//! whatsoever! Orchestrator is mildly stable though.
//! The good parts of this submodule will get assimilated into CaligulaFacade
//! once I get back to working on the stdiomux branch. Don't rely on this module
//! whatsoever! CaligulaFacade is mildly stable though.

mod client;
mod facade;

pub use facade::make_herder_facade_impl;
pub use facade::make_legacy_facade_impl;
use futures::stream::BoxStream;

use crate::herder_api::{HerdAction, HerdEvent, TopLevelHerdEvent};
Expand All @@ -20,7 +20,7 @@ use crate::herder_api::{HerdAction, HerdEvent, TopLevelHerdEvent};
///
/// Making it a trait is so that we can easily test the UI as a separate
/// component from the backend.
pub trait HerderFacade {
pub trait LegacyFacade {
async fn start_herd<A: HerdAction>(
&mut self,
action: A,
Expand Down
112 changes: 112 additions & 0 deletions src/facade/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//! Exposes the [`CaligulaFacade`], which is a facade that orchestrates all
//! "high-level" work and tracks the state of worker tasks.

use std::path::PathBuf;

pub use self::{
disks::DiskList,
legacy_facade::{DaemonError, StartWriterError},
workflow::{
Orchestrator, OrchestratorExt,
write_verify::{WVState, WriteVerifyWorkflow, WriteVerifyWorkflowError},
},
};
use crate::{
escalation::EscalationMethod,
facade::{analyze_input::FileAnalysis, workflow::hash::HashWorkflow},
};

mod analyze_input;
mod disks;
mod legacy_facade;
mod real;
pub mod watch;
pub mod workflow;

/// Main facade for UI implementations to interact with the rest of the
/// program's logic.
///
/// This trait is split up into several subtraits, each representing a different
/// kind of action the UI can take.
///
/// The API for this can be considered "mostly" stable. I'll be changing out the
/// error types, but in general, the overall shape of this API can be used for
/// new UI developments.
pub trait CaligulaFacade:
Sync
+ Send
+ DiskWatcher
+ FileAnalyzer
+ Escalator
+ Orchestrator<WriteVerifyWorkflow>
+ Orchestrator<HashWorkflow>
+ 'static
{
}

impl<F> CaligulaFacade for F where
F: Sync
+ Send
+ DiskWatcher
+ FileAnalyzer
+ Escalator
+ Orchestrator<WriteVerifyWorkflow>
+ Orchestrator<HashWorkflow>
+ 'static
{
}

/// Represents an interface to the disk querying subsystem.
pub trait DiskWatcher {
/// Get a handle for watching the list of disks available. This may update
/// as disks are added and removed to the system.
///
/// Although this returns a handle immediately, the initial results may take
/// a while to load.
#[expect(unused, reason = "Stub interface created for later use.")]
fn watch_disks(&self) -> watch::Watch<DiskList>;
}

/// Represents an interface to the file analysis subsystem.
pub trait FileAnalyzer {
/// Analyze a file to guess how we should handle it.
///
/// This is a fairly fast operation, expected to take 1-3 seconds to run.
///
/// Returns the results of the analysis, or an error if the file could not
/// be read. This method is fault-tolerant, so the only errors that can
/// cause this operation to fail are I/O errors.
///
/// The intended workflow is that you call it once, to fill up your UI's
/// wizard with data, and then ask the user for more information if
/// there's anything that's not certain.
#[expect(unused, reason = "Stub interface created for later use.")]
async fn analyze_file(&self, input: PathBuf) -> std::io::Result<FileAnalysis>;
}

/// Represents an interface to the privilege escalation subsystem.
pub trait Escalator {
/// Attempt to spawn a child process as root using the provided escalation
/// method (or [`None`] to automatically guess which one to use).
///
/// Returns [`Ok`] if we successfully managed to escalate, or an error if we
/// failed. If we were already escalated before this was called, returns
/// [`Ok`].
///
/// Once this is called, all future workflows will be routed through the
/// escalated child process rather than executing at the parent's
/// permission level!
///
/// If your requested method involves the terminal, you should switch back
/// to the non-alternate screen before calling this.
async fn escalate(&self, method: Option<EscalationMethod>) -> Result<(), DaemonError>;

/// Returns whether or not we have a child process running as root.
#[expect(unused, reason = "Stub interface created for later use.")]
fn is_escalated(&self) -> bool;
}

/// Make the actual prod-used CaligulaFacade implementation.
pub fn make_real_facade(log_path: &str) -> impl CaligulaFacade {
self::real::FacadeImpl::new(legacy_facade::make_legacy_facade_impl(log_path))
}
136 changes: 136 additions & 0 deletions src/facade/real.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
use std::{path::PathBuf, sync::Arc, time::Instant};

use futures::StreamExt;

use super::legacy_facade::{DaemonError, LegacyFacade};
use crate::{
escalation::EscalationMethod,
facade::{
DiskList, DiskWatcher, Escalator, FileAnalyzer, Orchestrator, WVState, WriteVerifyWorkflow,
analyze_input::FileAnalysis,
watch::Watch,
workflow::hash::{HashWorkflow, HashingState},
},
};

/// Actual CaligulaFacade implementation used by Caligula.
pub struct FacadeImpl<H> {
inner: tokio::sync::Mutex<Inner<H>>,
}

struct Inner<H> {
// TODO: get rid of the entire herder facade thing altogether. just assimilate the good parts
// into CaligulaFacade.
h: H,
escalation: Option<Option<EscalationMethod>>,
}

impl<H> FacadeImpl<H> {
pub fn new(h: H) -> Self {
Self {
inner: Inner {
h,
escalation: None,
}
.into(),
}
}
}

impl<H: LegacyFacade + Send + 'static> Orchestrator<WriteVerifyWorkflow> for FacadeImpl<H> {
async fn start_workflow(&self, params: WriteVerifyWorkflow) -> Watch<WVState> {
tracing::info!("Requesting herder to start");

// request the herder to start the action
let mut inner = self.inner.lock().await;
let esc = inner.escalation.is_some();
let handle: crate::facade::legacy_facade::HerdHandle<
crate::herder_api::write_verify::WriteVerifyEvent,
> = match inner.h.start_herd(params.make_child_config(), esc).await {
Ok(handle) => handle,
Err(e) => {
// oh god what a shitshow
// TODO: refactor the shit out of this thing
return Watch {
rx: tokio::sync::watch::channel(WVState::error(
Instant::now(),
match e {
crate::facade::StartWriterError::UnexpectedFirstStatus(s) => {
crate::facade::WriteVerifyWorkflowError::Unexpected(s)
}
crate::facade::StartWriterError::Failed(f) => {
crate::facade::WriteVerifyWorkflowError::Worker(f)
}
crate::facade::StartWriterError::DaemonError(daemon_error) => {
crate::facade::WriteVerifyWorkflowError::Daemon(Arc::new(
daemon_error,
))
}
},
))
.1,
};
}
};
drop(inner);

// create state reduction task
let (tx_state, rx_state) = tokio::sync::watch::channel(WVState::initial(
Instant::now(),
!params.compression.is_identity(),
handle.initial_info.input_file_bytes,
));
let mut events = handle.events;
let _jh = tokio::spawn(async move {
while !tx_state.borrow().is_finished() && !tx_state.is_closed() {
let event = events.next().await;
tx_state.send_modify(move |state| {
*state = std::mem::take(state).on_status(Instant::now(), event);
});
}
});
super::watch::Watch { rx: rx_state }
}
}

impl<H: LegacyFacade + Send + 'static> Orchestrator<HashWorkflow> for FacadeImpl<H> {
async fn start_workflow(&self, _workflow: HashWorkflow) -> Watch<HashingState> {
unimplemented!(
"Until this is implemented, for testing purposes, you may replace this with test values."
)
}
}

impl<H: LegacyFacade> Escalator for FacadeImpl<H> {
async fn escalate(&self, method: Option<EscalationMethod>) -> Result<(), DaemonError> {
let mut inner = self.inner.lock().await;
inner.escalation = Some(method);
inner.h.ensure_escalated_daemon().await?;
Ok(())
}

fn is_escalated(&self) -> bool {
// TODO: this is badly implemented but it's good enough for writing new UIs
// against. It will be improved when we get rid of herder facade.
let Ok(lock) = self.inner.try_lock() else {
return false;
};
lock.escalation.is_some()
}
}

impl<H> DiskWatcher for FacadeImpl<H> {
fn watch_disks(&self) -> Watch<DiskList> {
unimplemented!(
"Until this is implemented, for testing purposes, you may replace this with test values."
)
}
}

impl<H> FileAnalyzer for FacadeImpl<H> {
async fn analyze_file(&self, _input: PathBuf) -> std::io::Result<FileAnalysis> {
unimplemented!(
"Until this is implemented, for testing purposes, you may replace this with test values."
)
}
}
File renamed without changes.
Loading
Loading