| @@ -902,6 +902,7 @@ dependencies = [ | |||||
| "bincode", | "bincode", | ||||
| "clap 3.2.20", | "clap 3.2.20", | ||||
| "dora-core", | "dora-core", | ||||
| "dora-download", | |||||
| "dora-message", | "dora-message", | ||||
| "dora-node-api", | "dora-node-api", | ||||
| "eyre", | "eyre", | ||||
| @@ -932,6 +933,16 @@ dependencies = [ | |||||
| "zenoh-config", | "zenoh-config", | ||||
| ] | ] | ||||
| [[package]] | |||||
| name = "dora-download" | |||||
| version = "0.1.0" | |||||
| dependencies = [ | |||||
| "eyre", | |||||
| "reqwest", | |||||
| "tempfile", | |||||
| "tokio", | |||||
| ] | |||||
| [[package]] | [[package]] | ||||
| name = "dora-examples" | name = "dora-examples" | ||||
| version = "0.0.0" | version = "0.0.0" | ||||
| @@ -1053,6 +1064,7 @@ version = "0.1.0" | |||||
| dependencies = [ | dependencies = [ | ||||
| "clap 3.2.20", | "clap 3.2.20", | ||||
| "dora-core", | "dora-core", | ||||
| "dora-download", | |||||
| "dora-message", | "dora-message", | ||||
| "dora-metrics", | "dora-metrics", | ||||
| "dora-node-api", | "dora-node-api", | ||||
| @@ -1068,9 +1080,7 @@ dependencies = [ | |||||
| "opentelemetry", | "opentelemetry", | ||||
| "opentelemetry-system-metrics", | "opentelemetry-system-metrics", | ||||
| "pyo3", | "pyo3", | ||||
| "reqwest", | |||||
| "serde_yaml 0.8.23", | "serde_yaml 0.8.23", | ||||
| "tempfile", | |||||
| "tokio", | "tokio", | ||||
| "tokio-stream", | "tokio-stream", | ||||
| "tracing", | "tracing", | ||||
| @@ -13,6 +13,7 @@ members = [ | |||||
| "libraries/communication-layer", | "libraries/communication-layer", | ||||
| "libraries/core", | "libraries/core", | ||||
| "libraries/message", | "libraries/message", | ||||
| "libraries/extensions/download", | |||||
| "libraries/extensions/telemetry/*", | "libraries/extensions/telemetry/*", | ||||
| "libraries/extensions/zenoh-logger", | "libraries/extensions/zenoh-logger", | ||||
| ] | ] | ||||
| @@ -2,7 +2,7 @@ use crate::graph::read_descriptor; | |||||
| use dora_core::{ | use dora_core::{ | ||||
| adjust_shared_library_path, | adjust_shared_library_path, | ||||
| config::{InputMapping, UserInputMapping}, | config::{InputMapping, UserInputMapping}, | ||||
| descriptor::{self, CoreNodeKind, OperatorSource}, | |||||
| descriptor::{self, source_is_url, CoreNodeKind, OperatorSource}, | |||||
| }; | }; | ||||
| use eyre::{bail, eyre, Context}; | use eyre::{bail, eyre, Context}; | ||||
| use std::{env::consts::EXE_EXTENSION, path::Path}; | use std::{env::consts::EXE_EXTENSION, path::Path}; | ||||
| @@ -39,15 +39,15 @@ pub fn check(dataflow_path: &Path, runtime: &Path) -> eyre::Result<()> { | |||||
| for node in &nodes { | for node in &nodes { | ||||
| match &node.kind { | match &node.kind { | ||||
| descriptor::CoreNodeKind::Custom(node) => { | descriptor::CoreNodeKind::Custom(node) => { | ||||
| let mut args = node.run.split_ascii_whitespace(); | |||||
| let raw = Path::new( | |||||
| args.next() | |||||
| .ok_or_else(|| eyre!("`run` field must not be empty"))?, | |||||
| ); | |||||
| let path = if raw.extension().is_none() { | |||||
| raw.with_extension(EXE_EXTENSION) | |||||
| let path = if source_is_url(&node.source) { | |||||
| todo!("check URL"); | |||||
| } else { | } else { | ||||
| raw.to_owned() | |||||
| let raw = Path::new(&node.source); | |||||
| if raw.extension().is_none() { | |||||
| raw.with_extension(EXE_EXTENSION) | |||||
| } else { | |||||
| raw.to_owned() | |||||
| } | |||||
| }; | }; | ||||
| base.join(&path) | base.join(&path) | ||||
| .canonicalize() | .canonicalize() | ||||
| @@ -57,7 +57,7 @@ pub fn check(dataflow_path: &Path, runtime: &Path) -> eyre::Result<()> { | |||||
| for operator_definition in &node.operators { | for operator_definition in &node.operators { | ||||
| match &operator_definition.config.source { | match &operator_definition.config.source { | ||||
| OperatorSource::SharedLibrary(path) => { | OperatorSource::SharedLibrary(path) => { | ||||
| if OperatorSource::is_url(path) { | |||||
| if source_is_url(path) { | |||||
| todo!("check URL"); | todo!("check URL"); | ||||
| } else { | } else { | ||||
| let path = adjust_shared_library_path(Path::new(&path))?; | let path = adjust_shared_library_path(Path::new(&path))?; | ||||
| @@ -67,7 +67,7 @@ pub fn check(dataflow_path: &Path, runtime: &Path) -> eyre::Result<()> { | |||||
| } | } | ||||
| } | } | ||||
| OperatorSource::Python(path) => { | OperatorSource::Python(path) => { | ||||
| if OperatorSource::is_url(path) { | |||||
| if source_is_url(path) { | |||||
| todo!("check URL"); | todo!("check URL"); | ||||
| } else { | } else { | ||||
| if !base.join(&path).exists() { | if !base.join(&path).exists() { | ||||
| @@ -76,7 +76,7 @@ pub fn check(dataflow_path: &Path, runtime: &Path) -> eyre::Result<()> { | |||||
| } | } | ||||
| } | } | ||||
| OperatorSource::Wasm(path) => { | OperatorSource::Wasm(path) => { | ||||
| if OperatorSource::is_url(path) { | |||||
| if source_is_url(path) { | |||||
| todo!("check URL"); | todo!("check URL"); | ||||
| } else { | } else { | ||||
| if !base.join(&path).exists() { | if !base.join(&path).exists() { | ||||
| @@ -27,3 +27,4 @@ tracing-subscriber = "0.3.15" | |||||
| futures-concurrency = "5.0.1" | futures-concurrency = "5.0.1" | ||||
| zenoh = { git = "https://github.com/eclipse-zenoh/zenoh.git" } | zenoh = { git = "https://github.com/eclipse-zenoh/zenoh.git" } | ||||
| serde_json = "1.0.86" | serde_json = "1.0.86" | ||||
| dora-download = { path = "../../libraries/extensions/download" } | |||||
| @@ -1,5 +1,9 @@ | |||||
| use super::command_init_common_env; | use super::command_init_common_env; | ||||
| use dora_core::{config::NodeId, descriptor}; | |||||
| use dora_core::{ | |||||
| config::NodeId, | |||||
| descriptor::{self, source_is_url}, | |||||
| }; | |||||
| use dora_download::download_file; | |||||
| use eyre::{eyre, WrapErr}; | use eyre::{eyre, WrapErr}; | ||||
| use std::{env::consts::EXE_EXTENSION, path::Path}; | use std::{env::consts::EXE_EXTENSION, path::Path}; | ||||
| @@ -10,25 +14,31 @@ pub(super) fn spawn_custom_node( | |||||
| communication: &dora_core::config::CommunicationConfig, | communication: &dora_core::config::CommunicationConfig, | ||||
| working_dir: &Path, | working_dir: &Path, | ||||
| ) -> eyre::Result<tokio::task::JoinHandle<eyre::Result<(), eyre::Error>>> { | ) -> eyre::Result<tokio::task::JoinHandle<eyre::Result<(), eyre::Error>>> { | ||||
| let mut args = node.run.split_ascii_whitespace(); | |||||
| let cmd = { | |||||
| let raw = Path::new( | |||||
| args.next() | |||||
| .ok_or_else(|| eyre!("`run` field must not be empty"))?, | |||||
| ); | |||||
| let path = if raw.extension().is_none() { | |||||
| let mut temp_file = None; | |||||
| let path = if source_is_url(&node.source) { | |||||
| // try to download the shared library | |||||
| let tmp = download_file(&node.source).wrap_err("failed to download custom node")?; | |||||
| let path = tmp.path().to_owned(); | |||||
| temp_file = Some(tmp); | |||||
| path | |||||
| } else { | |||||
| let raw = Path::new(&node.source); | |||||
| if raw.extension().is_none() { | |||||
| raw.with_extension(EXE_EXTENSION) | raw.with_extension(EXE_EXTENSION) | ||||
| } else { | } else { | ||||
| raw.to_owned() | raw.to_owned() | ||||
| }; | |||||
| working_dir | |||||
| .join(&path) | |||||
| .canonicalize() | |||||
| .wrap_err_with(|| format!("no node exists at `{}`", path.display()))? | |||||
| } | |||||
| }; | }; | ||||
| let cmd = working_dir | |||||
| .join(&path) | |||||
| .canonicalize() | |||||
| .wrap_err_with(|| format!("no node exists at `{}`", path.display()))?; | |||||
| let mut command = tokio::process::Command::new(cmd); | let mut command = tokio::process::Command::new(cmd); | ||||
| command.args(args); | |||||
| if let Some(args) = &node.args { | |||||
| command.args(args.split_ascii_whitespace()); | |||||
| } | |||||
| command_init_common_env(&mut command, &node_id, communication)?; | command_init_common_env(&mut command, &node_id, communication)?; | ||||
| command.env( | command.env( | ||||
| "DORA_NODE_RUN_CONFIG", | "DORA_NODE_RUN_CONFIG", | ||||
| @@ -45,11 +55,16 @@ pub(super) fn spawn_custom_node( | |||||
| } | } | ||||
| } | } | ||||
| let mut child = command | |||||
| .spawn() | |||||
| .wrap_err_with(|| format!("failed to run command `{}`", &node.run))?; | |||||
| let mut child = command.spawn().wrap_err_with(|| { | |||||
| format!( | |||||
| "failed to run executable `{}` with args `{}`", | |||||
| node.source, | |||||
| node.args.as_deref().unwrap_or_default() | |||||
| ) | |||||
| })?; | |||||
| let result = tokio::spawn(async move { | let result = tokio::spawn(async move { | ||||
| let status = child.wait().await.context("child process failed")?; | let status = child.wait().await.context("child process failed")?; | ||||
| std::mem::drop(temp_file); | |||||
| if status.success() { | if status.success() { | ||||
| tracing::info!("node {node_id} finished"); | tracing::info!("node {node_id} finished"); | ||||
| Ok(()) | Ok(()) | ||||
| @@ -37,8 +37,7 @@ flume = "0.10.14" | |||||
| dora-message = { path = "../../libraries/message" } | dora-message = { path = "../../libraries/message" } | ||||
| tracing = "0.1.36" | tracing = "0.1.36" | ||||
| tracing-subscriber = "0.3.15" | tracing-subscriber = "0.3.15" | ||||
| tempfile = "3.3.0" | |||||
| reqwest = "0.11.12" | |||||
| dora-download = { path = "../../libraries/extensions/download" } | |||||
| [features] | [features] | ||||
| tracing = ["opentelemetry", "dora-tracing"] | tracing = ["opentelemetry", "dora-tracing"] | ||||
| @@ -6,7 +6,7 @@ use dora_node_api::communication::{self, CommunicationLayer}; | |||||
| use eyre::Context; | use eyre::Context; | ||||
| #[cfg(feature = "tracing")] | #[cfg(feature = "tracing")] | ||||
| use opentelemetry::sdk::trace::Tracer; | use opentelemetry::sdk::trace::Tracer; | ||||
| use std::{any::Any, io::Write}; | |||||
| use std::any::Any; | |||||
| use tokio::sync::mpsc::Sender; | use tokio::sync::mpsc::Sender; | ||||
| #[cfg(not(feature = "tracing"))] | #[cfg(not(feature = "tracing"))] | ||||
| @@ -85,23 +85,3 @@ pub enum OperatorEvent { | |||||
| Panic(Box<dyn Any + Send>), | Panic(Box<dyn Any + Send>), | ||||
| Finished, | Finished, | ||||
| } | } | ||||
| fn download_file<T>(url: T) -> Result<tempfile::NamedTempFile, eyre::ErrReport> | |||||
| where | |||||
| T: reqwest::IntoUrl + std::fmt::Display + Copy, | |||||
| { | |||||
| let response = tokio::runtime::Handle::current().block_on(async { | |||||
| reqwest::get(url) | |||||
| .await | |||||
| .wrap_err_with(|| format!("failed to request operator from `{url}`"))? | |||||
| .bytes() | |||||
| .await | |||||
| .wrap_err("failed to read operator from `{uri}`") | |||||
| })?; | |||||
| let mut tmp = | |||||
| tempfile::NamedTempFile::new().wrap_err("failed to create temp file for operator")?; | |||||
| tmp.as_file_mut() | |||||
| .write_all(&response) | |||||
| .wrap_err("failed to write downloaded operator to file")?; | |||||
| Ok(tmp) | |||||
| } | |||||
| @@ -1,7 +1,8 @@ | |||||
| #![allow(clippy::borrow_deref_ref)] // clippy warns about code generated by #[pymethods] | #![allow(clippy::borrow_deref_ref)] // clippy warns about code generated by #[pymethods] | ||||
| use super::{download_file, OperatorEvent, Tracer}; | |||||
| use dora_core::{config::DataId, descriptor::OperatorSource}; | |||||
| use super::{OperatorEvent, Tracer}; | |||||
| use dora_core::{config::DataId, descriptor::source_is_url}; | |||||
| use dora_download::download_file; | |||||
| use dora_node_api::communication::Publisher; | use dora_node_api::communication::Publisher; | ||||
| use dora_operator_api_python::metadata_to_pydict; | use dora_operator_api_python::metadata_to_pydict; | ||||
| use eyre::{bail, eyre, Context}; | use eyre::{bail, eyre, Context}; | ||||
| @@ -41,7 +42,7 @@ pub fn spawn( | |||||
| tracer: Tracer, | tracer: Tracer, | ||||
| ) -> eyre::Result<()> { | ) -> eyre::Result<()> { | ||||
| let mut temp_file = None; | let mut temp_file = None; | ||||
| let path = if OperatorSource::is_url(source) { | |||||
| let path = if source_is_url(source) { | |||||
| // try to download the shared library | // try to download the shared library | ||||
| let tmp = download_file(source).wrap_err("failed to download Python operator")?; | let tmp = download_file(source).wrap_err("failed to download Python operator")?; | ||||
| let path = tmp.path().to_owned(); | let path = tmp.path().to_owned(); | ||||
| @@ -1,5 +1,6 @@ | |||||
| use super::{download_file, OperatorEvent, Tracer}; | |||||
| use dora_core::{adjust_shared_library_path, config::DataId, descriptor::OperatorSource}; | |||||
| use super::{OperatorEvent, Tracer}; | |||||
| use dora_core::{adjust_shared_library_path, config::DataId, descriptor::source_is_url}; | |||||
| use dora_download::download_file; | |||||
| use dora_node_api::communication::Publisher; | use dora_node_api::communication::Publisher; | ||||
| use dora_operator_api_types::{ | use dora_operator_api_types::{ | ||||
| safer_ffi::closure::ArcDynFn1, DoraDropOperator, DoraInitOperator, DoraInitResult, DoraOnInput, | safer_ffi::closure::ArcDynFn1, DoraDropOperator, DoraInitOperator, DoraInitResult, DoraOnInput, | ||||
| @@ -27,7 +28,7 @@ pub fn spawn( | |||||
| tracer: Tracer, | tracer: Tracer, | ||||
| ) -> eyre::Result<()> { | ) -> eyre::Result<()> { | ||||
| let mut temp_file = None; | let mut temp_file = None; | ||||
| let path = if OperatorSource::is_url(source) { | |||||
| let path = if source_is_url(source) { | |||||
| // try to download the shared library | // try to download the shared library | ||||
| let tmp = download_file(source).wrap_err("failed to download shared library operator")?; | let tmp = download_file(source).wrap_err("failed to download shared library operator")?; | ||||
| let path = tmp.path().to_owned(); | let path = tmp.path().to_owned(); | ||||
| @@ -5,14 +5,14 @@ communication: | |||||
| nodes: | nodes: | ||||
| - id: cxx-node-rust-api | - id: cxx-node-rust-api | ||||
| custom: | custom: | ||||
| run: ../../target/debug/cxx-dataflow-example-node-rust-api | |||||
| source: ../../target/debug/cxx-dataflow-example-node-rust-api | |||||
| inputs: | inputs: | ||||
| tick: dora/timer/millis/300 | tick: dora/timer/millis/300 | ||||
| outputs: | outputs: | ||||
| - counter | - counter | ||||
| - id: cxx-node-c-api | - id: cxx-node-c-api | ||||
| custom: | custom: | ||||
| run: build/node_c_api | |||||
| source: build/node_c_api | |||||
| inputs: | inputs: | ||||
| tick: dora/timer/millis/300 | tick: dora/timer/millis/300 | ||||
| outputs: | outputs: | ||||
| @@ -5,7 +5,7 @@ communication: | |||||
| nodes: | nodes: | ||||
| - id: c_node | - id: c_node | ||||
| custom: | custom: | ||||
| run: build/c_node | |||||
| source: build/c_node | |||||
| inputs: | inputs: | ||||
| timer: dora/timer/secs/1 | timer: dora/timer/secs/1 | ||||
| outputs: | outputs: | ||||
| @@ -20,6 +20,6 @@ nodes: | |||||
| - counter | - counter | ||||
| - id: c_sink | - id: c_sink | ||||
| custom: | custom: | ||||
| run: build/c_sink | |||||
| source: build/c_sink | |||||
| inputs: | inputs: | ||||
| counter: runtime-node/c_operator/counter | counter: runtime-node/c_operator/counter | ||||
| @@ -5,7 +5,7 @@ communication: | |||||
| nodes: | nodes: | ||||
| - id: rust-node | - id: rust-node | ||||
| custom: | custom: | ||||
| run: ../../target/debug/iceoryx-example-node | |||||
| source: ../../target/debug/iceoryx-example-node | |||||
| inputs: | inputs: | ||||
| tick: dora/timer/millis/300 | tick: dora/timer/millis/300 | ||||
| outputs: | outputs: | ||||
| @@ -21,6 +21,6 @@ nodes: | |||||
| - status | - status | ||||
| - id: rust-sink | - id: rust-sink | ||||
| custom: | custom: | ||||
| run: ../../target/debug/iceoryx-example-sink | |||||
| source: ../../target/debug/iceoryx-example-sink | |||||
| inputs: | inputs: | ||||
| message: runtime-node/rust-operator/status | message: runtime-node/rust-operator/status | ||||
| @@ -5,12 +5,12 @@ communication: | |||||
| nodes: | nodes: | ||||
| - id: webcam | - id: webcam | ||||
| custom: | custom: | ||||
| run: ./webcam.py | |||||
| source: ./webcam.py | |||||
| inputs: | inputs: | ||||
| tick: dora/timer/millis/100 | tick: dora/timer/millis/100 | ||||
| outputs: | outputs: | ||||
| - image | - image | ||||
| - id: object_detection | - id: object_detection | ||||
| operator: | operator: | ||||
| python: object_detection.py | python: object_detection.py | ||||
| @@ -5,12 +5,12 @@ communication: | |||||
| nodes: | nodes: | ||||
| - id: no_webcam | - id: no_webcam | ||||
| custom: | custom: | ||||
| run: ./no_webcam.py | |||||
| source: ./no_webcam.py | |||||
| inputs: | inputs: | ||||
| tick: dora/timer/millis/100 | tick: dora/timer/millis/100 | ||||
| outputs: | outputs: | ||||
| - image | - image | ||||
| - id: object_detection | - id: object_detection | ||||
| operator: | operator: | ||||
| python: object_detection.py | python: object_detection.py | ||||
| @@ -6,7 +6,7 @@ nodes: | |||||
| - id: rust-node | - id: rust-node | ||||
| custom: | custom: | ||||
| build: cargo build -p rust-dataflow-example-node | build: cargo build -p rust-dataflow-example-node | ||||
| run: ../../target/debug/rust-dataflow-example-node | |||||
| source: ../../target/debug/rust-dataflow-example-node | |||||
| inputs: | inputs: | ||||
| tick: dora/timer/millis/300 | tick: dora/timer/millis/300 | ||||
| outputs: | outputs: | ||||
| @@ -24,6 +24,6 @@ nodes: | |||||
| - id: rust-sink | - id: rust-sink | ||||
| custom: | custom: | ||||
| build: cargo build -p rust-dataflow-example-sink | build: cargo build -p rust-dataflow-example-sink | ||||
| run: ../../target/debug/rust-dataflow-example-sink | |||||
| source: ../../target/debug/rust-dataflow-example-sink | |||||
| inputs: | inputs: | ||||
| message: runtime-node/rust-operator/status | message: runtime-node/rust-operator/status | ||||
| @@ -169,10 +169,8 @@ pub enum OperatorSource { | |||||
| Wasm(String), | Wasm(String), | ||||
| } | } | ||||
| impl OperatorSource { | |||||
| pub fn is_url(source: &str) -> bool { | |||||
| source.contains("://") | |||||
| } | |||||
| pub fn source_is_url(source: &str) -> bool { | |||||
| source.contains("://") | |||||
| } | } | ||||
| #[derive(Debug, Serialize, Deserialize, Clone)] | #[derive(Debug, Serialize, Deserialize, Clone)] | ||||
| @@ -186,7 +184,9 @@ pub struct PythonOperatorConfig { | |||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct CustomNode { | pub struct CustomNode { | ||||
| pub run: String, | |||||
| pub source: String, | |||||
| #[serde(default, skip_serializing_if = "Option::is_none")] | |||||
| pub args: Option<String>, | |||||
| pub env: Option<BTreeMap<String, EnvValue>>, | pub env: Option<BTreeMap<String, EnvValue>>, | ||||
| pub working_directory: Option<BTreeMap<String, EnvValue>>, | pub working_directory: Option<BTreeMap<String, EnvValue>>, | ||||
| #[serde(default, skip_serializing_if = "Option::is_none")] | #[serde(default, skip_serializing_if = "Option::is_none")] | ||||
| @@ -0,0 +1,13 @@ | |||||
| [package] | |||||
| name = "dora-download" | |||||
| version = "0.1.0" | |||||
| edition = "2021" | |||||
| license = "Apache-2.0" | |||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | |||||
| [dependencies] | |||||
| eyre = "0.6.8" | |||||
| tempfile = "3.3.0" | |||||
| reqwest = "0.11.12" | |||||
| tokio = { version = "1.17.0" } | |||||
| @@ -0,0 +1,22 @@ | |||||
| use eyre::Context; | |||||
| use std::io::Write; | |||||
| pub fn download_file<T>(url: T) -> Result<tempfile::NamedTempFile, eyre::ErrReport> | |||||
| where | |||||
| T: reqwest::IntoUrl + std::fmt::Display + Copy, | |||||
| { | |||||
| let response = tokio::runtime::Handle::current().block_on(async { | |||||
| reqwest::get(url) | |||||
| .await | |||||
| .wrap_err_with(|| format!("failed to request operator from `{url}`"))? | |||||
| .bytes() | |||||
| .await | |||||
| .wrap_err("failed to read operator from `{uri}`") | |||||
| })?; | |||||
| let mut tmp = | |||||
| tempfile::NamedTempFile::new().wrap_err("failed to create temp file for operator")?; | |||||
| tmp.as_file_mut() | |||||
| .write_all(&response) | |||||
| .wrap_err("failed to write downloaded operator to file")?; | |||||
| Ok(tmp) | |||||
| } | |||||