Draft auto-update process for comet in devtools

This commit is contained in:
Héctor Ramón Jiménez 2025-04-28 09:48:55 +02:00
parent a105ad4f9f
commit 267583c2a9
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
6 changed files with 250 additions and 107 deletions

1
Cargo.lock generated
View file

@ -2464,6 +2464,7 @@ dependencies = [
"iced_debug", "iced_debug",
"iced_program", "iced_program",
"iced_widget", "iced_widget",
"log",
] ]
[[package]] [[package]]

View file

@ -7,8 +7,6 @@ use crate::futures::Subscription;
pub use internal::Span; pub use internal::Span;
use std::io;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum Primitive { pub enum Primitive {
Quad, Quad,
@ -36,8 +34,8 @@ pub fn init(name: &str) {
internal::init(name); internal::init(name);
} }
pub fn toggle_comet() -> Result<(), io::Error> { pub fn quit() -> bool {
internal::toggle_comet() internal::quit()
} }
pub fn theme_changed(f: impl FnOnce() -> Option<theme::Palette>) { pub fn theme_changed(f: impl FnOnce() -> Option<theme::Palette>) {
@ -118,8 +116,6 @@ mod internal {
use beacon::client::{self, Client}; use beacon::client::{self, Client};
use beacon::span; use beacon::span;
use std::io;
use std::process;
use std::sync::atomic::{self, AtomicBool, AtomicUsize}; use std::sync::atomic::{self, AtomicBool, AtomicUsize};
use std::sync::{LazyLock, RwLock}; use std::sync::{LazyLock, RwLock};
@ -129,25 +125,13 @@ mod internal {
name.clone_into(&mut NAME.write().expect("Write application name")); 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() { if BEACON.is_connected() {
BEACON.quit(); BEACON.quit();
Ok(()) true
} else { } else {
let _ = process::Command::new("iced_comet") false
.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(())
} }
} }
@ -323,8 +307,8 @@ mod internal {
pub fn init(_name: &str) {} pub fn init(_name: &str) {}
pub fn toggle_comet() -> Result<(), io::Error> { pub fn quit() -> bool {
Ok(()) false
} }
pub fn theme_changed(_f: impl FnOnce() -> Option<theme::Palette>) {} pub fn theme_changed(_f: impl FnOnce() -> Option<theme::Palette>) {}

View file

@ -20,3 +20,5 @@ time-travel = ["iced_program/time-travel"]
iced_debug.workspace = true iced_debug.workspace = true
iced_program.workspace = true iced_program.workspace = true
iced_widget.workspace = true iced_widget.workspace = true
log.workspace = true

108
devtools/src/comet.rs Normal file
View file

@ -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<Result<(), Error>> {
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<Result<Installation, Error>> {
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<String> },
IoFailed(Arc<io::Error>),
}
impl From<io::Error> for Error {
fn from(error: io::Error) -> Self {
Self::IoFailed(Arc::new(error))
}
}

View file

@ -1,4 +1,6 @@
use crate::futures::futures::channel::mpsc; use crate::futures::futures::channel::mpsc;
use crate::futures::futures::channel::oneshot;
use crate::futures::futures::stream::{self, StreamExt};
use crate::runtime::Task; use crate::runtime::Task;
use std::thread; use std::thread;
@ -17,3 +19,25 @@ where
Task::stream(receiver) Task::stream(receiver)
} }
pub fn try_spawn_blocking<T, E>(
f: impl FnOnce(mpsc::Sender<T>) -> Result<(), E> + Send + 'static,
) -> Task<Result<T, E>>
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()),
))
}

View file

@ -6,6 +6,7 @@ use iced_widget::core;
use iced_widget::runtime; use iced_widget::runtime;
use iced_widget::runtime::futures; use iced_widget::runtime::futures;
mod comet;
mod executor; mod executor;
mod time_machine; mod time_machine;
@ -24,7 +25,6 @@ use crate::widget::{
}; };
use std::fmt; use std::fmt;
use std::io;
use std::thread; use std::thread;
pub fn attach(program: impl Program + 'static) -> impl Program { pub fn attach(program: impl Program + 'static) -> impl Program {
@ -124,9 +124,9 @@ where
enum Message { enum Message {
HideNotification, HideNotification,
ToggleComet, ToggleComet,
CometLaunched(Result<(), comet::Error>),
InstallComet, InstallComet,
InstallationLogged(String), InstallationProgressed(Result<comet::Installation, comet::Error>),
InstallationFinished,
CancelSetup, CancelSetup,
} }
@ -136,10 +136,15 @@ enum Mode {
} }
enum Setup { enum Setup {
Idle, Idle { goal: Goal },
Running { logs: Vec<String> }, Running { logs: Vec<String> },
} }
enum Goal {
Installation,
Update { revision: Option<String> },
}
impl<P> DevTools<P> impl<P> DevTools<P>
where where
P: Program + 'static, P: Program + 'static,
@ -174,12 +179,34 @@ where
} }
Message::ToggleComet => { Message::ToggleComet => {
if let Mode::Setup(setup) = &self.mode { if let Mode::Setup(setup) = &self.mode {
if matches!(setup, Setup::Idle) { if matches!(setup, Setup::Idle { .. }) {
self.mode = Mode::None; self.mode = Mode::None;
} }
} else if let Err(error) = debug::toggle_comet() {
if error.kind() == io::ErrorKind::NotFound { Task::none()
self.mode = Mode::Setup(Setup::Idle); } 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 = self.mode =
Mode::Setup(Setup::Running { logs: Vec::new() }); Mode::Setup(Setup::Running { logs: Vec::new() });
executor::spawn_blocking(|mut sender| { comet::install()
use std::io::{BufRead, BufReader}; .map(Message::InstallationProgressed)
use std::process::{Command, Stdio}; .map(Event::Message)
}
let Ok(install) = Command::new("cargo") Message::InstallationProgressed(Ok(installation)) => {
.args([ let Mode::Setup(Setup::Running { logs }) = &mut self.mode
"install", else {
"--locked", return Task::none();
"--git", };
"https://github.com/iced-rs/comet.git",
"--rev",
"eb114ba564a872acbd95e337d13e55f5f667b2f3",
])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
else {
return;
};
let mut stderr = BufReader::new( match installation {
install.stderr.expect("stderr must be piped"), comet::Installation::Logged(log) => {
); logs.push(log);
Task::none()
let mut log = String::new(); }
comet::Installation::Finished => {
while let Ok(n) = stderr.read_line(&mut log) { self.mode = Mode::None;
if n == 0 { comet::launch().discard()
break;
}
let _ = sender.try_send(
Message::InstallationLogged(log.clone()),
);
log.clear();
} }
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 => { Message::InstallationProgressed(_error) => {
self.mode = Mode::None; // TODO
let _ = debug::toggle_comet();
Task::none() Task::none()
} }
Message::CancelSetup => { Message::CancelSetup => {
@ -317,40 +312,69 @@ where
Mode::None => None, Mode::None => None,
Mode::Setup(setup) => { Mode::Setup(setup) => {
let stage: Element<'_, _, Theme, P::Renderer> = match setup { let stage: Element<'_, _, Theme, P::Renderer> = match setup {
Setup::Idle => { Setup::Idle { goal } => {
let controls = row![ let controls = row![
button(text("Cancel").center().width(Fill)) button(text("Cancel").center().width(Fill))
.width(100) .width(100)
.on_press(Message::CancelSetup) .on_press(Message::CancelSetup)
.style(button::danger), .style(button::danger),
horizontal_space(), horizontal_space(),
button(text("Install").center().width(Fill)) button(
.width(100) text(match goal {
.on_press(Message::InstallComet) Goal::Installation => "Install",
.style(button::success), Goal::Update { .. } => "Update",
})
.center()
.width(Fill)
)
.width(100)
.on_press(Message::InstallComet)
.style(button::success),
]; ];
column![ let command = container(
text("comet is not installed!").size(20), text(
"In order to display performance metrics, the \ "cargo install --locked \
comet debugger must be installed in your system.", --git https://github.com/iced-rs/comet.git",
"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)
) )
.width(Fill) .size(14),
.padding(5) )
.style(container::dark), .width(Fill)
controls, .padding(5)
] .style(container::dark);
.spacing(20)
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() .into()
} }
Setup::Running { logs } => column![ Setup::Running { logs } => column![