From 267583c2a9ce271e102399cfd098a7f0be49f146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Mon, 28 Apr 2025 09:48:55 +0200 Subject: [PATCH] Draft auto-update process for `comet` in `devtools` --- Cargo.lock | 1 + debug/src/lib.rs | 30 ++---- devtools/Cargo.toml | 2 + devtools/src/comet.rs | 108 ++++++++++++++++++++++ devtools/src/executor.rs | 24 +++++ devtools/src/lib.rs | 192 ++++++++++++++++++++++----------------- 6 files changed, 250 insertions(+), 107 deletions(-) create mode 100644 devtools/src/comet.rs diff --git a/Cargo.lock b/Cargo.lock index 5318d98d..920dcf1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2464,6 +2464,7 @@ dependencies = [ "iced_debug", "iced_program", "iced_widget", + "log", ] [[package]] diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 5329c1a7..83a424d2 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -7,8 +7,6 @@ use crate::futures::Subscription; pub use internal::Span; -use std::io; - #[derive(Debug, Clone, Copy)] pub enum Primitive { Quad, @@ -36,8 +34,8 @@ pub fn init(name: &str) { internal::init(name); } -pub fn toggle_comet() -> Result<(), io::Error> { - internal::toggle_comet() +pub fn quit() -> bool { + internal::quit() } pub fn theme_changed(f: impl FnOnce() -> Option) { @@ -118,8 +116,6 @@ mod internal { use beacon::client::{self, Client}; use beacon::span; - use std::io; - use std::process; use std::sync::atomic::{self, AtomicBool, AtomicUsize}; use std::sync::{LazyLock, RwLock}; @@ -129,25 +125,13 @@ mod internal { name.clone_into(&mut NAME.write().expect("Write application name")); } - pub fn toggle_comet() -> Result<(), io::Error> { + pub fn quit() -> bool { if BEACON.is_connected() { BEACON.quit(); - Ok(()) + true } else { - let _ = process::Command::new("iced_comet") - .stdin(process::Stdio::null()) - .stdout(process::Stdio::null()) - .stderr(process::Stdio::null()) - .spawn()?; - - if let Some(palette) = - LAST_PALETTE.read().expect("Read last palette").as_ref() - { - log(client::Event::ThemeChanged(*palette)); - } - - Ok(()) + false } } @@ -323,8 +307,8 @@ mod internal { pub fn init(_name: &str) {} - pub fn toggle_comet() -> Result<(), io::Error> { - Ok(()) + pub fn quit() -> bool { + false } pub fn theme_changed(_f: impl FnOnce() -> Option) {} diff --git a/devtools/Cargo.toml b/devtools/Cargo.toml index 44fa9fb4..04c3792b 100644 --- a/devtools/Cargo.toml +++ b/devtools/Cargo.toml @@ -20,3 +20,5 @@ time-travel = ["iced_program/time-travel"] iced_debug.workspace = true iced_program.workspace = true iced_widget.workspace = true + +log.workspace = true diff --git a/devtools/src/comet.rs b/devtools/src/comet.rs new file mode 100644 index 00000000..941e4f61 --- /dev/null +++ b/devtools/src/comet.rs @@ -0,0 +1,108 @@ +use crate::executor; +use crate::runtime::Task; + +use std::io; +use std::process; +use std::sync::Arc; + +pub const COMPATIBLE_REVISION: &str = + "69dd2283886dccdaa1ee6e1c274af62f7250bc38"; + +pub fn launch() -> Task> { + executor::try_spawn_blocking(|mut sender| { + let cargo_install = process::Command::new("cargo") + .args(["install", "--list"]) + .output()?; + + let installed_packages = String::from_utf8_lossy(&cargo_install.stdout); + + for line in installed_packages.lines() { + if !line.starts_with("iced_comet ") { + continue; + } + + let Some((_, revision)) = line.rsplit_once("?rev=") else { + return Err(Error::Outdated { revision: None }); + }; + + let Some((revision, _)) = revision.rsplit_once("#") else { + return Err(Error::Outdated { revision: None }); + }; + + if revision != COMPATIBLE_REVISION { + return Err(Error::Outdated { + revision: Some(revision.to_owned()), + }); + } + + let _ = process::Command::new("iced_comet") + .stdin(process::Stdio::null()) + .stdout(process::Stdio::null()) + .stderr(process::Stdio::null()) + .spawn()?; + + let _ = sender.try_send(()); + return Ok(()); + } + + Err(Error::NotFound) + }) +} + +pub fn install() -> Task> { + executor::try_spawn_blocking(|mut sender| { + use std::io::{BufRead, BufReader}; + use std::process::{Command, Stdio}; + + let install = Command::new("cargo") + .args([ + "install", + "--locked", + "--git", + "https://github.com/iced-rs/comet.git", + "--rev", + COMPATIBLE_REVISION, + ]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn()?; + + let mut stderr = + BufReader::new(install.stderr.expect("stderr must be piped")); + + let mut log = String::new(); + + while let Ok(n) = stderr.read_line(&mut log) { + if n == 0 { + break; + } + + let _ = sender.try_send(Installation::Logged(log.clone())); + log.clear(); + } + + let _ = sender.try_send(Installation::Finished); + + Ok(()) + }) +} + +#[derive(Debug, Clone)] +pub enum Installation { + Logged(String), + Finished, +} + +#[derive(Debug, Clone)] +pub enum Error { + NotFound, + Outdated { revision: Option }, + IoFailed(Arc), +} + +impl From for Error { + fn from(error: io::Error) -> Self { + Self::IoFailed(Arc::new(error)) + } +} diff --git a/devtools/src/executor.rs b/devtools/src/executor.rs index 5d7d5397..1e7317a2 100644 --- a/devtools/src/executor.rs +++ b/devtools/src/executor.rs @@ -1,4 +1,6 @@ use crate::futures::futures::channel::mpsc; +use crate::futures::futures::channel::oneshot; +use crate::futures::futures::stream::{self, StreamExt}; use crate::runtime::Task; use std::thread; @@ -17,3 +19,25 @@ where Task::stream(receiver) } + +pub fn try_spawn_blocking( + f: impl FnOnce(mpsc::Sender) -> Result<(), E> + Send + 'static, +) -> Task> +where + T: Send + 'static, + E: Send + 'static, +{ + let (sender, receiver) = mpsc::channel(1); + let (error_sender, error_receiver) = oneshot::channel(); + + let _ = thread::spawn(move || { + if let Err(error) = f(sender) { + let _ = error_sender.send(Err(error)); + } + }); + + Task::stream(stream::select( + receiver.map(Ok), + stream::once(error_receiver).filter_map(async |result| result.ok()), + )) +} diff --git a/devtools/src/lib.rs b/devtools/src/lib.rs index 1126bcf9..18c118bf 100644 --- a/devtools/src/lib.rs +++ b/devtools/src/lib.rs @@ -6,6 +6,7 @@ use iced_widget::core; use iced_widget::runtime; use iced_widget::runtime::futures; +mod comet; mod executor; mod time_machine; @@ -24,7 +25,6 @@ use crate::widget::{ }; use std::fmt; -use std::io; use std::thread; pub fn attach(program: impl Program + 'static) -> impl Program { @@ -124,9 +124,9 @@ where enum Message { HideNotification, ToggleComet, + CometLaunched(Result<(), comet::Error>), InstallComet, - InstallationLogged(String), - InstallationFinished, + InstallationProgressed(Result), CancelSetup, } @@ -136,10 +136,15 @@ enum Mode { } enum Setup { - Idle, + Idle { goal: Goal }, Running { logs: Vec }, } +enum Goal { + Installation, + Update { revision: Option }, +} + impl

DevTools

where P: Program + 'static, @@ -174,12 +179,34 @@ where } Message::ToggleComet => { if let Mode::Setup(setup) = &self.mode { - if matches!(setup, Setup::Idle) { + if matches!(setup, Setup::Idle { .. }) { self.mode = Mode::None; } - } else if let Err(error) = debug::toggle_comet() { - if error.kind() == io::ErrorKind::NotFound { - self.mode = Mode::Setup(Setup::Idle); + + Task::none() + } else if debug::quit() { + Task::none() + } else { + comet::launch() + .map(Message::CometLaunched) + .map(Event::Message) + } + } + Message::CometLaunched(Ok(())) => Task::none(), + Message::CometLaunched(Err(error)) => { + match error { + comet::Error::NotFound => { + self.mode = Mode::Setup(Setup::Idle { + goal: Goal::Installation, + }); + } + comet::Error::Outdated { revision } => { + self.mode = Mode::Setup(Setup::Idle { + goal: Goal::Update { revision }, + }); + } + comet::Error::IoFailed(error) => { + log::error!("comet failed to run: {error}"); } } @@ -189,62 +216,30 @@ where self.mode = Mode::Setup(Setup::Running { logs: Vec::new() }); - executor::spawn_blocking(|mut sender| { - use std::io::{BufRead, BufReader}; - use std::process::{Command, Stdio}; + comet::install() + .map(Message::InstallationProgressed) + .map(Event::Message) + } - let Ok(install) = Command::new("cargo") - .args([ - "install", - "--locked", - "--git", - "https://github.com/iced-rs/comet.git", - "--rev", - "eb114ba564a872acbd95e337d13e55f5f667b2f3", - ]) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::piped()) - .spawn() - else { - return; - }; + Message::InstallationProgressed(Ok(installation)) => { + let Mode::Setup(Setup::Running { logs }) = &mut self.mode + else { + return Task::none(); + }; - let mut stderr = BufReader::new( - install.stderr.expect("stderr must be piped"), - ); - - let mut log = String::new(); - - while let Ok(n) = stderr.read_line(&mut log) { - if n == 0 { - break; - } - - let _ = sender.try_send( - Message::InstallationLogged(log.clone()), - ); - - log.clear(); + match installation { + comet::Installation::Logged(log) => { + logs.push(log); + Task::none() + } + comet::Installation::Finished => { + self.mode = Mode::None; + comet::launch().discard() } - - let _ = sender.try_send(Message::InstallationFinished); - }) - .map(Event::Message) - } - Message::InstallationLogged(log) => { - if let Mode::Setup(Setup::Running { logs }) = &mut self.mode - { - logs.push(log); } - - Task::none() } - Message::InstallationFinished => { - self.mode = Mode::None; - - let _ = debug::toggle_comet(); - + Message::InstallationProgressed(_error) => { + // TODO Task::none() } Message::CancelSetup => { @@ -317,40 +312,69 @@ where Mode::None => None, Mode::Setup(setup) => { let stage: Element<'_, _, Theme, P::Renderer> = match setup { - Setup::Idle => { + Setup::Idle { goal } => { let controls = row![ button(text("Cancel").center().width(Fill)) .width(100) .on_press(Message::CancelSetup) .style(button::danger), horizontal_space(), - button(text("Install").center().width(Fill)) - .width(100) - .on_press(Message::InstallComet) - .style(button::success), + button( + text(match goal { + Goal::Installation => "Install", + Goal::Update { .. } => "Update", + }) + .center() + .width(Fill) + ) + .width(100) + .on_press(Message::InstallComet) + .style(button::success), ]; - column![ - text("comet is not installed!").size(20), - "In order to display performance metrics, the \ - comet debugger must be installed in your system.", - "The comet debugger is an official companion tool \ - that helps you debug your iced applications.", - "Do you wish to install it with the following \ - command?", - container( - text( - "cargo install --locked \ - --git https://github.com/iced-rs/comet.git" - ) - .size(14) + let command = container( + text( + "cargo install --locked \ + --git https://github.com/iced-rs/comet.git", ) - .width(Fill) - .padding(5) - .style(container::dark), - controls, - ] - .spacing(20) + .size(14), + ) + .width(Fill) + .padding(5) + .style(container::dark); + + match goal { + Goal::Installation => column![ + text("comet is not installed!").size(20), + "In order to display performance \ + metrics, the comet debugger must \ + be installed in your system.", + "The comet debugger is an official \ + companion tool that helps you debug \ + your iced applications.", + "Do you wish to install it with the \ + following command?", + command, + controls, + ] + .spacing(20), + Goal::Update { revision } => column![ + text("comet is out of date!").size(20), + text!( + "The installed revision is \"{current}\", \ + but the latest compatible is \"{compatible}\".", + current = revision + .as_deref() + .unwrap_or("Unknown"), + compatible = comet::COMPATIBLE_REVISION, + ), + "Do you wish to update it with the following \ + command?", + command, + controls, + ] + .spacing(20), + } .into() } Setup::Running { logs } => column![