Repository: tangle-network/gadget (language: rust)

Issue:
[TASK] Create gadget-specific data directories in blueprint manager
# Overview

Some services are going to need to store additional files. `blueprint-manager`, provided a base directory, could create a unique data directory for each active gadget.


Discussion / hints:

Fix patch:
diff --git a/blueprint-manager/Cargo.toml b/blueprint-manager/Cargo.toml
index 00ab24a7c..bc8eec78e 100644
--- a/blueprint-manager/Cargo.toml
+++ b/blueprint-manager/Cargo.toml
@@ -6,7 +6,7 @@ publish = false
 
 [[bin]]
 name = "blueprint-manager"
-path = "src/cli.rs"
+path = "src/main.rs"
 
 [dependencies]
 sp-core = { workspace = true }
@@ -18,7 +18,7 @@ structopt = { workspace = true }
 tangle-subxt = { workspace = true }
 toml = { workspace = true }
 hex = { workspace = true }
-tokio = { workspace = true, features = ["process", "io-util", "signal"] }
+tokio = { workspace = true, features = ["process", "io-util", "signal", "macros"] }
 reqwest = { workspace = true }
 sha2 = { workspace = true }
 futures = { workspace = true }
diff --git a/blueprint-manager/src/cli.rs b/blueprint-manager/src/cli.rs
deleted file mode 100644
index 123f7733d..000000000
--- a/blueprint-manager/src/cli.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-use blueprint_manager::config::BlueprintManagerConfig;
-use blueprint_manager::run_blueprint_manager;
-use blueprint_manager::sdk;
-use blueprint_manager::sdk::utils::msg_to_error;
-use gadget_io::GadgetConfig;
-use sdk::entry;
-use structopt::StructOpt;
-
-fn main() -> color_eyre::Result<()> {
-    let body = async {
-        color_eyre::install()?;
-        let blueprint_manager_config = BlueprintManagerConfig::from_args();
-
-        entry::setup_blueprint_manager_logger(
-            blueprint_manager_config.verbose,
-            blueprint_manager_config.pretty,
-            "gadget",
-        )?;
-
-        if let Some(gadget_config) = blueprint_manager_config.gadget_config.as_ref() {
-            let gadget_config_settings = std::fs::read_to_string(gadget_config)?;
-            let gadget_config: GadgetConfig = toml::from_str(&gadget_config_settings)
-                .map_err(|err| msg_to_error(err.to_string()))?;
-
-            // Allow CTRL-C to shutdown this CLI application instance
-            let shutdown_signal = async move {
-                let _ = tokio::signal::ctrl_c().await;
-            };
-
-            let handle =
-                run_blueprint_manager(blueprint_manager_config, gadget_config, shutdown_signal)
-                    .await?;
-
-            handle.await
-        } else {
-            Err(msg_to_error(
-                "Gadget config file is required when running the blueprint manager in CLI mode"
-                    .to_string(),
-            ))
-        }
-    };
-    tokio::runtime::Builder::new_multi_thread()
-        .enable_all()
-        .build()
-        .expect("Failed building the Runtime")
-        .block_on(body)
-}
diff --git a/blueprint-manager/src/config.rs b/blueprint-manager/src/config.rs
index e657b4698..4f7ae5761 100644
--- a/blueprint-manager/src/config.rs
+++ b/blueprint-manager/src/config.rs
@@ -8,11 +8,14 @@ use structopt::StructOpt;
 )]
 pub struct BlueprintManagerConfig {
     /// The path to the gadget configuration file
-    #[structopt(parse(from_os_str), short = "s", long = "gadget-config")]
+    #[structopt(parse(from_os_str), short = "s", long)]
     pub gadget_config: Option<PathBuf>,
     /// The path to the keystore
-    #[structopt(short = "k", long = "keystore-uri")]
+    #[structopt(short = "k", long)]
     pub keystore_uri: String,
+    /// The directory in which all gadgets will store their data
+    #[structopt(long, short = "d", parse(from_os_str))]
+    pub data_dir: Option<PathBuf>,
     /// The verbosity level, can be used multiple times
     #[structopt(long, short = "v", parse(from_occurrences))]
     pub verbose: i32,
diff --git a/blueprint-manager/src/executor/mod.rs b/blueprint-manager/src/executor/mod.rs
index 54ca8bcee..7235ebf91 100644
--- a/blueprint-manager/src/executor/mod.rs
+++ b/blueprint-manager/src/executor/mod.rs
@@ -147,6 +147,16 @@ pub async fn run_blueprint_manager<F: SendFuture<'static, ()>>(
     let _span = span.enter();
     info!("Starting blueprint manager ... waiting for start signal ...");
 
+    if let Some(data_dir) = &blueprint_manager_config.data_dir {
+        if !data_dir.exists() {
+            info!(
+                "Data directory does not exist, creating it at `{}`",
+                data_dir.display()
+            );
+            std::fs::create_dir_all(data_dir)?;
+        }
+    }
+
     let (tangle_key, ecdsa_key) = {
         let keystore = GenericKeyStore::<parking_lot::RawRwLock>::Fs(FilesystemKeystore::open(
             &gadget_config.keystore_uri,
diff --git a/blueprint-manager/src/main.rs b/blueprint-manager/src/main.rs
new file mode 100644
index 000000000..3d6230303
--- /dev/null
+++ b/blueprint-manager/src/main.rs
@@ -0,0 +1,46 @@
+use blueprint_manager::config::BlueprintManagerConfig;
+use blueprint_manager::run_blueprint_manager;
+use blueprint_manager::sdk;
+use blueprint_manager::sdk::utils::msg_to_error;
+use gadget_io::GadgetConfig;
+use sdk::entry;
+use structopt::StructOpt;
+
+#[tokio::main]
+#[allow(clippy::needless_return)]
+async fn main() -> color_eyre::Result<()> {
+    color_eyre::install()?;
+    let mut blueprint_manager_config = BlueprintManagerConfig::from_args();
+
+    if let Some(data_dir) = blueprint_manager_config.data_dir.as_mut() {
+        *data_dir = std::path::absolute(&data_dir)?;
+    }
+
+    entry::setup_blueprint_manager_logger(
+        blueprint_manager_config.verbose,
+        blueprint_manager_config.pretty,
+        "gadget",
+    )?;
+
+    let Some(gadget_config) = blueprint_manager_config.gadget_config.as_ref() else {
+        return Err(msg_to_error(
+            "Gadget config file is required when running the blueprint manager in CLI mode"
+                .to_string(),
+        ));
+    };
+
+    let gadget_config_settings = std::fs::read_to_string(gadget_config)?;
+    let gadget_config: GadgetConfig =
+        toml::from_str(&gadget_config_settings).map_err(|err| msg_to_error(err.to_string()))?;
+
+    // Allow CTRL-C to shutdown this CLI application instance
+    let shutdown_signal = async move {
+        let _ = tokio::signal::ctrl_c().await;
+    };
+
+    let handle =
+        run_blueprint_manager(blueprint_manager_config, gadget_config, shutdown_signal).await?;
+    handle.await?;
+
+    Ok(())
+}
diff --git a/blueprint-manager/src/sdk/utils.rs b/blueprint-manager/src/sdk/utils.rs
index b6b35e2c8..00cd019ff 100644
--- a/blueprint-manager/src/sdk/utils.rs
+++ b/blueprint-manager/src/sdk/utils.rs
@@ -1,7 +1,4 @@
-use crate::config::BlueprintManagerConfig;
 use crate::protocols::resolver::NativeGithubMetadata;
-use gadget_io::GadgetConfig;
-use gadget_sdk::config::Protocol;
 use gadget_sdk::{info, warn};
 use sha2::Digest;
 use std::path::Path;
@@ -38,51 +35,6 @@ pub fn bounded_string_to_string(string: BoundedString) -> Result<String, FromUtf
     String::from_utf8(bytes.clone())
 }
 
-pub fn generate_process_arguments(
-    gadget_config: &GadgetConfig,
-    opt: &BlueprintManagerConfig,
-    blueprint_id: u64,
-    service_id: u64,
-    protocol: Protocol,
-) -> color_eyre::Result<Vec<String>> {
-    let mut arguments = vec![];
-    arguments.push("run".to_string());
-
-    if opt.test_mode {
-        arguments.push("--test-mode".to_string());
-    }
-
-    for bootnode in &gadget_config.bootnodes {
-        arguments.push(format!("--bootnodes={}", bootnode));
-    }
-
-    arguments.extend([
-        format!("--bind-addr={}", gadget_config.bind_addr),
-        format!("--bind-port={}", gadget_config.bind_port),
-        format!("--url={}", gadget_config.url),
-        format!("--keystore-uri={}", gadget_config.keystore_uri),
-        format!("--chain={}", gadget_config.chain),
-        format!("--verbose={}", opt.verbose),
-        format!("--pretty={}", opt.pretty),
-        format!("--blueprint-id={}", blueprint_id),
-        format!("--service-id={}", service_id),
-        format!("--protocol={}", protocol),
-        format!(
-            "--log-id=Blueprint-{blueprint_id}-Service-{service_id}-{}",
-            opt.instance_id.clone().unwrap_or_else(|| format!(
-                "{}-{}",
-                gadget_config.bind_addr, gadget_config.bind_port
-            ))
-        ),
-    ]);
-
-    if let Some(keystore_password) = &gadget_config.keystore_password {
-        arguments.push(format!("--keystore-password={}", keystore_password));
-    }
-
-    Ok(arguments)
-}
-
 pub fn hash_bytes_to_hex<T: AsRef<[u8]>>(input: T) -> String {
     let mut hasher = sha2::Sha256::default();
     hasher.update(input);
diff --git a/blueprint-manager/src/sources/mod.rs b/blueprint-manager/src/sources/mod.rs
index 6dd2ae7f0..04ee1349c 100644
--- a/blueprint-manager/src/sources/mod.rs
+++ b/blueprint-manager/src/sources/mod.rs
@@ -1,11 +1,10 @@
 use crate::config::BlueprintManagerConfig;
 use crate::executor::event_handler::VerifiedBlueprint;
 use crate::gadget::ActiveGadgets;
-use crate::sdk::utils::{
-    chmod_x_file, generate_process_arguments, generate_running_process_status_handle, is_windows,
-};
+use crate::sdk::utils::{chmod_x_file, generate_running_process_status_handle, is_windows};
 use async_trait::async_trait;
 use gadget_io::GadgetConfig;
+use gadget_sdk::config::Protocol;
 use gadget_sdk::{error, info, warn};
 use std::path::PathBuf;
 
@@ -32,85 +31,137 @@ pub async fn handle<'a>(
     let blueprint_id = blueprint_source.blueprint_id();
     let service_str = blueprint_source.name();
 
-    if !active_gadgets.contains_key(&blueprint_id) {
-        let mut binary_download_path = blueprint_source.get_binary().await?;
+    if active_gadgets.contains_key(&blueprint_id) {
+        return Ok(());
+    }
 
-        // Ensure the binary is executable
-        if is_windows() {
-            if binary_download_path.extension().is_none() {
-                binary_download_path.set_extension("exe");
-            }
-        } else if let Err(err) = chmod_x_file(&binary_download_path).await {
-            warn!("Failed to chmod +x the binary: {err}");
+    let mut binary_download_path = blueprint_source.get_binary().await?;
+
+    // Ensure the binary is executable
+    if is_windows() {
+        if binary_download_path.extension().is_none() {
+            binary_download_path.set_extension("exe");
         }
+    } else if let Err(err) = chmod_x_file(&binary_download_path).await {
+        warn!("Failed to chmod +x the binary: {err}");
+    }
 
-        for service_id in &blueprint.services {
-            let sub_service_str = format!("{service_str}-{service_id}");
-            let arguments = generate_process_arguments(
-                gadget_config,
-                blueprint_manager_opts,
-                blueprint_id,
-                *service_id,
-                blueprint.protocol,
-            )?;
-
-            // Add required env vars for all child processes/gadgets
-            let mut env_vars = vec![
-                ("RPC_URL".to_string(), gadget_config.url.to_string()),
-                (
-                    "KEYSTORE_URI".to_string(),
-                    blueprint_manager_opts.keystore_uri.clone(),
-                ),
-                ("DATA_DIR".to_string(), gadget_config.keystore_uri.clone()),
-                ("BLUEPRINT_ID".to_string(), format!("{}", blueprint_id)),
-                ("SERVICE_ID".to_string(), format!("{}", service_id)),
-            ];
-
-            // Ensure our child process inherits the current processes' environment vars
-            env_vars.extend(std::env::vars());
-
-            if blueprint.registration_mode {
-                env_vars.push(("REGISTRATION_MODE_ON".to_string(), "true".to_string()));
-            }
+    for service_id in &blueprint.services {
+        let sub_service_str = format!("{service_str}-{service_id}");
+        let arguments = generate_process_arguments(
+            gadget_config,
+            blueprint_manager_opts,
+            blueprint_id,
+            *service_id,
+            blueprint.protocol,
+        )?;
+
+        // Add required env vars for all child processes/gadgets
+        let mut env_vars = vec![
+            ("RPC_URL".to_string(), gadget_config.url.to_string()),
+            (
+                "KEYSTORE_URI".to_string(),
+                blueprint_manager_opts.keystore_uri.clone(),
+            ),
+            ("BLUEPRINT_ID".to_string(), format!("{}", blueprint_id)),
+            ("SERVICE_ID".to_string(), format!("{}", service_id)),
+        ];
+
+        if let Some(base) = &blueprint_manager_opts.data_dir {
+            let data_dir = base.join(format!("blueprint-{blueprint_id}-{sub_service_str}"));
+            env_vars.push((
+                "DATA_DIR".to_string(),
+                data_dir.to_string_lossy().into_owned(),
+            ))
+        }
 
-            info!("Starting protocol: {sub_service_str} with args: {arguments:?}");
-
-            // Now that the file is loaded, spawn the process
-            let process_handle = tokio::process::Command::new(&binary_download_path)
-                .kill_on_drop(true)
-                .stdout(std::process::Stdio::inherit()) // Inherit the stdout of this process
-                .stderr(std::process::Stdio::inherit()) // Inherit the stderr of this process
-                .stdin(std::process::Stdio::null())
-                .current_dir(&std::env::current_dir()?)
-                .envs(env_vars)
-                .args(arguments)
-                .spawn()?;
-
-            if blueprint.registration_mode {
-                // We must wait for the process to exit successfully
-                let status = process_handle.wait_with_output().await?;
-                if !status.status.success() {
-                    error!(
-                        "Protocol (registration mode) {sub_service_str} failed to execute: {status:?}"
-                    );
-                } else {
-                    info!(
-                        "***Protocol (registration mode) {sub_service_str} executed successfully***"
-                    );
-                }
-            } else {
-                // A normal running gadget binary. Store the process handle and let the event loop handle the rest
+        // Ensure our child process inherits the current processes' environment vars
+        env_vars.extend(std::env::vars());
 
-                let (status_handle, abort) =
-                    generate_running_process_status_handle(process_handle, &sub_service_str);
+        if blueprint.registration_mode {
+            env_vars.push(("REGISTRATION_MODE_ON".to_string(), "true".to_string()));
+        }
 
-                active_gadgets
-                    .entry(blueprint_id)
-                    .or_default()
-                    .insert(*service_id, (status_handle, Some(abort)));
+        info!("Starting protocol: {sub_service_str} with args: {arguments:?}");
+
+        // Now that the file is loaded, spawn the process
+        let process_handle = tokio::process::Command::new(&binary_download_path)
+            .kill_on_drop(true)
+            .stdout(std::process::Stdio::inherit()) // Inherit the stdout of this process
+            .stderr(std::process::Stdio::inherit()) // Inherit the stderr of this process
+            .stdin(std::process::Stdio::null())
+            .current_dir(&std::env::current_dir()?)
+            .envs(env_vars)
+            .args(arguments)
+            .spawn()?;
+
+        if blueprint.registration_mode {
+            // We must wait for the process to exit successfully
+            let status = process_handle.wait_with_output().await?;
+            if !status.status.success() {
+                error!(
+                    "Protocol (registration mode) {sub_service_str} failed to execute: {status:?}"
+                );
+            } else {
+                info!("***Protocol (registration mode) {sub_service_str} executed successfully***");
             }
+        } else {
+            // A normal running gadget binary. Store the process handle and let the event loop handle the rest
+
+            let (status_handle, abort) =
+                generate_running_process_status_handle(process_handle, &sub_service_str);
+
+            active_gadgets
+                .entry(blueprint_id)
+                .or_default()
+                .insert(*service_id, (status_handle, Some(abort)));
         }
     }
 
     Ok(())
 }
+
+pub fn generate_process_arguments(
+    gadget_config: &GadgetConfig,
+    opt: &BlueprintManagerConfig,
+    blueprint_id: u64,
+    service_id: u64,
+    protocol: Protocol,
+) -> color_eyre::Result<Vec<String>> {
+    let mut arguments = vec![];
+    arguments.push("run".to_string());
+
+    if opt.test_mode {
+        arguments.push("--test-mode".to_string());
+    }
+
+    for bootnode in &gadget_config.bootnodes {
+        arguments.push(format!("--bootnodes={}", bootnode));
+    }
+
+    arguments.extend([
+        format!("--bind-addr={}", gadget_config.bind_addr),
+        format!("--bind-port={}", gadget_config.bind_port),
+        format!("--url={}", gadget_config.url),
+        format!("--keystore-uri={}", gadget_config.keystore_uri),
+        format!("--chain={}", gadget_config.chain),
+        format!("--verbose={}", opt.verbose),
+        format!("--pretty={}", opt.pretty),
+        format!("--blueprint-id={}", blueprint_id),
+        format!("--service-id={}", service_id),
+        format!("--protocol={}", protocol),
+        format!(
+            "--log-id=Blueprint-{blueprint_id}-Service-{service_id}-{}",
+            opt.instance_id.clone().unwrap_or_else(|| format!(
+                "{}-{}",
+                gadget_config.bind_addr, gadget_config.bind_port
+            ))
+        ),
+    ]);
+
+    if let Some(keystore_password) = &gadget_config.keystore_password {
+        arguments.push(format!("--keystore-password={}", keystore_password));
+    }
+
+    Ok(arguments)
+}
diff --git a/sdk/src/config.rs b/sdk/src/config.rs
index 3c8818052..83318e2ab 100644
--- a/sdk/src/config.rs
+++ b/sdk/src/config.rs
@@ -11,6 +11,7 @@ use eigensdk::crypto_bls;
 use gadget_io::SupportedChains;
 use libp2p::Multiaddr;
 use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
 use std::str::FromStr;
 use structopt::StructOpt;
 use url::Url;
@@ -82,6 +83,10 @@ pub struct GadgetConfiguration<RwLock: lock_api::RawRwLock> {
     /// * In Memory: `file::memory:` or `:memory:`
     /// * Filesystem: `file:/path/to/keystore` or `file:///path/to/keystore`
     pub keystore_uri: String,
+    /// Data directory exclusively for this gadget
+    ///
+    /// This will be `None` if the blueprint manager was not provided a base directory.
+    pub data_dir: Option<PathBuf>,
     /// Blueprint ID for this gadget.
     pub blueprint_id: u64,
     /// Service ID for this gadget.
@@ -133,6 +138,7 @@ impl<RwLock: lock_api::RawRwLock> Clone for GadgetConfiguration<RwLock> {
         Self {
             rpc_endpoint: self.rpc_endpoint.clone(),
             keystore_uri: self.keystore_uri.clone(),
+            data_dir: self.data_dir.clone(),
             blueprint_id: self.blueprint_id,
             service_id: self.service_id,
             is_registration: self.is_registration,
@@ -153,6 +159,7 @@ impl<RwLock: lock_api::RawRwLock> Default for GadgetConfiguration<RwLock> {
         Self {
             rpc_endpoint: "http://localhost:9944".to_string(),
             keystore_uri: "file::memory:".to_string(),
+            data_dir: None,
             blueprint_id: 0,
             service_id: Some(0),
             is_registration: false,
@@ -337,6 +344,7 @@ fn load_inner<RwLock: lock_api::RawRwLock>(
         span,
         rpc_endpoint: url.to_string(),
         keystore_uri,
+        data_dir: std::env::var("DATA_DIR").ok().map(PathBuf::from),
         blueprint_id,
         // If the registration mode is on, we don't need the service ID
         service_id: if is_registration {


Test patch:
diff --git a/blueprint-test-utils/src/lib.rs b/blueprint-test-utils/src/lib.rs
index f3f29ee1b..aca6376f9 100644
--- a/blueprint-test-utils/src/lib.rs
+++ b/blueprint-test-utils/src/lib.rs
@@ -70,6 +70,8 @@ pub async fn run_test_blueprint_manager<T: Send + Clone + 'static>(
         keystore_uri.display()
     );
 
+    let data_dir = std::path::absolute("./target/data/").expect("Failed to get current directory");
+
     let keystore_uri_normalized =
         std::path::absolute(keystore_uri).expect("Failed to resolve keystore URI");
     let keystore_uri_str = format!("file:{}", keystore_uri_normalized.display());
@@ -86,6 +88,7 @@ pub async fn run_test_blueprint_manager<T: Send + Clone + 'static>(
     let blueprint_manager_config = BlueprintManagerConfig {
         gadget_config: None,
         keystore_uri: keystore_uri_str.clone(),
+        data_dir: Some(data_dir),
         verbose: input.verbose,
         pretty: input.pretty,
         instance_id: Some(NAME_IDS[input.instance_id as usize].to_string()),