Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Open FsStore + iroh router at startup
Daniel Norman committed 7 days ago
commit 97f078f090e136b7a48fb4a6cd932105d9c3ba73
parent ee81763df1a61adf450fc144f13c5c8af2f89cf4
7 files changed +111 -5
modified Cargo.lock
@@ -6384,6 +6384,7 @@ dependencies = [
 "radicle-job",
 "radicle-localtime",
 "radicle-surf",
+
 "rand 0.9.4",
 "serde",
 "serde_json",
 "sqlite",
modified crates/radicle-tauri/src/commands/startup.rs
@@ -5,6 +5,7 @@ use radicle::node::{Handle, Node, NOTIFICATIONS_DB_FILE};

use radicle_types::config::{Config, Version};
use radicle_types::error::Error;
+
use radicle_types::seeder;
use radicle_types::traits::Profile;
use radicle_types::{domain, AppState};

@@ -67,7 +68,7 @@ pub(crate) fn check_radicle_cli(ctx: tauri::State<AppState>) -> Result<(), Error
}

#[tauri::command]
-
pub(crate) fn startup(app: AppHandle) -> Result<Config, Error> {
+
pub(crate) async fn startup(app: AppHandle) -> Result<Config, Error> {
    let profile = radicle::Profile::load()?;
    let home = profile.home();

@@ -83,8 +84,11 @@ pub(crate) fn startup(app: AppHandle) -> Result<Config, Error> {
    let inbox_service = domain::inbox::service::Service::new(inbox_db);
    let patch_service = domain::patch::service::Service::new(cob_db);

-
    let node_handle = app.app_handle().clone();
+
    // Bring iroh up before the first frontend command. Anything previously
+
    // tagged in the FsStore is served the moment the router is up.
+
    let seeder = seeder::bootstrap(home.path()).await?;

+
    let node_handle = app.app_handle().clone();
    let node = Node::new(profile.home().socket_from_env());

    app.manage(inbox_service);
@@ -97,7 +101,11 @@ pub(crate) fn startup(app: AppHandle) -> Result<Config, Error> {
        }
    });

-
    let state = AppState { profile };
+
    let state = AppState {
+
        profile,
+
        blobs: seeder.blobs,
+
        iroh_router: seeder.router,
+
    };
    app.manage(state.clone());

    Ok(state.config())
modified crates/radicle-tauri/src/lib.rs
@@ -1,6 +1,7 @@
mod commands;

use radicle_types::AppState;
+
use tauri::Manager;

use commands::{auth, cob, diff, inbox, profile, repo, startup, thread};

@@ -75,6 +76,19 @@ pub fn run() {
            thread::create_issue_comment,
            thread::create_patch_comment,
        ])
-
        .run(tauri::generate_context!())
-
        .expect("error while running tauri application");
+
        .build(tauri::generate_context!())
+
        .expect("error while building tauri application")
+
        .run(|app, event| {
+
            // Shut down the iroh router cleanly so peers see us go away
+
            // instead of timing out.
+
            if let tauri::RunEvent::Exit = event {
+
                if let Some(state) = app.try_state::<AppState>() {
+
                    log::info!("Shutting down iroh router");
+
                    let router = state.iroh_router.clone();
+
                    tauri::async_runtime::block_on(async move {
+
                        let _ = router.shutdown().await;
+
                    });
+
                }
+
            }
+
        });
}
modified crates/radicle-types/Cargo.toml
@@ -23,6 +23,7 @@ iroh = { version = "0.97" }
iroh-blobs = { version = "0.99", features = ["fs-store"] }
cid = { version = "0.11" }
futures-lite = { version = "2" }
+
rand = { version = "0.9" }
serde = { version = "1.0.0", features = ["derive"] }
serde_json = { version = "1.0.0" }
sqlite = { version = "0.37.0", features = ["bundled"] }
modified crates/radicle-types/src/error.rs
@@ -167,6 +167,10 @@ pub enum Error {
    /// Serde JSON error.
    #[error(transparent)]
    SerdeJSON(#[from] serde_json::error::Error),
+

+
    /// Iroh / iroh-blobs error.
+
    #[error("iroh: {0}")]
+
    Iroh(String),
}

impl Error {
modified crates/radicle-types/src/lib.rs
@@ -14,6 +14,7 @@ pub mod error;
pub mod oid;
pub mod outbound;
pub mod repo;
+
pub mod seeder;
pub mod source;
pub mod syntax;
pub mod test;
@@ -22,6 +23,8 @@ pub mod traits;
#[derive(Clone)]
pub struct AppState {
    pub profile: radicle::Profile,
+
    pub blobs: iroh_blobs::store::fs::FsStore,
+
    pub iroh_router: iroh::protocol::Router,
}

impl Repo for AppState {}
added crates/radicle-types/src/seeder.rs
@@ -0,0 +1,75 @@
+
use std::path::{Path, PathBuf};
+

+
use iroh::protocol::Router;
+
use iroh_blobs::store::fs::FsStore;
+
use iroh_blobs::BlobsProtocol;
+
use radicle_artifact::share::EndpointPreset;
+

+
use crate::error::Error;
+

+
const ARTIFACTS_DIR: &str = "artifacts";
+
const STORE_DIR: &str = "store";
+
const KEY_FILE: &str = "iroh.key";
+

+
pub struct Seeder {
+
    pub blobs: FsStore,
+
    pub router: Router,
+
}
+

+
/// Bootstrap the iroh seeder: generate or load the persisted iroh key, open
+
/// the FsStore, and spawn the BlobsProtocol router. Anything previously
+
/// tagged in the store is reachable the moment this returns.
+
pub async fn bootstrap(home: &Path) -> Result<Seeder, Error> {
+
    let dir = home.join(ARTIFACTS_DIR);
+
    std::fs::create_dir_all(&dir)?;
+

+
    let secret = load_or_generate_key(&dir.join(KEY_FILE))?;
+
    let blobs = FsStore::load(dir.join(STORE_DIR))
+
        .await
+
        .map_err(|e| Error::Iroh(e.to_string()))?;
+

+
    let preset = EndpointPreset::from_env().map_err(|e| Error::Iroh(e.to_string()))?;
+
    let endpoint = iroh::Endpoint::builder(preset)
+
        .secret_key(secret)
+
        .bind()
+
        .await
+
        .map_err(|e| Error::Iroh(e.to_string()))?;
+

+
    let blobs_protocol = BlobsProtocol::new(&blobs, None);
+
    let router = Router::builder(endpoint)
+
        .accept(iroh_blobs::ALPN, blobs_protocol)
+
        .spawn();
+

+
    Ok(Seeder { blobs, router })
+
}
+

+
fn load_or_generate_key(path: &PathBuf) -> Result<iroh::SecretKey, Error> {
+
    if path.exists() {
+
        let bytes = std::fs::read(path)?;
+
        let bytes: [u8; 32] = bytes
+
            .try_into()
+
            .map_err(|_| Error::Iroh(format!("malformed iroh key at {}", path.display())))?;
+
        Ok(iroh::SecretKey::from_bytes(&bytes))
+
    } else {
+
        let secret = iroh::SecretKey::generate(&mut rand::rng());
+
        write_key(path, &secret)?;
+
        Ok(secret)
+
    }
+
}
+

+
#[cfg(unix)]
+
fn write_key(path: &Path, secret: &iroh::SecretKey) -> Result<(), Error> {
+
    use std::os::unix::fs::OpenOptionsExt;
+

+
    let mut opts = std::fs::OpenOptions::new();
+
    opts.write(true).create_new(true).mode(0o600);
+
    let mut f = opts.open(path)?;
+
    std::io::Write::write_all(&mut f, &secret.to_bytes())?;
+
    Ok(())
+
}
+

+
#[cfg(not(unix))]
+
fn write_key(path: &Path, secret: &iroh::SecretKey) -> Result<(), Error> {
+
    std::fs::write(path, secret.to_bytes())?;
+
    Ok(())
+
}