This pull request initiates a series of CLI rework efforts aimed at improving maintainability, consistency, and user experience.tags/v0.3.12-fix
| @@ -381,7 +381,7 @@ pub fn resolve_dataflow(dataflow: String) -> eyre::Result<PathBuf> { | |||
| #[pyfunction] | |||
| #[pyo3(signature = (dataflow_path, uv=None))] | |||
| pub fn run(dataflow_path: String, uv: Option<bool>) -> eyre::Result<()> { | |||
| dora_cli::command::run(dataflow_path, uv.unwrap_or_default()) | |||
| dora_cli::run_func(dataflow_path, uv.unwrap_or_default()) | |||
| } | |||
| #[pymodule] | |||
| @@ -5,9 +5,13 @@ use dora_core::{ | |||
| }; | |||
| use dora_message::{descriptor::NodeSource, BuildId}; | |||
| use eyre::Context; | |||
| use std::collections::BTreeMap; | |||
| use std::{collections::BTreeMap, net::IpAddr}; | |||
| use crate::{connect_to_coordinator, resolve_dataflow, session::DataflowSession}; | |||
| use super::{default_tracing, Executable}; | |||
| use crate::{ | |||
| common::{connect_to_coordinator, local_working_dir, resolve_dataflow}, | |||
| session::DataflowSession, | |||
| }; | |||
| use distributed::{build_distributed_dataflow, wait_until_dataflow_built}; | |||
| use local::build_dataflow_locally; | |||
| @@ -16,9 +20,42 @@ mod distributed; | |||
| mod git; | |||
| mod local; | |||
| #[derive(Debug, clap::Args)] | |||
| /// Run build commands provided in the given dataflow. | |||
| pub struct Build { | |||
| /// Path to the dataflow descriptor file | |||
| #[clap(value_name = "PATH")] | |||
| dataflow: String, | |||
| /// Address of the dora coordinator | |||
| #[clap(long, value_name = "IP")] | |||
| coordinator_addr: Option<IpAddr>, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, value_name = "PORT")] | |||
| coordinator_port: Option<u16>, | |||
| // Use UV to build nodes. | |||
| #[clap(long, action)] | |||
| uv: bool, | |||
| // Run build on local machine | |||
| #[clap(long, action)] | |||
| local: bool, | |||
| } | |||
| impl Executable for Build { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| default_tracing()?; | |||
| build( | |||
| self.dataflow, | |||
| self.coordinator_addr, | |||
| self.coordinator_port, | |||
| self.uv, | |||
| self.local, | |||
| ) | |||
| } | |||
| } | |||
| pub fn build( | |||
| dataflow: String, | |||
| coordinator_addr: Option<std::net::IpAddr>, | |||
| coordinator_addr: Option<IpAddr>, | |||
| coordinator_port: Option<u16>, | |||
| uv: bool, | |||
| force_local: bool, | |||
| @@ -104,7 +141,7 @@ pub fn build( | |||
| BuildKind::ThroughCoordinator { | |||
| mut coordinator_session, | |||
| } => { | |||
| let local_working_dir = super::local_working_dir( | |||
| let local_working_dir = local_working_dir( | |||
| &dataflow_path, | |||
| &dataflow_descriptor, | |||
| &mut *coordinator_session, | |||
| @@ -1,11 +1,15 @@ | |||
| use crate::connect_to_coordinator; | |||
| use super::{default_tracing, Executable}; | |||
| use crate::{common::connect_to_coordinator, LOCALHOST}; | |||
| use communication_layer_request_reply::TcpRequestReplyConnection; | |||
| use dora_core::descriptor::DescriptorExt; | |||
| use dora_core::{descriptor::Descriptor, topics::DORA_COORDINATOR_PORT_CONTROL_DEFAULT}; | |||
| use dora_message::{cli_to_coordinator::ControlRequest, coordinator_to_cli::ControlRequestReply}; | |||
| use eyre::{bail, Context}; | |||
| use std::{ | |||
| io::{IsTerminal, Write}, | |||
| net::SocketAddr, | |||
| }; | |||
| use std::{net::IpAddr, path::PathBuf}; | |||
| use termcolor::{Color, ColorChoice, ColorSpec, WriteColor}; | |||
| pub fn check_environment(coordinator_addr: SocketAddr) -> eyre::Result<()> { | |||
| @@ -75,3 +79,39 @@ pub fn daemon_running(session: &mut TcpRequestReplyConnection) -> Result<bool, e | |||
| Ok(running) | |||
| } | |||
| #[derive(Debug, clap::Args)] | |||
| /// Check if the coordinator and the daemon is running. | |||
| pub struct Check { | |||
| /// Path to the dataflow descriptor file (enables additional checks) | |||
| #[clap(long, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] | |||
| dataflow: Option<PathBuf>, | |||
| /// Address of the dora coordinator | |||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||
| coordinator_addr: IpAddr, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||
| coordinator_port: u16, | |||
| } | |||
| impl Executable for Check { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| default_tracing()?; | |||
| match self.dataflow { | |||
| Some(dataflow) => { | |||
| let working_dir = dataflow | |||
| .canonicalize() | |||
| .context("failed to canonicalize dataflow path")? | |||
| .parent() | |||
| .ok_or_else(|| eyre::eyre!("dataflow path has no parent dir"))? | |||
| .to_owned(); | |||
| Descriptor::blocking_read(&dataflow)?.check(&working_dir)?; | |||
| check_environment((self.coordinator_addr, self.coordinator_port).into())? | |||
| } | |||
| None => check_environment((self.coordinator_addr, self.coordinator_port).into())?, | |||
| } | |||
| Ok(()) | |||
| } | |||
| } | |||
| @@ -0,0 +1,66 @@ | |||
| use super::Executable; | |||
| use crate::LISTEN_WILDCARD; | |||
| use dora_coordinator::Event; | |||
| use dora_core::topics::{DORA_COORDINATOR_PORT_CONTROL_DEFAULT, DORA_COORDINATOR_PORT_DEFAULT}; | |||
| #[cfg(feature = "tracing")] | |||
| use dora_tracing::TracingBuilder; | |||
| use eyre::Context; | |||
| use std::net::{IpAddr, SocketAddr}; | |||
| use tokio::runtime::Builder; | |||
| use tracing::level_filters::LevelFilter; | |||
| #[derive(Debug, clap::Args)] | |||
| /// Run coordinator | |||
| pub struct Coordinator { | |||
| /// Network interface to bind to for daemon communication | |||
| #[clap(long, default_value_t = LISTEN_WILDCARD)] | |||
| interface: IpAddr, | |||
| /// Port number to bind to for daemon communication | |||
| #[clap(long, default_value_t = DORA_COORDINATOR_PORT_DEFAULT)] | |||
| port: u16, | |||
| /// Network interface to bind to for control communication | |||
| #[clap(long, default_value_t = LISTEN_WILDCARD)] | |||
| control_interface: IpAddr, | |||
| /// Port number to bind to for control communication | |||
| #[clap(long, default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||
| control_port: u16, | |||
| /// Suppresses all log output to stdout. | |||
| #[clap(long)] | |||
| quiet: bool, | |||
| } | |||
| impl Executable for Coordinator { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| #[cfg(feature = "tracing")] | |||
| { | |||
| let name = "dora-coordinator"; | |||
| let mut builder = TracingBuilder::new(name); | |||
| if !self.quiet { | |||
| builder = builder.with_stdout("info"); | |||
| } | |||
| builder = builder.with_file(name, LevelFilter::INFO)?; | |||
| builder | |||
| .build() | |||
| .wrap_err("failed to set up tracing subscriber")?; | |||
| } | |||
| let rt = Builder::new_multi_thread() | |||
| .enable_all() | |||
| .build() | |||
| .context("tokio runtime failed")?; | |||
| rt.block_on(async { | |||
| let bind = SocketAddr::new(self.interface, self.port); | |||
| let bind_control = SocketAddr::new(self.control_interface, self.control_port); | |||
| let (port, task) = | |||
| dora_coordinator::start(bind, bind_control, futures::stream::empty::<Event>()) | |||
| .await?; | |||
| if !self.quiet { | |||
| println!("Listening for incoming daemon connection on {port}"); | |||
| } | |||
| task.await | |||
| }) | |||
| .context("failed to run dora-coordinator") | |||
| } | |||
| } | |||
| @@ -0,0 +1,91 @@ | |||
| use super::Executable; | |||
| use crate::{common::handle_dataflow_result, session::DataflowSession}; | |||
| use dora_core::topics::{ | |||
| DORA_COORDINATOR_PORT_DEFAULT, DORA_DAEMON_LOCAL_LISTEN_PORT_DEFAULT, LOCALHOST, | |||
| }; | |||
| use dora_daemon::LogDestination; | |||
| #[cfg(feature = "tracing")] | |||
| use dora_tracing::TracingBuilder; | |||
| use eyre::Context; | |||
| use std::{ | |||
| net::{IpAddr, SocketAddr}, | |||
| path::PathBuf, | |||
| }; | |||
| use tokio::runtime::Builder; | |||
| use tracing::level_filters::LevelFilter; | |||
| #[derive(Debug, clap::Args)] | |||
| /// Run daemon | |||
| pub struct Daemon { | |||
| /// Unique identifier for the machine (required for distributed dataflows) | |||
| #[clap(long)] | |||
| machine_id: Option<String>, | |||
| /// Local listen port for event such as dynamic node. | |||
| #[clap(long, default_value_t = DORA_DAEMON_LOCAL_LISTEN_PORT_DEFAULT)] | |||
| local_listen_port: u16, | |||
| /// Address and port number of the dora coordinator | |||
| #[clap(long, short, default_value_t = LOCALHOST)] | |||
| coordinator_addr: IpAddr, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, default_value_t = DORA_COORDINATOR_PORT_DEFAULT)] | |||
| coordinator_port: u16, | |||
| #[clap(long, hide = true)] | |||
| run_dataflow: Option<PathBuf>, | |||
| /// Suppresses all log output to stdout. | |||
| #[clap(long)] | |||
| quiet: bool, | |||
| } | |||
| impl Executable for Daemon { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| #[cfg(feature = "tracing")] | |||
| { | |||
| let name = "dora-daemon"; | |||
| let filename = self | |||
| .machine_id | |||
| .as_ref() | |||
| .map(|id| format!("{name}-{id}")) | |||
| .unwrap_or(name.to_string()); | |||
| let mut builder = TracingBuilder::new(name); | |||
| if !self.quiet { | |||
| builder = builder.with_stdout("info,zenoh=warn"); | |||
| } | |||
| builder = builder.with_file(filename, LevelFilter::INFO)?; | |||
| builder | |||
| .build() | |||
| .wrap_err("failed to set up tracing subscriber")?; | |||
| } | |||
| let rt = Builder::new_multi_thread() | |||
| .enable_all() | |||
| .build() | |||
| .context("tokio runtime failed")?; | |||
| rt.block_on(async { | |||
| match self.run_dataflow { | |||
| Some(dataflow_path) => { | |||
| tracing::info!("Starting dataflow `{}`", dataflow_path.display()); | |||
| if self.coordinator_addr != LOCALHOST { | |||
| tracing::info!( | |||
| "Not using coordinator addr {} as `run_dataflow` is for local dataflow only. Please use the `start` command for remote coordinator", | |||
| self.coordinator_addr | |||
| ); | |||
| } | |||
| let dataflow_session = | |||
| DataflowSession::read_session(&dataflow_path).context("failed to read DataflowSession")?; | |||
| let result = dora_daemon::Daemon::run_dataflow(&dataflow_path, | |||
| dataflow_session.build_id, dataflow_session.local_build, dataflow_session.session_id, false, | |||
| LogDestination::Tracing, | |||
| ).await?; | |||
| handle_dataflow_result(result, None) | |||
| } | |||
| None => { | |||
| dora_daemon::Daemon::run(SocketAddr::new(self.coordinator_addr, self.coordinator_port), self.machine_id, self.local_listen_port).await | |||
| } | |||
| } | |||
| }) | |||
| .context("failed to run dora-daemon") | |||
| } | |||
| } | |||
| @@ -0,0 +1,28 @@ | |||
| use super::{default_tracing, up, Executable}; | |||
| use dora_core::topics::{DORA_COORDINATOR_PORT_CONTROL_DEFAULT, LOCALHOST}; | |||
| use std::net::IpAddr; | |||
| use std::path::PathBuf; | |||
| #[derive(Debug, clap::Args)] | |||
| /// Destroy running coordinator and daemon. If some dataflows are still running, they will be stopped first. | |||
| pub struct Destroy { | |||
| /// Use a custom configuration | |||
| #[clap(long, hide = true)] | |||
| config: Option<PathBuf>, | |||
| /// Address of the dora coordinator | |||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||
| coordinator_addr: IpAddr, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||
| coordinator_port: u16, | |||
| } | |||
| impl Executable for Destroy { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| default_tracing()?; | |||
| up::destroy( | |||
| self.config.as_deref(), | |||
| (self.coordinator_addr, self.coordinator_port).into(), | |||
| ) | |||
| } | |||
| } | |||
| @@ -1,11 +1,35 @@ | |||
| use std::{fs::File, io::Write, path::Path}; | |||
| use super::Executable; | |||
| use dora_core::descriptor::{Descriptor, DescriptorExt}; | |||
| use eyre::Context; | |||
| use std::{ | |||
| fs::File, | |||
| io::Write, | |||
| path::{Path, PathBuf}, | |||
| }; | |||
| const MERMAID_TEMPLATE: &str = include_str!("graph/mermaid-template.html"); | |||
| #[derive(Debug, clap::Args)] | |||
| /// Generate a visualization of the given graph using mermaid.js. Use --open to open browser. | |||
| pub struct Graph { | |||
| /// Path to the dataflow descriptor file | |||
| #[clap(value_name = "PATH", value_hint = clap::ValueHint::FilePath)] | |||
| dataflow: PathBuf, | |||
| /// Visualize the dataflow as a Mermaid diagram (instead of HTML) | |||
| #[clap(long, action)] | |||
| mermaid: bool, | |||
| /// Open the HTML visualization in the browser | |||
| #[clap(long, action)] | |||
| open: bool, | |||
| } | |||
| const MERMAID_TEMPLATE: &str = include_str!("mermaid-template.html"); | |||
| impl Executable for Graph { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| create(self.dataflow, self.mermaid, self.open) | |||
| } | |||
| } | |||
| pub(crate) fn create(dataflow: std::path::PathBuf, mermaid: bool, open: bool) -> eyre::Result<()> { | |||
| fn create(dataflow: std::path::PathBuf, mermaid: bool, open: bool) -> eyre::Result<()> { | |||
| if mermaid { | |||
| let visualized = visualize_as_mermaid(&dataflow)?; | |||
| println!("{visualized}"); | |||
| @@ -0,0 +1 @@ | |||
| !*template.html | |||
| @@ -0,0 +1,59 @@ | |||
| use std::io::Write; | |||
| use super::{default_tracing, Executable}; | |||
| use crate::{ | |||
| common::{connect_to_coordinator, query_running_dataflows}, | |||
| LOCALHOST, | |||
| }; | |||
| use clap::Args; | |||
| use communication_layer_request_reply::TcpRequestReplyConnection; | |||
| use dora_core::topics::DORA_COORDINATOR_PORT_CONTROL_DEFAULT; | |||
| use dora_message::coordinator_to_cli::DataflowStatus; | |||
| use eyre::eyre; | |||
| use tabwriter::TabWriter; | |||
| #[derive(Debug, Args)] | |||
| /// List running dataflows. | |||
| pub struct ListArgs { | |||
| /// Address of the dora coordinator | |||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||
| pub coordinator_addr: std::net::IpAddr, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||
| pub coordinator_port: u16, | |||
| } | |||
| impl Executable for ListArgs { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| default_tracing()?; | |||
| let mut session = | |||
| connect_to_coordinator((self.coordinator_addr, self.coordinator_port).into()) | |||
| .map_err(|_| eyre!("Failed to connect to coordinator"))?; | |||
| list(&mut *session) | |||
| } | |||
| } | |||
| fn list(session: &mut TcpRequestReplyConnection) -> Result<(), eyre::ErrReport> { | |||
| let list = query_running_dataflows(session)?; | |||
| let mut tw = TabWriter::new(vec![]); | |||
| tw.write_all(b"UUID\tName\tStatus\n")?; | |||
| for entry in list.0 { | |||
| let uuid = entry.id.uuid; | |||
| let name = entry.id.name.unwrap_or_default(); | |||
| let status = match entry.status { | |||
| DataflowStatus::Running => "Running", | |||
| DataflowStatus::Finished => "Succeeded", | |||
| DataflowStatus::Failed => "Failed", | |||
| }; | |||
| tw.write_all(format!("{uuid}\t{name}\t{status}\n").as_bytes())?; | |||
| } | |||
| tw.flush()?; | |||
| let formatted = String::from_utf8(tw.into_inner()?)?; | |||
| println!("{formatted}"); | |||
| Ok(()) | |||
| } | |||
| @@ -1,9 +1,54 @@ | |||
| use super::{default_tracing, Executable}; | |||
| use crate::common::{connect_to_coordinator, query_running_dataflows}; | |||
| use bat::{Input, PrettyPrinter}; | |||
| use clap::Args; | |||
| use communication_layer_request_reply::TcpRequestReplyConnection; | |||
| use dora_core::topics::{DORA_COORDINATOR_PORT_CONTROL_DEFAULT, LOCALHOST}; | |||
| use dora_message::{cli_to_coordinator::ControlRequest, coordinator_to_cli::ControlRequestReply}; | |||
| use eyre::{bail, Context, Result}; | |||
| use uuid::Uuid; | |||
| use bat::{Input, PrettyPrinter}; | |||
| #[derive(Debug, Args)] | |||
| /// Show logs of a given dataflow and node. | |||
| pub struct LogsArgs { | |||
| /// Identifier of the dataflow | |||
| #[clap(value_name = "UUID_OR_NAME")] | |||
| pub dataflow: Option<String>, | |||
| /// Show logs for the given node | |||
| #[clap(value_name = "NAME")] | |||
| pub node: String, | |||
| /// Address of the dora coordinator | |||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||
| pub coordinator_addr: std::net::IpAddr, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||
| pub coordinator_port: u16, | |||
| } | |||
| impl Executable for LogsArgs { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| default_tracing()?; | |||
| let mut session = | |||
| connect_to_coordinator((self.coordinator_addr, self.coordinator_port).into()) | |||
| .wrap_err("failed to connect to dora coordinator")?; | |||
| let list = | |||
| query_running_dataflows(&mut *session).wrap_err("failed to query running dataflows")?; | |||
| if let Some(dataflow) = self.dataflow { | |||
| let uuid = Uuid::parse_str(&dataflow).ok(); | |||
| let name = if uuid.is_some() { None } else { Some(dataflow) }; | |||
| logs(&mut *session, uuid, name, self.node) | |||
| } else { | |||
| let active = list.get_active(); | |||
| let uuid = match &active[..] { | |||
| [] => bail!("No dataflows are running"), | |||
| [uuid] => uuid.clone(), | |||
| _ => inquire::Select::new("Choose dataflow to show logs:", active).prompt()?, | |||
| }; | |||
| logs(&mut *session, Some(uuid.uuid), None, self.node) | |||
| } | |||
| } | |||
| } | |||
| pub fn logs( | |||
| session: &mut TcpRequestReplyConnection, | |||
| @@ -1,60 +1,104 @@ | |||
| pub use build::build; | |||
| pub use logs::logs; | |||
| pub use run::run; | |||
| pub use start::start; | |||
| use std::path::{Path, PathBuf}; | |||
| use communication_layer_request_reply::TcpRequestReplyConnection; | |||
| use dora_core::descriptor::Descriptor; | |||
| use dora_message::{cli_to_coordinator::ControlRequest, coordinator_to_cli::ControlRequestReply}; | |||
| use eyre::{bail, Context, ContextCompat}; | |||
| mod build; | |||
| pub mod check; | |||
| mod check; | |||
| mod coordinator; | |||
| mod daemon; | |||
| mod destroy; | |||
| mod graph; | |||
| mod list; | |||
| mod logs; | |||
| mod new; | |||
| mod run; | |||
| mod runtime; | |||
| mod self_; | |||
| mod start; | |||
| pub mod up; | |||
| fn local_working_dir( | |||
| dataflow_path: &Path, | |||
| dataflow_descriptor: &Descriptor, | |||
| coordinator_session: &mut TcpRequestReplyConnection, | |||
| ) -> eyre::Result<Option<PathBuf>> { | |||
| Ok( | |||
| if dataflow_descriptor | |||
| .nodes | |||
| .iter() | |||
| .all(|n| n.deploy.as_ref().map(|d| d.machine.as_ref()).is_none()) | |||
| && cli_and_daemon_on_same_machine(coordinator_session)? | |||
| { | |||
| Some( | |||
| dunce::canonicalize(dataflow_path) | |||
| .context("failed to canonicalize dataflow file path")? | |||
| .parent() | |||
| .context("dataflow path has no parent dir")? | |||
| .to_owned(), | |||
| ) | |||
| } else { | |||
| None | |||
| }, | |||
| ) | |||
| mod stop; | |||
| mod up; | |||
| pub use run::run_func; | |||
| use build::Build; | |||
| use check::Check; | |||
| use coordinator::Coordinator; | |||
| use daemon::Daemon; | |||
| use destroy::Destroy; | |||
| use eyre::Context; | |||
| use graph::Graph; | |||
| use list::ListArgs; | |||
| use logs::LogsArgs; | |||
| use new::NewArgs; | |||
| use run::Run; | |||
| use runtime::Runtime; | |||
| use self_::SelfSubCommand; | |||
| use start::Start; | |||
| use stop::Stop; | |||
| use up::Up; | |||
| /// dora-rs cli client | |||
| #[derive(Debug, clap::Subcommand)] | |||
| pub enum Command { | |||
| Check(Check), | |||
| Graph(Graph), | |||
| Build(Build), | |||
| New(NewArgs), | |||
| Run(Run), | |||
| Up(Up), | |||
| Destroy(Destroy), | |||
| Start(Start), | |||
| Stop(Stop), | |||
| List(ListArgs), | |||
| // Planned for future releases: | |||
| // Dashboard, | |||
| #[command(allow_missing_positional = true)] | |||
| Logs(LogsArgs), | |||
| // Metrics, | |||
| // Stats, | |||
| // Get, | |||
| // Upgrade, | |||
| Daemon(Daemon), | |||
| Runtime(Runtime), | |||
| Coordinator(Coordinator), | |||
| Self_ { | |||
| #[clap(subcommand)] | |||
| command: SelfSubCommand, | |||
| }, | |||
| } | |||
| fn default_tracing() -> eyre::Result<()> { | |||
| #[cfg(feature = "tracing")] | |||
| { | |||
| use dora_tracing::TracingBuilder; | |||
| TracingBuilder::new("dora-cli") | |||
| .with_stdout("warn") | |||
| .build() | |||
| .wrap_err("failed to set up tracing subscriber")?; | |||
| } | |||
| Ok(()) | |||
| } | |||
| pub trait Executable { | |||
| fn execute(self) -> eyre::Result<()>; | |||
| } | |||
| fn cli_and_daemon_on_same_machine(session: &mut TcpRequestReplyConnection) -> eyre::Result<bool> { | |||
| let reply_raw = session | |||
| .request(&serde_json::to_vec(&ControlRequest::CliAndDefaultDaemonOnSameMachine).unwrap()) | |||
| .wrap_err("failed to send start dataflow message")?; | |||
| let result: ControlRequestReply = | |||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||
| match result { | |||
| ControlRequestReply::CliAndDefaultDaemonIps { | |||
| default_daemon, | |||
| cli, | |||
| } => Ok(default_daemon.is_some() && default_daemon == cli), | |||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||
| other => bail!("unexpected start dataflow reply: {other:?}"), | |||
| impl Executable for Command { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| match self { | |||
| Command::Check(args) => args.execute(), | |||
| Command::Coordinator(args) => args.execute(), | |||
| Command::Graph(args) => args.execute(), | |||
| Command::Build(args) => args.execute(), | |||
| Command::New(args) => args.execute(), | |||
| Command::Run(args) => args.execute(), | |||
| Command::Up(args) => args.execute(), | |||
| Command::Destroy(args) => args.execute(), | |||
| Command::Start(args) => args.execute(), | |||
| Command::Stop(args) => args.execute(), | |||
| Command::List(args) => args.execute(), | |||
| Command::Logs(args) => args.execute(), | |||
| Command::Daemon(args) => args.execute(), | |||
| Command::Self_ { command } => command.execute(), | |||
| Command::Runtime(args) => args.execute(), | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| use clap::Args; | |||
| use super::{default_tracing, Executable}; | |||
| #[derive(Debug, Args)] | |||
| /// Generate a new project or node. Choose the language between Rust, Python, C or C++. | |||
| pub struct NewArgs { | |||
| #[clap(flatten)] | |||
| // TODO!: better impl | |||
| args: crate::CommandNew, | |||
| /// Internal flag for creating with path dependencies | |||
| #[clap(hide = true, long)] | |||
| pub internal_create_with_path_dependencies: bool, | |||
| } | |||
| impl Executable for NewArgs { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| default_tracing()?; | |||
| crate::template::create(self.args, self.internal_create_with_path_dependencies) | |||
| } | |||
| } | |||
| @@ -1,12 +1,38 @@ | |||
| use super::Executable; | |||
| use crate::{ | |||
| common::{handle_dataflow_result, resolve_dataflow}, | |||
| output::print_log_message, | |||
| session::DataflowSession, | |||
| }; | |||
| use dora_daemon::{flume, Daemon, LogDestination}; | |||
| use dora_tracing::TracingBuilder; | |||
| use eyre::Context; | |||
| use tokio::runtime::Builder; | |||
| use crate::{ | |||
| handle_dataflow_result, output::print_log_message, resolve_dataflow, session::DataflowSession, | |||
| }; | |||
| #[derive(Debug, clap::Args)] | |||
| /// Run a dataflow locally. | |||
| /// | |||
| /// Directly runs the given dataflow without connecting to a dora | |||
| /// coordinator or daemon. The dataflow is executed on the local machine. | |||
| pub struct Run { | |||
| /// Path to the dataflow descriptor file | |||
| #[clap(value_name = "PATH")] | |||
| dataflow: String, | |||
| // Use UV to run nodes. | |||
| #[clap(long, action)] | |||
| uv: bool, | |||
| } | |||
| pub fn run_func(dataflow: String, uv: bool) -> eyre::Result<()> { | |||
| #[cfg(feature = "tracing")] | |||
| { | |||
| let log_level = std::env::var("RUST_LOG").ok().unwrap_or("info".to_string()); | |||
| TracingBuilder::new("run") | |||
| .with_stdout(log_level) | |||
| .build() | |||
| .wrap_err("failed to set up tracing subscriber")?; | |||
| } | |||
| pub fn run(dataflow: String, uv: bool) -> Result<(), eyre::Error> { | |||
| let dataflow_path = resolve_dataflow(dataflow).context("could not resolve dataflow")?; | |||
| let dataflow_session = | |||
| DataflowSession::read_session(&dataflow_path).context("failed to read DataflowSession")?; | |||
| @@ -32,3 +58,9 @@ pub fn run(dataflow: String, uv: bool) -> Result<(), eyre::Error> { | |||
| ))?; | |||
| handle_dataflow_result(result, None) | |||
| } | |||
| impl Executable for Run { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| run_func(self.dataflow, self.uv) | |||
| } | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| use eyre::Context; | |||
| use super::Executable; | |||
| #[derive(Debug, clap::Args)] | |||
| /// Run runtime | |||
| pub struct Runtime; | |||
| impl Executable for Runtime { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| // No tracing: Do not set the runtime in the cli. | |||
| // ref: 72b4be808122574fcfda69650954318e0355cc7b cli::run | |||
| dora_runtime::main().context("Failed to run dora-runtime") | |||
| } | |||
| } | |||
| @@ -0,0 +1,139 @@ | |||
| use super::{default_tracing, Executable}; | |||
| use clap::Subcommand; | |||
| use eyre::{bail, Context}; | |||
| #[derive(Debug, Subcommand)] | |||
| /// Dora CLI self-management commands | |||
| pub enum SelfSubCommand { | |||
| /// Check for updates or update the CLI | |||
| Update { | |||
| /// Only check for updates without installing | |||
| #[clap(long)] | |||
| check_only: bool, | |||
| }, | |||
| /// Remove The Dora CLI from the system | |||
| Uninstall { | |||
| /// Force uninstallation without confirmation | |||
| #[clap(long)] | |||
| force: bool, | |||
| }, | |||
| } | |||
| impl Executable for SelfSubCommand { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| default_tracing()?; | |||
| match self { | |||
| SelfSubCommand::Update { check_only } => { | |||
| println!("Checking for updates..."); | |||
| #[cfg(target_os = "linux")] | |||
| let bin_path_in_archive = format!("dora-cli-{}/dora", env!("TARGET")); | |||
| #[cfg(target_os = "macos")] | |||
| let bin_path_in_archive = format!("dora-cli-{}/dora", env!("TARGET")); | |||
| #[cfg(target_os = "windows")] | |||
| let bin_path_in_archive = String::from("dora.exe"); | |||
| let status = self_update::backends::github::Update::configure() | |||
| .repo_owner("dora-rs") | |||
| .repo_name("dora") | |||
| .bin_path_in_archive(&bin_path_in_archive) | |||
| .bin_name("dora") | |||
| .show_download_progress(true) | |||
| .current_version(env!("CARGO_PKG_VERSION")) | |||
| .build()?; | |||
| if check_only { | |||
| // Only check if an update is available | |||
| match status.get_latest_release() { | |||
| Ok(release) => { | |||
| let current_version = self_update::cargo_crate_version!(); | |||
| if current_version != release.version { | |||
| println!( | |||
| "An update is available: {}. Run 'dora self update' to update", | |||
| release.version | |||
| ); | |||
| } else { | |||
| println!( | |||
| "Dora CLI is already at the latest version: {}", | |||
| current_version | |||
| ); | |||
| } | |||
| } | |||
| Err(e) => println!("Failed to check for updates: {}", e), | |||
| } | |||
| } else { | |||
| // Perform the actual update | |||
| match status.update() { | |||
| Ok(update_status) => match update_status { | |||
| self_update::Status::UpToDate(version) => { | |||
| println!("Dora CLI is already at the latest version: {}", version); | |||
| } | |||
| self_update::Status::Updated(version) => { | |||
| println!("Successfully updated Dora CLI to version: {}", version); | |||
| } | |||
| }, | |||
| Err(e) => println!("Failed to update: {}", e), | |||
| } | |||
| } | |||
| } | |||
| SelfSubCommand::Uninstall { force } => { | |||
| if !force { | |||
| let confirmed = | |||
| inquire::Confirm::new("Are you sure you want to uninstall Dora CLI?") | |||
| .with_default(false) | |||
| .prompt() | |||
| .wrap_err("Uninstallation cancelled")?; | |||
| if !confirmed { | |||
| println!("Uninstallation cancelled"); | |||
| return Ok(()); | |||
| } | |||
| } | |||
| println!("Uninstalling Dora CLI..."); | |||
| #[cfg(feature = "python")] | |||
| { | |||
| println!("Detected Python installation..."); | |||
| // Try uv pip uninstall first | |||
| let uv_status = std::process::Command::new("uv") | |||
| .args(["pip", "uninstall", "dora-rs-cli"]) | |||
| .status(); | |||
| if let Ok(status) = uv_status { | |||
| if status.success() { | |||
| println!("Dora CLI has been successfully uninstalled via uv pip."); | |||
| return Ok(()); | |||
| } | |||
| } | |||
| // Fall back to regular pip uninstall | |||
| println!("Trying with pip..."); | |||
| let status = std::process::Command::new("pip") | |||
| .args(["uninstall", "-y", "dora-rs-cli"]) | |||
| .status() | |||
| .wrap_err("Failed to run pip uninstall")?; | |||
| if status.success() { | |||
| println!("Dora CLI has been successfully uninstalled via pip."); | |||
| } else { | |||
| bail!("Failed to uninstall Dora CLI via pip."); | |||
| } | |||
| } | |||
| #[cfg(not(feature = "python"))] | |||
| { | |||
| match self_replace::self_delete() { | |||
| Ok(_) => { | |||
| println!("Dora CLI has been successfully uninstalled."); | |||
| } | |||
| Err(e) => { | |||
| bail!("Failed to uninstall Dora CLI: {}", e); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| Ok(()) | |||
| } | |||
| } | |||
| @@ -14,7 +14,7 @@ use std::{path::PathBuf, sync::mpsc, time::Duration}; | |||
| use tracing::{error, info}; | |||
| use uuid::Uuid; | |||
| use crate::handle_dataflow_result; | |||
| use crate::common::handle_dataflow_result; | |||
| use crate::output::print_log_message; | |||
| pub fn attach_dataflow( | |||
| @@ -1,70 +1,101 @@ | |||
| use super::{default_tracing, Executable}; | |||
| use crate::{ | |||
| command::start::attach::attach_dataflow, | |||
| common::{connect_to_coordinator, local_working_dir, resolve_dataflow}, | |||
| output::print_log_message, | |||
| session::DataflowSession, | |||
| }; | |||
| use communication_layer_request_reply::{TcpConnection, TcpRequestReplyConnection}; | |||
| use dora_core::descriptor::{Descriptor, DescriptorExt}; | |||
| use dora_core::{ | |||
| descriptor::{Descriptor, DescriptorExt}, | |||
| topics::{DORA_COORDINATOR_PORT_CONTROL_DEFAULT, LOCALHOST}, | |||
| }; | |||
| use dora_message::{ | |||
| cli_to_coordinator::ControlRequest, common::LogMessage, coordinator_to_cli::ControlRequestReply, | |||
| }; | |||
| use eyre::{bail, Context}; | |||
| use std::{ | |||
| net::{SocketAddr, TcpStream}, | |||
| net::{IpAddr, SocketAddr, TcpStream}, | |||
| path::PathBuf, | |||
| }; | |||
| use uuid::Uuid; | |||
| use crate::{ | |||
| connect_to_coordinator, output::print_log_message, resolve_dataflow, session::DataflowSession, | |||
| }; | |||
| use attach::attach_dataflow; | |||
| mod attach; | |||
| pub fn start( | |||
| #[derive(Debug, clap::Args)] | |||
| /// Start the given dataflow path. Attach a name to the running dataflow by using --name. | |||
| pub struct Start { | |||
| /// Path to the dataflow descriptor file | |||
| #[clap(value_name = "PATH")] | |||
| dataflow: String, | |||
| /// Assign a name to the dataflow | |||
| #[clap(long)] | |||
| name: Option<String>, | |||
| coordinator_socket: SocketAddr, | |||
| /// Address of the dora coordinator | |||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||
| coordinator_addr: IpAddr, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||
| coordinator_port: u16, | |||
| /// Attach to the dataflow and wait for its completion | |||
| #[clap(long, action)] | |||
| attach: bool, | |||
| /// Run the dataflow in background | |||
| #[clap(long, action)] | |||
| detach: bool, | |||
| /// Enable hot reloading (Python only) | |||
| #[clap(long, action)] | |||
| hot_reload: bool, | |||
| // Use UV to run nodes. | |||
| #[clap(long, action)] | |||
| uv: bool, | |||
| ) -> eyre::Result<()> { | |||
| let (dataflow, dataflow_descriptor, mut session, dataflow_id) = | |||
| start_dataflow(dataflow, name, coordinator_socket, uv)?; | |||
| } | |||
| let attach = match (attach, detach) { | |||
| (true, true) => eyre::bail!("both `--attach` and `--detach` are given"), | |||
| (true, false) => true, | |||
| (false, true) => false, | |||
| (false, false) => { | |||
| println!("attaching to dataflow (use `--detach` to run in background)"); | |||
| true | |||
| } | |||
| }; | |||
| impl Executable for Start { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| default_tracing()?; | |||
| let coordinator_socket = (self.coordinator_addr, self.coordinator_port).into(); | |||
| if attach { | |||
| let log_level = env_logger::Builder::new() | |||
| .filter_level(log::LevelFilter::Info) | |||
| .parse_default_env() | |||
| .build() | |||
| .filter(); | |||
| let (dataflow, dataflow_descriptor, mut session, dataflow_id) = | |||
| start_dataflow(self.dataflow, self.name, coordinator_socket, self.uv)?; | |||
| attach_dataflow( | |||
| dataflow_descriptor, | |||
| dataflow, | |||
| dataflow_id, | |||
| &mut *session, | |||
| hot_reload, | |||
| coordinator_socket, | |||
| log_level, | |||
| ) | |||
| } else { | |||
| let print_daemon_name = dataflow_descriptor.nodes.iter().any(|n| n.deploy.is_some()); | |||
| // wait until dataflow is started | |||
| wait_until_dataflow_started( | |||
| dataflow_id, | |||
| &mut session, | |||
| coordinator_socket, | |||
| log::LevelFilter::Info, | |||
| print_daemon_name, | |||
| ) | |||
| let attach = match (self.attach, self.detach) { | |||
| (true, true) => eyre::bail!("both `--attach` and `--detach` are given"), | |||
| (true, false) => true, | |||
| (false, true) => false, | |||
| (false, false) => { | |||
| println!("attaching to dataflow (use `--detach` to run in background)"); | |||
| true | |||
| } | |||
| }; | |||
| if attach { | |||
| let log_level = env_logger::Builder::new() | |||
| .filter_level(log::LevelFilter::Info) | |||
| .parse_default_env() | |||
| .build() | |||
| .filter(); | |||
| attach_dataflow( | |||
| dataflow_descriptor, | |||
| dataflow, | |||
| dataflow_id, | |||
| &mut *session, | |||
| self.hot_reload, | |||
| coordinator_socket, | |||
| log_level, | |||
| ) | |||
| } else { | |||
| let print_daemon_name = dataflow_descriptor.nodes.iter().any(|n| n.deploy.is_some()); | |||
| // wait until dataflow is started | |||
| wait_until_dataflow_started( | |||
| dataflow_id, | |||
| &mut session, | |||
| coordinator_socket, | |||
| log::LevelFilter::Info, | |||
| print_daemon_name, | |||
| ) | |||
| } | |||
| } | |||
| } | |||
| @@ -83,8 +114,7 @@ fn start_dataflow( | |||
| let mut session = connect_to_coordinator(coordinator_socket) | |||
| .wrap_err("failed to connect to dora coordinator")?; | |||
| let local_working_dir = | |||
| super::local_working_dir(&dataflow, &dataflow_descriptor, &mut *session)?; | |||
| let local_working_dir = local_working_dir(&dataflow, &dataflow_descriptor, &mut *session)?; | |||
| let dataflow_id = { | |||
| let dataflow = dataflow_descriptor.clone(); | |||
| @@ -0,0 +1,111 @@ | |||
| use super::{default_tracing, Executable}; | |||
| use crate::common::{connect_to_coordinator, handle_dataflow_result, query_running_dataflows}; | |||
| use communication_layer_request_reply::TcpRequestReplyConnection; | |||
| use dora_core::topics::{DORA_COORDINATOR_PORT_CONTROL_DEFAULT, LOCALHOST}; | |||
| use dora_message::cli_to_coordinator::ControlRequest; | |||
| use dora_message::coordinator_to_cli::ControlRequestReply; | |||
| use duration_str::parse; | |||
| use eyre::{bail, Context}; | |||
| use std::net::IpAddr; | |||
| use std::time::Duration; | |||
| use uuid::Uuid; | |||
| #[derive(Debug, clap::Args)] | |||
| /// Stop the given dataflow UUID. If no id is provided, you will be able to choose between the running dataflows. | |||
| pub struct Stop { | |||
| /// UUID of the dataflow that should be stopped | |||
| uuid: Option<Uuid>, | |||
| /// Name of the dataflow that should be stopped | |||
| #[clap(long)] | |||
| name: Option<String>, | |||
| /// Kill the dataflow if it doesn't stop after the given duration | |||
| #[clap(long, value_name = "DURATION")] | |||
| #[arg(value_parser = parse)] | |||
| grace_duration: Option<Duration>, | |||
| /// Address of the dora coordinator | |||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||
| coordinator_addr: IpAddr, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||
| coordinator_port: u16, | |||
| } | |||
| impl Executable for Stop { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| default_tracing()?; | |||
| let mut session = | |||
| connect_to_coordinator((self.coordinator_addr, self.coordinator_port).into()) | |||
| .wrap_err("could not connect to dora coordinator")?; | |||
| match (self.uuid, self.name) { | |||
| (Some(uuid), _) => stop_dataflow(uuid, self.grace_duration, &mut *session), | |||
| (None, Some(name)) => stop_dataflow_by_name(name, self.grace_duration, &mut *session), | |||
| (None, None) => stop_dataflow_interactive(self.grace_duration, &mut *session), | |||
| } | |||
| } | |||
| } | |||
| fn stop_dataflow_interactive( | |||
| grace_duration: Option<Duration>, | |||
| session: &mut TcpRequestReplyConnection, | |||
| ) -> eyre::Result<()> { | |||
| let list = query_running_dataflows(session).wrap_err("failed to query running dataflows")?; | |||
| let active = list.get_active(); | |||
| if active.is_empty() { | |||
| eprintln!("No dataflows are running"); | |||
| } else { | |||
| let selection = inquire::Select::new("Choose dataflow to stop:", active).prompt()?; | |||
| stop_dataflow(selection.uuid, grace_duration, session)?; | |||
| } | |||
| Ok(()) | |||
| } | |||
| fn stop_dataflow( | |||
| uuid: Uuid, | |||
| grace_duration: Option<Duration>, | |||
| session: &mut TcpRequestReplyConnection, | |||
| ) -> Result<(), eyre::ErrReport> { | |||
| let reply_raw = session | |||
| .request( | |||
| &serde_json::to_vec(&ControlRequest::Stop { | |||
| dataflow_uuid: uuid, | |||
| grace_duration, | |||
| }) | |||
| .unwrap(), | |||
| ) | |||
| .wrap_err("failed to send dataflow stop message")?; | |||
| let result: ControlRequestReply = | |||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||
| match result { | |||
| ControlRequestReply::DataflowStopped { uuid, result } => { | |||
| handle_dataflow_result(result, Some(uuid)) | |||
| } | |||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||
| other => bail!("unexpected stop dataflow reply: {other:?}"), | |||
| } | |||
| } | |||
| fn stop_dataflow_by_name( | |||
| name: String, | |||
| grace_duration: Option<Duration>, | |||
| session: &mut TcpRequestReplyConnection, | |||
| ) -> Result<(), eyre::ErrReport> { | |||
| let reply_raw = session | |||
| .request( | |||
| &serde_json::to_vec(&ControlRequest::StopByName { | |||
| name, | |||
| grace_duration, | |||
| }) | |||
| .unwrap(), | |||
| ) | |||
| .wrap_err("failed to send dataflow stop_by_name message")?; | |||
| let result: ControlRequestReply = | |||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||
| match result { | |||
| ControlRequestReply::DataflowStopped { uuid, result } => { | |||
| handle_dataflow_result(result, Some(uuid)) | |||
| } | |||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||
| other => bail!("unexpected stop dataflow reply: {other:?}"), | |||
| } | |||
| } | |||
| @@ -1,8 +1,27 @@ | |||
| use crate::{command::check::daemon_running, connect_to_coordinator, LOCALHOST}; | |||
| use super::check::daemon_running; | |||
| use super::{default_tracing, Executable}; | |||
| use crate::{common::connect_to_coordinator, LOCALHOST}; | |||
| use dora_core::topics::DORA_COORDINATOR_PORT_CONTROL_DEFAULT; | |||
| use dora_message::{cli_to_coordinator::ControlRequest, coordinator_to_cli::ControlRequestReply}; | |||
| use eyre::{bail, Context, ContextCompat}; | |||
| use std::path::PathBuf; | |||
| use std::{fs, net::SocketAddr, path::Path, process::Command, time::Duration}; | |||
| #[derive(Debug, clap::Args)] | |||
| /// Spawn coordinator and daemon in local mode (with default config) | |||
| pub struct Up { | |||
| /// Use a custom configuration | |||
| #[clap(long, hide = true, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] | |||
| config: Option<PathBuf>, | |||
| } | |||
| impl Executable for Up { | |||
| fn execute(self) -> eyre::Result<()> { | |||
| default_tracing()?; | |||
| up(self.config.as_deref()) | |||
| } | |||
| } | |||
| #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] | |||
| struct UpConfig {} | |||
| @@ -0,0 +1,117 @@ | |||
| use crate::formatting::FormatDataflowError; | |||
| use communication_layer_request_reply::{RequestReplyLayer, TcpLayer, TcpRequestReplyConnection}; | |||
| use dora_core::descriptor::{source_is_url, Descriptor}; | |||
| use dora_download::download_file; | |||
| use dora_message::{ | |||
| cli_to_coordinator::ControlRequest, | |||
| coordinator_to_cli::{ControlRequestReply, DataflowList, DataflowResult}, | |||
| }; | |||
| use eyre::{bail, Context, ContextCompat}; | |||
| use std::{ | |||
| env::current_dir, | |||
| net::SocketAddr, | |||
| path::{Path, PathBuf}, | |||
| }; | |||
| use tokio::runtime::Builder; | |||
| use uuid::Uuid; | |||
| pub(crate) fn handle_dataflow_result( | |||
| result: DataflowResult, | |||
| uuid: Option<Uuid>, | |||
| ) -> Result<(), eyre::Error> { | |||
| if result.is_ok() { | |||
| Ok(()) | |||
| } else { | |||
| Err(match uuid { | |||
| Some(uuid) => { | |||
| eyre::eyre!("Dataflow {uuid} failed:\n{}", FormatDataflowError(&result)) | |||
| } | |||
| None => { | |||
| eyre::eyre!("Dataflow failed:\n{}", FormatDataflowError(&result)) | |||
| } | |||
| }) | |||
| } | |||
| } | |||
| pub(crate) fn query_running_dataflows( | |||
| session: &mut TcpRequestReplyConnection, | |||
| ) -> eyre::Result<DataflowList> { | |||
| let reply_raw = session | |||
| .request(&serde_json::to_vec(&ControlRequest::List).unwrap()) | |||
| .wrap_err("failed to send list message")?; | |||
| let reply: ControlRequestReply = | |||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||
| let ids = match reply { | |||
| ControlRequestReply::DataflowList(list) => list, | |||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||
| other => bail!("unexpected list dataflow reply: {other:?}"), | |||
| }; | |||
| Ok(ids) | |||
| } | |||
| pub(crate) fn connect_to_coordinator( | |||
| coordinator_addr: SocketAddr, | |||
| ) -> std::io::Result<Box<TcpRequestReplyConnection>> { | |||
| TcpLayer::new().connect(coordinator_addr) | |||
| } | |||
| pub(crate) fn resolve_dataflow(dataflow: String) -> eyre::Result<PathBuf> { | |||
| let dataflow = if source_is_url(&dataflow) { | |||
| // try to download the shared library | |||
| let target_path = current_dir().context("Could not access the current dir")?; | |||
| let rt = Builder::new_current_thread() | |||
| .enable_all() | |||
| .build() | |||
| .context("tokio runtime failed")?; | |||
| rt.block_on(async { download_file(&dataflow, &target_path).await }) | |||
| .wrap_err("failed to download dataflow yaml file")? | |||
| } else { | |||
| PathBuf::from(dataflow) | |||
| }; | |||
| Ok(dataflow) | |||
| } | |||
| pub(crate) fn local_working_dir( | |||
| dataflow_path: &Path, | |||
| dataflow_descriptor: &Descriptor, | |||
| coordinator_session: &mut TcpRequestReplyConnection, | |||
| ) -> eyre::Result<Option<PathBuf>> { | |||
| Ok( | |||
| if dataflow_descriptor | |||
| .nodes | |||
| .iter() | |||
| .all(|n| n.deploy.as_ref().map(|d| d.machine.as_ref()).is_none()) | |||
| && cli_and_daemon_on_same_machine(coordinator_session)? | |||
| { | |||
| Some( | |||
| dunce::canonicalize(dataflow_path) | |||
| .context("failed to canonicalize dataflow file path")? | |||
| .parent() | |||
| .context("dataflow path has no parent dir")? | |||
| .to_owned(), | |||
| ) | |||
| } else { | |||
| None | |||
| }, | |||
| ) | |||
| } | |||
| pub(crate) fn cli_and_daemon_on_same_machine( | |||
| session: &mut TcpRequestReplyConnection, | |||
| ) -> eyre::Result<bool> { | |||
| let reply_raw = session | |||
| .request(&serde_json::to_vec(&ControlRequest::CliAndDefaultDaemonOnSameMachine).unwrap()) | |||
| .wrap_err("failed to send start dataflow message")?; | |||
| let result: ControlRequestReply = | |||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||
| match result { | |||
| ControlRequestReply::CliAndDefaultDaemonIps { | |||
| default_daemon, | |||
| cli, | |||
| } => Ok(default_daemon.is_some() && default_daemon == cli), | |||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||
| other => bail!("unexpected start dataflow reply: {other:?}"), | |||
| } | |||
| } | |||
| @@ -1,42 +1,19 @@ | |||
| use colored::Colorize; | |||
| use communication_layer_request_reply::{RequestReplyLayer, TcpLayer, TcpRequestReplyConnection}; | |||
| use dora_coordinator::Event; | |||
| use dora_core::{ | |||
| descriptor::{source_is_url, Descriptor, DescriptorExt}, | |||
| topics::{ | |||
| DORA_COORDINATOR_PORT_CONTROL_DEFAULT, DORA_COORDINATOR_PORT_DEFAULT, | |||
| DORA_DAEMON_LOCAL_LISTEN_PORT_DEFAULT, | |||
| }, | |||
| }; | |||
| use dora_daemon::{Daemon, LogDestination}; | |||
| use dora_download::download_file; | |||
| use dora_message::{ | |||
| cli_to_coordinator::ControlRequest, | |||
| coordinator_to_cli::{ControlRequestReply, DataflowList, DataflowResult, DataflowStatus}, | |||
| }; | |||
| #[cfg(feature = "tracing")] | |||
| use dora_tracing::TracingBuilder; | |||
| use duration_str::parse; | |||
| use eyre::{bail, Context}; | |||
| use formatting::FormatDataflowError; | |||
| use std::{env::current_dir, io::Write, net::SocketAddr}; | |||
| use command::Executable; | |||
| use std::{ | |||
| net::{IpAddr, Ipv4Addr}, | |||
| path::PathBuf, | |||
| time::Duration, | |||
| }; | |||
| use tabwriter::TabWriter; | |||
| use tokio::runtime::Builder; | |||
| use tracing::level_filters::LevelFilter; | |||
| use uuid::Uuid; | |||
| pub mod command; | |||
| mod command; | |||
| mod common; | |||
| mod formatting; | |||
| mod graph; | |||
| pub mod output; | |||
| pub mod session; | |||
| mod template; | |||
| pub use command::run_func; | |||
| const LOCALHOST: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); | |||
| const LISTEN_WILDCARD: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); | |||
| @@ -44,228 +21,7 @@ const LISTEN_WILDCARD: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); | |||
| #[clap(version)] | |||
| pub struct Args { | |||
| #[clap(subcommand)] | |||
| command: Command, | |||
| } | |||
| /// dora-rs cli client | |||
| #[derive(Debug, clap::Subcommand)] | |||
| enum Command { | |||
| /// Check if the coordinator and the daemon is running. | |||
| Check { | |||
| /// Path to the dataflow descriptor file (enables additional checks) | |||
| #[clap(long, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] | |||
| dataflow: Option<PathBuf>, | |||
| /// Address of the dora coordinator | |||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||
| coordinator_addr: IpAddr, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||
| coordinator_port: u16, | |||
| }, | |||
| /// Generate a visualization of the given graph using mermaid.js. Use --open to open browser. | |||
| Graph { | |||
| /// Path to the dataflow descriptor file | |||
| #[clap(value_name = "PATH", value_hint = clap::ValueHint::FilePath)] | |||
| dataflow: PathBuf, | |||
| /// Visualize the dataflow as a Mermaid diagram (instead of HTML) | |||
| #[clap(long, action)] | |||
| mermaid: bool, | |||
| /// Open the HTML visualization in the browser | |||
| #[clap(long, action)] | |||
| open: bool, | |||
| }, | |||
| /// Run build commands provided in the given dataflow. | |||
| Build { | |||
| /// Path to the dataflow descriptor file | |||
| #[clap(value_name = "PATH")] | |||
| dataflow: String, | |||
| /// Address of the dora coordinator | |||
| #[clap(long, value_name = "IP")] | |||
| coordinator_addr: Option<IpAddr>, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, value_name = "PORT")] | |||
| coordinator_port: Option<u16>, | |||
| // Use UV to build nodes. | |||
| #[clap(long, action)] | |||
| uv: bool, | |||
| // Run build on local machine | |||
| #[clap(long, action)] | |||
| local: bool, | |||
| }, | |||
| /// Generate a new project or node. Choose the language between Rust, Python, C or C++. | |||
| New { | |||
| #[clap(flatten)] | |||
| args: CommandNew, | |||
| #[clap(hide = true, long)] | |||
| internal_create_with_path_dependencies: bool, | |||
| }, | |||
| /// Run a dataflow locally. | |||
| /// | |||
| /// Directly runs the given dataflow without connecting to a dora | |||
| /// coordinator or daemon. The dataflow is executed on the local machine. | |||
| Run { | |||
| /// Path to the dataflow descriptor file | |||
| #[clap(value_name = "PATH")] | |||
| dataflow: String, | |||
| // Use UV to run nodes. | |||
| #[clap(long, action)] | |||
| uv: bool, | |||
| }, | |||
| /// Spawn coordinator and daemon in local mode (with default config) | |||
| Up { | |||
| /// Use a custom configuration | |||
| #[clap(long, hide = true, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] | |||
| config: Option<PathBuf>, | |||
| }, | |||
| /// Destroy running coordinator and daemon. If some dataflows are still running, they will be stopped first. | |||
| Destroy { | |||
| /// Use a custom configuration | |||
| #[clap(long, hide = true)] | |||
| config: Option<PathBuf>, | |||
| /// Address of the dora coordinator | |||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||
| coordinator_addr: IpAddr, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||
| coordinator_port: u16, | |||
| }, | |||
| /// Start the given dataflow path. Attach a name to the running dataflow by using --name. | |||
| Start { | |||
| /// Path to the dataflow descriptor file | |||
| #[clap(value_name = "PATH")] | |||
| dataflow: String, | |||
| /// Assign a name to the dataflow | |||
| #[clap(long)] | |||
| name: Option<String>, | |||
| /// Address of the dora coordinator | |||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||
| coordinator_addr: IpAddr, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||
| coordinator_port: u16, | |||
| /// Attach to the dataflow and wait for its completion | |||
| #[clap(long, action)] | |||
| attach: bool, | |||
| /// Run the dataflow in background | |||
| #[clap(long, action)] | |||
| detach: bool, | |||
| /// Enable hot reloading (Python only) | |||
| #[clap(long, action)] | |||
| hot_reload: bool, | |||
| // Use UV to run nodes. | |||
| #[clap(long, action)] | |||
| uv: bool, | |||
| }, | |||
| /// Stop the given dataflow UUID. If no id is provided, you will be able to choose between the running dataflows. | |||
| Stop { | |||
| /// UUID of the dataflow that should be stopped | |||
| uuid: Option<Uuid>, | |||
| /// Name of the dataflow that should be stopped | |||
| #[clap(long)] | |||
| name: Option<String>, | |||
| /// Kill the dataflow if it doesn't stop after the given duration | |||
| #[clap(long, value_name = "DURATION")] | |||
| #[arg(value_parser = parse)] | |||
| grace_duration: Option<Duration>, | |||
| /// Address of the dora coordinator | |||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||
| coordinator_addr: IpAddr, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||
| coordinator_port: u16, | |||
| }, | |||
| /// List running dataflows. | |||
| List { | |||
| /// Address of the dora coordinator | |||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||
| coordinator_addr: IpAddr, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||
| coordinator_port: u16, | |||
| }, | |||
| // Planned for future releases: | |||
| // Dashboard, | |||
| /// Show logs of a given dataflow and node. | |||
| #[command(allow_missing_positional = true)] | |||
| Logs { | |||
| /// Identifier of the dataflow | |||
| #[clap(value_name = "UUID_OR_NAME")] | |||
| dataflow: Option<String>, | |||
| /// Show logs for the given node | |||
| #[clap(value_name = "NAME")] | |||
| node: String, | |||
| /// Address of the dora coordinator | |||
| #[clap(long, value_name = "IP", default_value_t = LOCALHOST)] | |||
| coordinator_addr: IpAddr, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, value_name = "PORT", default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||
| coordinator_port: u16, | |||
| }, | |||
| // Metrics, | |||
| // Stats, | |||
| // Get, | |||
| // Upgrade, | |||
| /// Run daemon | |||
| Daemon { | |||
| /// Unique identifier for the machine (required for distributed dataflows) | |||
| #[clap(long)] | |||
| machine_id: Option<String>, | |||
| /// Local listen port for event such as dynamic node. | |||
| #[clap(long, default_value_t = DORA_DAEMON_LOCAL_LISTEN_PORT_DEFAULT)] | |||
| local_listen_port: u16, | |||
| /// Address and port number of the dora coordinator | |||
| #[clap(long, short, default_value_t = LOCALHOST)] | |||
| coordinator_addr: IpAddr, | |||
| /// Port number of the coordinator control server | |||
| #[clap(long, default_value_t = DORA_COORDINATOR_PORT_DEFAULT)] | |||
| coordinator_port: u16, | |||
| #[clap(long, hide = true)] | |||
| run_dataflow: Option<PathBuf>, | |||
| /// Suppresses all log output to stdout. | |||
| #[clap(long)] | |||
| quiet: bool, | |||
| }, | |||
| /// Run runtime | |||
| Runtime, | |||
| /// Run coordinator | |||
| Coordinator { | |||
| /// Network interface to bind to for daemon communication | |||
| #[clap(long, default_value_t = LISTEN_WILDCARD)] | |||
| interface: IpAddr, | |||
| /// Port number to bind to for daemon communication | |||
| #[clap(long, default_value_t = DORA_COORDINATOR_PORT_DEFAULT)] | |||
| port: u16, | |||
| /// Network interface to bind to for control communication | |||
| #[clap(long, default_value_t = LISTEN_WILDCARD)] | |||
| control_interface: IpAddr, | |||
| /// Port number to bind to for control communication | |||
| #[clap(long, default_value_t = DORA_COORDINATOR_PORT_CONTROL_DEFAULT)] | |||
| control_port: u16, | |||
| /// Suppresses all log output to stdout. | |||
| #[clap(long)] | |||
| quiet: bool, | |||
| }, | |||
| /// Dora CLI self-management commands | |||
| Self_ { | |||
| #[clap(subcommand)] | |||
| command: SelfSubCommand, | |||
| }, | |||
| } | |||
| #[derive(Debug, clap::Subcommand)] | |||
| enum SelfSubCommand { | |||
| /// Check for updates or update the CLI | |||
| Update { | |||
| /// Only check for updates without installing | |||
| #[clap(long)] | |||
| check_only: bool, | |||
| }, | |||
| /// Remove The Dora CLI from the system | |||
| Uninstall { | |||
| /// Force uninstallation without confirmation | |||
| #[clap(long)] | |||
| force: bool, | |||
| }, | |||
| command: command::Command, | |||
| } | |||
| #[derive(Debug, clap::Args)] | |||
| @@ -298,503 +54,13 @@ enum Lang { | |||
| } | |||
| pub fn lib_main(args: Args) { | |||
| if let Err(err) = run_cli(args) { | |||
| if let Err(err) = args.command.execute() { | |||
| eprintln!("\n\n{}", "[ERROR]".bold().red()); | |||
| eprintln!("{err:?}"); | |||
| std::process::exit(1); | |||
| } | |||
| } | |||
| fn run_cli(args: Args) -> eyre::Result<()> { | |||
| tracing_log::LogTracer::init()?; | |||
| #[cfg(feature = "tracing")] | |||
| match &args.command { | |||
| Command::Daemon { | |||
| quiet, machine_id, .. | |||
| } => { | |||
| let name = "dora-daemon"; | |||
| let filename = machine_id | |||
| .as_ref() | |||
| .map(|id| format!("{name}-{id}")) | |||
| .unwrap_or(name.to_string()); | |||
| let mut builder = TracingBuilder::new(name); | |||
| if !quiet { | |||
| builder = builder.with_stdout("info,zenoh=warn"); | |||
| } | |||
| builder = builder.with_file(filename, LevelFilter::INFO)?; | |||
| builder | |||
| .build() | |||
| .wrap_err("failed to set up tracing subscriber")?; | |||
| } | |||
| Command::Runtime => { | |||
| // Do not set the runtime in the cli. | |||
| } | |||
| Command::Coordinator { quiet, .. } => { | |||
| let name = "dora-coordinator"; | |||
| let mut builder = TracingBuilder::new(name); | |||
| if !quiet { | |||
| builder = builder.with_stdout("info"); | |||
| } | |||
| builder = builder.with_file(name, LevelFilter::INFO)?; | |||
| builder | |||
| .build() | |||
| .wrap_err("failed to set up tracing subscriber")?; | |||
| } | |||
| Command::Run { .. } | Command::Build { .. } => { | |||
| let log_level = std::env::var("RUST_LOG").ok().unwrap_or("info".to_string()); | |||
| TracingBuilder::new("run") | |||
| .with_stdout(log_level) | |||
| .build() | |||
| .wrap_err("failed to set up tracing subscriber")?; | |||
| } | |||
| _ => { | |||
| TracingBuilder::new("dora-cli") | |||
| .with_stdout("warn") | |||
| .build() | |||
| .wrap_err("failed to set up tracing subscriber")?; | |||
| } | |||
| }; | |||
| match args.command { | |||
| Command::Check { | |||
| dataflow, | |||
| coordinator_addr, | |||
| coordinator_port, | |||
| } => match dataflow { | |||
| Some(dataflow) => { | |||
| let working_dir = dataflow | |||
| .canonicalize() | |||
| .context("failed to canonicalize dataflow path")? | |||
| .parent() | |||
| .ok_or_else(|| eyre::eyre!("dataflow path has no parent dir"))? | |||
| .to_owned(); | |||
| Descriptor::blocking_read(&dataflow)?.check(&working_dir)?; | |||
| command::check::check_environment((coordinator_addr, coordinator_port).into())? | |||
| } | |||
| None => command::check::check_environment((coordinator_addr, coordinator_port).into())?, | |||
| }, | |||
| Command::Graph { | |||
| dataflow, | |||
| mermaid, | |||
| open, | |||
| } => { | |||
| graph::create(dataflow, mermaid, open)?; | |||
| } | |||
| Command::Build { | |||
| dataflow, | |||
| coordinator_addr, | |||
| coordinator_port, | |||
| uv, | |||
| local, | |||
| } => command::build(dataflow, coordinator_addr, coordinator_port, uv, local)?, | |||
| Command::New { | |||
| args, | |||
| internal_create_with_path_dependencies, | |||
| } => template::create(args, internal_create_with_path_dependencies)?, | |||
| Command::Run { dataflow, uv } => command::run(dataflow, uv)?, | |||
| Command::Up { config } => { | |||
| command::up::up(config.as_deref())?; | |||
| } | |||
| Command::Logs { | |||
| dataflow, | |||
| node, | |||
| coordinator_addr, | |||
| coordinator_port, | |||
| } => { | |||
| let mut session = connect_to_coordinator((coordinator_addr, coordinator_port).into()) | |||
| .wrap_err("failed to connect to dora coordinator")?; | |||
| let list = query_running_dataflows(&mut *session) | |||
| .wrap_err("failed to query running dataflows")?; | |||
| if let Some(dataflow) = dataflow { | |||
| let uuid = Uuid::parse_str(&dataflow).ok(); | |||
| let name = if uuid.is_some() { None } else { Some(dataflow) }; | |||
| command::logs(&mut *session, uuid, name, node)? | |||
| } else { | |||
| let active: Vec<dora_message::coordinator_to_cli::DataflowIdAndName> = | |||
| list.get_active(); | |||
| let uuid = match &active[..] { | |||
| [] => bail!("No dataflows are running"), | |||
| [uuid] => uuid.clone(), | |||
| _ => inquire::Select::new("Choose dataflow to show logs:", active).prompt()?, | |||
| }; | |||
| command::logs(&mut *session, Some(uuid.uuid), None, node)? | |||
| } | |||
| } | |||
| Command::Start { | |||
| dataflow, | |||
| name, | |||
| coordinator_addr, | |||
| coordinator_port, | |||
| attach, | |||
| detach, | |||
| hot_reload, | |||
| uv, | |||
| } => { | |||
| let coordinator_socket = (coordinator_addr, coordinator_port).into(); | |||
| command::start( | |||
| dataflow, | |||
| name, | |||
| coordinator_socket, | |||
| attach, | |||
| detach, | |||
| hot_reload, | |||
| uv, | |||
| )? | |||
| } | |||
| Command::List { | |||
| coordinator_addr, | |||
| coordinator_port, | |||
| } => match connect_to_coordinator((coordinator_addr, coordinator_port).into()) { | |||
| Ok(mut session) => list(&mut *session)?, | |||
| Err(_) => { | |||
| bail!("No dora coordinator seems to be running."); | |||
| } | |||
| }, | |||
| Command::Stop { | |||
| uuid, | |||
| name, | |||
| grace_duration, | |||
| coordinator_addr, | |||
| coordinator_port, | |||
| } => { | |||
| let mut session = connect_to_coordinator((coordinator_addr, coordinator_port).into()) | |||
| .wrap_err("could not connect to dora coordinator")?; | |||
| match (uuid, name) { | |||
| (Some(uuid), _) => stop_dataflow(uuid, grace_duration, &mut *session)?, | |||
| (None, Some(name)) => stop_dataflow_by_name(name, grace_duration, &mut *session)?, | |||
| (None, None) => stop_dataflow_interactive(grace_duration, &mut *session)?, | |||
| } | |||
| } | |||
| Command::Destroy { | |||
| config, | |||
| coordinator_addr, | |||
| coordinator_port, | |||
| } => command::up::destroy( | |||
| config.as_deref(), | |||
| (coordinator_addr, coordinator_port).into(), | |||
| )?, | |||
| Command::Coordinator { | |||
| interface, | |||
| port, | |||
| control_interface, | |||
| control_port, | |||
| quiet, | |||
| } => { | |||
| let rt = Builder::new_multi_thread() | |||
| .enable_all() | |||
| .build() | |||
| .context("tokio runtime failed")?; | |||
| rt.block_on(async { | |||
| let bind = SocketAddr::new(interface, port); | |||
| let bind_control = SocketAddr::new(control_interface, control_port); | |||
| let (port, task) = | |||
| dora_coordinator::start(bind, bind_control, futures::stream::empty::<Event>()) | |||
| .await?; | |||
| if !quiet { | |||
| println!("Listening for incoming daemon connection on {port}"); | |||
| } | |||
| task.await | |||
| }) | |||
| .context("failed to run dora-coordinator")? | |||
| } | |||
| Command::Daemon { | |||
| coordinator_addr, | |||
| coordinator_port, | |||
| local_listen_port, | |||
| machine_id, | |||
| run_dataflow, | |||
| quiet: _, | |||
| } => { | |||
| let rt = Builder::new_multi_thread() | |||
| .enable_all() | |||
| .build() | |||
| .context("tokio runtime failed")?; | |||
| rt.block_on(async { | |||
| match run_dataflow { | |||
| Some(dataflow_path) => { | |||
| tracing::info!("Starting dataflow `{}`", dataflow_path.display()); | |||
| if coordinator_addr != LOCALHOST { | |||
| tracing::info!( | |||
| "Not using coordinator addr {} as `run_dataflow` is for local dataflow only. Please use the `start` command for remote coordinator", | |||
| coordinator_addr | |||
| ); | |||
| } | |||
| let dataflow_session = | |||
| DataflowSession::read_session(&dataflow_path).context("failed to read DataflowSession")?; | |||
| let result = Daemon::run_dataflow(&dataflow_path, | |||
| dataflow_session.build_id, dataflow_session.local_build, dataflow_session.session_id, false, | |||
| LogDestination::Tracing, | |||
| ).await?; | |||
| handle_dataflow_result(result, None) | |||
| } | |||
| None => { | |||
| Daemon::run(SocketAddr::new(coordinator_addr, coordinator_port), machine_id, local_listen_port).await | |||
| } | |||
| } | |||
| }) | |||
| .context("failed to run dora-daemon")? | |||
| } | |||
| Command::Runtime => dora_runtime::main().context("Failed to run dora-runtime")?, | |||
| Command::Self_ { command } => match command { | |||
| SelfSubCommand::Update { check_only } => { | |||
| println!("Checking for updates..."); | |||
| #[cfg(target_os = "linux")] | |||
| let bin_path_in_archive = format!("dora-cli-{}/dora", env!("TARGET")); | |||
| #[cfg(target_os = "macos")] | |||
| let bin_path_in_archive = format!("dora-cli-{}/dora", env!("TARGET")); | |||
| #[cfg(target_os = "windows")] | |||
| let bin_path_in_archive = String::from("dora.exe"); | |||
| let status = self_update::backends::github::Update::configure() | |||
| .repo_owner("dora-rs") | |||
| .repo_name("dora") | |||
| .bin_path_in_archive(&bin_path_in_archive) | |||
| .bin_name("dora") | |||
| .show_download_progress(true) | |||
| .current_version(env!("CARGO_PKG_VERSION")) | |||
| .build()?; | |||
| if check_only { | |||
| // Only check if an update is available | |||
| match status.get_latest_release() { | |||
| Ok(release) => { | |||
| let current_version = self_update::cargo_crate_version!(); | |||
| if current_version != release.version { | |||
| println!( | |||
| "An update is available: {}. Run 'dora self update' to update", | |||
| release.version | |||
| ); | |||
| } else { | |||
| println!( | |||
| "Dora CLI is already at the latest version: {}", | |||
| current_version | |||
| ); | |||
| } | |||
| } | |||
| Err(e) => println!("Failed to check for updates: {}", e), | |||
| } | |||
| } else { | |||
| // Perform the actual update | |||
| match status.update() { | |||
| Ok(update_status) => match update_status { | |||
| self_update::Status::UpToDate(version) => { | |||
| println!("Dora CLI is already at the latest version: {}", version); | |||
| } | |||
| self_update::Status::Updated(version) => { | |||
| println!("Successfully updated Dora CLI to version: {}", version); | |||
| } | |||
| }, | |||
| Err(e) => println!("Failed to update: {}", e), | |||
| } | |||
| } | |||
| } | |||
| SelfSubCommand::Uninstall { force } => { | |||
| if !force { | |||
| let confirmed = | |||
| inquire::Confirm::new("Are you sure you want to uninstall Dora CLI?") | |||
| .with_default(false) | |||
| .prompt() | |||
| .wrap_err("Uninstallation cancelled")?; | |||
| if !confirmed { | |||
| println!("Uninstallation cancelled"); | |||
| return Ok(()); | |||
| } | |||
| } | |||
| println!("Uninstalling Dora CLI..."); | |||
| #[cfg(feature = "python")] | |||
| { | |||
| println!("Detected Python installation..."); | |||
| // Try uv pip uninstall first | |||
| let uv_status = std::process::Command::new("uv") | |||
| .args(["pip", "uninstall", "dora-rs-cli"]) | |||
| .status(); | |||
| if let Ok(status) = uv_status { | |||
| if status.success() { | |||
| println!("Dora CLI has been successfully uninstalled via uv pip."); | |||
| return Ok(()); | |||
| } | |||
| } | |||
| // Fall back to regular pip uninstall | |||
| println!("Trying with pip..."); | |||
| let status = std::process::Command::new("pip") | |||
| .args(["uninstall", "-y", "dora-rs-cli"]) | |||
| .status() | |||
| .wrap_err("Failed to run pip uninstall")?; | |||
| if status.success() { | |||
| println!("Dora CLI has been successfully uninstalled via pip."); | |||
| } else { | |||
| bail!("Failed to uninstall Dora CLI via pip."); | |||
| } | |||
| } | |||
| #[cfg(not(feature = "python"))] | |||
| { | |||
| match self_replace::self_delete() { | |||
| Ok(_) => { | |||
| println!("Dora CLI has been successfully uninstalled."); | |||
| } | |||
| Err(e) => { | |||
| bail!("Failed to uninstall Dora CLI: {}", e); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| }, | |||
| }; | |||
| Ok(()) | |||
| } | |||
| fn stop_dataflow_interactive( | |||
| grace_duration: Option<Duration>, | |||
| session: &mut TcpRequestReplyConnection, | |||
| ) -> eyre::Result<()> { | |||
| let list = query_running_dataflows(session).wrap_err("failed to query running dataflows")?; | |||
| let active = list.get_active(); | |||
| if active.is_empty() { | |||
| eprintln!("No dataflows are running"); | |||
| } else { | |||
| let selection = inquire::Select::new("Choose dataflow to stop:", active).prompt()?; | |||
| stop_dataflow(selection.uuid, grace_duration, session)?; | |||
| } | |||
| Ok(()) | |||
| } | |||
| fn stop_dataflow( | |||
| uuid: Uuid, | |||
| grace_duration: Option<Duration>, | |||
| session: &mut TcpRequestReplyConnection, | |||
| ) -> Result<(), eyre::ErrReport> { | |||
| let reply_raw = session | |||
| .request( | |||
| &serde_json::to_vec(&ControlRequest::Stop { | |||
| dataflow_uuid: uuid, | |||
| grace_duration, | |||
| }) | |||
| .unwrap(), | |||
| ) | |||
| .wrap_err("failed to send dataflow stop message")?; | |||
| let result: ControlRequestReply = | |||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||
| match result { | |||
| ControlRequestReply::DataflowStopped { uuid, result } => { | |||
| handle_dataflow_result(result, Some(uuid)) | |||
| } | |||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||
| other => bail!("unexpected stop dataflow reply: {other:?}"), | |||
| } | |||
| } | |||
| fn handle_dataflow_result(result: DataflowResult, uuid: Option<Uuid>) -> Result<(), eyre::Error> { | |||
| if result.is_ok() { | |||
| Ok(()) | |||
| } else { | |||
| Err(match uuid { | |||
| Some(uuid) => { | |||
| eyre::eyre!("Dataflow {uuid} failed:\n{}", FormatDataflowError(&result)) | |||
| } | |||
| None => { | |||
| eyre::eyre!("Dataflow failed:\n{}", FormatDataflowError(&result)) | |||
| } | |||
| }) | |||
| } | |||
| } | |||
| fn stop_dataflow_by_name( | |||
| name: String, | |||
| grace_duration: Option<Duration>, | |||
| session: &mut TcpRequestReplyConnection, | |||
| ) -> Result<(), eyre::ErrReport> { | |||
| let reply_raw = session | |||
| .request( | |||
| &serde_json::to_vec(&ControlRequest::StopByName { | |||
| name, | |||
| grace_duration, | |||
| }) | |||
| .unwrap(), | |||
| ) | |||
| .wrap_err("failed to send dataflow stop_by_name message")?; | |||
| let result: ControlRequestReply = | |||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||
| match result { | |||
| ControlRequestReply::DataflowStopped { uuid, result } => { | |||
| handle_dataflow_result(result, Some(uuid)) | |||
| } | |||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||
| other => bail!("unexpected stop dataflow reply: {other:?}"), | |||
| } | |||
| } | |||
| fn list(session: &mut TcpRequestReplyConnection) -> Result<(), eyre::ErrReport> { | |||
| let list = query_running_dataflows(session)?; | |||
| let mut tw = TabWriter::new(vec![]); | |||
| tw.write_all(b"UUID\tName\tStatus\n")?; | |||
| for entry in list.0 { | |||
| let uuid = entry.id.uuid; | |||
| let name = entry.id.name.unwrap_or_default(); | |||
| let status = match entry.status { | |||
| DataflowStatus::Running => "Running", | |||
| DataflowStatus::Finished => "Succeeded", | |||
| DataflowStatus::Failed => "Failed", | |||
| }; | |||
| tw.write_all(format!("{uuid}\t{name}\t{status}\n").as_bytes())?; | |||
| } | |||
| tw.flush()?; | |||
| let formatted = String::from_utf8(tw.into_inner()?)?; | |||
| println!("{formatted}"); | |||
| Ok(()) | |||
| } | |||
| fn query_running_dataflows(session: &mut TcpRequestReplyConnection) -> eyre::Result<DataflowList> { | |||
| let reply_raw = session | |||
| .request(&serde_json::to_vec(&ControlRequest::List).unwrap()) | |||
| .wrap_err("failed to send list message")?; | |||
| let reply: ControlRequestReply = | |||
| serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?; | |||
| let ids = match reply { | |||
| ControlRequestReply::DataflowList(list) => list, | |||
| ControlRequestReply::Error(err) => bail!("{err}"), | |||
| other => bail!("unexpected list dataflow reply: {other:?}"), | |||
| }; | |||
| Ok(ids) | |||
| } | |||
| fn connect_to_coordinator( | |||
| coordinator_addr: SocketAddr, | |||
| ) -> std::io::Result<Box<TcpRequestReplyConnection>> { | |||
| TcpLayer::new().connect(coordinator_addr) | |||
| } | |||
| fn resolve_dataflow(dataflow: String) -> eyre::Result<PathBuf> { | |||
| let dataflow = if source_is_url(&dataflow) { | |||
| // try to download the shared library | |||
| let target_path = current_dir().context("Could not access the current dir")?; | |||
| let rt = Builder::new_current_thread() | |||
| .enable_all() | |||
| .build() | |||
| .context("tokio runtime failed")?; | |||
| rt.block_on(async { download_file(&dataflow, &target_path).await }) | |||
| .wrap_err("failed to download dataflow yaml file")? | |||
| } else { | |||
| PathBuf::from(dataflow) | |||
| }; | |||
| Ok(dataflow) | |||
| } | |||
| #[cfg(feature = "python")] | |||
| use clap::Parser; | |||
| #[cfg(feature = "python")] | |||
| @@ -804,8 +70,6 @@ use pyo3::{ | |||
| wrap_pyfunction, Bound, PyResult, Python, | |||
| }; | |||
| use crate::session::DataflowSession; | |||
| #[cfg(feature = "python")] | |||
| #[pyfunction] | |||
| fn py_main(_py: Python) -> PyResult<()> { | |||