From 61d3a52dd6d6a0dbd2a91f757ec629162b8c7b09 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Fri, 22 Nov 2024 16:12:44 +0000 Subject: [PATCH] feat: switch to kameo actor system (dropping actix) --- Cargo.lock | 118 ++++---- Cargo.toml | 5 +- crates/cli/Cargo.toml | 3 +- crates/cli/README.md | 12 + crates/cli/src/alerts/handlers/notify_user.rs | 40 ++- .../cli/src/alerts/handlers/update_shout.rs | 12 +- crates/cli/src/alerts/mod.rs | 15 +- crates/cli/src/base_actor.rs | 19 ++ crates/cli/src/file_watcher.rs | 111 +++++--- crates/cli/src/macros/actor.rs | 201 +++++++++++++ crates/cli/src/macros/mod.rs | 3 + crates/cli/src/macros/send.rs | 53 ++++ crates/cli/src/macros/spawn.rs | 25 ++ crates/cli/src/main.rs | 17 +- crates/cli/src/repo/branch.rs | 2 +- crates/cli/src/repo/handlers/advance_main.rs | 50 ++-- crates/cli/src/repo/handlers/advance_next.rs | 56 ++-- .../cli/src/repo/handlers/check_ci_status.rs | 46 +-- crates/cli/src/repo/handlers/clone_repo.rs | 34 ++- .../repo/handlers/load_config_from_repo.rs | 60 ++-- .../src/repo/handlers/receive_ci_status.rs | 79 +++--- .../src/repo/handlers/receive_repo_config.rs | 21 +- .../cli/src/repo/handlers/register_webhook.rs | 85 +++--- .../src/repo/handlers/unregister_webhook.rs | 33 ++- crates/cli/src/repo/handlers/validate_repo.rs | 133 +++++---- .../src/repo/handlers/webhook_notification.rs | 82 +++--- .../src/repo/handlers/webhook_registered.rs | 22 +- crates/cli/src/repo/load.rs | 2 +- crates/cli/src/repo/mod.rs | 162 ++++++----- .../cli/src/repo/tests/branch/advance_next.rs | 12 +- crates/cli/src/repo/tests/branch/mod.rs | 4 +- crates/cli/src/repo/tests/given.rs | 36 ++- .../src/repo/tests/handlers/advance_main.rs | 25 +- .../src/repo/tests/handlers/advance_next.rs | 22 +- .../repo/tests/handlers/check_ci_status.rs | 26 +- .../cli/src/repo/tests/handlers/clone_repo.rs | 126 ++++---- .../tests/handlers/load_config_from_repo.rs | 58 ++-- .../src/repo/tests/handlers/loaded_config.rs | 26 +- .../repo/tests/handlers/receive_ci_status.rs | 74 +++-- .../repo/tests/handlers/register_webhook.rs | 40 +-- .../src/repo/tests/handlers/validate_repo.rs | 179 +++++------- .../tests/handlers/webhook_notification.rs | 268 +++++++++--------- .../repo/tests/handlers/webhook_registered.rs | 34 +-- crates/cli/src/repo/tests/load.rs | 24 +- crates/cli/src/repo/tests/mod.rs | 48 ++-- crates/cli/src/repo/tests/when.rs | 34 +-- crates/cli/src/root.rs | 221 +++++++++++++++ .../src/server/actor/handlers/file_updated.rs | 30 +- crates/cli/src/server/actor/handlers/mod.rs | 1 - .../actor/handlers/receive_app_config.rs | 47 +-- .../handlers/receive_valid_app_config.rs | 126 ++++---- .../server/actor/handlers/server_update.rs | 24 +- .../cli/src/server/actor/handlers/shutdown.rs | 39 +-- .../server/actor/handlers/shutdown_trigger.rs | 12 +- .../actor/handlers/subscribe_updates.rs | 10 - crates/cli/src/server/actor/messages.rs | 22 +- crates/cli/src/server/actor/mod.rs | 231 +++++++++------ crates/cli/src/server/actor/tests/given.rs | 12 +- .../server/actor/tests/receive_app_config.rs | 52 ++-- crates/cli/src/server/mod.rs | 164 +++-------- crates/cli/src/tests.rs | 41 --- .../src/tui/actor/handlers/server_update.rs | 143 +++++----- crates/cli/src/tui/actor/handlers/tick.rs | 22 +- crates/cli/src/tui/actor/mod.rs | 67 +++-- crates/cli/src/tui/actor/model.rs | 8 +- crates/cli/src/webhook/handlers/mod.rs | 1 - .../src/webhook/handlers/shutdown_webhook.rs | 13 - crates/cli/src/webhook/messages.rs | 4 - crates/cli/src/webhook/mod.rs | 61 ++-- crates/cli/src/webhook/router.rs | 64 +++-- crates/cli/src/webhook/server.rs | 60 ++-- crates/core/Cargo.toml | 3 - crates/core/src/config/server.rs | 10 + crates/core/src/git/repository/mod.rs | 5 +- crates/core/src/git/repository/open/mod.rs | 2 +- crates/core/src/git/validation/remotes.rs | 3 - crates/core/src/macros/message.rs | 12 - crates/forge-forgejo/src/webhook/register.rs | 13 +- justfile | 1 + quickfix-564 | 3 + 80 files changed, 2360 insertions(+), 1704 deletions(-) create mode 100644 crates/cli/src/base_actor.rs create mode 100644 crates/cli/src/macros/actor.rs create mode 100644 crates/cli/src/macros/mod.rs create mode 100644 crates/cli/src/macros/send.rs create mode 100644 crates/cli/src/macros/spawn.rs create mode 100644 crates/cli/src/root.rs delete mode 100644 crates/cli/src/server/actor/handlers/subscribe_updates.rs delete mode 100644 crates/cli/src/webhook/handlers/mod.rs delete mode 100644 crates/cli/src/webhook/handlers/shutdown_webhook.rs delete mode 100644 crates/cli/src/webhook/messages.rs create mode 100644 quickfix-564 diff --git a/Cargo.lock b/Cargo.lock index f9a1a69..67dae1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,63 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "actix" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" -dependencies = [ - "actix-macros", - "actix-rt", - "actix_derive", - "bitflags 2.6.0", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot", - "pin-project-lite", - "smallvec", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "actix-rt" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" -dependencies = [ - "actix-macros", - "futures-core", - "tokio", -] - -[[package]] -name = "actix_derive" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "addr2line" version = "0.21.0" @@ -807,6 +750,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "either" version = "1.13.0" @@ -1123,8 +1072,6 @@ dependencies = [ name = "git-next" version = "0.13.11" dependencies = [ - "actix", - "actix-rt", "anyhow", "assert2", "bon", @@ -1139,6 +1086,7 @@ dependencies = [ "git-next-core", "git-next-forge-forgejo", "git-next-forge-github", + "kameo", "kxio", "lazy_static", "lettre", @@ -1171,7 +1119,6 @@ dependencies = [ name = "git-next-core" version = "0.13.11" dependencies = [ - "actix", "assert2", "async-trait", "derive-with", @@ -2747,6 +2694,36 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kameo" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62237a96597618543798a36ec723eb75c5ac301e2690243fd600be1f5eb3dd2d" +dependencies = [ + "dyn-clone", + "futures", + "itertools", + "kameo_macros", + "once_cell", + "serde", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "kameo_macros" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bbbd8e8d7b02bc67eae0dcbdb82c0a71cc7cc61734059ee3e7439a1ee1e0e85" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", + "uuid", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -4354,6 +4331,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.52.0", ] @@ -4389,6 +4367,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-tungstenite" version = "0.21.0" @@ -4691,6 +4680,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom 0.2.15", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index cc3e187..9fb6d95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,9 +101,8 @@ take-until = "0.2" notify = "7.0" # Actors -actix = "0.13" -actix-rt = "2.9" -tokio = { version = "1.37", features = ["rt", "macros"] } +kameo = "0.13" +tokio = { version = "1.37", features = ["full"] } # email lettre = "0.11" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index e478c2b..b4d585a 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -50,8 +50,7 @@ git-conventional = { workspace = true } toml = { workspace = true } # Actors -actix = { workspace = true } -actix-rt = { workspace = true } +kameo = { workspace = true } tokio = { workspace = true } # boilerplate diff --git a/crates/cli/README.md b/crates/cli/README.md index 965297f..1215823 100644 --- a/crates/cli/README.md +++ b/crates/cli/README.md @@ -655,6 +655,18 @@ stateDiagram-v2 forge_github --> core ``` +## Actor Supervision Tree + +```mermaid +mindmap + Root + Alerts + FileWatcher + Server + Repo 1 + Repo 2 +``` + ## License `git-next` is released under the [MIT License](./LICENSE). diff --git a/crates/cli/src/alerts/handlers/notify_user.rs b/crates/cli/src/alerts/handlers/notify_user.rs index ea1b3a5..ec6be55 100644 --- a/crates/cli/src/alerts/handlers/notify_user.rs +++ b/crates/cli/src/alerts/handlers/notify_user.rs @@ -1,19 +1,22 @@ // -use actix::prelude::*; - -use tracing::{info, Instrument as _}; +use kameo::message::{Context, Message}; +use tracing::debug; use crate::alerts::{ desktop::send_desktop_notification, email::send_email, messages::NotifyUser, webhook::send_webhook, AlertsActor, }; -impl Handler for AlertsActor { - type Result = (); +impl Message for AlertsActor { + type Reply = (); - fn handle(&mut self, msg: NotifyUser, ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + msg: NotifyUser, + _ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { let Some(shout) = &self.shout else { - info!("No shout config available"); + debug!("No shout config available"); return; }; let net = self.net.clone(); @@ -21,21 +24,16 @@ impl Handler for AlertsActor { let Some(user_notification) = self.history.sendable(msg.peel()) else { return; }; - async move { - if let Some(webhook_config) = shout.webhook() { - send_webhook(&user_notification, webhook_config, &net).await; - } - if let Some(email_config) = shout.email() { - send_email(&user_notification, email_config); - } - if let Some(desktop) = shout.desktop() { - if desktop { - send_desktop_notification(&user_notification); - } + if let Some(webhook_config) = shout.webhook() { + send_webhook(&user_notification, webhook_config, &net).await; + } + if let Some(email_config) = shout.email() { + send_email(&user_notification, email_config); + } + if let Some(desktop) = shout.desktop() { + if desktop { + send_desktop_notification(&user_notification); } } - .in_current_span() - .into_actor(self) - .wait(ctx); } } diff --git a/crates/cli/src/alerts/handlers/update_shout.rs b/crates/cli/src/alerts/handlers/update_shout.rs index 7e48841..5e95018 100644 --- a/crates/cli/src/alerts/handlers/update_shout.rs +++ b/crates/cli/src/alerts/handlers/update_shout.rs @@ -1,12 +1,16 @@ // -use actix::prelude::*; +use kameo::message::{Context, Message}; use crate::alerts::{messages::UpdateShout, AlertsActor}; -impl Handler for AlertsActor { - type Result = (); +impl Message for AlertsActor { + type Reply = (); - fn handle(&mut self, msg: UpdateShout, _ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + msg: UpdateShout, + _ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { self.shout.replace(msg.peel()); } } diff --git a/crates/cli/src/alerts/mod.rs b/crates/cli/src/alerts/mod.rs index eaf1194..00a3551 100644 --- a/crates/cli/src/alerts/mod.rs +++ b/crates/cli/src/alerts/mod.rs @@ -1,11 +1,15 @@ // -use actix::prelude::*; - use derive_more::derive::Constructor; use git_next_core::{git::UserNotification, server::Shout}; pub use history::History; +use kameo::{mailbox::unbounded::UnboundedMailbox, Actor}; + +use crate::{ + default_on_actor_link_died, default_on_actor_panic, default_on_actor_start, + default_on_actor_stop, +}; mod desktop; mod email; @@ -24,9 +28,12 @@ pub struct AlertsActor { history: History, // record of alerts sent recently (e.g. 24 hours) net: kxio::net::Net, } - impl Actor for AlertsActor { - type Context = Context; + type Mailbox = UnboundedMailbox; + default_on_actor_start!(this, actor_ref); + default_on_actor_panic!(this, actor_ref, err); + default_on_actor_link_died!(this, actor_ref, id, reason); + default_on_actor_stop!(this, actor_ref, reason); } fn short_message(user_notification: &UserNotification) -> String { diff --git a/crates/cli/src/base_actor.rs b/crates/cli/src/base_actor.rs new file mode 100644 index 0000000..320a798 --- /dev/null +++ b/crates/cli/src/base_actor.rs @@ -0,0 +1,19 @@ +// +use kameo::{actor::ActorRef, Actor}; + +use crate::{spawn, spawn_in_thread}; + +/// Adds a spawn mathod to the actor. +pub trait BaseActor: Actor { + async fn spawn(self, parent_actor_ref: ActorRef) -> ActorRef { + spawn!(parent_actor_ref, self) + } + + async fn spawn_in_thread( + self, + parent_actor_ref: ActorRef, + ) -> ActorRef { + spawn_in_thread!(parent_actor_ref, self) + } +} +impl BaseActor for T {} diff --git a/crates/cli/src/file_watcher.rs b/crates/cli/src/file_watcher.rs index a85d70a..e541422 100644 --- a/crates/cli/src/file_watcher.rs +++ b/crates/cli/src/file_watcher.rs @@ -1,50 +1,83 @@ // -use actix::prelude::*; +use std::{path::PathBuf, sync::mpsc::Receiver}; -use actix::Recipient; -use anyhow::{Context, Result}; -use notify::{event::ModifyKind, Watcher}; +use anyhow::Context; +use kameo::{mailbox::unbounded::UnboundedMailbox, message::Message, Actor}; +use notify::{event::ModifyKind, RecommendedWatcher, Watcher}; use tracing::{error, info}; -use std::{ - path::PathBuf, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, - time::Duration, +use git_next_core::message; + +use crate::{ + default_on_actor_link_died, default_on_actor_panic, default_on_actor_stop, on_actor_start, + publish, tell, MessageBus, }; -#[derive(Debug, Message)] -#[rtype(result = "()")] -pub struct FileUpdated; +message!( + FileUpdated, + "Notification that watched file has been updated" +); -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("io")] - Io(#[from] std::io::Error), +message!(Watch, "Watch for the next event on the file"); + +pub struct FileWatcherActor { + file_updates_bus: MessageBus, + file: PathBuf, + event_receiver: Option>>, + watcher: Option, } -pub fn watch_file(path: PathBuf, recipient: Recipient) -> Result> { - let (tx, rx) = std::sync::mpsc::channel(); - let shutdown = Arc::new(AtomicBool::default()); - let mut handler = notify::recommended_watcher(tx).context("file watcher")?; - handler - .watch(&path, notify::RecursiveMode::NonRecursive) - .with_context(|| format!("Watching: {path:?}"))?; - let thread_shutdown = shutdown.clone(); - actix_rt::task::spawn_blocking(move || { - loop { - if thread_shutdown.load(Ordering::Relaxed) { - drop(handler); - break; - } - for result in rx.try_iter() { +impl FileWatcherActor { + pub const fn new(file_updates_bus: MessageBus, file: PathBuf) -> Self { + Self { + file_updates_bus, + file, + event_receiver: None, + watcher: None, + } + } +} + +impl Actor for FileWatcherActor { + type Mailbox = UnboundedMailbox; + + on_actor_start!(this, actor_ref, { + // + let (tx, rx) = std::sync::mpsc::channel(); + this.event_receiver.replace(rx); + + let mut watcher = notify::recommended_watcher(tx).context("file watcher")?; + watcher + .watch(&this.file, notify::RecursiveMode::NonRecursive) + .with_context(|| format!("Watching: {:?}", this.file))?; + this.watcher.replace(watcher); + + tell!("file_watcher", actor_ref, Watch)?; + + Ok(()) + }); + + default_on_actor_panic!(this, actor_ref, err); + default_on_actor_link_died!(this, actor_ref, id, reason); + default_on_actor_stop!(this, actor_ref, reason); +} + +impl Message for FileWatcherActor { + type Reply = color_eyre::Result<()>; + + async fn handle( + &mut self, + msg: Watch, + ctx: kameo::message::Context<'_, Self, Self::Reply>, + ) -> Self::Reply { + if let Some(rx) = &self.event_receiver { + while let Ok(result) = rx.recv() { match result { Ok(event) => match event.kind { notify::EventKind::Modify(ModifyKind::Data(_)) => { - info!("File modified"); - recipient.do_send(FileUpdated); + info!("==================================================================================="); + info!(?event, "File modified"); + publish!("file_updates_bus", self.file_updates_bus, FileUpdated)?; break; } notify::EventKind::Modify(_) @@ -55,12 +88,12 @@ pub fn watch_file(path: PathBuf, recipient: Recipient) -> Result { /* do nothing */ } }, Err(err) => { - error!(?err, "Watching file: {path:?}"); + error!(?err, "Watching file: {:?}", self.file); } } } - std::thread::sleep(Duration::from_millis(1000)); } - }); - Ok(shutdown) + tell!("file_watcher", ctx.actor_ref(), msg)?; + Ok(()) + } } diff --git a/crates/cli/src/macros/actor.rs b/crates/cli/src/macros/actor.rs new file mode 100644 index 0000000..fa1e697 --- /dev/null +++ b/crates/cli/src/macros/actor.rs @@ -0,0 +1,201 @@ +// + +/// Called when the actor starts, before it processes any messages. +/// +/// Messages sent internally by the actor during `on_start` are prioritized and processed +/// before any externally sent messages, even if external messages are received first. +/// +/// This ensures that the actor can properly initialize before handling external messages. +/// +/// # Example +/// +/// ```rust +/// impl Actor for ServerActor { +/// type Mailbox = UnboundedMailbox; +/// +/// on_actor_start!(this, actor_ref, { +/// // handle start here +/// Ok(()) +/// }); +/// } +/// ``` +#[macro_export] +macro_rules! on_actor_start { + ($this:ident, $actor_ref:ident, $body:expr) => { + #[allow(unused_variables)] + #[tracing::instrument(skip_all)] + async fn on_start( + &mut self, + actor_ref: kameo::actor::ActorRef, + ) -> std::result::Result<(), kameo::error::BoxError> { + tracing::debug!(?actor_ref, "{}", ::name()); + let $this = self; + let $actor_ref = actor_ref; + $body + } + }; +} + +#[macro_export] +macro_rules! default_on_actor_start { + ($this:ident, $actor_ref:ident) => { + $crate::on_actor_start!($this, $actor_ref, { Ok(()) }); + }; +} + +/// Called swhen the actor encounters a panic or an error during "tell" message handling. +/// +/// This method gives the actor an opportunity to clean up or reset its state and determine +/// whether it should be stopped or continue processing messages. +/// +/// # Parameters +/// - `err`: The panic or error that occurred. +/// +/// # Returns +/// - `Some(ActorStopReason)`: Stops the actor. +/// - `None`: Allows the actor to continue processing messages. +/// +/// # Example +/// +/// ```rust +/// impl Actor for ServerActor { +/// type Mailbox = UnboundedMailbox; +/// +/// on_actor_panic!(this, actor_ref, err, { +/// // handle panic here +/// Ok(()) +/// }); +/// } +/// ``` +#[macro_export] +macro_rules! on_actor_panic { + ($this:ident, $actor_ref:ident, $err:ident, $body:expr) => { + #[allow(unused_variables)] + #[tracing::instrument(skip_all)] + async fn on_panic( + &mut self, + actor_ref: kameo::actor::WeakActorRef, + err: kameo::error::PanicError, + ) -> std::result::Result, kameo::error::BoxError> { + tracing::debug!(?actor_ref, %err, "{}", ::name()); + let $this = self; + let $actor_ref = actor_ref; + let $err = err; + $body + } + }; +} + +#[macro_export] +macro_rules! default_on_actor_panic { + ($this:ident, $actor_ref:ident, $err:ident) => { + $crate::on_actor_panic!($this, $actor_ref, $err, { + Ok(Some(kameo::error::ActorStopReason::Panicked($err))) + }); + }; +} + +/// Called when a linked actor dies. +/// +/// By default, the actor will stop if the reason for the linked actor's death is anything other +/// than `Normal`. You can customize this behavior in the implementation. +/// +/// # Returns +/// Whether the actor should stop or continue processing messages. +/// +/// # Example +/// +/// ```rust +/// impl Actor for ServerActor { +/// type Mailbox = UnboundedMailbox; +/// +/// on_actor_link_died!(this, actor_ref, id, reason, { +/// // handle link death here +/// Ok(()) +/// }); +/// } +/// ``` +#[macro_export] +macro_rules! on_actor_link_died { + ($this:ident, $actor_ref:ident, $id:ident, $reason:ident, $body:expr) => { + #[allow(unused_variables)] + #[tracing::instrument(skip_all)] + async fn on_link_died( + &mut self, + actor_ref: kameo::actor::WeakActorRef, + id: kameo::actor::ActorID, + reason: kameo::error::ActorStopReason, + ) -> std::result::Result, kameo::error::BoxError> { + tracing::debug!(?actor_ref, %id, %reason, "{}", ::name()); + let $this = self; + let $actor_ref = actor_ref; + let $id = id; + let $reason = reason; + $body + } + }; +} + +#[macro_export] +macro_rules! default_on_actor_link_died { + ($this:ident, $actor_ref:ident, $id:ident, $reason:ident) => { + $crate::on_actor_link_died!($this, $actor_ref, $id, $reason, { + match &$reason { + kameo::error::ActorStopReason::Normal => Ok(None), + kameo::error::ActorStopReason::Killed + | kameo::error::ActorStopReason::Panicked(_) + | kameo::error::ActorStopReason::LinkDied { .. } => { + Ok(Some(kameo::error::ActorStopReason::LinkDied { + id: $id, + reason: Box::new($reason), + })) + } + } + }); + }; +} + +/// Called before the actor stops. +/// +/// This allows the actor to perform any necessary cleanup or release resources before being fully stopped. +/// +/// # Parameters +/// - `reason`: The reason why the actor is being stopped. +/// +/// # Example +/// +/// ```rust +/// impl Actor for ServerActor { +/// type Mailbox = UnboundedMailbox; +/// +/// on_actor_stop!(this, actor_ref, id, reason, { +/// // handle stop here +/// Ok(()) +/// }); +/// } +/// ``` +#[macro_export] +macro_rules! on_actor_stop { + ($this:ident, $actor_ref:ident, $reason:ident, $body:expr) => { + #[allow(unused_variables)] + #[tracing::instrument(skip_all)] + async fn on_stop( + &mut self, + actor_ref: kameo::actor::WeakActorRef, + reason: kameo::error::ActorStopReason, + ) -> std::result::Result<(), kameo::error::BoxError> { + tracing::debug!(?actor_ref, %reason, "{}", ::name()); + let $this = self; + let $actor_ref = actor_ref; + let $reason = reason; + $body + } + }; +} + +#[macro_export] +macro_rules! default_on_actor_stop { + ($this:ident, $actor_ref:ident, $reason:ident) => { + $crate::on_actor_stop!($this, $actor_ref, $reason, { Ok(()) }); + }; +} diff --git a/crates/cli/src/macros/mod.rs b/crates/cli/src/macros/mod.rs new file mode 100644 index 0000000..b1b8d27 --- /dev/null +++ b/crates/cli/src/macros/mod.rs @@ -0,0 +1,3 @@ +mod actor; +mod send; +mod spawn; diff --git a/crates/cli/src/macros/send.rs b/crates/cli/src/macros/send.rs new file mode 100644 index 0000000..da0fe3f --- /dev/null +++ b/crates/cli/src/macros/send.rs @@ -0,0 +1,53 @@ +// +#[macro_export] +macro_rules! tell { + ($actor_ref:expr, $message:expr) => { + tell!(stringify!($actor_ref), $actor_ref, $message) + }; + ($actor_name:expr, $actor_ref:expr, $message:expr) => {{ + tracing::debug!(actor = $actor_name, msg = stringify!($message), "send"); + $actor_ref.tell($message).await + }}; +} + +#[macro_export] +macro_rules! subscribe { + ($message_bus:expr, $actor_ref:expr) => { + subscribe!( + stringify!($message_bus), + $message_bus, + stringify!($actor_ref), + $actor_ref + ) + }; + ($message_bus:expr, $actor_name:expr, $actor_ref:expr) => { + subscribe!( + stringify!($message_bus), + $message_bus, + $actor_name, + $actor_ref + ) + }; + ($bus_name:expr, $message_bus:expr, $actor_ref:expr) => { + subscribe!($bus_name, $message_bus, stringify!($actor_ref), $actor_ref) + }; + ($bus_name:expr, $message_bus:expr, $actor_name:expr, $actor_ref:expr) => {{ + tracing::debug!(msg_bus = $bus_name, actor = $actor_name, "subscribe"); + $message_bus + .tell(kameo::actor::pubsub::Subscribe($actor_ref)) + .await + }}; +} + +#[macro_export] +macro_rules! publish { + ($message_bus:expr, $message:expr) => { + publish!(stringify!($message_bus), $message_bus, $message) + }; + ($bus_name:expr, $message_bus:expr, $message:expr) => {{ + tracing::debug!(bus = $bus_name, msg = stringify!($message), "publish"); + $message_bus + .tell(kameo::actor::pubsub::Publish($message)) + .await + }}; +} diff --git a/crates/cli/src/macros/spawn.rs b/crates/cli/src/macros/spawn.rs new file mode 100644 index 0000000..a679a45 --- /dev/null +++ b/crates/cli/src/macros/spawn.rs @@ -0,0 +1,25 @@ +// +/// spawns a new actor and sets up bi-directional links +#[macro_export] +macro_rules! spawn { + ($parent:expr, $actor:expr) => {{ + tracing::debug!("spawning : {}", stringify!($actor)); + let new_actor_ref = kameo::spawn($actor); + new_actor_ref.link(&$parent).await; + $parent.link(&new_actor_ref).await; + tracing::debug!("spawned : {}", stringify!($actor)); + new_actor_ref + }}; +} + +#[macro_export] +macro_rules! spawn_in_thread { + ($parent:expr, $actor:expr) => {{ + tracing::debug!("spawning in thread : {}", stringify!($actor)); + let new_actor_ref = kameo::actor::spawn_in_thread($actor); + new_actor_ref.link(&$parent).await; + $parent.link(&new_actor_ref).await; + tracing::debug!("spawned in thread : {}", stringify!($actor)); + new_actor_ref + }}; +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 8826c87..226a348 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -2,10 +2,13 @@ #![allow(clippy::module_name_repetitions)] mod alerts; +mod base_actor; mod file_watcher; mod forge; mod init; +mod macros; mod repo; +mod root; mod server; #[cfg(feature = "tui")] @@ -13,9 +16,11 @@ mod tui; #[cfg(test)] mod tests; + mod webhook; use git_next_core::git; +use kameo::actor::{pubsub::PubSub, ActorRef}; use std::path::PathBuf; @@ -23,6 +28,8 @@ use clap::Parser; use color_eyre::Result; use kxio::{fs, net}; +pub type MessageBus = ActorRef>; + #[derive(Parser, Debug)] #[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())] struct Commands { @@ -47,6 +54,8 @@ enum Server { } fn main() -> Result<()> { + color_eyre::install()?; + let fs = fs::new(PathBuf::default()); let net = net::new(); let repository_factory = git::repository::factory::real(); @@ -59,16 +68,16 @@ fn main() -> Result<()> { #[cfg(not(feature = "tui"))] Server::Start {} => server::start( false, - fs, - net, + &fs, + &net, repository_factory, std::time::Duration::from_secs(10), ), #[cfg(feature = "tui")] Server::Start { ui } => server::start( ui, - fs, - net, + &fs, + &net, repository_factory, std::time::Duration::from_secs(10), ), diff --git a/crates/cli/src/repo/branch.rs b/crates/cli/src/repo/branch.rs index 9963ccd..0decb75 100644 --- a/crates/cli/src/repo/branch.rs +++ b/crates/cli/src/repo/branch.rs @@ -20,7 +20,7 @@ pub fn advance_next( commit: Option, force: git_next_core::git::push::Force, repo_details: &RepoDetails, - repo_config: RepoConfig, + repo_config: &RepoConfig, open_repository: &dyn OpenRepositoryLike, message_token: MessageToken, ) -> Result { diff --git a/crates/cli/src/repo/handlers/advance_main.rs b/crates/cli/src/repo/handlers/advance_main.rs index 1c18485..c076026 100644 --- a/crates/cli/src/repo/handlers/advance_main.rs +++ b/crates/cli/src/repo/handlers/advance_main.rs @@ -1,8 +1,8 @@ // -use actix::prelude::*; - use git_next_core::{git, RepoConfigSource}; +use color_eyre::Result; +use kameo::message::{Context, Message}; use tracing::warn; use crate::{ @@ -15,45 +15,55 @@ use crate::{ server::actor::messages::RepoUpdate, }; -impl Handler for RepoActor { - type Result = (); +impl Message for RepoActor { + type Reply = Result<()>; #[tracing::instrument(name = "RepoActor::AdvanceMainTo", skip_all, fields(repo = %self.repo_details, commit = ?msg))] - fn handle(&mut self, msg: AdvanceMain, ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + msg: AdvanceMain, + ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { let Some(repo_config) = self.repo_details.repo_config.clone() else { warn!("No config loaded"); - return; + return Ok(()); }; let Some(open_repository) = &self.open_repository else { - return; + return Ok(()); }; - let repo_details = self.repo_details.clone(); - let addr = ctx.address(); - let message_token = self.message_token; let commit = msg.peel(); - self.update_tui(RepoUpdate::AdvancingMain { commit: commit.clone(), - }); - - if let Err(err) = advance_main(commit, &repo_details, &repo_config, &**open_repository) { + }) + .await?; + if let Err(err) = advance_main(commit, &self.repo_details, &repo_config, &**open_repository) + { warn!("advance main: {err}"); - self.alert_tui(format!("advance main: {err}")); + self.alert_tui(format!("advance main: {err}")).await?; } else { - self.update_tui(RepoUpdate::MainUpdated); + self.update_tui(RepoUpdate::MainUpdated).await?; if let Some(open_repository) = &self.open_repository { match open_repository.fetch() { - Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)), - Err(err) => self.alert_tui(format!("fetching: {err}")), + Ok(()) => { + self.update_tui_log(git::graph::log(&self.repo_details)) + .await?; + } + Err(err) => self.alert_tui(format!("fetching: {err}")).await?, } } match repo_config.source() { RepoConfigSource::Repo => { - do_send(&addr, LoadConfigFromRepo, self.log.as_ref()); + do_send(&ctx.actor_ref(), LoadConfigFromRepo, self.log.as_ref()).await?; } RepoConfigSource::Server => { - do_send(&addr, ValidateRepo::new(message_token), self.log.as_ref()); + do_send( + &ctx.actor_ref(), + ValidateRepo::new(self.message_token), + self.log.as_ref(), + ) + .await?; } } } + Ok(()) } } diff --git a/crates/cli/src/repo/handlers/advance_next.rs b/crates/cli/src/repo/handlers/advance_next.rs index 76211c4..bf5ba8c 100644 --- a/crates/cli/src/repo/handlers/advance_next.rs +++ b/crates/cli/src/repo/handlers/advance_next.rs @@ -1,8 +1,9 @@ // -use actix::prelude::*; +use color_eyre::Result; +use kameo::message::{Context, Message}; +use tracing::warn; use git_next_core::git; -use tracing::{warn, Instrument}; use crate::{ repo::{ @@ -14,15 +15,19 @@ use crate::{ server::actor::messages::RepoUpdate, }; -impl Handler for RepoActor { - type Result = (); +impl Message for RepoActor { + type Reply = Result<()>; - fn handle(&mut self, msg: AdvanceNext, ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + msg: AdvanceNext, + ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { let Some(repo_config) = &self.repo_details.repo_config else { - return; + return Ok(()); }; let Some(open_repository) = &self.open_repository else { - return; + return Ok(()); }; let AdvanceNextPayload { @@ -30,45 +35,44 @@ impl Handler for RepoActor { main, dev_commit_history, } = msg.peel(); - let repo_details = self.repo_details.clone(); - let repo_config = repo_config.clone(); - let addr = ctx.address(); - let (commit, force) = find_next_commit_on_dev(&next, &main, &dev_commit_history); if let Some(commit) = &commit { self.update_tui(RepoUpdate::AdvancingNext { commit: commit.clone(), force: force.clone(), - }); + }) + .await?; }; match advance_next( commit, force, - &repo_details, + &self.repo_details, repo_config, &**open_repository, self.message_token, ) { Ok(message_token) => { - self.update_tui(RepoUpdate::NextUpdated); + self.update_tui(RepoUpdate::NextUpdated).await?; match open_repository.fetch() { - Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)), - Err(err) => self.alert_tui(format!("fetching: {err}")), + Ok(()) => { + self.update_tui_log(git::graph::log(&self.repo_details)) + .await?; + } + Err(err) => self.alert_tui(format!("fetching: {err}")).await?, } // INFO: pause to allow any CI checks to be started - let sleep_duration = self.sleep_duration; - let log = self.log.clone(); - async move { - actix_rt::time::sleep(sleep_duration).await; - do_send(&addr, ValidateRepo::new(message_token), log.as_ref()); - } - .in_current_span() - .into_actor(self) - .wait(ctx); + tokio::time::sleep(self.sleep_duration).await; + Ok(do_send( + &ctx.actor_ref(), + ValidateRepo::new(message_token), + self.log.as_ref(), + ) + .await?) } Err(err) => { warn!("advance next: {err}"); - self.alert_tui(err.to_string()); + self.alert_tui(err.to_string()).await?; + Ok(()) } } } diff --git a/crates/cli/src/repo/handlers/check_ci_status.rs b/crates/cli/src/repo/handlers/check_ci_status.rs index b031502..79a09c6 100644 --- a/crates/cli/src/repo/handlers/check_ci_status.rs +++ b/crates/cli/src/repo/handlers/check_ci_status.rs @@ -1,7 +1,7 @@ // -use actix::prelude::*; - -use tracing::{debug, warn, Instrument as _}; +use color_eyre::Result; +use kameo::message::{Context, Message}; +use tracing::{debug, warn}; use crate::{ repo::{ @@ -12,30 +12,30 @@ use crate::{ server::actor::messages::RepoUpdate, }; -impl Handler for RepoActor { - type Result = (); +impl Message for RepoActor { + type Reply = Result<()>; - fn handle(&mut self, msg: CheckCIStatus, ctx: &mut Self::Context) -> Self::Result { - crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus"); - - let addr = ctx.address(); - let forge = self.forge.duplicate(); + async fn handle( + &mut self, + msg: CheckCIStatus, + ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { + crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus").await; let next = msg.peel(); - let log = self.log.clone(); - - self.update_tui(RepoUpdate::CheckingCI); + self.update_tui(RepoUpdate::CheckingCI).await?; // get the status - pass, fail, pending (all others map to fail, e.g. error) - async move { - match forge.commit_status(&next).await { - Ok(status) => { - debug!("got status: {status:?}"); - do_send(&addr, ReceiveCIStatus::new((next, status)), log.as_ref()); - } - Err(err) => warn!(?err, "fetching commit status"), + match self.forge.commit_status(&next).await { + Ok(status) => { + debug!("got status: {status:?}"); + do_send( + &ctx.actor_ref(), + ReceiveCIStatus::new((next, status)), + self.log.as_ref(), + ) + .await?; } + Err(err) => warn!(?err, "fetching commit status"), } - .in_current_span() - .into_actor(self) - .wait(ctx); + Ok(()) } } diff --git a/crates/cli/src/repo/handlers/clone_repo.rs b/crates/cli/src/repo/handlers/clone_repo.rs index 642c2b8..ce804a8 100644 --- a/crates/cli/src/repo/handlers/clone_repo.rs +++ b/crates/cli/src/repo/handlers/clone_repo.rs @@ -1,8 +1,9 @@ // -use actix::prelude::*; +use color_eyre::Result; +use kameo::message::{Context, Message}; +use tracing::{debug, instrument, warn}; use git_next_core::git; -use tracing::{debug, instrument, warn}; use crate::{ repo::{ @@ -13,31 +14,38 @@ use crate::{ server::actor::messages::RepoUpdate, }; -impl Handler for RepoActor { - type Result = (); +impl Message for RepoActor { + type Reply = Result<()>; #[instrument(name = "RepoActor::CloneRepo", skip_all, fields(repo = %self.repo_details))] - fn handle(&mut self, _msg: CloneRepo, ctx: &mut Self::Context) -> Self::Result { - logger(self.log.as_ref(), "Handler: CloneRepo: start"); - self.update_tui(RepoUpdate::Opening); + async fn handle( + &mut self, + _msg: CloneRepo, + ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { + logger(self.log.as_ref(), "Handler: CloneRepo: start").await; + self.update_tui(RepoUpdate::Opening).await?; debug!("Handler: CloneRepo: start"); match git::repository::open(&*self.repository_factory, &self.repo_details) { Ok(repository) => { - logger(self.log.as_ref(), "open okay"); + logger(self.log.as_ref(), "open okay").await; debug!("open okay"); - self.update_tui(RepoUpdate::Opened); + self.update_tui(RepoUpdate::Opened).await?; self.open_repository.replace(repository); if self.repo_details.repo_config.is_none() { - do_send(&ctx.address(), LoadConfigFromRepo, self.log.as_ref()); + debug!("no repo config, need to load from repo"); + do_send(&ctx.actor_ref(), LoadConfigFromRepo, self.log.as_ref()).await?; } else { - do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref()); + debug!("have repo config"); + do_send(&ctx.actor_ref(), RegisterWebhook::new(), self.log.as_ref()).await?; } } Err(err) => { - logger(self.log.as_ref(), "open failed"); + logger(self.log.as_ref(), "open failed").await; warn!("Could not open repo: {err:?}"); - self.alert_tui(err.to_string()); + self.alert_tui(err.to_string()).await?; } } debug!("Handler: CloneRepo: finish"); + Ok(()) } } diff --git a/crates/cli/src/repo/handlers/load_config_from_repo.rs b/crates/cli/src/repo/handlers/load_config_from_repo.rs index de0da29..22113c7 100644 --- a/crates/cli/src/repo/handlers/load_config_from_repo.rs +++ b/crates/cli/src/repo/handlers/load_config_from_repo.rs @@ -1,10 +1,10 @@ // -use actix::prelude::*; +use color_eyre::Result; +use kameo::message::{Context, Message}; +use tracing::{debug, instrument}; use git_next_core::git::UserNotification; -use tracing::{debug, instrument, Instrument as _}; - use crate::{ repo::{ do_send, load, @@ -14,41 +14,43 @@ use crate::{ server::actor::messages::RepoUpdate, }; -impl Handler for RepoActor { - type Result = (); +impl Message for RepoActor { + type Reply = Result<()>; + #[instrument(name = "Repocrate::repo::LoadConfigFromRepo", skip_all, fields(repo = %self.repo_details))] - fn handle(&mut self, _msg: LoadConfigFromRepo, ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + _msg: LoadConfigFromRepo, + ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { debug!("Handler: LoadConfigFromRepo: start"); - self.update_tui(RepoUpdate::LoadingConfigFromRepo); + self.update_tui(RepoUpdate::LoadingConfigFromRepo).await?; let Some(open_repository) = &self.open_repository else { - return; + return Ok(()); }; - let open_repository = open_repository.duplicate(); - let repo_details = self.repo_details.clone(); - let forge_alias = repo_details.forge.forge_alias().clone(); - let repo_alias = repo_details.repo_alias.clone(); - let addr = ctx.address(); - let notify_user_recipient = self.notify_user_recipient.clone(); - let log = self.log.clone(); - async move { - match load::config_from_repository(repo_details, &*open_repository).await { - Ok(repo_config) => { - do_send(&addr, ReceiveRepoConfig::new(repo_config), log.as_ref()); - } - Err(err) => notify_user( - notify_user_recipient.as_ref(), + match load::config_from_repository(&self.repo_details, &**open_repository).await { + Ok(repo_config) => { + do_send( + &ctx.actor_ref(), + ReceiveRepoConfig::new(repo_config), + self.log.as_ref(), + ) + .await?; + } + Err(err) => { + notify_user( + self.notify_user_recipient.as_ref(), UserNotification::RepoConfigLoadFailure { - forge_alias, - repo_alias, + forge_alias: self.repo_details.forge.forge_alias().clone(), + repo_alias: self.repo_details.repo_alias.clone(), reason: err.to_string(), }, - log.as_ref(), - ), + self.log.as_ref(), + ) + .await?; } } - .in_current_span() - .into_actor(self) - .wait(ctx); debug!("Handler: LoadConfigFromRepo: finish"); + Ok(()) } } diff --git a/crates/cli/src/repo/handlers/receive_ci_status.rs b/crates/cli/src/repo/handlers/receive_ci_status.rs index 3dc1737..6bfdddb 100644 --- a/crates/cli/src/repo/handlers/receive_ci_status.rs +++ b/crates/cli/src/repo/handlers/receive_ci_status.rs @@ -1,8 +1,9 @@ // -use actix::prelude::*; +use color_eyre::Result; +use kameo::message::{Context, Message}; +use tracing::debug; use git_next_core::git::{forge::commit::Status, graph, UserNotification}; -use tracing::{debug, Instrument}; use crate::{ repo::{ @@ -13,64 +14,64 @@ use crate::{ server::actor::messages::RepoUpdate, }; -impl Handler for RepoActor { - type Result = (); +impl Message for RepoActor { + type Reply = Result<()>; - fn handle(&mut self, msg: ReceiveCIStatus, ctx: &mut Self::Context) -> Self::Result { - logger(self.log.as_ref(), "start: ReceiveCIStatus"); + async fn handle( + &mut self, + msg: ReceiveCIStatus, + ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { + logger(self.log.as_ref(), "start: ReceiveCIStatus").await; let (next, status) = msg.peel(); self.update_tui(RepoUpdate::ReceiveCIStatus { status: status.clone(), - }); + }) + .await?; debug!(?status, ""); let graph_log = graph::log(&self.repo_details); - self.update_tui_log(graph_log.clone()); - - let addr = ctx.address(); - let forge_alias = self.repo_details.forge.forge_alias().clone(); - let repo_alias = self.repo_details.repo_alias.clone(); - let message_token = self.message_token; - let sleep_duration = self.sleep_duration; + self.update_tui_log(graph_log.clone()).await?; match status { Status::Pass => { - do_send(&addr, AdvanceMain::new(next), self.log.as_ref()); + do_send(&ctx.actor_ref(), AdvanceMain::new(next), self.log.as_ref()).await?; } Status::Pending => { - let log = self.log.clone(); - async move { - actix_rt::time::sleep(sleep_duration).await; - do_send(&addr, ValidateRepo::new(message_token), log.as_ref()); - } - .in_current_span() - .into_actor(self) - .wait(ctx); + tokio::time::sleep(self.sleep_duration).await; + do_send( + &ctx.actor_ref(), + ValidateRepo::new(self.message_token), + self.log.as_ref(), + ) + .await?; } Status::Fail => { tracing::warn!("Checks have failed"); - notify_user( self.notify_user_recipient.as_ref(), UserNotification::CICheckFailed { - forge_alias, - repo_alias, + forge_alias: self.repo_details.forge.forge_alias().clone(), + repo_alias: self.repo_details.repo_alias.clone(), commit: next, log: graph_log, }, self.log.as_ref(), - ); - let log = self.log.clone(); - async move { - debug!("sleeping before retrying..."); - logger(log.as_ref(), "before sleep"); - actix_rt::time::sleep(sleep_duration).await; - logger(log.as_ref(), "after sleep"); - do_send(&addr, ValidateRepo::new(message_token), log.as_ref()); - } - .in_current_span() - .into_actor(self) - .wait(ctx); + ) + .await?; + debug!("sleeping before retrying..."); + logger(self.log.clone().as_ref(), "before sleep").await; + tokio::time::sleep(self.sleep_duration).await; + logger(self.log.clone().as_ref(), "after sleep").await; + do_send( + &ctx.actor_ref(), + ValidateRepo::new(self.message_token), + self.log.as_ref(), + ) + .await?; + } + Status::Error(err) => { + tracing::warn!(?err, "Check CI Status"); } - Status::Error(_) => todo!(), } + Ok(()) } } diff --git a/crates/cli/src/repo/handlers/receive_repo_config.rs b/crates/cli/src/repo/handlers/receive_repo_config.rs index c444587..07028a3 100644 --- a/crates/cli/src/repo/handlers/receive_repo_config.rs +++ b/crates/cli/src/repo/handlers/receive_repo_config.rs @@ -1,5 +1,6 @@ // -use actix::prelude::*; +use color_eyre::Result; +use kameo::message::{Context, Message}; use tracing::instrument; use crate::{ @@ -11,16 +12,22 @@ use crate::{ server::actor::messages::RepoUpdate, }; -impl Handler for RepoActor { - type Result = (); +impl Message for RepoActor { + type Reply = Result<()>; #[instrument(name = "RepoActor::ReceiveRepoConfig", skip_all, fields(repo = %self.repo_details, branches = ?msg))] - fn handle(&mut self, msg: ReceiveRepoConfig, ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + msg: ReceiveRepoConfig, + ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { let repo_config = msg.peel(); self.update_tui(RepoUpdate::ReceiveRepoConfig { repo_config: repo_config.clone(), - }); + }) + .await?; self.repo_details.repo_config.replace(repo_config); - self.update_tui_branches(); - do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref()); + self.update_tui_branches().await?; + do_send(&ctx.actor_ref(), RegisterWebhook::new(), self.log.as_ref()).await?; + Ok(()) } } diff --git a/crates/cli/src/repo/handlers/register_webhook.rs b/crates/cli/src/repo/handlers/register_webhook.rs index 8e6704f..4fa9b72 100644 --- a/crates/cli/src/repo/handlers/register_webhook.rs +++ b/crates/cli/src/repo/handlers/register_webhook.rs @@ -1,7 +1,7 @@ // -use actix::prelude::*; - -use tracing::{debug, error, Instrument as _}; +use color_eyre::Result; +use kameo::message::{Context, Message}; +use tracing::{debug, error}; use crate::{ repo::{ @@ -14,51 +14,52 @@ use crate::{ use git_next_core::git::UserNotification; -impl Handler for RepoActor { - type Result = (); +impl Message for RepoActor { + type Reply = Result<()>; - fn handle(&mut self, _msg: RegisterWebhook, ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + _msg: RegisterWebhook, + ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { if self.webhook_id.is_none() { - let forge_alias = self.repo_details.forge.forge_alias().clone(); - let repo_alias = self.repo_details.repo_alias.clone(); - let repo_listen_url = self - .listen_url - .repo_url(forge_alias.clone(), repo_alias.clone()); - let forge = self.forge.duplicate(); - let addr = ctx.address(); - let notify_user_recipient = self.notify_user_recipient.clone(); - let log = self.log.clone(); - self.update_tui(RepoUpdate::RegisteringWebhook); + self.update_tui(RepoUpdate::RegisteringWebhook).await?; debug!("registering webhook"); - async move { - match forge.register_webhook(&repo_listen_url).await { - Ok(registered_webhook) => { - debug!(?registered_webhook, "webhook registered"); - do_send( - &addr, - WebhookRegistered::from(registered_webhook), - log.as_ref(), - ); - } - Err(err) => { - error!(?err, "failed to register webhook"); - notify_user( - notify_user_recipient.as_ref(), - UserNotification::WebhookRegistration { - forge_alias, - repo_alias, - reason: err.to_string(), - }, - log.as_ref(), - ); - } + match self + .forge + .register_webhook(&self.listen_url.repo_url( + self.repo_details.forge.forge_alias().clone(), + self.repo_details.repo_alias.clone(), + )) + .await + { + Ok(registered_webhook) => { + debug!(?registered_webhook, "webhook registered"); + do_send( + &ctx.actor_ref(), + WebhookRegistered::from(registered_webhook), + self.log.as_ref(), + ) + .await?; + } + Err(err) => { + error!(?err, "failed to register webhook"); + notify_user( + self.notify_user_recipient.clone().as_ref(), + UserNotification::WebhookRegistration { + forge_alias: self.repo_details.forge.forge_alias().clone(), + repo_alias: self.repo_details.repo_alias.clone(), + reason: err.to_string(), + }, + self.log.as_ref(), + ) + .await?; } } - .in_current_span() - .into_actor(self) - .wait(ctx); } else { - self.alert_tui("already have a webhook id - cant register webhook"); + self.alert_tui("already have a webhook id - cant register webhook") + .await?; } + Ok(()) } } diff --git a/crates/cli/src/repo/handlers/unregister_webhook.rs b/crates/cli/src/repo/handlers/unregister_webhook.rs index c071b2c..f1f738b 100644 --- a/crates/cli/src/repo/handlers/unregister_webhook.rs +++ b/crates/cli/src/repo/handlers/unregister_webhook.rs @@ -1,32 +1,31 @@ // -use actix::prelude::*; - -use tracing::{debug, warn, Instrument as _}; +use color_eyre::Result; +use kameo::message::{Context, Message}; +use tracing::{debug, warn}; use crate::{ repo::{messages::UnRegisterWebhook, RepoActor}, server::actor::messages::RepoUpdate, }; -impl Handler for RepoActor { - type Result = (); +impl Message for RepoActor { + type Reply = Result<()>; - fn handle(&mut self, _msg: UnRegisterWebhook, ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + _msg: UnRegisterWebhook, + _ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { let Some(webhook_id) = self.webhook_id.take() else { - return; + return Ok(()); }; - self.update_tui(RepoUpdate::UnregisteringWebhook); - let forge = self.forge.duplicate(); + self.update_tui(RepoUpdate::UnregisteringWebhook).await?; debug!("unregistering webhook"); - async move { - match forge.unregister_webhook(&webhook_id).await { - Ok(()) => debug!("unregistered webhook"), - Err(err) => warn!(?err, "unregistering webhook"), - } + match self.forge.unregister_webhook(&webhook_id).await { + Ok(()) => debug!("unregistered webhook"), + Err(err) => warn!(?err, "unregistering webhook"), } - .in_current_span() - .into_actor(self) - .wait(ctx); debug!("unregistering webhook done"); + Ok(()) } } diff --git a/crates/cli/src/repo/handlers/validate_repo.rs b/crates/cli/src/repo/handlers/validate_repo.rs index 876ba68..9d12f66 100644 --- a/crates/cli/src/repo/handlers/validate_repo.rs +++ b/crates/cli/src/repo/handlers/validate_repo.rs @@ -1,7 +1,9 @@ // -use actix::prelude::*; +use std::collections::HashMap; -use tracing::{info, instrument, Instrument as _}; +use color_eyre::Result; +use kameo::message::{Context, Message}; +use tracing::{debug, info, instrument, warn}; use crate::{ repo::{ @@ -11,20 +13,23 @@ use crate::{ }, server::actor::messages::RepoUpdate, }; - use git_next_core::git::{ push::Force, validation::positions::{validate, Error, Positions}, UserNotification, }; +use git_next_core::s; -impl Handler for RepoActor { - type Result = (); +impl Message for RepoActor { + type Reply = Result<()>; #[instrument(name = "RepoActor::ValidateRepo", skip_all, fields(repo = %self.repo_details, token = %&*msg))] - fn handle(&mut self, msg: ValidateRepo, ctx: &mut Self::Context) -> Self::Result { - logger(self.log.as_ref(), "start: ValidateRepo"); - + async fn handle( + &mut self, + msg: ValidateRepo, + ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { + logger(self.log.as_ref(), "start: ValidateRepo").await; // Message Token - make sure we are only triggered for the latest/current token match self.token_status(msg.peel()) { TokenStatus::Current => {} // do nothing @@ -32,38 +37,38 @@ impl Handler for RepoActor { logger( self.log.as_ref(), format!("discarded: old message token: {}", self.message_token), - ); - return; // message is expired + ) + .await; + return Ok(()); // message is expired } TokenStatus::New(message_token) => { self.message_token = message_token; logger( self.log.as_ref(), format!("new message token: {}", self.message_token), - ); + ) + .await; } } logger( self.log.as_ref(), format!("accepted token: {}", self.message_token), - ); - - self.update_tui(RepoUpdate::ValidateRepo); - + ) + .await; + self.update_tui(RepoUpdate::ValidateRepo).await?; // Repository positions let Some(ref open_repository) = self.open_repository else { - logger(self.log.as_ref(), "no open repository"); - self.alert_tui("repo not open"); - return; + logger(self.log.as_ref(), "no open repository").await; + self.alert_tui("repo not open").await?; + return Ok(()); }; - logger(self.log.as_ref(), "have open repository"); + logger(self.log.as_ref(), "have open repository").await; let Some(repo_config) = self.repo_details.repo_config.clone() else { - logger(self.log.as_ref(), "no repo config"); - self.alert_tui("no repo config"); - return; + logger(self.log.as_ref(), "no repo config").await; + self.alert_tui("no repo config").await?; + return Ok(()); }; - logger(self.log.as_ref(), "have repo config"); - + logger(self.log.as_ref(), "have repo config").await; match validate(&**open_repository, &self.repo_details, &repo_config) { Ok(( Positions { @@ -75,70 +80,88 @@ impl Handler for RepoActor { }, git_log, )) => { - info!(%main, %next, %dev, "positions"); - self.update_tui_log(git_log); + let mut positions = HashMap::new(); + positions + .entry(s!(s!(main.sha())[0..7])) + .or_insert_with(Vec::new) + .extend(["main"]); + positions + .entry(s!(s!(next.sha())[0..7])) + .or_insert_with(Vec::new) + .extend(["next"]); + positions + .entry(s!(s!(dev.sha())[0..7])) + .or_insert_with(Vec::new) + .extend(["dev"]); + info!(?positions, ""); + self.update_tui_log(git_log).await?; if next_is_valid && next != main { info!("Checking CI"); - do_send(&ctx.address(), CheckCIStatus::new(next), self.log.as_ref()); + do_send( + &ctx.actor_ref(), + CheckCIStatus::new(next), + self.log.as_ref(), + ) + .await?; } else if next != dev { info!("Advance next"); self.update_tui(RepoUpdate::AdvancingNext { commit: next.clone(), force: Force::No, - }); + }) + .await?; do_send( - &ctx.address(), + &ctx.actor_ref(), AdvanceNext::new(AdvanceNextPayload { next, main, dev_commit_history, }), self.log.as_ref(), - ); + ) + .await?; } else { info!("do nothing"); - self.update_tui(RepoUpdate::Okay { main, next, dev }); + self.update_tui(RepoUpdate::Okay { main, next, dev }) + .await?; } } Err(Error::Retryable(message)) => { - info!(?message, "Retryable"); - self.alert_tui(format!("retryable: {message}")); - logger(self.log.as_ref(), message); - let addr = ctx.address(); - let message_token = self.message_token; - let sleep_duration = self.sleep_duration; - let log = self.log.clone(); - async move { - info!("sleeping before retrying..."); - logger(log.as_ref(), "before sleep"); - actix_rt::time::sleep(sleep_duration).await; - logger(log.as_ref(), "after sleep"); - do_send(&addr, ValidateRepo::new(message_token), log.as_ref()); - } - .in_current_span() - .into_actor(self) - .wait(ctx); + warn!(?message, "Retryable"); + self.alert_tui(format!("retryable: {message}")).await?; + logger(self.log.as_ref(), message).await; + debug!("sleeping before retrying..."); + tokio::time::sleep(self.sleep_duration).await; + do_send( + &ctx.actor_ref(), + ValidateRepo::new(self.message_token), + self.log.as_ref(), + ) + .await?; } Err(Error::UserIntervention(user_notification)) => { - info!(?user_notification, "User Intervention"); - self.alert_tui(format!("USER INTERVENTION: {user_notification}")); + warn!(?user_notification, "User Intervention"); + self.alert_tui(format!("USER INTERVENTION: {user_notification}")) + .await?; if let UserNotification::CICheckFailed { log, .. } | UserNotification::DevNotBasedOnMain { log, .. } = &user_notification { - self.update_tui_log(log.clone()); + self.update_tui_log(log.clone()).await?; } notify_user( self.notify_user_recipient.as_ref(), user_notification, self.log.as_ref(), - ); + ) + .await?; } Err(Error::NonRetryable(message)) => { - info!(?message, "NonRetryable"); - self.alert_tui(format!("Error: {message}")); - logger(self.log.as_ref(), message); + warn!(?message, "NonRetryable"); + self.alert_tui(format!("Error: {message}")).await?; + logger(self.log.as_ref(), message).await; } } + Ok(()) } } diff --git a/crates/cli/src/repo/handlers/webhook_notification.rs b/crates/cli/src/repo/handlers/webhook_notification.rs index f5fb83d..7095b9e 100644 --- a/crates/cli/src/repo/handlers/webhook_notification.rs +++ b/crates/cli/src/repo/handlers/webhook_notification.rs @@ -1,8 +1,14 @@ // -use actix::prelude::*; - +use color_eyre::Result; +use kameo::message::{Context, Message}; use tracing::{info, instrument, warn}; +use git_next_core::{ + git::{Commit, ForgeLike}, + webhook::{push::Branch, Push}, + BranchName, WebhookAuth, +}; + use crate::{ repo::{ do_send, logger, @@ -12,21 +18,19 @@ use crate::{ server::actor::messages::RepoUpdate, }; -use git_next_core::{ - git::{Commit, ForgeLike}, - webhook::{push::Branch, Push}, - BranchName, WebhookAuth, -}; - -impl Handler for RepoActor { - type Result = (); +impl Message for RepoActor { + type Reply = Result<()>; #[instrument(name = "RepoActor::WebhookMessage", skip_all, fields(token = %self.message_token, repo = %self.repo_details))] - fn handle(&mut self, msg: WebhookNotification, ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + msg: WebhookNotification, + ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { let Some(config) = &self.repo_details.repo_config else { - logger(self.log.as_ref(), "server has no repo config"); + logger(self.log.as_ref(), "server has no repo config").await; warn!("No repo config"); - return; + return Ok(()); }; if validate_notification( &msg, @@ -34,72 +38,78 @@ impl Handler for RepoActor { &*self.forge, self.log.as_ref(), ) + .await .is_err() { - return; + return Ok(()); } - let body = msg.body(); - match self.forge.parse_webhook_body(body) { + match self.forge.parse_webhook_body(msg.body()) { Err(err) => { - logger(self.log.as_ref(), "message parse error - not a push"); + logger(self.log.as_ref(), "message parse error - not a push").await; warn!(?err, "Not a 'push'"); - return; + return Ok(()); } Ok(push) => match push.branch(config.branches()) { None => { - logger(self.log.as_ref(), "unknown branch"); + logger(self.log.as_ref(), "unknown branch").await; warn!( ?push, "Unrecognised branch, we should be filtering to only the ones we want" ); - return; + return Ok(()); } Some(Branch::Main) => { self.update_tui(RepoUpdate::WebhookReceived { branch: Branch::Main, push: push.clone(), - }); + }) + .await?; if handle_push( push, &config.branches().main(), &mut self.last_main_commit, self.log.as_ref(), ) + .await .is_err() { - return; + return Ok(()); }; } Some(Branch::Next) => { self.update_tui(RepoUpdate::WebhookReceived { branch: Branch::Next, push: push.clone(), - }); + }) + .await?; if handle_push( push, &config.branches().next(), &mut self.last_next_commit, self.log.as_ref(), ) + .await .is_err() { - return; + return Ok(()); }; } Some(Branch::Dev) => { self.update_tui(RepoUpdate::WebhookReceived { branch: Branch::Dev, push: push.clone(), - }); + }) + .await?; if handle_push( push, &config.branches().dev(), &mut self.last_dev_commit, self.log.as_ref(), ) + .await .is_err() { - return; + return Ok(()); }; } }, @@ -110,27 +120,29 @@ impl Handler for RepoActor { "New commit" ); do_send( - &ctx.address(), + &ctx.actor_ref(), ValidateRepo::new(message_token), self.log.as_ref(), - ); + ) + .await?; + Ok(()) } } -fn validate_notification( +async fn validate_notification( msg: &WebhookNotification, webhook_auth: Option<&WebhookAuth>, forge: &dyn ForgeLike, log: Option<&ActorLog>, ) -> Result<(), ()> { let Some(expected_authorization) = webhook_auth else { - logger(log, "server has no auth token"); + logger(log, "server has no auth token").await; warn!("Don't know what authorization to expect"); return Err(()); }; if !forge.is_message_authorised(msg, expected_authorization) { - logger(log, "message authorisation is invalid"); + logger(log, "message authorisation is invalid").await; warn!( "Invalid authorization - expected {}", expected_authorization @@ -138,22 +150,22 @@ fn validate_notification( return Err(()); } if forge.should_ignore_message(msg) { - logger(log, "forge sent ignorable message"); + logger(log, "forge sent ignorable message").await; return Err(()); } Ok(()) } -fn handle_push( +async fn handle_push( push: Push, branch: &BranchName, last_commit: &mut Option, log: Option<&ActorLog>, ) -> Result<(), ()> { - logger(log, format!("message is for {branch} branch")); + logger(log, format!("message is for {branch} branch")).await; let commit = Commit::from(push); if last_commit.as_ref() == Some(&commit) { - logger(log, format!("not a new commit on {branch}")); + logger(log, format!("not a new commit on {branch}")).await; info!( %branch , %commit, diff --git a/crates/cli/src/repo/handlers/webhook_registered.rs b/crates/cli/src/repo/handlers/webhook_registered.rs index dbb03ff..e33caa1 100644 --- a/crates/cli/src/repo/handlers/webhook_registered.rs +++ b/crates/cli/src/repo/handlers/webhook_registered.rs @@ -1,5 +1,6 @@ // -use actix::prelude::*; +use color_eyre::Result; +use kameo::message::{Context, Message}; use tracing::instrument; use crate::{ @@ -11,17 +12,24 @@ use crate::{ server::actor::messages::RepoUpdate, }; -impl Handler for RepoActor { - type Result = (); +impl Message for RepoActor { + type Reply = Result<()>; + #[instrument(name = "RepoActor::WebhookRegistered", skip_all, fields(repo = %self.repo_details, webhook_id = %msg.webhook_id()))] - fn handle(&mut self, msg: WebhookRegistered, ctx: &mut Self::Context) -> Self::Result { - self.update_tui(RepoUpdate::RegisteredWebhook); + async fn handle( + &mut self, + msg: WebhookRegistered, + ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { + self.update_tui(RepoUpdate::RegisteredWebhook).await?; self.webhook_id.replace(msg.webhook_id().clone()); self.webhook_auth.replace(msg.webhook_auth().clone()); do_send( - &ctx.address(), + &ctx.actor_ref(), ValidateRepo::new(self.message_token), self.log.as_ref(), - ); + ) + .await?; + Ok(()) } } diff --git a/crates/cli/src/repo/load.rs b/crates/cli/src/repo/load.rs index 405ccbd..d52eb86 100644 --- a/crates/cli/src/repo/load.rs +++ b/crates/cli/src/repo/load.rs @@ -12,7 +12,7 @@ use tracing::{info, instrument}; /// Loads the [RepoConfig] from the `.git-next.toml` file in the repository #[instrument(skip_all, fields(branch = %repo_details.branch))] pub async fn config_from_repository( - repo_details: RepoDetails, + repo_details: &RepoDetails, open_repository: &dyn OpenRepositoryLike, ) -> Result { info!("Loading .git-next.toml from repo"); diff --git a/crates/cli/src/repo/mod.rs b/crates/cli/src/repo/mod.rs index 4c39bb2..4984874 100644 --- a/crates/cli/src/repo/mod.rs +++ b/crates/cli/src/repo/mod.rs @@ -1,13 +1,12 @@ // -use actix::prelude::*; +use std::sync::Arc; -use crate::{ - alerts::messages::NotifyUser, - server::{actor::messages::RepoUpdate, ServerActor}, -}; +use color_eyre::{eyre::eyre, Result}; use derive_more::Deref; +use kameo::{actor::ActorRef, mailbox::unbounded::UnboundedMailbox, Actor}; use kxio::net::Net; -use tracing::{info, instrument, warn, Instrument}; +use tokio::sync::RwLock; +use tracing::{debug, info, instrument, warn}; use git_next_core::{ git::{ @@ -19,6 +18,16 @@ use git_next_core::{ WebhookAuth, WebhookId, }; +use crate::{ + alerts::{messages::NotifyUser, AlertsActor}, + default_on_actor_link_died, default_on_actor_panic, default_on_actor_start, on_actor_stop, + server::{ + actor::messages::{RepoUpdate, ServerUpdate}, + ServerActor, + }, + tell, +}; + mod branch; pub mod handlers; mod load; @@ -29,14 +38,21 @@ mod notifications; pub mod tests; #[derive(Clone, Debug, Default)] -pub struct ActorLog(std::sync::Arc>>); +pub struct ActorLog(Arc>>); impl Deref for ActorLog { - type Target = std::sync::Arc>>; + type Target = Arc>>; fn deref(&self) -> &Self::Target { &self.0 } } +impl ActorLog { + pub async fn log(&self, message: impl Into + Send) { + let message = message.into(); + debug!(%message, "log"); + self.write().await.push(message); + } +} /// An actor that represents a Git Repository. /// @@ -45,6 +61,7 @@ impl Deref for ActorLog { #[derive(Debug, derive_more::Display, derive_with::With)] #[display("{}:{}:{}", generation, repo_details.forge.forge_alias(), repo_details.repo_alias)] pub struct RepoActor { + ui: bool, sleep_duration: std::time::Duration, generation: git::Generation, message_token: messages::MessageToken, @@ -60,12 +77,13 @@ pub struct RepoActor { net: Net, forge: Box, log: Option, - notify_user_recipient: Option>, - server_addr: Option>, + notify_user_recipient: Option>, + server_actor_ref: ActorRef, } impl RepoActor { #[allow(clippy::too_many_arguments)] pub fn new( + ui: bool, repo_details: git::RepoDetails, forge: Box, listen_url: ListenUrl, @@ -73,11 +91,12 @@ impl RepoActor { net: Net, repository_factory: Box, sleep_duration: std::time::Duration, - notify_user_recipient: Option>, - server_addr: Option>, + notify_user_recipient: Option>, + server_actor_ref: ActorRef, ) -> Self { let message_token = messages::MessageToken::default(); Self { + ui, generation, message_token, repo_details, @@ -94,111 +113,114 @@ impl RepoActor { sleep_duration, log: None, notify_user_recipient, - server_addr, + server_actor_ref, } } - fn update_tui_branches(&self) { + async fn update_tui_branches(&self) -> Result<()> { if cfg!(feature = "tui") { use crate::server::actor::messages::RepoUpdate; let Some(repo_config) = &self.repo_details.repo_config else { - return; + return Ok(()); }; let branches = repo_config.branches().clone(); - self.update_tui(RepoUpdate::Branches { branches }); + self.update_tui(RepoUpdate::Branches { branches }).await?; } + Ok(()) } #[allow(unused_variables)] - fn update_tui_log(&self, log: git::graph::Log) { + async fn update_tui_log(&self, log: git::graph::Log) -> Result<()> { if cfg!(feature = "tui") { - self.update_tui(RepoUpdate::Log { log }); + self.update_tui(RepoUpdate::Log { log }).await?; } + Ok(()) } #[allow(unused_variables)] - fn alert_tui(&self, alert: impl Into) { + async fn alert_tui(&self, alert: impl Into + Send) -> Result<()> { if cfg!(feature = "tui") { self.update_tui(RepoUpdate::Alert { alert: alert.into(), - }); + }) + .await?; } + Ok(()) } #[allow(unused_variables)] - fn update_tui(&self, repo_update: RepoUpdate) { - if cfg!(feature = "tui") { - let Some(server_addr) = &self.server_addr else { - return; - }; - - let update = crate::server::actor::messages::ServerUpdate::RepoUpdate { - forge_alias: self.repo_details.forge.forge_alias().clone(), - repo_alias: self.repo_details.repo_alias.clone(), - repo_update, - }; - server_addr.do_send(update); + #[instrument(skip_all)] + async fn update_tui(&self, repo_update: RepoUpdate) -> Result<()> { + if cfg!(feature = "tui") && self.ui { + tell!( + "server", + self.server_actor_ref, + ServerUpdate::RepoUpdate { + forge_alias: self.repo_details.forge.forge_alias().clone(), + repo_alias: self.repo_details.repo_alias.clone(), + repo_update, + } + )?; } + Ok(()) } } impl Actor for RepoActor { - type Context = Context; - #[instrument(name = "RepoActor::stopping", skip_all, fields(repo = %self.repo_details))] - fn stopping(&mut self, ctx: &mut Self::Context) -> Running { - tracing::debug!("stopping"); + type Mailbox = UnboundedMailbox; + + default_on_actor_start!(this, actor_ref); + default_on_actor_panic!(this, actor_ref, err); + default_on_actor_link_died!(this, actor_ref, id, reason); + + on_actor_stop!(this, actor_ref, reason, { info!("Checking webhook"); - match self.webhook_id.take() { - Some(webhook_id) => { - tracing::warn!("stopping - unregistering webhook"); - info!(%webhook_id, "Unregistring webhook"); - let forge = self.forge.duplicate(); - async move { - if let Err(err) = forge.unregister_webhook(&webhook_id).await { - warn!("unregistering webhook: {err}"); - } - } - .in_current_span() - .into_actor(self) - .wait(ctx); - Running::Continue + if let Some(webhook_id) = this.webhook_id.take() { + tracing::warn!("stopping - unregistering webhook"); + info!(%webhook_id, "Unregistring webhook"); + let forge = this.forge.duplicate(); + if let Err(err) = forge.unregister_webhook(&webhook_id).await { + warn!("unregistering webhook: {err}"); } - None => Running::Stop, } - } + Ok(()) + }); } -pub fn do_send(addr: &Addr, msg: M, log: Option<&ActorLog>) +pub async fn do_send( + repo_actor_ref: &ActorRef, + msg: M, + log: Option<&ActorLog>, +) -> Result<()> where - M: actix::Message + Send + 'static + std::fmt::Debug, - RepoActor: actix::Handler, - ::Result: Send, + M: Send + Sync + 'static + std::fmt::Debug, + RepoActor: kameo::message::Message, + >::Reply: Send + Sync + 'static + std::fmt::Debug, + <>::Reply as kameo::Reply>::Error: std::fmt::Debug, { let log_message = format!("send: {msg:?}"); - info!(log_message); - logger(log, log_message); + logger(log, log_message.clone()).await; if cfg!(not(test)) { - // #[cfg(not(test))] - addr.do_send(msg); + tell!(repo_actor_ref, msg).map_err(|e| eyre!(format!("error: {log_message}: {e:?}")))?; } + Ok(()) } -pub fn logger(log: Option<&ActorLog>, message: impl Into) { +pub async fn logger(log: Option<&ActorLog>, message: impl Into + Send) { if let Some(log) = log { - let message: String = message.into(); - tracing::debug!(message); - let _ = log.write().map(|mut l| l.push(message)); + log.log(message).await; } } -pub fn notify_user( - recipient: Option<&Recipient>, +pub async fn notify_user( + recipient: Option<&ActorRef>, user_notification: UserNotification, log: Option<&ActorLog>, -) { +) -> Result<()> { let msg = NotifyUser::from(user_notification); let log_message = format!("send: {msg:?}"); - tracing::debug!(log_message); - logger(log, log_message); + debug!(log_message); + logger(log, log_message).await; if let Some(recipient) = &recipient { - recipient.do_send(msg); + tell!("alerts", recipient, msg)?; } + Ok(()) } diff --git a/crates/cli/src/repo/tests/branch/advance_next.rs b/crates/cli/src/repo/tests/branch/advance_next.rs index 5a57cd3..38be667 100644 --- a/crates/cli/src/repo/tests/branch/advance_next.rs +++ b/crates/cli/src/repo/tests/branch/advance_next.rs @@ -8,7 +8,7 @@ fn advance_next_sut( main: &Commit, dev_commit_history: &[Commit], repo_details: &RepoDetails, - repo_config: RepoConfig, + repo_config: &RepoConfig, open_repository: &dyn OpenRepositoryLike, message_token: MessageToken, ) -> branch::Result { @@ -43,7 +43,7 @@ mod when_at_dev { main, dev_commit_history, &repo_details, - repo_config, + &repo_config, &open_repository, message_token, ) @@ -78,7 +78,7 @@ mod can_advance { main, dev_commit_history, &repo_details, - repo_config, + &repo_config, &open_repository, message_token, ) @@ -109,7 +109,7 @@ mod can_advance { main, dev_commit_history, &repo_details, - repo_config, + &repo_config, &open_repository, message_token, ) @@ -149,7 +149,7 @@ mod can_advance { main, dev_commit_history, &repo_details, - repo_config, + &repo_config, &open_repository, message_token, ) @@ -181,7 +181,7 @@ mod can_advance { main, dev_commit_history, &repo_details, - repo_config, + &repo_config, &open_repository, message_token, ) diff --git a/crates/cli/src/repo/tests/branch/mod.rs b/crates/cli/src/repo/tests/branch/mod.rs index 0341e7d..874ecb3 100644 --- a/crates/cli/src/repo/tests/branch/mod.rs +++ b/crates/cli/src/repo/tests/branch/mod.rs @@ -10,7 +10,7 @@ mod advance_next; use crate::git; use crate::repo::branch; -#[actix_rt::test] +#[tokio::test] async fn test_find_next_commit_on_dev_when_next_is_at_main() { let next = given::a_commit(); // and main let expected = given::a_commit(); @@ -27,7 +27,7 @@ async fn test_find_next_commit_on_dev_when_next_is_at_main() { assert_eq!(force, Force::No, "should not try to force"); } -#[actix_rt::test] +#[tokio::test] async fn test_find_next_commit_on_dev_when_next_is_not_on_dev() { let next = given::a_commit(); let main = given::a_commit(); diff --git a/crates/cli/src/repo/tests/given.rs b/crates/cli/src/repo/tests/given.rs index 6c2810c..79fff6a 100644 --- a/crates/cli/src/repo/tests/given.rs +++ b/crates/cli/src/repo/tests/given.rs @@ -1,7 +1,14 @@ +use crate::{ + alerts::{AlertsActor, History}, + server::{actor::messages::ServerUpdate, ServerActor}, +}; + // use super::*; use git_next_core::server::ListenUrl; +use kameo::actor::{pubsub::PubSub, ActorRef}; +use kxio::{fs::FileSystem, net::Net}; pub fn has_all_valid_remote_defaults( open_repository: &mut MockOpenRepositoryLike, @@ -66,7 +73,7 @@ pub fn a_name() -> String { let one_char = || CHARSET[rng.gen_range(0..CHARSET.len())] as char; iter::repeat_with(one_char).take(len).collect() } - generate(5) + generate(7) } pub fn maybe_a_number() -> Option { @@ -197,6 +204,7 @@ pub fn a_repo_actor( repo_details: RepoDetails, repository_factory: Box, forge: Box, + server_actor: ActorRef, net: kxio::net::Net, ) -> (RepoActor, ActorLog) { let listen_url = given::a_listen_url(); @@ -205,21 +213,43 @@ pub fn a_repo_actor( let actors_log = log.clone(); ( RepoActor::new( + false, repo_details, forge, listen_url, generation, net, repository_factory, - std::time::Duration::from_nanos(1), - None, + std::time::Duration::from_nanos(0), None, + server_actor, ) .with_log(actors_log), log, ) } +pub fn a_server_actor(fs: FileSystem, net: Net) -> ActorRef { + let alerts_actor_ref = kameo::spawn(AlertsActor::new( + None, + History::new(Duration::from_secs(0)), + net.clone(), + )); + let server_updates_actor_ref = kameo::spawn(PubSub::::new()); + let repo_factor = Box::new(MockRepositoryFactory::new()); + let duration = Duration::from_secs(0); + let server_actor = ServerActor::new( + false, // ui + fs, + net, + alerts_actor_ref, + server_updates_actor_ref, + repo_factor, + duration, + ); + kameo::spawn(server_actor) +} + pub fn a_hostname() -> Hostname { Hostname::new(given::a_name()) } diff --git a/crates/cli/src/repo/tests/handlers/advance_main.rs b/crates/cli/src/repo/tests/handlers/advance_main.rs index 2f89656..daacea1 100644 --- a/crates/cli/src/repo/tests/handlers/advance_main.rs +++ b/crates/cli/src/repo/tests/handlers/advance_main.rs @@ -1,7 +1,9 @@ +use crate::{repo::messages::AdvanceMain, tell}; + // use super::*; -#[actix::test] +#[tokio::test] async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult { //given let fs = given::a_filesystem(); @@ -37,21 +39,16 @@ async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::AdvanceMain::new(next_commit.clone())) - .await?; - System::current().stop(); + tell!(addr, AdvanceMain::new(next_commit.clone()))?; //then tracing::debug!(?log, ""); - log.read().map_err(|e| e.to_string()).map(|l| { - assert!(l - .iter() - .any(|message| message.contains("send: LoadConfigFromRepo"))); - })?; + log.require_message_containing("send: LoadConfigFromRepo") + .await?; Ok(()) } -#[actix::test] +#[test_log::test(tokio::test)] async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult { //given let fs = given::a_filesystem(); @@ -87,12 +84,10 @@ async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::AdvanceMain::new(next_commit.clone())) - .await?; - System::current().stop(); + tell!(addr, AdvanceMain::new(next_commit.clone()))?; //then - tracing::debug!(?log, ""); - log.require_message_containing("send: ValidateRepo")?; + tracing::debug!(?log, "log"); + log.require_message_containing("send: ValidateRepo").await?; Ok(()) } diff --git a/crates/cli/src/repo/tests/handlers/advance_next.rs b/crates/cli/src/repo/tests/handlers/advance_next.rs index 6570be8..612c9d5 100644 --- a/crates/cli/src/repo/tests/handlers/advance_next.rs +++ b/crates/cli/src/repo/tests/handlers/advance_next.rs @@ -1,11 +1,14 @@ use std::time::Duration; -use crate::repo::messages::AdvanceNextPayload; +use crate::{ + repo::messages::{AdvanceNext, AdvanceNextPayload}, + tell, +}; // use super::*; -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn should_fetch_then_push_then_revalidate() -> TestResult { //given let fs = given::a_filesystem(); @@ -40,19 +43,18 @@ async fn should_fetch_then_push_then_revalidate() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::AdvanceNext::new( - AdvanceNextPayload { + tell!( + addr, + AdvanceNext::new(AdvanceNextPayload { next: next_commit.clone(), main: next_commit.clone(), dev_commit_history, - }, - )) - .await?; - actix_rt::time::sleep(Duration::from_millis(9)).await; - System::current().stop(); + }) + )?; + tokio::time::sleep(Duration::from_millis(9)).await; //then tracing::debug!(?log, ""); - log.require_message_containing("send: ValidateRepo")?; + log.require_message_containing("send: ValidateRepo").await?; Ok(()) } diff --git a/crates/cli/src/repo/tests/handlers/check_ci_status.rs b/crates/cli/src/repo/tests/handlers/check_ci_status.rs index c682b08..f951e7c 100644 --- a/crates/cli/src/repo/tests/handlers/check_ci_status.rs +++ b/crates/cli/src/repo/tests/handlers/check_ci_status.rs @@ -1,18 +1,19 @@ +use crate::{repo::messages::CheckCIStatus, tell}; + // use super::*; -#[actix::test] +#[tokio::test] async fn should_passthrough_to_receive_ci_status() -> TestResult { //given let fs = given::a_filesystem(); let (open_repository, repo_details) = given::an_open_repository(&fs); let next_commit = given::a_named_commit("next"); let mut forge = git::MockForgeLike::new(); - when::commit_status( - &mut forge, - next_commit.clone(), - git::forge::commit::Status::Pass, - ); + forge + .expect_commit_status() + .with(mockall::predicate::eq(next_commit.clone())) + .return_once(|_| Ok(git::forge::commit::Status::Pass)); //when let (addr, log) = when::start_actor_with_open_repository( @@ -20,18 +21,11 @@ async fn should_passthrough_to_receive_ci_status() -> TestResult { repo_details, Box::new(forge), ); - addr.send(crate::repo::messages::CheckCIStatus::new( - next_commit.clone(), - )) - .await?; - System::current().stop(); + tell!(addr, CheckCIStatus::new(next_commit.clone()))?; //then tracing::debug!(?log, ""); - log.read().map_err(|e| e.to_string()).map(|l| { - assert!(l - .iter() - .any(|message| message.contains("send: ReceiveCIStatus"))); - })?; + log.require_message_containing("send: ReceiveCIStatus") + .await?; Ok(()) } diff --git a/crates/cli/src/repo/tests/handlers/clone_repo.rs b/crates/cli/src/repo/tests/handlers/clone_repo.rs index 5f89c58..7204b14 100644 --- a/crates/cli/src/repo/tests/handlers/clone_repo.rs +++ b/crates/cli/src/repo/tests/handlers/clone_repo.rs @@ -1,7 +1,11 @@ +use kxio::net::Net; + +use crate::tell; + // use super::*; -#[actix::test] +#[test_log::test(tokio::test)] async fn should_clone() -> TestResult { //given let fs = given::a_filesystem(); @@ -23,13 +27,19 @@ async fn should_clone() -> TestResult { let _ = cloned_ref.write().map(|mut l| l.push(())); Ok(Box::new(open_repository)) }); + let net: Net = given::a_network().into(); //when - let (addr, _log) = when::start_actor(repository_factory, repo_details, given::a_forge()); - addr.send(CloneRepo::new()).await?; - System::current().stop(); - + let (addr, _log) = when::start_actor( + repository_factory, + repo_details, + given::a_forge(), + fs.as_real(), + net, + ); + tell!(addr, CloneRepo::new())?; //then + tick(1).await; cloned .read() .map_err(|e| e.to_string()) @@ -38,7 +48,7 @@ async fn should_clone() -> TestResult { Ok(()) } -#[actix::test] +#[tokio::test] async fn should_open() -> TestResult { //given let fs = given::a_filesystem(); @@ -60,13 +70,20 @@ async fn should_open() -> TestResult { Ok(Box::new(open_repository)) }); fs.dir(&repo_details.gitdir).create()?; + let net: Net = given::a_network().into(); //when - let (addr, _log) = when::start_actor(repository_factory, repo_details, given::a_forge()); - addr.send(CloneRepo::new()).await?; - System::current().stop(); + let (addr, _log) = when::start_actor( + repository_factory, + repo_details, + given::a_forge(), + fs.as_real(), + net, + ); + tell!(addr, CloneRepo::new())?; //then + tick(1).await; opened .read() .map_err(|e| e.to_string()) @@ -78,7 +95,7 @@ async fn should_open() -> TestResult { /// The server config can optionally include the names of the main, next and dev /// branches. When it doesn't we should load the `.git-next.yaml` from from the /// repo and get the branch names from there by sending a [LoadConfigFromRepo] message. -#[actix::test] +#[tokio::test] async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResult { //given let fs = given::a_filesystem(); @@ -95,21 +112,28 @@ async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResu let mut repository_factory = MockRepositoryFactory::new(); expect::open_repository(&mut repository_factory, open_repository); fs.dir(&repo_details.gitdir).create()?; + let net: Net = given::a_network().into(); //when - let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge()); - addr.send(CloneRepo::new()).await?; - System::current().stop(); + let (addr, log) = when::start_actor( + repository_factory, + repo_details, + given::a_forge(), + fs.as_real(), + net, + ); + tell!(addr, CloneRepo::new())?; //then - log.require_message_containing("send: LoadConfigFromRepo")?; + log.require_message_containing("send: LoadConfigFromRepo") + .await?; Ok(()) } /// The server config can optionally include the names of the main, next and dev /// branches. When it does we should register the webhook by sending [RegisterWebhook] message. -#[actix::test] +#[test_log::test(tokio::test)] async fn when_server_has_repo_config_should_send_register_webhook() -> TestResult { //given let fs = given::a_filesystem(); @@ -124,71 +148,23 @@ async fn when_server_has_repo_config_should_send_register_webhook() -> TestResul let mut repository_factory = MockRepositoryFactory::new(); expect::open_repository(&mut repository_factory, open_repository); fs.dir(&repo_details.gitdir).create()?; + let net: Net = given::a_network().into(); //when - let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge()); - addr.send(CloneRepo::new()).await?; - System::current().stop(); - - //then - log.require_message_containing("send: RegisterWebhook")?; - - Ok(()) -} - -#[test_log::test(actix::test)] -async fn opened_repo_with_no_default_push_should_not_proceed() -> TestResult { - //given - let fs = given::a_filesystem(); - let (mut open_repository, repo_details) = given::an_open_repository(&fs); - open_repository - .expect_fetch() - .times(1) - .return_once(|| Ok(())); - - given::has_remote_defaults( - &mut open_repository, - HashMap::from([ - (Direction::Push, None), - (Direction::Fetch, repo_details.remote_url()), - ]), + let (addr, log) = when::start_actor( + repository_factory, + repo_details, + given::a_forge(), + fs.as_real(), + net, ); - - let mut repository_factory = MockRepositoryFactory::new(); - expect::open_repository(&mut repository_factory, open_repository); - fs.dir(&repo_details.gitdir).create()?; - - //when - let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge()); - addr.send(CloneRepo::new()).await?; - System::current().stop(); + tell!(addr, CloneRepo::new())?; //then - log.require_message_containing("open failed")?; - - Ok(()) -} - -#[test_log::test(actix::test)] -async fn opened_repo_with_no_default_fetch_should_not_proceed() -> TestResult { - //given - let fs = given::a_filesystem(); - let (mut open_repository, repo_details) = given::an_open_repository(&fs); - open_repository - .expect_fetch() - .times(1) - .return_once(|| Err(git::fetch::Error::NoFetchRemoteFound)); - let mut repository_factory = MockRepositoryFactory::new(); - expect::open_repository(&mut repository_factory, open_repository); - fs.dir(&repo_details.gitdir).create()?; - - //when - let (addr, log) = when::start_actor(repository_factory, repo_details, given::a_forge()); - addr.send(CloneRepo::new()).await?; - System::current().stop(); - - //then - log.require_message_containing("open failed")?; + tick(1).await; + debug!(?log, ""); + log.require_message_containing("send: RegisterWebhook") + .await?; Ok(()) } diff --git a/crates/cli/src/repo/tests/handlers/load_config_from_repo.rs b/crates/cli/src/repo/tests/handlers/load_config_from_repo.rs index 8b84113..a685efd 100644 --- a/crates/cli/src/repo/tests/handlers/load_config_from_repo.rs +++ b/crates/cli/src/repo/tests/handlers/load_config_from_repo.rs @@ -1,37 +1,33 @@ +use crate::{repo::messages::LoadConfigFromRepo, tell}; + // use super::*; -#[actix::test] +#[tokio::test] async fn when_read_file_ok_should_send_config_loaded() -> TestResult { //given let fs = given::a_filesystem(); let (mut open_repository, repo_details) = given::an_open_repository(&fs); - let mut load_config_open_repo = MockOpenRepositoryLike::new(); + // let mut load_config_open_repo = MockOpenRepositoryLike::new(); let branches = given::repo_branches(); let remote_branches = vec![branches.main(), branches.next(), branches.dev()]; - load_config_open_repo - .expect_read_file() - .return_once(move |_, _| { - Ok(format!( - r#" + open_repository.expect_read_file().return_once(move |_, _| { + Ok(format!( + r#" [branches] main = "{}" next = "{}" dev = "{}" "#, - branches.main(), - branches.next(), - branches.dev() - )) - }); - - load_config_open_repo - .expect_remote_branches() - .return_once(|| Ok(remote_branches)); + branches.main(), + branches.next(), + branches.dev() + )) + }); open_repository - .expect_duplicate() - .return_once(|| Box::new(load_config_open_repo)); + .expect_remote_branches() + .return_once(|| Ok(remote_branches)); //when let (addr, log) = when::start_actor_with_open_repository( @@ -39,44 +35,36 @@ async fn when_read_file_ok_should_send_config_loaded() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::LoadConfigFromRepo::new()) - .await?; - System::current().stop(); + tell!(addr, LoadConfigFromRepo::new())?; //then tracing::debug!(?log, ""); - log.require_message_containing("send: ReceiveRepoConfig")?; - log.no_message_contains("send: NotifyUsers")?; + log.require_message_containing("send: ReceiveRepoConfig") + .await?; + log.no_message_contains("send: NotifyUsers").await?; Ok(()) } -#[actix::test] +#[tokio::test] async fn when_read_file_err_should_notify_user() -> TestResult { //given let fs = given::a_filesystem(); let (mut open_repository, repo_details) = given::an_open_repository(&fs); - let mut load_config_open_repo = MockOpenRepositoryLike::new(); - load_config_open_repo + open_repository .expect_read_file() .return_once(move |_, _| Err(git::file::Error::FileNotFound)); - open_repository - .expect_duplicate() - .return_once(|| Box::new(load_config_open_repo)); - //when let (addr, log) = when::start_actor_with_open_repository( Box::new(open_repository), repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::LoadConfigFromRepo::new()) - .await?; - System::current().stop(); + tell!(addr, LoadConfigFromRepo::new())?; //then tracing::debug!(?log, ""); - log.require_message_containing("send: NotifyUser")?; - log.no_message_contains("send: ReceiveRepoConfig")?; + log.require_message_containing("send: NotifyUser").await?; + log.no_message_contains("send: ReceiveRepoConfig").await?; Ok(()) } diff --git a/crates/cli/src/repo/tests/handlers/loaded_config.rs b/crates/cli/src/repo/tests/handlers/loaded_config.rs index 2f7ce90..2587d53 100644 --- a/crates/cli/src/repo/tests/handlers/loaded_config.rs +++ b/crates/cli/src/repo/tests/handlers/loaded_config.rs @@ -1,7 +1,9 @@ // +use crate::{repo::messages::ReceiveRepoConfig, tell}; + use super::*; -#[actix::test] +#[tokio::test] async fn should_store_repo_config_in_actor() -> TestResult { //given let fs = given::a_filesystem(); @@ -15,16 +17,15 @@ async fn should_store_repo_config_in_actor() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ReceiveRepoConfig::new( - new_repo_config.clone(), - )) - .await?; - System::current().stop(); + tell!(addr, ReceiveRepoConfig::new(new_repo_config.clone()))?; //then tracing::debug!(?log, ""); - let reo_actor_view = addr.send(ExamineActor).await?; + let reo_actor_view = addr + .ask(ExamineActor) + .await + .map_err(|e| format!("examine actor: {e:?}"))?; assert_eq!( reo_actor_view.repo_details.repo_config, Some(new_repo_config) @@ -32,7 +33,7 @@ async fn should_store_repo_config_in_actor() -> TestResult { Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn should_register_webhook() -> TestResult { //given let fs = given::a_filesystem(); @@ -46,14 +47,11 @@ async fn should_register_webhook() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ReceiveRepoConfig::new( - new_repo_config.clone(), - )) - .await?; - System::current().stop(); + tell!(addr, ReceiveRepoConfig::new(new_repo_config.clone()))?; //then tracing::debug!(?log, ""); - log.require_message_containing("send: RegisterWebhook")?; + log.require_message_containing("send: RegisterWebhook") + .await?; Ok(()) } diff --git a/crates/cli/src/repo/tests/handlers/receive_ci_status.rs b/crates/cli/src/repo/tests/handlers/receive_ci_status.rs index d0c709d..5f97287 100644 --- a/crates/cli/src/repo/tests/handlers/receive_ci_status.rs +++ b/crates/cli/src/repo/tests/handlers/receive_ci_status.rs @@ -1,9 +1,13 @@ use std::time::Duration; +use git::forge::commit::Status; + +use crate::{repo::messages::ReceiveCIStatus, tell}; + // use super::*; -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn when_pass_should_advance_main_to_next() -> TestResult { //given let fs = given::a_filesystem(); @@ -16,24 +20,19 @@ async fn when_pass_should_advance_main_to_next() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ReceiveCIStatus::new(( - next_commit.clone(), - git::forge::commit::Status::Pass, - ))) - .await?; - System::current().stop(); + tell!( + addr, + ReceiveCIStatus::new((next_commit.clone(), Status::Pass)) + )?; //then tracing::debug!(?log, ""); - log.read().map_err(|e| e.to_string()).map(|l| { - let expected = format!("send: AdvanceMain({next_commit:?})"); - tracing::debug!(%expected,""); - assert!(l.iter().any(|message| message.contains(&expected))); - })?; + log.require_message_containing(format!("send: AdvanceMain({next_commit:?})")) + .await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn when_pending_should_recheck_ci_status() -> TestResult { //given let fs = given::a_filesystem(); @@ -46,47 +45,42 @@ async fn when_pending_should_recheck_ci_status() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ReceiveCIStatus::new(( - next_commit.clone(), - git::forge::commit::Status::Pending, - ))) - .await?; - actix_rt::time::sleep(Duration::from_millis(9)).await; - System::current().stop(); + tell!( + addr, + ReceiveCIStatus::new((next_commit.clone(), Status::Pending)) + )?; + tokio::time::sleep(Duration::from_millis(9)).await; //then tracing::debug!(?log, ""); - log.require_message_containing("send: ValidateRepo")?; + log.require_message_containing("send: ValidateRepo").await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn when_fail_should_recheck_after_delay() -> TestResult { //given let fs = given::a_filesystem(); let (open_repository, repo_details) = given::an_open_repository(&fs); let next_commit = given::a_named_commit("next"); - - //when - let (addr, log) = when::start_actor_with_open_repository( + let (repo_actor_ref, log) = when::start_actor_with_open_repository( Box::new(open_repository), repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ReceiveCIStatus::new(( - next_commit.clone(), - git::forge::commit::Status::Fail, - ))) - .await?; - actix_rt::time::sleep(Duration::from_millis(9)).await; - System::current().stop(); + + //when + tell!( + repo_actor_ref, + ReceiveCIStatus::new((next_commit.clone(), Status::Fail)) + )?; //then - log.require_message_containing("send: ValidateRepo")?; + log.require_message_containing("send: ValidateRepo").await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn when_fail_should_notify_user() -> TestResult { //given let fs = given::a_filesystem(); @@ -99,14 +93,12 @@ async fn when_fail_should_notify_user() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ReceiveCIStatus::new(( - next_commit.clone(), - git::forge::commit::Status::Fail, - ))) - .await?; - System::current().stop(); + tell!( + addr, + ReceiveCIStatus::new((next_commit.clone(), Status::Fail)) + )?; //then - log.require_message_containing("send: NotifyUser")?; + log.require_message_containing("send: NotifyUser").await?; Ok(()) } diff --git a/crates/cli/src/repo/tests/handlers/register_webhook.rs b/crates/cli/src/repo/tests/handlers/register_webhook.rs index 46105ee..71a5ae0 100644 --- a/crates/cli/src/repo/tests/handlers/register_webhook.rs +++ b/crates/cli/src/repo/tests/handlers/register_webhook.rs @@ -1,71 +1,57 @@ +use crate::{repo::messages::RegisterWebhook, tell}; + // use super::*; -#[actix::test] +#[tokio::test] async fn when_registered_ok_should_send_webhook_registered() -> TestResult { //given let fs = given::a_filesystem(); let (open_repository, repo_details) = given::an_open_repository(&fs); let registered_webhook = given::a_registered_webhook(); - let mut my_forge = git::MockForgeLike::new(); - my_forge + let mut forge = git::MockForgeLike::new(); + forge .expect_register_webhook() .return_once(move |_| Ok(registered_webhook)); - let mut forge = git::MockForgeLike::new(); - forge.expect_duplicate().return_once(|| Box::new(my_forge)); - //when let (addr, log) = when::start_actor_with_open_repository( Box::new(open_repository), repo_details, Box::new(forge), ); - addr.send(crate::repo::messages::RegisterWebhook::new()) - .await?; - System::current().stop(); - + tell!(addr, RegisterWebhook::new())?; //then tracing::debug!(?log, ""); - log.read().map_err(|e| e.to_string()).map(|l| { - assert!(l - .iter() - .any(|message| message.contains("send: WebhookRegistered"))); - })?; + log.require_message_containing("send: WebhookRegistered") + .await?; Ok(()) } -#[actix::test] +#[tokio::test] async fn when_registered_error_should_send_notify_user() -> TestResult { //given let fs = given::a_filesystem(); let (open_repository, repo_details) = given::an_open_repository(&fs); - let mut my_forge = git::MockForgeLike::new(); - my_forge.expect_register_webhook().return_once(move |_| { + let mut forge = git::MockForgeLike::new(); + forge.expect_register_webhook().return_once(move |_| { Err(git::forge::webhook::Error::FailedToRegister( "foo".to_string(), )) }); - let mut forge = git::MockForgeLike::new(); - forge.expect_duplicate().return_once(|| Box::new(my_forge)); - //when let (addr, log) = when::start_actor_with_open_repository( Box::new(open_repository), repo_details, Box::new(forge), ); - addr.send(crate::repo::messages::RegisterWebhook::new()) - .await?; - System::current().stop(); + tell!(addr, crate::repo::messages::RegisterWebhook::new())?; //then tracing::debug!(?log, ""); - log.read() - .map_err(|e| e.to_string()) - .map(|l| assert!(l.iter().any(|message| message.contains("send: NotifyUser"))))?; + log.require_message_containing("send: NotifyUser").await?; Ok(()) } diff --git a/crates/cli/src/repo/tests/handlers/validate_repo.rs b/crates/cli/src/repo/tests/handlers/validate_repo.rs index 8fce618..839f0bd 100644 --- a/crates/cli/src/repo/tests/handlers/validate_repo.rs +++ b/crates/cli/src/repo/tests/handlers/validate_repo.rs @@ -1,9 +1,14 @@ -use crate::repo::messages::{AdvanceNext, AdvanceNextPayload}; - // +use kxio::net::Net; + +use crate::{ + repo::messages::{AdvanceNext, AdvanceNextPayload, ValidateRepo}, + tell, +}; + use super::*; -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_to_main( ) -> TestResult { //given @@ -41,18 +46,15 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_t repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ValidateRepo::new( - MessageToken::default(), - )) - .await?; - System::current().stop(); + tell!(addr, ValidateRepo::new(MessageToken::default()))?; //then - log.require_message_containing(format!("Branch {} has been reset", branches.next()))?; + log.require_message_containing(format!("Branch {} has been reset", branches.next())) + .await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_reset_to_dev( ) -> TestResult { //given @@ -92,11 +94,10 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_r repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ValidateRepo::new( - MessageToken::default(), - )) - .await?; - System::current().stop(); + tell!( + addr, + crate::repo::messages::ValidateRepo::new(MessageToken::default()) + )?; //then let expected = AdvanceNext::new(AdvanceNextPayload { @@ -104,11 +105,12 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_r main: main_commit, dev_commit_history: dev_branch_log, }); - log.require_message_containing(format!("send: {expected:?}",))?; + log.require_message_containing(format!("send: {expected:?}",)) + .await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn repo_with_next_not_on_or_near_main_should_be_reset() -> TestResult { //given let fs = given::a_filesystem(); @@ -145,18 +147,15 @@ async fn repo_with_next_not_on_or_near_main_should_be_reset() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ValidateRepo::new( - MessageToken::default(), - )) - .await?; - System::current().stop(); + tell!(addr, ValidateRepo::new(MessageToken::default()))?; //then - log.require_message_containing(format!("Branch {} has been reset", branches.next()))?; + log.require_message_containing(format!("Branch {} has been reset", branches.next())) + .await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn repo_with_next_not_based_on_main_should_be_reset() -> TestResult { //given let fs = given::a_filesystem(); @@ -193,18 +192,15 @@ async fn repo_with_next_not_based_on_main_should_be_reset() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ValidateRepo::new( - MessageToken::default(), - )) - .await?; - System::current().stop(); + tell!(addr, ValidateRepo::new(MessageToken::default()))?; //then - log.require_message_containing(format!("Branch {} has been reset", branches.next()))?; + log.require_message_containing(format!("Branch {} has been reset", branches.next())) + .await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn repo_with_next_ahead_of_main_should_check_ci_status() -> TestResult { //given let fs = given::a_filesystem(); @@ -240,18 +236,15 @@ async fn repo_with_next_ahead_of_main_should_check_ci_status() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ValidateRepo::new( - MessageToken::default(), - )) - .await?; - System::current().stop(); + tell!(addr, ValidateRepo::new(MessageToken::default()))?; //then - log.require_message_containing(format!("send: CheckCIStatus({next_commit:?})"))?; + log.require_message_containing(format!("send: CheckCIStatus({next_commit:?})")) + .await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn repo_with_dev_and_next_on_main_should_do_nothing() -> TestResult { // Do nothing, when the situation changes we will hear about it via a webhook //given @@ -288,18 +281,14 @@ async fn repo_with_dev_and_next_on_main_should_do_nothing() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ValidateRepo::new( - MessageToken::default(), - )) - .await?; - System::current().stop(); + tell!(addr, ValidateRepo::new(MessageToken::default()))?; //then - log.no_message_contains("send:")?; + log.no_message_contains("send:").await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult { //given let fs = given::a_filesystem(); @@ -337,11 +326,7 @@ async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ValidateRepo::new( - MessageToken::default(), - )) - .await?; - System::current().stop(); + tell!(addr, ValidateRepo::new(MessageToken::default()))?; //then let expected = AdvanceNext::new(AdvanceNextPayload { @@ -349,11 +334,12 @@ async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult { main: main_commit, dev_commit_history: dev_branch_log, }); - log.require_message_containing(format!("send: {expected:?}"))?; + log.require_message_containing(format!("send: {expected:?}")) + .await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> TestResult { //given let fs = given::a_filesystem(); @@ -391,96 +377,87 @@ async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ValidateRepo::new( - MessageToken::default(), - )) - .await?; - System::current().stop(); + tell!(addr, ValidateRepo::new(MessageToken::default()))?; //then - log.require_message_containing("send: NotifyUser")?; + log.require_message_containing("send: NotifyUser").await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn should_accept_message_with_current_token() -> TestResult { //given let fs = given::a_filesystem(); let repo_details = given::repo_details(&fs); - - //when + let net: Net = given::a_network().into(); + let server_actor_ref = given::a_server_actor(fs.as_real(), net.clone()); let (actor, log) = given::a_repo_actor( repo_details, git::repository::factory::mock(), given::a_forge(), - given::a_network().into(), + server_actor_ref, + net, ); let actor = actor.with_message_token(MessageToken::new(2_u32)); - let addr = actor.start(); - addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new( - 2_u32, - ))) - .await?; - System::current().stop(); + let addr = kameo::spawn(actor); + // + //when + tell!(addr, ValidateRepo::new(MessageToken::new(2_u32)))?; //then - log.require_message_containing("accepted token: 2")?; + log.require_message_containing("accepted token: 2").await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn should_accept_message_with_new_token() -> TestResult { //given let fs = given::a_filesystem(); let repo_details = given::repo_details(&fs); + let net: Net = given::a_network().into(); //when let (actor, log) = given::a_repo_actor( repo_details, git::repository::factory::mock(), given::a_forge(), - given::a_network().into(), + given::a_server_actor(fs.as_real(), net.clone()), + net, ); let actor = actor.with_message_token(MessageToken::new(2_u32)); - let addr = actor.start(); - addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new( - 3_u32, - ))) - .await?; - System::current().stop(); + let addr = kameo::spawn(actor); + tell!(addr, ValidateRepo::new(MessageToken::new(3_u32)))?; //then - log.require_message_containing("accepted token: 3")?; + log.require_message_containing("accepted token: 3").await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn should_reject_message_with_expired_token() -> TestResult { //given let fs = given::a_filesystem(); let repo_details = given::repo_details(&fs); + let net: Net = given::a_network().into(); //when let (actor, log) = given::a_repo_actor( repo_details, git::repository::factory::mock(), given::a_forge(), - given::a_network().into(), + given::a_server_actor(fs.as_real(), net.clone()), + net, ); let actor = actor.with_message_token(MessageToken::new(4_u32)); - let addr = actor.start(); - addr.send(crate::repo::messages::ValidateRepo::new(MessageToken::new( - 3_u32, - ))) - .await?; - System::current().stop(); + let addr = kameo::spawn(actor); + tell!(addr, ValidateRepo::new(MessageToken::new(3_u32)))?; //then - log.no_message_contains("accepted token")?; + log.no_message_contains("accepted token").await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] // NOTE: failed then passed on retry: count = 6 async fn should_send_validate_repo_when_retryable_error() -> TestResult { //given @@ -497,20 +474,16 @@ async fn should_send_validate_repo_when_retryable_error() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ValidateRepo::new( - MessageToken::default(), - )) - .await?; - actix_rt::time::sleep(std::time::Duration::from_millis(2)).await; - System::current().stop(); + tell!(addr, ValidateRepo::new(MessageToken::default()))?; + tokio::time::sleep(std::time::Duration::from_millis(2)).await; //then - log.require_message_containing("accepted token: 0")?; - log.require_message_containing("send: ValidateRepo")?; + log.require_message_containing("accepted token: 0").await?; + log.require_message_containing("send: ValidateRepo").await?; Ok(()) } -#[test_log::test(actix::test)] +#[test_log::test(tokio::test)] async fn should_send_notify_user_when_non_retryable_error() -> TestResult { //given let fs = given::a_filesystem(); @@ -546,15 +519,11 @@ async fn should_send_notify_user_when_non_retryable_error() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::ValidateRepo::new( - MessageToken::default(), - )) - .await?; - actix_rt::time::sleep(std::time::Duration::from_millis(1)).await; - System::current().stop(); + tell!(addr, ValidateRepo::new(MessageToken::default()))?; + tokio::time::sleep(std::time::Duration::from_millis(1)).await; //then - log.require_message_containing("accepted token")?; - log.require_message_containing("send: NotifyUser")?; + log.require_message_containing("accepted token").await?; + log.require_message_containing("send: NotifyUser").await?; Ok(()) } diff --git a/crates/cli/src/repo/tests/handlers/webhook_notification.rs b/crates/cli/src/repo/tests/handlers/webhook_notification.rs index 9f6994a..242953c 100644 --- a/crates/cli/src/repo/tests/handlers/webhook_notification.rs +++ b/crates/cli/src/repo/tests/handlers/webhook_notification.rs @@ -1,7 +1,11 @@ +use kxio::net::Net; + +use crate::{repo::messages::WebhookNotification, tell}; + // use super::*; -#[actix::test] +#[tokio::test] async fn when_no_expected_auth_token_drop_notification() -> TestResult { //given let fs = given::a_filesystem(); @@ -12,30 +16,30 @@ async fn when_no_expected_auth_token_drop_notification() -> TestResult { let body = Body::new(String::new()); let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body); let repository_factory = MockRepositoryFactory::new(); + let net: Net = given::a_network().into(); let (actor, log) = given::a_repo_actor( repo_details, Box::new(repository_factory), given::a_forge(), - given::a_network().into(), + given::a_server_actor(fs.as_real(), net.clone()), + net, ); let actor = actor.with_webhook_auth(None); //when - actor - .start() - .send(crate::repo::messages::WebhookNotification::new( - forge_notification, - )) - .await?; - System::current().stop(); + tell!( + kameo::spawn(actor), + WebhookNotification::new(forge_notification) + )?; //then - log.no_message_contains("send")?; - log.require_message_containing("server has no auth token")?; + log.no_message_contains("send").await?; + log.require_message_containing("server has no auth token") + .await?; Ok(()) } -#[actix::test] +#[tokio::test] async fn when_no_repo_config_drop_notification() -> TestResult { //given let fs = given::a_filesystem(); @@ -46,30 +50,30 @@ async fn when_no_repo_config_drop_notification() -> TestResult { let body = Body::new(String::new()); let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body); let repository_factory = MockRepositoryFactory::new(); + let net: Net = given::a_network().into(); let (actor, log) = given::a_repo_actor( repo_details, Box::new(repository_factory), given::a_forge(), - given::a_network().into(), + given::a_server_actor(fs.as_real(), net.clone()), + net, ); let actor = actor.with_webhook_auth(Some(given::a_webhook_auth())); //when - actor - .start() - .send(crate::repo::messages::WebhookNotification::new( - forge_notification, - )) - .await?; - System::current().stop(); + tell!( + kameo::spawn(actor), + WebhookNotification::new(forge_notification) + )?; //then - log.no_message_contains("send")?; - log.require_message_containing("server has no repo config")?; + log.no_message_contains("send").await?; + log.require_message_containing("server has no repo config") + .await?; Ok(()) } -#[actix::test] +#[tokio::test] async fn when_message_auth_is_invalid_drop_notification() -> TestResult { //given let fs = given::a_filesystem(); @@ -84,30 +88,30 @@ async fn when_message_auth_is_invalid_drop_notification() -> TestResult { forge .expect_is_message_authorised() .return_once(|_, _| false); // is not valid + let net: Net = given::a_network().into(); let (actor, log) = given::a_repo_actor( repo_details, Box::new(repository_factory), forge, - given::a_network().into(), + given::a_server_actor(fs.as_real(), net.clone()), + net, ); let actor = actor.with_webhook_auth(Some(given::a_webhook_auth())); //when - actor - .start() - .send(crate::repo::messages::WebhookNotification::new( - forge_notification, - )) - .await?; - System::current().stop(); + tell!( + kameo::spawn(actor), + WebhookNotification::new(forge_notification) + )?; //then - log.no_message_contains("send")?; - log.require_message_containing("message authorisation is invalid")?; + log.no_message_contains("send").await?; + log.require_message_containing("message authorisation is invalid") + .await?; Ok(()) } -#[actix::test] +#[tokio::test] async fn when_message_is_ignorable_drop_notification() -> TestResult { //given let fs = given::a_filesystem(); @@ -126,30 +130,30 @@ async fn when_message_is_ignorable_drop_notification() -> TestResult { forge .expect_parse_webhook_body() .return_once(|_| Err(git::forge::webhook::Error::NetworkResponseEmpty)); + let net: Net = given::a_network().into(); let (actor, log) = given::a_repo_actor( repo_details, Box::new(repository_factory), forge, - given::a_network().into(), + given::a_server_actor(fs.as_real(), net.clone()), + net, ); let actor = actor.with_webhook_auth(Some(given::a_webhook_auth())); //when - actor - .start() - .send(crate::repo::messages::WebhookNotification::new( - forge_notification, - )) - .await?; - System::current().stop(); + tell!( + kameo::spawn(actor), + WebhookNotification::new(forge_notification) + )?; //then - log.no_message_contains("send")?; - log.require_message_containing("forge sent ignorable message")?; + log.no_message_contains("send").await?; + log.require_message_containing("forge sent ignorable message") + .await?; Ok(()) } -#[actix::test] +#[tokio::test] async fn when_message_is_not_a_push_drop_notification() -> TestResult { //given let fs = given::a_filesystem(); @@ -168,30 +172,30 @@ async fn when_message_is_not_a_push_drop_notification() -> TestResult { forge .expect_parse_webhook_body() .return_once(|_| Err(git::forge::webhook::Error::NetworkResponseEmpty)); + let net: Net = given::a_network().into(); let (actor, log) = given::a_repo_actor( repo_details, Box::new(repository_factory), forge, - given::a_network().into(), + given::a_server_actor(fs.as_real(), net.clone()), + net, ); let actor = actor.with_webhook_auth(Some(given::a_webhook_auth())); //when - actor - .start() - .send(crate::repo::messages::WebhookNotification::new( - forge_notification, - )) - .await?; - System::current().stop(); + tell!( + kameo::spawn(actor), + WebhookNotification::new(forge_notification) + )?; //then - log.no_message_contains("send")?; - log.require_message_containing("message parse error - not a push")?; + log.no_message_contains("send").await?; + log.require_message_containing("message parse error - not a push") + .await?; Ok(()) } -#[actix::test] +#[tokio::test] async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResult { //given let fs = given::a_filesystem(); @@ -214,32 +218,31 @@ async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResul .return_once(|_, _| true); // is valid forge.expect_should_ignore_message().returning(|_| false); forge.expect_parse_webhook_body().return_once(|_| Ok(push)); + let net: Net = given::a_network().into(); let (actor, log) = given::a_repo_actor( repo_details, Box::new(repository_factory), forge, - given::a_network().into(), + given::a_server_actor(fs.as_real(), net.clone()), + net, ); let actor = actor .with_webhook_auth(Some(given::a_webhook_auth())) .with_last_main_commit(commit); //when - actor - .start() - .send(crate::repo::messages::WebhookNotification::new( - forge_notification, - )) - .await?; - System::current().stop(); + tell!( + kameo::spawn(actor), + WebhookNotification::new(forge_notification) + )?; //then - log.no_message_contains("send")?; - log.require_message_containing("unknown branch")?; + log.no_message_contains("send").await?; + log.require_message_containing("unknown branch").await?; Ok(()) } -#[actix::test] +#[tokio::test] async fn when_message_is_push_already_seen_commit_to_main() -> TestResult { //given let fs = given::a_filesystem(); @@ -263,32 +266,32 @@ async fn when_message_is_push_already_seen_commit_to_main() -> TestResult { .return_once(|_, _| true); // is valid forge.expect_should_ignore_message().returning(|_| false); forge.expect_parse_webhook_body().return_once(|_| Ok(push)); + let net: Net = given::a_network().into(); let (actor, log) = given::a_repo_actor( repo_details, Box::new(repository_factory), forge, - given::a_network().into(), + given::a_server_actor(fs.as_real(), net.clone()), + net, ); let actor = actor .with_webhook_auth(Some(given::a_webhook_auth())) .with_last_main_commit(commit); //when - actor - .start() - .send(crate::repo::messages::WebhookNotification::new( - forge_notification, - )) - .await?; - System::current().stop(); + tell!( + kameo::spawn(actor), + WebhookNotification::new(forge_notification) + )?; //then - log.no_message_contains("send")?; - log.require_message_containing(format!("not a new commit on {main}"))?; + log.no_message_contains("send").await?; + log.require_message_containing(format!("not a new commit on {main}")) + .await?; Ok(()) } -#[actix::test] +#[tokio::test] async fn when_message_is_push_already_seen_commit_to_next() -> TestResult { //given let fs = given::a_filesystem(); @@ -301,9 +304,9 @@ async fn when_message_is_push_already_seen_commit_to_next() -> TestResult { let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body); let repository_factory = MockRepositoryFactory::new(); let commit = given::a_commit(); - let next = repo_config.branches().next(); + let next_branch = repo_config.branches().next(); let push = given::a_push() - .with_branch(next.clone()) + .with_branch(next_branch.clone()) .with_sha(commit.sha().to_string()) .with_message(commit.message().to_string()); let mut forge = given::a_forge(); @@ -312,32 +315,32 @@ async fn when_message_is_push_already_seen_commit_to_next() -> TestResult { .return_once(|_, _| true); // is valid forge.expect_should_ignore_message().returning(|_| false); forge.expect_parse_webhook_body().return_once(|_| Ok(push)); + let net: Net = given::a_network().into(); let (actor, log) = given::a_repo_actor( repo_details, Box::new(repository_factory), forge, - given::a_network().into(), + given::a_server_actor(fs.as_real(), net.clone()), + net, ); let actor = actor .with_webhook_auth(Some(given::a_webhook_auth())) .with_last_next_commit(commit); //when - actor - .start() - .send(crate::repo::messages::WebhookNotification::new( - forge_notification, - )) - .await?; - System::current().stop(); + tell!( + kameo::spawn(actor), + WebhookNotification::new(forge_notification) + )?; //then - log.no_message_contains("send")?; - log.require_message_containing(format!("not a new commit on {next}"))?; + log.no_message_contains("send").await?; + log.require_message_containing(format!("not a new commit on {next_branch}")) + .await?; Ok(()) } -#[actix::test] +#[tokio::test] async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult { //given let fs = given::a_filesystem(); @@ -361,32 +364,32 @@ async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult { .return_once(|_, _| true); // is valid forge.expect_should_ignore_message().returning(|_| false); forge.expect_parse_webhook_body().return_once(|_| Ok(push)); + let net: Net = given::a_network().into(); let (actor, log) = given::a_repo_actor( repo_details, Box::new(repository_factory), forge, - given::a_network().into(), + given::a_server_actor(fs.as_real(), net.clone()), + net, ); let actor = actor .with_webhook_auth(Some(given::a_webhook_auth())) .with_last_dev_commit(commit); //when - actor - .start() - .send(crate::repo::messages::WebhookNotification::new( - forge_notification, - )) - .await?; - System::current().stop(); + tell!( + kameo::spawn(actor), + WebhookNotification::new(forge_notification) + )?; //then - log.no_message_contains("send")?; - log.require_message_containing(format!("not a new commit on {dev}"))?; + log.no_message_contains("send").await?; + log.require_message_containing(format!("not a new commit on {dev}")) + .await?; Ok(()) } -#[actix::test] +#[tokio::test] async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo() -> TestResult { //given let fs = given::a_filesystem(); @@ -409,32 +412,33 @@ async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo( .return_once(|_, _| true); // is valid forge.expect_should_ignore_message().returning(|_| false); forge.expect_parse_webhook_body().return_once(|_| Ok(push)); + let net: Net = given::a_network().into(); let (actor, log) = given::a_repo_actor( repo_details, Box::new(repository_factory), forge, - given::a_network().into(), + given::a_server_actor(fs.as_real(), net.clone()), + net, ); let actor = actor .with_webhook_auth(Some(given::a_webhook_auth())) .with_last_main_commit(given::a_commit()); //when - let addr = actor.start(); - addr.send(crate::repo::messages::WebhookNotification::new( - forge_notification, - )) - .await?; - System::current().stop(); + let addr = kameo::spawn(actor); + tell!(addr, WebhookNotification::new(forge_notification))?; //then - let view = addr.send(ExamineActor).await?; + let view = addr + .ask(ExamineActor) + .await + .map_err(|e| format!("examine actor: {e:?}"))?; assert_eq!(view.last_main_commit, Some(push_commit)); - log.require_message_containing("send: ValidateRepo")?; + log.require_message_containing("send: ValidateRepo").await?; Ok(()) } -#[actix::test] +#[tokio::test] async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo() -> TestResult { //given let fs = given::a_filesystem(); @@ -457,32 +461,33 @@ async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo( .return_once(|_, _| true); // is valid forge.expect_should_ignore_message().returning(|_| false); forge.expect_parse_webhook_body().return_once(|_| Ok(push)); + let net: Net = given::a_network().into(); let (actor, log) = given::a_repo_actor( repo_details, Box::new(repository_factory), forge, - given::a_network().into(), + given::a_server_actor(fs.as_real(), net.clone()), + net, ); let actor = actor .with_webhook_auth(Some(given::a_webhook_auth())) .with_last_next_commit(given::a_commit()); //when - let addr = actor.start(); - addr.send(crate::repo::messages::WebhookNotification::new( - forge_notification, - )) - .await?; - System::current().stop(); + let addr = kameo::spawn(actor); + tell!(addr, WebhookNotification::new(forge_notification))?; //then - let view = addr.send(ExamineActor).await?; + let view = addr + .ask(ExamineActor) + .await + .map_err(|e| format!("examine actor: {e:?}"))?; assert_eq!(view.last_next_commit, Some(push_commit)); - log.require_message_containing("send: ValidateRepo")?; + log.require_message_containing("send: ValidateRepo").await?; Ok(()) } -#[actix::test] +#[tokio::test] async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo() -> TestResult { //given let fs = given::a_filesystem(); @@ -505,27 +510,28 @@ async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo() .return_once(|_, _| true); // is valid forge.expect_should_ignore_message().returning(|_| false); forge.expect_parse_webhook_body().return_once(|_| Ok(push)); + let net: Net = given::a_network().into(); let (actor, log) = given::a_repo_actor( repo_details, Box::new(repository_factory), forge, - given::a_network().into(), + given::a_server_actor(fs.as_real(), net.clone()), + net, ); let actor = actor .with_webhook_auth(Some(given::a_webhook_auth())) .with_last_dev_commit(given::a_commit()); //when - let addr = actor.start(); - addr.send(crate::repo::messages::WebhookNotification::new( - forge_notification, - )) - .await?; - System::current().stop(); + let addr = kameo::spawn(actor); + tell!(addr, WebhookNotification::new(forge_notification))?; //then - let view = addr.send(ExamineActor).await?; + let view = addr + .ask(ExamineActor) + .await + .map_err(|e| format!("examine actor: {e:?}"))?; assert_eq!(view.last_dev_commit, Some(push_commit)); - log.require_message_containing("send: ValidateRepo")?; + log.require_message_containing("send: ValidateRepo").await?; Ok(()) } diff --git a/crates/cli/src/repo/tests/handlers/webhook_registered.rs b/crates/cli/src/repo/tests/handlers/webhook_registered.rs index e435988..fca374e 100644 --- a/crates/cli/src/repo/tests/handlers/webhook_registered.rs +++ b/crates/cli/src/repo/tests/handlers/webhook_registered.rs @@ -1,7 +1,9 @@ +use crate::{repo::messages::WebhookRegistered, tell}; + // use super::*; -#[actix::test] +#[tokio::test] async fn should_store_webhook_details() -> TestResult { //given let fs = given::a_filesystem(); @@ -15,21 +17,23 @@ async fn should_store_webhook_details() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::WebhookRegistered::new(( - webhook_id.clone(), - webhook_auth.clone(), - ))) - .await?; - System::current().stop(); + tell!( + addr, + WebhookRegistered::new((webhook_id.clone(), webhook_auth.clone())) + )?; //then - let view = addr.send(ExamineActor).await?; + // let view = addr.ask(ExamineActor).await.expect("examine actor"); + let view = addr + .ask(ExamineActor) + .await + .map_err(|e| format!("examine actor: {e:?}"))?; assert_eq!(view.webhook_id, Some(webhook_id)); assert_eq!(view.webhook_auth, Some(webhook_auth)); Ok(()) } -#[actix::test] +#[tokio::test] async fn should_send_validate_repo_message() -> TestResult { //given let fs = given::a_filesystem(); @@ -43,14 +47,12 @@ async fn should_send_validate_repo_message() -> TestResult { repo_details, given::a_forge(), ); - addr.send(crate::repo::messages::WebhookRegistered::new(( - webhook_id.clone(), - webhook_auth.clone(), - ))) - .await?; - System::current().stop(); + tell!( + addr, + WebhookRegistered::new((webhook_id.clone(), webhook_auth.clone())) + )?; //then - log.require_message_containing("send: ValidateRepo")?; + log.require_message_containing("send: ValidateRepo").await?; Ok(()) } diff --git a/crates/cli/src/repo/tests/load.rs b/crates/cli/src/repo/tests/load.rs index 81a391a..924d15a 100644 --- a/crates/cli/src/repo/tests/load.rs +++ b/crates/cli/src/repo/tests/load.rs @@ -4,7 +4,7 @@ use super::*; use crate::git::file; use crate::repo::load; -#[actix::test] +#[tokio::test] async fn when_file_not_found_should_error() -> TestResult { //given let fs = given::a_filesystem(); @@ -13,7 +13,7 @@ async fn when_file_not_found_should_error() -> TestResult { .expect_read_file() .returning(|_, _| Err(file::Error::FileNotFound)); //when - let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await); + let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await); //then debug!("Got: {err:?}"); assert!(matches!( @@ -22,7 +22,7 @@ async fn when_file_not_found_should_error() -> TestResult { )); Ok(()) } -#[actix::test] +#[tokio::test] async fn when_file_format_invalid_should_error() -> TestResult { //given let fs = given::a_filesystem(); @@ -32,14 +32,14 @@ async fn when_file_format_invalid_should_error() -> TestResult { .expect_read_file() .return_once(move |_, _| Ok(contents)); //when - let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await); + let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await); //then debug!("Got: {err:?}"); assert!(matches!(err, load::Error::Toml(_))); Ok(()) } -#[actix::test] +#[tokio::test] async fn when_main_branch_is_missing_should_error() -> TestResult { //given let fs = given::a_filesystem(); @@ -64,14 +64,14 @@ async fn when_main_branch_is_missing_should_error() -> TestResult { .expect_remote_branches() .return_once(move || Ok(branches)); //when - let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await); + let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await); //then debug!("Got: {err:?}"); assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == main)); Ok(()) } -#[actix::test] +#[tokio::test] async fn when_next_branch_is_missing_should_error() -> TestResult { //given let fs = given::a_filesystem(); @@ -96,14 +96,14 @@ async fn when_next_branch_is_missing_should_error() -> TestResult { .expect_remote_branches() .return_once(move || Ok(branches)); //when - let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await); + let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await); //then debug!("Got: {err:?}"); assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == next)); Ok(()) } -#[actix::test] +#[tokio::test] async fn when_dev_branch_is_missing_should_error() -> TestResult { //given let fs = given::a_filesystem(); @@ -128,14 +128,14 @@ async fn when_dev_branch_is_missing_should_error() -> TestResult { .expect_remote_branches() .return_once(move || Ok(branches)); //when - let_assert!(Err(err) = load::config_from_repository(repo_details, &open_repository).await); + let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await); //then debug!("Got: {err:?}"); assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == dev)); Ok(()) } -#[actix::test] +#[tokio::test] async fn when_valid_file_should_return_repo_config() -> TestResult { //given let fs = given::a_filesystem(); @@ -161,7 +161,7 @@ async fn when_valid_file_should_return_repo_config() -> TestResult { .expect_remote_branches() .return_once(move || Ok(branches)); //when - let_assert!(Ok(result) = load::config_from_repository(repo_details, &open_repository).await); + let_assert!(Ok(result) = load::config_from_repository(&repo_details, &open_repository).await); //then debug!("Got: {result:?}"); assert_eq!(result, repo_config); diff --git a/crates/cli/src/repo/tests/mod.rs b/crates/cli/src/repo/tests/mod.rs index 9249721..a4e1d97 100644 --- a/crates/cli/src/repo/tests/mod.rs +++ b/crates/cli/src/repo/tests/mod.rs @@ -1,6 +1,4 @@ // -use actix::prelude::*; - use crate::{ git, repo::{ @@ -12,7 +10,6 @@ use crate::{ use git_next_core::{ git::{ commit::Sha, - forge::commit::Status, repository::{ factory::{mock, MockRepositoryFactory, RepositoryFactory}, open::{MockOpenRepositoryLike, OpenRepositoryLike}, @@ -28,12 +25,17 @@ use git_next_core::{ }; use assert2::let_assert; +use kameo::{ + message::{Context, Message}, + Reply, +}; use mockall::predicate::eq; use tracing::{debug, error}; use std::{ collections::{BTreeMap, HashMap}, sync::{Arc, RwLock}, + time::Duration, }; type TestResult = Result<(), Box>; @@ -45,33 +47,43 @@ mod handlers; mod load; mod when; +pub async fn tick(millis: u64) { + tokio::time::sleep(Duration::from_millis(millis)).await; +} + impl ActorLog { - pub fn no_message_contains(&self, needle: impl AsRef + std::fmt::Display) -> TestResult { - if self.find_in_messages(needle.as_ref())? { + pub async fn no_message_contains( + &self, + needle: impl AsRef + Send + std::fmt::Display, + ) -> TestResult { + if self.find_in_messages(needle.as_ref()).await? { error!(?self, ""); panic!("found unexpected message: {needle}"); } Ok(()) } - pub fn require_message_containing( + pub async fn require_message_containing( &self, - needle: impl AsRef + std::fmt::Display, + needle: impl AsRef + Send + std::fmt::Display, ) -> TestResult { - if !self.find_in_messages(needle.as_ref())? { + if !self.find_in_messages(needle.as_ref()).await? { error!(?self, ""); panic!("expected message not found: {needle}"); } Ok(()) } - fn find_in_messages( + async fn find_in_messages( &self, - needle: impl AsRef, + needle: impl AsRef + Send, ) -> Result> { + // Very short sleep to allow tests to get a chance to tick + // This should be enough for most tests. + tokio::time::sleep(Duration::from_millis(5)).await; let found = self .read() - .map_err(|e| e.to_string())? + .await .iter() .any(|message| message.contains(needle.as_ref())); Ok(found) @@ -79,15 +91,19 @@ impl ActorLog { } message!(ExamineActor => RepoActorView, "Request a view of the current state of the [RepoActor]."); -impl Handler for RepoActor { - type Result = RepoActorView; +impl Message for RepoActor { + type Reply = RepoActorView; - fn handle(&mut self, _msg: ExamineActor, _ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + _msg: ExamineActor, + _ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { let repo_actor: &Self = self; - Self::Result::from(repo_actor) + Self::Reply::from(repo_actor) } } -#[derive(Debug, MessageResponse)] +#[derive(Debug, Reply)] pub struct RepoActorView { pub repo_details: RepoDetails, pub webhook_id: Option, // INFO: if [None] then no webhook is configured diff --git a/crates/cli/src/repo/tests/when.rs b/crates/cli/src/repo/tests/when.rs index 83d67a1..350a8e5 100644 --- a/crates/cli/src/repo/tests/when.rs +++ b/crates/cli/src/repo/tests/when.rs @@ -1,37 +1,37 @@ // +use kameo::actor::ActorRef; +use kxio::{fs::FileSystem, net::Net}; + +use crate::server::ServerActor; + use super::*; pub fn start_actor( repository_factory: MockRepositoryFactory, repo_details: RepoDetails, forge: Box, -) -> (actix::Addr, ActorLog) { + fs: FileSystem, + net: Net, +) -> (ActorRef, ActorLog) { let (actor, log) = given::a_repo_actor( repo_details, Box::new(repository_factory), forge, - given::a_network().into(), + given::a_server_actor(fs, net.clone()), + net, ); - (actor.start(), log) + (kameo::spawn(actor), log) } pub fn start_actor_with_open_repository( open_repository: Box, repo_details: RepoDetails, forge: Box, -) -> (actix::Addr, ActorLog) { - let (actor, log) = given::a_repo_actor(repo_details, mock(), forge, given::a_network().into()); +) -> (ActorRef, ActorLog) { + let fs = given::a_filesystem(); + let net: Net = given::a_network().into(); + let server_actor_ref: ActorRef = given::a_server_actor(fs.as_real(), net.clone()); + let (actor, log) = given::a_repo_actor(repo_details, mock(), forge, server_actor_ref, net); let actor = actor.with_open_repository(Some(open_repository)); - (actor.start(), log) -} - -pub fn commit_status(forge: &mut MockForgeLike, commit: Commit, status: Status) { - let mut commit_status_forge = MockForgeLike::new(); - commit_status_forge - .expect_commit_status() - .with(mockall::predicate::eq(commit)) - .return_once(|_| Ok(status)); - forge - .expect_duplicate() - .return_once(move || Box::new(commit_status_forge)); + (kameo::spawn(actor), log) } diff --git a/crates/cli/src/root.rs b/crates/cli/src/root.rs new file mode 100644 index 0000000..94fca5c --- /dev/null +++ b/crates/cli/src/root.rs @@ -0,0 +1,221 @@ +// + +// Root actor for all other actors - supervises them all + +use std::time::Duration; + +use color_eyre::Result; +use derive_more::derive::Constructor; +use git_next_core::{git::RepositoryFactory, s}; +use kameo::{ + actor::{pubsub::PubSub, ActorRef}, + mailbox::unbounded::UnboundedMailbox, + message::{Context, Message}, + Actor, +}; +use kxio::{fs::FileSystem, net::Net}; + +#[cfg(feature = "tui")] +use crate::tui::Tui; +use crate::{ + alerts::{AlertsActor, History}, + base_actor::BaseActor, + default_on_actor_panic, default_on_actor_start, + file_watcher::{FileUpdated, FileWatcherActor}, + on_actor_link_died, on_actor_stop, publish, + server::{actor::messages::ServerUpdate, ServerActor}, + subscribe, MessageBus, +}; + +#[derive(Debug)] +pub struct RootActor { + ui: bool, + fs: FileSystem, + net: Net, + sleep_duration: std::time::Duration, + tx_shutdown: tokio::sync::mpsc::Sender, + + alerts_actor_ref: Option>, + server_updates_bus: Option>, + server_actor_ref: Option>, + file_updates_bus: Option>, + file_watcher_actor_ref: Option>, + + #[cfg(feature = "tui")] + tui_actor_ref: Option>, +} + +impl RootActor { + pub const fn new( + ui: bool, + fs: FileSystem, + net: Net, + sleep_duration: std::time::Duration, + tx_shutdown: tokio::sync::mpsc::Sender, + ) -> Self { + Self { + ui, + fs, + net, + sleep_duration, + tx_shutdown, + + alerts_actor_ref: None, + server_updates_bus: None, + server_actor_ref: None, + file_updates_bus: None, + file_watcher_actor_ref: None, + + #[cfg(feature = "tui")] + tui_actor_ref: None, + } + } +} + +const A_DAY: Duration = Duration::from_secs(24 * 60 * 60); + +type RootContext<'a> = Context<'a, RootActor, Result<()>>; + +#[derive(Constructor, Debug)] +pub struct Start { + repo: Box, +} + +impl Message for RootActor { + type Reply = Result<()>; + + async fn handle(&mut self, msg: Start, ctx: RootContext<'_>) -> Self::Reply { + let alerts_actor_ref = self.start_alerts_actor(&ctx).await?; + + let server_updates_bus = self.start_server_updates_bus(&ctx).await; + let file_updates_bus = self.start_file_updates_bus(&ctx).await; + + self.start_server_actor( + &ctx, + alerts_actor_ref, + server_updates_bus.clone(), + file_updates_bus.clone(), + msg.repo, + ) + .await?; + + self.start_file_watcher_actor(&ctx, file_updates_bus.clone()) + .await; + + #[cfg(feature = "tui")] + if self.ui { + self.start_tui_actor(&ctx, server_updates_bus).await?; + } + + // trigger initial config file to load + publish!(file_updates_bus, FileUpdated)?; + + Ok(()) + } +} +impl RootActor { + async fn start_alerts_actor(&mut self, ctx: &RootContext<'_>) -> Result> { + let actor_ref = AlertsActor::new(None, History::new(A_DAY), self.net.clone()) + .spawn(ctx.actor_ref()) + .await; + self.alerts_actor_ref.replace(actor_ref.clone()); + Ok(actor_ref) + } + + async fn start_file_watcher_actor( + &mut self, + ctx: &RootContext<'_>, + file_updates_bus: MessageBus, + ) -> ActorRef { + let actor_ref = FileWatcherActor::new( + file_updates_bus, + self.fs.base().join("git-next-server.toml"), + ) + .spawn_in_thread(ctx.actor_ref()) + .await; + self.file_watcher_actor_ref.replace(actor_ref.clone()); + actor_ref + } + + async fn start_server_updates_bus( + &mut self, + ctx: &RootContext<'_>, + ) -> MessageBus { + let actor_ref = PubSub::::new().spawn(ctx.actor_ref()).await; + self.server_updates_bus.replace(actor_ref.clone()); + actor_ref + } + + async fn start_file_updates_bus(&mut self, ctx: &RootContext<'_>) -> MessageBus { + let actor_ref = PubSub::::new().spawn(ctx.actor_ref()).await; + self.file_updates_bus.replace(actor_ref.clone()); + actor_ref + } + + async fn start_server_actor( + &mut self, + ctx: &RootContext<'_>, + alerts_actor_ref: ActorRef, + server_updates_bus: MessageBus, + file_updates_bus: MessageBus, + repo: Box, + ) -> Result> { + let actor_ref = ServerActor::new( + self.ui, + self.fs.clone(), + self.net.clone(), + alerts_actor_ref, + server_updates_bus, + repo, + self.sleep_duration, + ) + .spawn(ctx.actor_ref()) + .await; + subscribe!(file_updates_bus, "server", actor_ref.clone())?; + self.server_actor_ref.replace(actor_ref.clone()); + Ok(actor_ref) + } + + #[cfg(feature = "tui")] + async fn start_tui_actor( + &mut self, + ctx: &RootContext<'_>, + server_updates_bus: MessageBus, + ) -> Result> { + let actor_ref = Tui::new().spawn_in_thread(ctx.actor_ref()).await; + self.tui_actor_ref.replace(actor_ref.clone()); + subscribe!(server_updates_bus, actor_ref.clone())?; + Ok(actor_ref) + } +} + +impl Actor for RootActor { + type Mailbox = UnboundedMailbox; + + default_on_actor_start!(this, actor_ref); + default_on_actor_panic!(this, actor_ref, err); + + on_actor_link_died!(this, actor_ref, id, reason, { + this.tx_shutdown.send(s!("link died")).await?; + match &reason { + kameo::error::ActorStopReason::Normal => Ok(None), + kameo::error::ActorStopReason::Killed + | kameo::error::ActorStopReason::Panicked(_) + | kameo::error::ActorStopReason::LinkDied { .. } => { + Ok(Some(kameo::error::ActorStopReason::LinkDied { + id, + reason: Box::new(reason), + })) + } + } + }); + + on_actor_stop!(this, actor_ref, reason, { + #[allow(clippy::expect_used)] + this.tx_shutdown + .send(s!("stopping")) + .await + .expect("send shutdown"); + Ok(()) + }); +} diff --git a/crates/cli/src/server/actor/handlers/file_updated.rs b/crates/cli/src/server/actor/handlers/file_updated.rs index 95fe7c9..18a5d37 100644 --- a/crates/cli/src/server/actor/handlers/file_updated.rs +++ b/crates/cli/src/server/actor/handlers/file_updated.rs @@ -1,20 +1,36 @@ // -use actix::prelude::*; +use color_eyre::Result; +use kameo::message::{Context, Message}; use git_next_core::server::AppConfig; +use tracing::debug; use crate::{ file_watcher::FileUpdated, server::actor::{messages::ReceiveAppConfig, ServerActor}, + tell, }; -impl Handler for ServerActor { - type Result = (); +impl Message for ServerActor { + type Reply = Result<()>; - fn handle(&mut self, _msg: FileUpdated, ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + _msg: FileUpdated, + ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { + debug!("FileUpdated"); match AppConfig::load(&self.fs) { - Ok(app_config) => self.do_send(ReceiveAppConfig::new(app_config), ctx), - Err(err) => self.abort(ctx, format!("Failed to load config file. Error: {err}")), - }; + Ok(app_config) => Ok(tell!( + "server", + ctx.actor_ref(), + ReceiveAppConfig::new(app_config) + )?), + Err(err) => { + tracing::error!("Failed to load config file. Error: {err}"); + ctx.actor_ref().kill(); + Ok(()) + } + } } } diff --git a/crates/cli/src/server/actor/handlers/mod.rs b/crates/cli/src/server/actor/handlers/mod.rs index 4a5aea3..e882e85 100644 --- a/crates/cli/src/server/actor/handlers/mod.rs +++ b/crates/cli/src/server/actor/handlers/mod.rs @@ -4,4 +4,3 @@ mod receive_valid_app_config; mod server_update; mod shutdown; mod shutdown_trigger; -mod subscribe_updates; diff --git a/crates/cli/src/server/actor/handlers/receive_app_config.rs b/crates/cli/src/server/actor/handlers/receive_app_config.rs index a82acbb..cef857e 100644 --- a/crates/cli/src/server/actor/handlers/receive_app_config.rs +++ b/crates/cli/src/server/actor/handlers/receive_app_config.rs @@ -1,35 +1,46 @@ -use actix::prelude::*; +// +use color_eyre::Result; +use kameo::message::{Context, Message}; -use crate::server::actor::{ - messages::{ReceiveAppConfig, ReceiveValidAppConfig, ValidAppConfig}, - ServerActor, +use crate::{ + server::actor::{ + messages::{ReceiveAppConfig, ReceiveValidAppConfig, ValidAppConfig}, + ServerActor, + }, + tell, }; -impl Handler for ServerActor { - type Result = (); +impl Message for ServerActor { + type Reply = Result<()>; #[allow(clippy::cognitive_complexity)] - fn handle(&mut self, msg: ReceiveAppConfig, ctx: &mut Self::Context) -> Self::Result { - tracing::info!("recieved server config"); + async fn handle( + &mut self, + msg: ReceiveAppConfig, + ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { let Ok(socket_addr) = msg.listen_socket_addr() else { - return self.abort(ctx, "Unable to parse http.addr"); + self.abort(&ctx, "Unable to parse http.addr").await?; + return Ok(()); }; - let Some(server_storage) = self.server_storage(&msg) else { - return self.abort(ctx, "Server storage not available"); + self.abort(&ctx, "Server storage not available").await?; + return Ok(()); }; - if msg.listen().url().ends_with('/') { - return self.abort(ctx, "webhook.url must not end with a '/'"); + self.abort(&ctx, "webhook.url must not end with a '/'") + .await?; + return Ok(()); } - - self.do_send( + tell!( + "server", + ctx.actor_ref(), ReceiveValidAppConfig::new(ValidAppConfig::new( msg.peel(), socket_addr, server_storage, - )), - ctx, - ); + )) + )?; + Ok(()) } } diff --git a/crates/cli/src/server/actor/handlers/receive_valid_app_config.rs b/crates/cli/src/server/actor/handlers/receive_valid_app_config.rs index cb6d82f..bc60481 100644 --- a/crates/cli/src/server/actor/handlers/receive_valid_app_config.rs +++ b/crates/cli/src/server/actor/handlers/receive_valid_app_config.rs @@ -1,97 +1,111 @@ // -use actix::prelude::*; +use color_eyre::Result; +use kameo::{ + actor::ActorRef, + message::{Context, Message}, +}; use git_next_core::{ForgeAlias, RepoAlias}; -use tracing::info; use crate::{ alerts::messages::UpdateShout, + base_actor::BaseActor as _, repo::{messages::CloneRepo, RepoActor}, server::actor::{ messages::{ReceiveValidAppConfig, ServerUpdate, ValidAppConfig}, ServerActor, }, - webhook::{ - messages::ShutdownWebhook, - router::{AddWebhookRecipient, WebhookRouterActor}, - WebhookActor, - }, + spawn, tell, + webhook::{self, router::AddWebhookRecipient, WebhookActor}, }; -impl Handler for ServerActor { - type Result = (); +impl Message for ServerActor { + type Reply = Result<()>; - fn handle(&mut self, msg: ReceiveValidAppConfig, ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + msg: ReceiveValidAppConfig, + ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { let ValidAppConfig { app_config, socket_address, storage: server_storage, } = msg.peel(); // shutdown any existing webhook actor - if let Some(webhook_actor_addr) = self.webhook_actor_addr.take() { - webhook_actor_addr.do_send(ShutdownWebhook); + if let Some(webhook_actor_ref) = self.webhook_actor_ref.take() { + webhook_actor_ref.kill(); } self.generation.inc(); + // Webhook Server - info!("Starting Webhook Server..."); - let webhook_router = WebhookRouterActor::default().start(); - let listen_url = app_config.listen().url(); - let notify_user_recipient = self.alerts.clone().recipient(); - let server_addr = Some(ctx.address()); - // Forge Actors - for (forge_alias, forge_config) in app_config.forges() { - let repo_actors = self - .create_forge_repos( - forge_config, - forge_alias.clone(), - &server_storage, - listen_url, - ¬ify_user_recipient, - server_addr.clone(), - ) - .into_iter() - .map(start_repo_actor) - .collect::>(); - repo_actors - .iter() - .map(|(repo_alias, addr)| { - AddWebhookRecipient::new( + if let Some(webhook_router_actor_ref) = &self.webhook_router_actor_ref { + let listen_url = app_config.listen().url(); + let server_actor_ref = ctx.actor_ref(); + // Forge Actors + for (forge_alias, forge_config) in app_config.forges() { + let repo_actors = self + .create_forge_repos( + forge_config, forge_alias.clone(), - repo_alias.clone(), - addr.clone().recipient(), + &server_storage, + listen_url, + &self.alerts, + server_actor_ref.clone(), ) - }) - .for_each(|msg| webhook_router.do_send(msg)); - for (repo_alias, addr) in repo_actors { - self.repo_actors - .insert((forge_alias.clone(), repo_alias), addr); + .into_iter() + .map(|repo_actor_tuple| { + start_repo_actor(repo_actor_tuple, server_actor_ref.clone()) + }) + .collect::>(); + for repo_actor in repo_actors { + let (repo_alias, repo_actor_ref) = repo_actor.await?; + tell!( + webhook_router_actor_ref, + AddWebhookRecipient::new( + forge_alias.clone(), + repo_alias.clone(), + repo_actor_ref.clone(), + ) + )?; + + self.repo_actors + .insert((forge_alias.clone(), repo_alias), repo_actor_ref); + } } + let webhook_actor_ref = spawn!( + ctx.actor_ref(), + WebhookActor::new(socket_address, webhook_router_actor_ref.clone()) + ); + tell!(webhook_actor_ref, webhook::Start)?; + self.webhook_actor_ref.replace(webhook_actor_ref); } - let webhook_actor_addr = - WebhookActor::new(socket_address, webhook_router.recipient()).start(); - self.webhook_actor_addr.replace(webhook_actor_addr); let shout = app_config.shout().clone(); self.app_config.replace(app_config.clone()); - self.do_send( + tell!( + "server", + ctx.actor_ref(), ServerUpdate::AppConfigLoaded { app_config: ValidAppConfig { app_config, socket_address, storage: server_storage, }, - }, - ctx, - ); - self.alerts.do_send(UpdateShout::new(shout)); + } + )?; + tell!("alert", self.alerts, UpdateShout::new(shout))?; + Ok(()) } } -fn start_repo_actor(actor: (ForgeAlias, RepoAlias, RepoActor)) -> (RepoAlias, Addr) { - let (forge_name, repo_alias, actor) = actor; +async fn start_repo_actor( + actor: (ForgeAlias, RepoAlias, RepoActor), + server_actor_ref: ActorRef, +) -> Result<(RepoAlias, ActorRef)> { + let (forge_name, repo_alias, repo_actor) = actor; let span = tracing::info_span!("start_repo_actor", forge = %forge_name, repo = %repo_alias); let _guard = span.enter(); - let addr = actor.start(); - addr.do_send(CloneRepo); - tracing::info!("Started"); - (repo_alias, addr) + let repo_actor_ref = repo_actor.spawn(server_actor_ref).await; + tell!(repo_actor_ref, CloneRepo)?; + Ok((repo_alias, repo_actor_ref)) } diff --git a/crates/cli/src/server/actor/handlers/server_update.rs b/crates/cli/src/server/actor/handlers/server_update.rs index 0d5624d..08655d0 100644 --- a/crates/cli/src/server/actor/handlers/server_update.rs +++ b/crates/cli/src/server/actor/handlers/server_update.rs @@ -1,14 +1,20 @@ -use actix::Handler; - // -use crate::server::{actor::messages::ServerUpdate, ServerActor}; +use kameo::message::{Context, Message}; -impl Handler for ServerActor { - type Result = (); +use crate::{ + publish, + server::{actor::messages::ServerUpdate, ServerActor}, +}; - fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result { - self.subscribers.iter().for_each(move |subscriber| { - subscriber.do_send(msg.clone()); - }); +impl Message for ServerActor { + type Reply = color_eyre::Result<()>; + + async fn handle( + &mut self, + msg: ServerUpdate, + _ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { + publish!("server_updates_bus", self.server_updates_bus, msg)?; + Ok(()) } } diff --git a/crates/cli/src/server/actor/handlers/shutdown.rs b/crates/cli/src/server/actor/handlers/shutdown.rs index a1f65f0..91f0916 100644 --- a/crates/cli/src/server/actor/handlers/shutdown.rs +++ b/crates/cli/src/server/actor/handlers/shutdown.rs @@ -1,30 +1,35 @@ -//- - -use actix::prelude::*; -use tracing::debug; +// +use color_eyre::Result; +use kameo::message::{Context, Message}; +use tracing::{debug, info}; use crate::{ repo::messages::UnRegisterWebhook, server::actor::{messages::Shutdown, ServerActor}, - webhook::messages::ShutdownWebhook, + tell, }; -impl Handler for ServerActor { - type Result = (); +impl Message for ServerActor { + type Reply = Result<()>; - fn handle(&mut self, _msg: Shutdown, _ctx: &mut Self::Context) -> Self::Result { - self.repo_actors - .iter() - .for_each(|((forge_alias, repo_alias), addr)| { - debug!(%forge_alias, %repo_alias, "removing webhook"); - addr.do_send(UnRegisterWebhook::new()); - debug!(%forge_alias, %repo_alias, "removed webhook"); - }); + async fn handle( + &mut self, + _msg: Shutdown, + _ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { + for ((forge_alias, repo_alias), repo_actor_ref) in &self.repo_actors { + debug!(%forge_alias, %repo_alias, "removing webhook"); + tell!(repo_actor_ref, UnRegisterWebhook::new())?; + debug!(%forge_alias, %repo_alias, "removed webhook"); + repo_actor_ref.kill(); + info!(%forge_alias, %repo_alias, "killed repo actor"); + } debug!("server shutdown"); - if let Some(webhook) = self.webhook_actor_addr.take() { + if let Some(webhook_actor_ref) = self.webhook_actor_ref.take() { debug!("shutting down webhook"); - webhook.do_send(ShutdownWebhook); + webhook_actor_ref.kill(); debug!("webhook shutdown"); } + Ok(()) } } diff --git a/crates/cli/src/server/actor/handlers/shutdown_trigger.rs b/crates/cli/src/server/actor/handlers/shutdown_trigger.rs index 7469583..e1dd612 100644 --- a/crates/cli/src/server/actor/handlers/shutdown_trigger.rs +++ b/crates/cli/src/server/actor/handlers/shutdown_trigger.rs @@ -1,12 +1,16 @@ // -use actix::Handler; +use kameo::message::{Context, Message}; use crate::server::{actor::messages::ShutdownTrigger, ServerActor}; -impl Handler for ServerActor { - type Result = (); +impl Message for ServerActor { + type Reply = (); - fn handle(&mut self, msg: ShutdownTrigger, _ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + msg: ShutdownTrigger, + _ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { self.shutdown_trigger.replace(msg.peel()); } } diff --git a/crates/cli/src/server/actor/handlers/subscribe_updates.rs b/crates/cli/src/server/actor/handlers/subscribe_updates.rs deleted file mode 100644 index b844bf9..0000000 --- a/crates/cli/src/server/actor/handlers/subscribe_updates.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::server::actor::{messages::SubscribeToUpdates, ServerActor}; - -// -impl actix::Handler for ServerActor { - type Result = (); - - fn handle(&mut self, msg: SubscribeToUpdates, _ctx: &mut Self::Context) -> Self::Result { - self.subscribers.push(msg.peel()); - } -} diff --git a/crates/cli/src/server/actor/messages.rs b/crates/cli/src/server/actor/messages.rs index ca98552..3b5e165 100644 --- a/crates/cli/src/server/actor/messages.rs +++ b/crates/cli/src/server/actor/messages.rs @@ -1,6 +1,8 @@ // -use actix::{Message, Recipient}; +use std::net::SocketAddr; + use derive_more::Constructor; +use tokio::sync::mpsc::Sender; use git_next_core::{ git::{self, forge::commit::Status, graph::Log, Commit}, @@ -10,8 +12,6 @@ use git_next_core::{ ForgeAlias, RepoAlias, RepoBranches, RepoConfig, }; -use std::net::SocketAddr; - // receive server config message!( ReceiveAppConfig, @@ -38,8 +38,7 @@ message!( message!(Shutdown, "Notification to shutdown the server actor"); -#[derive(Clone, Debug, PartialEq, Eq, Message)] -#[rtype(result = "()")] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum ServerUpdate { /// List of all configured forges and aliases AppConfigLoaded { app_config: ValidAppConfig }, @@ -96,18 +95,11 @@ pub enum RepoUpdate { MainUpdated, } -message!( - SubscribeToUpdates, - Recipient, - "Subscribe to receive updates from the server" -); - /// Sends a channel to be used to shutdown the server -#[derive(Message, Constructor)] -#[rtype(result = "()")] -pub struct ShutdownTrigger(std::sync::mpsc::Sender); +#[derive(Constructor)] +pub struct ShutdownTrigger(Sender); impl ShutdownTrigger { - pub fn peel(self) -> std::sync::mpsc::Sender { + pub fn peel(self) -> Sender { self.0 } } diff --git a/crates/cli/src/server/actor/mod.rs b/crates/cli/src/server/actor/mod.rs index 7ecffff..9cdbf66 100644 --- a/crates/cli/src/server/actor/mod.rs +++ b/crates/cli/src/server/actor/mod.rs @@ -1,7 +1,33 @@ // -use actix::prelude::*; +use std::{ + collections::BTreeMap, + path::PathBuf, + sync::{Arc, RwLock}, +}; + +use kameo::{actor::ActorRef, mailbox::unbounded::UnboundedMailbox, message::Context, Actor}; +use kxio::{fs::FileSystem, net::Net}; +use tokio::sync::mpsc::Sender; +use tracing::{error, instrument, warn}; + +use git_next_core::{ + git::{repository::factory::RepositoryFactory, Generation, RepoDetails}, + s, + server::{self, AppConfig, ListenUrl, Storage}, + ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType, +}; + +use crate::{ + alerts::AlertsActor, + forge::Forge, + on_actor_link_died, on_actor_panic, on_actor_start, on_actor_stop, + repo::RepoActor, + spawn, tell, + webhook::{router::WebhookRouterActor, WebhookActor}, + MessageBus, +}; + use messages::{ReceiveAppConfig, ServerUpdate, Shutdown}; -use tracing::error; #[cfg(test)] mod tests; @@ -9,25 +35,6 @@ mod tests; mod handlers; pub mod messages; -use crate::{ - alerts::messages::NotifyUser, alerts::AlertsActor, forge::Forge, repo::RepoActor, - webhook::WebhookActor, -}; - -use git_next_core::{ - git::{repository::factory::RepositoryFactory, Generation, RepoDetails}, - server::{self, AppConfig, ListenUrl, Storage}, - ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType, -}; - -use kxio::{fs::FileSystem, net::Net}; - -use std::{ - collections::BTreeMap, - path::PathBuf, - sync::{Arc, RwLock}, -}; - #[derive(Debug, derive_more::Display, derive_more::From)] pub enum Error { #[display("Failed to create data directories")] @@ -41,65 +48,112 @@ pub enum Error { Config(server::Error), Io(std::io::Error), + + General(String), } +impl std::error::Error for Error {} + type Result = core::result::Result; #[allow(clippy::module_name_repetitions)] #[derive(derive_with::With)] #[with(message_log)] pub struct ServerActor { + ui: bool, app_config: Option, generation: Generation, - webhook_actor_addr: Option>, + webhook_actor_ref: Option>, fs: FileSystem, net: Net, - alerts: Addr, + alerts: ActorRef, repository_factory: Box, sleep_duration: std::time::Duration, - repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr>, + repo_actors: BTreeMap<(ForgeAlias, RepoAlias), ActorRef>, - shutdown_trigger: Option>, - subscribers: Vec>, + shutdown_trigger: Option>, + server_updates_bus: MessageBus, + webhook_router_actor_ref: Option>, // testing message_log: Option>>>, } + impl Actor for ServerActor { - type Context = Context; + type Mailbox = UnboundedMailbox; + + on_actor_start!(this, actor_ref, { + let webhook_router = spawn!(actor_ref, WebhookRouterActor::default()); + this.webhook_router_actor_ref.replace(webhook_router); + Ok(()) + }); + + on_actor_panic!(this, actor_ref, err, { + warn!(%err, "paniced: {}", ::name()); + Ok(Some(kameo::error::ActorStopReason::Panicked(err))) + }); + + on_actor_link_died!(this, actor_ref, id, reason, { + match &reason { + kameo::error::ActorStopReason::Killed | kameo::error::ActorStopReason::Normal => { + Ok(None) + } + kameo::error::ActorStopReason::Panicked(_) + | kameo::error::ActorStopReason::LinkDied { .. } => { + if let Some(actor_ref) = actor_ref.upgrade() { + tell!(actor_ref, Shutdown)?; + } + if let Some(trigger) = this.shutdown_trigger.take() { + trigger + .send(s!("link died")) + .await + .map_err(|e| format!("failed sending shutdown trigger: {e:?}"))?; + } + Ok(Some(kameo::error::ActorStopReason::LinkDied { + id, + reason: Box::new(reason), + })) + } + } + }); + + on_actor_stop!(this, actor_ref, reason, Ok(())); } impl ServerActor { pub fn new( + ui: bool, fs: FileSystem, net: Net, - alerts: Addr, + alerts: ActorRef, + server_updates_bus: MessageBus, repo: Box, sleep_duration: std::time::Duration, ) -> Self { - let generation = Generation::default(); Self { + ui, app_config: None, - generation, - webhook_actor_addr: None, + generation: Generation::default(), + webhook_actor_ref: None, fs, net, alerts, repository_factory: repo, shutdown_trigger: None, - subscribers: Vec::default(), + server_updates_bus, + webhook_router_actor_ref: None, sleep_duration, repo_actors: BTreeMap::new(), message_log: None, } } + fn create_forge_data_directories( &self, app_config: &AppConfig, server_dir: &std::path::Path, ) -> Result<()> { for (forge_name, _forge_config) in app_config.forges() { - let forge_dir: PathBuf = (&forge_name).into(); - let path = server_dir.join(&forge_dir); + let path = server_dir.join(PathBuf::from(&forge_name)); let path_handle = self.fs.path(&path); if path_handle.exists()? { if !path_handle.is_dir()? { @@ -110,45 +164,37 @@ impl ServerActor { self.fs.dir(&path).create_all()?; } } - Ok(()) } + #[instrument(skip_all)] fn create_forge_repos( &self, forge_config: &ForgeConfig, forge_name: ForgeAlias, server_storage: &Storage, listen_url: &ListenUrl, - notify_user_recipient: &Recipient, - server_addr: Option>, + notify_user_recipient: &ActorRef, + server_actor_ref: ActorRef, ) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> { - let span = - tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config); - - let _guard = span.enter(); - tracing::info!("Creating Forge"); - let mut repos = vec![]; + tracing::info!(%forge_name, %forge_config, ""); let creator = self.create_actor( forge_name, forge_config.clone(), server_storage, listen_url, - server_addr, + server_actor_ref, ); - for (repo_alias, server_repo_config) in forge_config.repos() { - let forge_repo = creator(( - repo_alias, - server_repo_config, - notify_user_recipient.clone(), - )); - tracing::info!( - alias = %forge_repo.1, - "Created Repo" - ); - repos.push(forge_repo); - } - repos + forge_config + .repos() + .map(|(repo_alias, server_repo_config)| { + creator(( + repo_alias, + server_repo_config, + notify_user_recipient.clone(), + )) + }) + .collect::>() } fn create_actor( @@ -157,9 +203,9 @@ impl ServerActor { forge_config: ForgeConfig, server_storage: &Storage, listen_url: &ListenUrl, - server_addr: Option>, + server_actor_ref: ActorRef, ) -> impl Fn( - (RepoAlias, &ServerRepoConfig, Recipient), + (RepoAlias, &ServerRepoConfig, ActorRef), ) -> (ForgeAlias, RepoAlias, RepoActor) { let server_storage = server_storage.clone(); let listen_url = listen_url.clone(); @@ -167,22 +213,19 @@ impl ServerActor { let repository_factory = self.repository_factory.duplicate(); let generation = self.generation; let sleep_duration = self.sleep_duration; + let ui = self.ui; move |(repo_alias, server_repo_config, notify_user_recipient)| { let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config); let _guard = span.enter(); - tracing::info!("Creating Repo"); - let gitdir = server_repo_config.gitdir().map_or_else( - || { - GitDir::new( - server_storage - .path() - .join(forge_name.to_string()) - .join(repo_alias.to_string()), - StoragePathType::Internal, - ) - }, - |gitdir| gitdir, - ); + let gitdir = server_repo_config.gitdir().unwrap_or_else(|| { + GitDir::new( + server_storage + .path() + .join(forge_name.to_string()) + .join(repo_alias.to_string()), + StoragePathType::Internal, + ) + }); // INFO: can't canonicalise gitdir as the path needs to exist to do that and we may not // have cloned the repo yet let repo_details = RepoDetails::new( @@ -194,8 +237,8 @@ impl ServerActor { gitdir, ); let forge = Forge::create(repo_details.clone(), net.clone()); - tracing::info!("Starting Repo Actor"); let actor = RepoActor::new( + ui, repo_details, forge, listen_url.clone(), @@ -204,7 +247,7 @@ impl ServerActor { repository_factory.duplicate(), sleep_duration, Some(notify_user_recipient), - server_addr.clone(), + server_actor_ref.clone(), ); (forge_name.clone(), repo_alias, actor) } @@ -231,31 +274,43 @@ impl ServerActor { } /// Attempts to gracefully shutdown the server before stopping the system. - fn abort(&mut self, ctx: &::Context, message: impl Into) { - self.do_send(crate::server::actor::messages::Shutdown, ctx); + async fn abort( + &mut self, + ctx: &Context<'_, Self, color_eyre::Result<()>>, + message: impl Into + Send, + ) -> Result<()> { + let message = message.into(); + error!(%message, "aborting"); + self.do_send(Shutdown, ctx).await?; if let Some(t) = self.shutdown_trigger.take() { - let _ = t.send(message.into()); - } else { - error!("{}", message.into()); - self.do_send(Shutdown, ctx); - // System::current().stop_with_code(1); + t.send(message) + .await + .map_err(|e| format!("failed sending shutdown trigger: {e:?}"))?; } + Ok(()) } - fn do_send(&self, msg: M, ctx: &::Context) + async fn do_send( + &self, + msg: M, + ctx: &Context<'_, Self, color_eyre::Result<()>>, + ) -> Result<()> where - M: actix::Message + Send + 'static + std::fmt::Debug, - Self: actix::Handler, - ::Result: Send, + M: Send + Sync + 'static + std::fmt::Debug, + Self: kameo::message::Message, + >::Reply: Send + Sync + 'static + std::fmt::Debug, + <>::Reply as kameo::Reply>::Error: std::fmt::Debug, { + let log_message = format!("send: {msg:?}"); if let Some(message_log) = &self.message_log { - let log_message = format!("send: {msg:?}"); if let Ok(mut log) = message_log.write() { - log.push(log_message); + log.push(log_message.clone()); } } if cfg!(not(test)) { - ctx.address().do_send(msg); + tell!(ctx.actor_ref(), msg) + .map_err(|e| format!("failed sending: {log_message}: {e:?}"))?; } + Ok(()) } } diff --git a/crates/cli/src/server/actor/tests/given.rs b/crates/cli/src/server/actor/tests/given.rs index 2769f12..bfdbc2e 100644 --- a/crates/cli/src/server/actor/tests/given.rs +++ b/crates/cli/src/server/actor/tests/given.rs @@ -1,10 +1,10 @@ +// use std::time::Duration; -use actix::prelude::*; +use kameo::actor::ActorRef; use crate::alerts::{AlertsActor, History}; -// pub fn a_filesystem() -> kxio::fs::TempFileSystem { #[allow(clippy::expect_used)] kxio::fs::temp().expect("temp fs") @@ -14,6 +14,10 @@ pub fn a_network() -> kxio::net::MockNet { kxio::net::mock() } -pub fn an_alerts_actor(net: kxio::net::Net) -> Addr { - AlertsActor::new(None, History::new(Duration::from_millis(1)), net).start() +pub fn an_alerts_actor(net: kxio::net::Net) -> ActorRef { + kameo::spawn(AlertsActor::new( + None, + History::new(Duration::from_millis(1)), + net, + )) } diff --git a/crates/cli/src/server/actor/tests/receive_app_config.rs b/crates/cli/src/server/actor/tests/receive_app_config.rs index f5fc625..4352735 100644 --- a/crates/cli/src/server/actor/tests/receive_app_config.rs +++ b/crates/cli/src/server/actor/tests/receive_app_config.rs @@ -1,19 +1,25 @@ // -use actix::prelude::*; - -use crate::server::actor::{tests::given, ReceiveAppConfig, ServerActor}; -use git_next_core::{ - git, - server::{AppConfig, Http, Listen, ListenUrl, Shout, Storage}, -}; - use std::{ collections::BTreeMap, sync::{Arc, RwLock}, }; -#[test_log::test(actix::test)] -async fn when_webhook_url_has_trailing_slash_should_not_send() { +use kameo::actor::pubsub::PubSub; + +use git_next_core::{ + git, + server::{AppConfig, Http, Listen, ListenUrl, Shout, Storage}, +}; + +use crate::{ + server::actor::{tests::given, ReceiveAppConfig, ServerActor}, + tell, +}; + +type TestResult = Result<(), Box>; + +#[test_log::test(tokio::test)] +async fn when_webhook_url_has_trailing_slash_should_not_send() -> TestResult { //given // parameters let fs = given::a_filesystem(); @@ -22,8 +28,18 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() { let repo = git::repository::factory::mock(); let duration = std::time::Duration::from_millis(1); + let file_update_subs = kameo::spawn(PubSub::new()); + // sut - let server = ServerActor::new(fs.as_real(), net.into(), alerts, repo, duration); + let server = ServerActor::new( + false, // ui + fs.as_real(), + net.into(), + alerts, + file_update_subs, + repo, + duration, + ); // collaborators let listen = Listen::new( @@ -39,13 +55,11 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() { let server = server.with_message_log(Some(message_log.clone())); //when - server.start().do_send(ReceiveAppConfig::new(AppConfig::new( - listen, - shout, - server_storage, - repos, - ))); - actix_rt::time::sleep(std::time::Duration::from_millis(1)).await; + tell!( + kameo::spawn(server), + ReceiveAppConfig::new(AppConfig::new(listen, shout, server_storage, repos,)) + )?; + tokio::time::sleep(std::time::Duration::from_millis(1)).await; //then // INFO: assert that ReceiveValidServerConfig is NOT sent @@ -53,4 +67,6 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() { assert!(message_log.read().iter().any(|log| !log .iter() .any(|line| line == "send: ReceiveValidServerConfig"))); + + Ok(()) } diff --git a/crates/cli/src/server/mod.rs b/crates/cli/src/server/mod.rs index e4c683a..85362b5 100644 --- a/crates/cli/src/server/mod.rs +++ b/crates/cli/src/server/mod.rs @@ -1,35 +1,22 @@ // +use std::{path::PathBuf, sync::Arc}; + +use color_eyre::{eyre::Context, Result}; +use kxio::{fs::FileSystem, net::Net}; +use tokio::sync::RwLock; +use tracing::info; + +use git_next_core::git::RepositoryFactory; + +use crate::{root::RootActor, tell}; + +pub use actor::ServerActor; + pub mod actor; #[cfg(test)] mod tests; -use actix::prelude::*; -use actix_rt::signal; -use actor::messages::ShutdownTrigger; - -use crate::{ - alerts::{AlertsActor, History}, - file_watcher::{watch_file, FileUpdated}, -}; - -#[allow(clippy::module_name_repetitions)] -pub use actor::ServerActor; - -use git_next_core::git::RepositoryFactory; - -use color_eyre::{eyre::Context, Result}; -use kxio::{fs::FileSystem, net::Net}; -use tracing::info; - -use std::{ - path::PathBuf, - sync::{atomic::Ordering, mpsc::channel, Arc, RwLock}, - time::Duration, -}; - -const A_DAY: Duration = Duration::from_secs(24 * 60 * 60); - pub fn init(fs: &FileSystem) -> Result<()> { let file_name = "git-next-server.toml"; let pathbuf = PathBuf::from(file_name); @@ -51,8 +38,8 @@ pub fn init(fs: &FileSystem) -> Result<()> { #[allow(clippy::too_many_lines)] pub fn start( ui: bool, - fs: FileSystem, - net: Net, + fs: &FileSystem, + net: &Net, repo: Box, sleep_duration: std::time::Duration, ) -> Result<()> { @@ -66,111 +53,48 @@ pub fn start( } let shutdown_message_holder: Arc>> = Arc::new(RwLock::new(None)); - let shutdown_message_holder_exec = shutdown_message_holder.clone(); - let file_watcher_err_holder: Arc>> = Arc::new(RwLock::new(None)); - let file_watcher_err_holder_exec = file_watcher_err_holder.clone(); - let execution = async move { - info!("Starting Alert Dispatcher..."); - let alerts_addr = AlertsActor::new(None, History::new(A_DAY), net.clone()).start(); + let shutdown_message_holder_clone = shutdown_message_holder.clone(); - info!("Starting Server..."); - let server = - ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start(); + #[allow(clippy::expect_used)] + tokio::runtime::Runtime::new()?.block_on(async { + let (tx_shutdown, mut rx_shutdown) = tokio::sync::mpsc::channel::(1); - info!("Starting File Watcher..."); - let watch_file = watch_file("git-next-server.toml".into(), server.clone().recipient()); - let fw_shutdown = match watch_file { - Ok(fw_shutdown) => fw_shutdown, - Err(err) => { - // shutdown now - server.do_send(crate::server::actor::messages::Shutdown); - actix_rt::time::sleep(std::time::Duration::from_millis(10)).await; - System::current().stop(); - let _ = file_watcher_err_holder_exec - .write() - .map(|mut o| o.replace(err)); - return; + let root_actor_ref = kameo::spawn(RootActor::new( + ui, + fs.clone(), + net.clone(), + sleep_duration, + tx_shutdown, + )); + tell!("root", root_actor_ref, crate::root::Start::new(repo)).expect("start root actor"); + + info!("Server running - Press Ctrl-C to stop..."); + tokio::select! { + _r = tokio::signal::ctrl_c() => { + info!("Ctrl-C received, shutting down..."); } - }; - - let (tx_shutdown, rx_shutdown) = channel::(); - if ui { - #[cfg(feature = "tui")] - { - use crate::server::actor::messages::SubscribeToUpdates; - use crate::tui; - - let tui_addr = tui::Tui::new(tx_shutdown.clone()).start(); - server.do_send(SubscribeToUpdates::new(tui_addr.clone().recipient())); - server.do_send(ShutdownTrigger::new(tx_shutdown)); - server.do_send(FileUpdated); // update file after ui subscription in place - loop { - let _ = tui_addr.send(tui::Tick).await; - if let Ok(message) = rx_shutdown.try_recv() { - let _ = shutdown_message_holder_exec - .write() - .map(|mut o| o.replace(message)); - break; - } - actix_rt::time::sleep(Duration::from_millis(16)).await; - } - } - } else { - server.do_send(ShutdownTrigger::new(tx_shutdown.clone())); - server.do_send(FileUpdated); - - info!("Server running - Press Ctrl-C to stop..."); - tokio::select! { - _r = signal::ctrl_c() => { - info!("Ctrl-C received, shutting down..."); - } - _x = async move { + _x = async move { loop{ - if let Ok(message) = rx_shutdown.try_recv() { - let _ = shutdown_message_holder_exec - .write() - .map(|mut o| o.replace(message)); + if let Some(message) = rx_shutdown.recv().await { + info!("rx shutdown received: {message}"); + let _ = shutdown_message_holder_clone.write().await.replace(message); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; break; } - actix_rt::task::yield_now().await; } } => { info!("signaled shutdown"); } - }; - } + }; + }); - // shutdown - fw_shutdown.store(true, Ordering::Relaxed); - server.do_send(crate::server::actor::messages::Shutdown); - actix_rt::time::sleep(std::time::Duration::from_millis(10)).await; - System::current().stop(); - }; - - let system = System::new(); - Arbiter::current().spawn(execution); - system.run()?; - - // check for error from server thread - #[allow(clippy::unwrap_used)] - if let Some(err) = &*shutdown_message_holder.read().unwrap() { - #[cfg(feature = "tui")] - if ui { - ratatui::restore(); - } - if !err.is_empty() { - return Err(color_eyre::eyre::eyre!(format!("{err}"))); - } + #[cfg(feature = "tui")] + if ui { + ratatui::restore(); } - // check for error from file watcher thread - #[allow(clippy::unwrap_used)] - if let Some(err) = &*file_watcher_err_holder.read().unwrap() { - #[cfg(feature = "tui")] - if ui { - ratatui::restore(); - } - return Err(color_eyre::eyre::eyre!(format!("{err}"))); + if let Some(ref message) = *shutdown_message_holder.blocking_write() { + info!(%message, "shutdown"); } Ok(()) diff --git a/crates/cli/src/tests.rs b/crates/cli/src/tests.rs index 7aa77a3..fab2935 100644 --- a/crates/cli/src/tests.rs +++ b/crates/cli/src/tests.rs @@ -38,44 +38,3 @@ mod init { Ok(()) } } -mod file_watcher { - use std::{sync::atomic::Ordering, time::Duration}; - - use actix::{Actor, Context, Handler}; - use rstest::*; - - use crate::file_watcher::{self, FileUpdated}; - - use super::TestResult; - - #[rstest] - #[actix::test] - #[timeout(Duration::from_millis(80))] - async fn should_not_block_calling_thread() -> TestResult { - let fs = kxio::fs::temp()?; - let path = fs.base().join("file"); - fs.file(&path).write("foo")?; - - let listener = Listener; - let l_addr = listener.start(); - let recipient = l_addr.recipient(); - - let fw_shutdown = file_watcher::watch_file(path, recipient)?; - std::thread::sleep(Duration::from_millis(10)); - fw_shutdown.store(true, Ordering::Relaxed); - - Ok(()) // was not blocked - } - - struct Listener; - impl Actor for Listener { - type Context = Context; - } - impl Handler for Listener { - type Result = (); - - fn handle(&mut self, _msg: FileUpdated, _ctx: &mut Self::Context) -> Self::Result { - // todo!() - } - } -} diff --git a/crates/cli/src/tui/actor/handlers/server_update.rs b/crates/cli/src/tui/actor/handlers/server_update.rs index d82963f..34c3aa8 100644 --- a/crates/cli/src/tui/actor/handlers/server_update.rs +++ b/crates/cli/src/tui/actor/handlers/server_update.rs @@ -1,6 +1,7 @@ // -use actix::Handler; +use kameo::message::{Context, Message}; use ratatui::style::Color; +use tracing::debug; use crate::{ server::actor::messages::{RepoUpdate, ServerUpdate}, @@ -12,10 +13,14 @@ static PREP: Color = Color::Gray; static ACTING: Color = Color::LightBlue; static WARN: Color = Color::Red; -impl Handler for Tui { - type Result = (); +impl Message for Tui { + type Reply = (); - fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + msg: ServerUpdate, + _ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { self.state.tap(); match msg { ServerUpdate::AppConfigLoaded { app_config } => { @@ -29,76 +34,82 @@ impl Handler for Tui { } => { if let ServerState::Configured { forges } = &mut self.state.mode { let Some(forge_state) = forges.get_mut(&forge_alias) else { + debug!("ServerState::Configured: no forge state available"); return; }; let Some(repo_state) = forge_state.repos.get_mut(&repo_alias) else { + debug!("ServerState::Configured: no repo state available"); return; }; repo_state.clear_alert(); - match repo_update { - RepoUpdate::Branches { branches } => { - repo_state.update_branches(branches); - } - RepoUpdate::Log { log } => { - repo_state.update_log(log); - } - RepoUpdate::ValidateRepo => repo_state.update_message("polling...", ACTING), - RepoUpdate::Okay { main, next, dev } => { - repo_state.clear_alert(); - repo_state.update_message("okay", OKAY); - *repo_state = repo_state.clone().ready(main, next, dev); - } - RepoUpdate::Alert { alert } => { - repo_state.alert(alert); - } - RepoUpdate::CheckingCI => { - repo_state.update_message("Checking CI status", ACTING); - } - RepoUpdate::AdvancingNext { commit, force: _ } => { - repo_state - .update_message(format!("advancing next to {commit}"), ACTING); - } - RepoUpdate::NextUpdated => { - repo_state.update_message("next updated - pause while CI starts", OKAY); - } - RepoUpdate::AdvancingMain { commit } => { - repo_state - .update_message(format!("advancing main to {commit}"), ACTING); - } - RepoUpdate::MainUpdated => { - repo_state.update_message("main updated", OKAY); - } - RepoUpdate::Opening => { - repo_state.update_message("opening...", PREP); - } - RepoUpdate::Opened => { - repo_state.update_message("opened", PREP); - } - RepoUpdate::LoadingConfigFromRepo => { - repo_state.update_message("loading config from repo...", PREP); - } - RepoUpdate::ReceiveCIStatus { status } => { - repo_state.update_message(format!("ci status: {status:?}"), WARN); - } - RepoUpdate::ReceiveRepoConfig { repo_config: _ } => { - repo_state.update_message("loaded config from repo", PREP); - } - RepoUpdate::RegisteringWebhook => { - repo_state.update_message("registering webhook...", PREP); - } - RepoUpdate::UnregisteringWebhook => { - repo_state.update_message("unregistering webhook...", PREP); - } - RepoUpdate::WebhookReceived { branch, push: _ } => { - repo_state - .update_message(format!("webhook update: {branch:?}"), ACTING); - } - RepoUpdate::RegisteredWebhook => { - repo_state.update_message("registered webhook", PREP); - } - } + debug!(?repo_update, "ServerState::Configured: RepoUpdate"); + handle_repo_update(repo_update, repo_state); } } } } } + +fn handle_repo_update(repo_update: RepoUpdate, repo_state: &mut crate::tui::actor::RepoState) { + match repo_update { + RepoUpdate::Branches { branches } => { + repo_state.update_branches(branches); + } + RepoUpdate::Log { log } => { + repo_state.update_log(log); + } + RepoUpdate::ValidateRepo => { + repo_state.update_message("polling...", ACTING); + } + RepoUpdate::Okay { main, next, dev } => { + repo_state.clear_alert(); + repo_state.update_message("okay", OKAY); + *repo_state = repo_state.clone().ready(main, next, dev); + } + RepoUpdate::Alert { alert } => { + repo_state.alert(alert); + } + RepoUpdate::CheckingCI => { + repo_state.update_message("Checking CI status", ACTING); + } + RepoUpdate::AdvancingNext { commit, force: _ } => { + repo_state.update_message(format!("advancing next to {commit}"), ACTING); + } + RepoUpdate::NextUpdated => { + repo_state.update_message("next updated - pause while CI starts", OKAY); + } + RepoUpdate::AdvancingMain { commit } => { + repo_state.update_message(format!("advancing main to {commit}"), ACTING); + } + RepoUpdate::MainUpdated => { + repo_state.update_message("main updated", OKAY); + } + RepoUpdate::Opening => { + repo_state.update_message("opening...", PREP); + } + RepoUpdate::Opened => { + repo_state.update_message("opened", PREP); + } + RepoUpdate::LoadingConfigFromRepo => { + repo_state.update_message("loading config from repo...", PREP); + } + RepoUpdate::ReceiveCIStatus { status } => { + repo_state.update_message(format!("ci status: {status:?}"), WARN); + } + RepoUpdate::ReceiveRepoConfig { repo_config: _ } => { + repo_state.update_message("loaded config from repo", PREP); + } + RepoUpdate::RegisteringWebhook => { + repo_state.update_message("registering webhook...", PREP); + } + RepoUpdate::UnregisteringWebhook => { + repo_state.update_message("unregistering webhook...", PREP); + } + RepoUpdate::WebhookReceived { branch, push: _ } => { + repo_state.update_message(format!("webhook update: {branch:?}"), ACTING); + } + RepoUpdate::RegisteredWebhook => { + repo_state.update_message("registered webhook", PREP); + } + } +} diff --git a/crates/cli/src/tui/actor/handlers/tick.rs b/crates/cli/src/tui/actor/handlers/tick.rs index 44ecb83..8371bc4 100644 --- a/crates/cli/src/tui/actor/handlers/tick.rs +++ b/crates/cli/src/tui/actor/handlers/tick.rs @@ -1,15 +1,25 @@ // -use actix::Handler; +use std::time::Duration; -use crate::tui::actor::{messages::Tick, Tui}; +use color_eyre::Result; +use kameo::message::{Context, Message}; -impl Handler for Tui { - type Result = std::io::Result<()>; +use crate::{ + tell, + tui::actor::{messages::Tick, Tui}, +}; - fn handle(&mut self, _msg: Tick, ctx: &mut Self::Context) -> Self::Result { +impl Message for Tui { + type Reply = Result<()>; + + async fn handle(&mut self, _msg: Tick, ctx: Context<'_, Self, Self::Reply>) -> Self::Reply { self.state.tap(); self.draw()?; - self.handle_input(ctx)?; + self.handle_input(&ctx.actor_ref())?; + + tokio::time::sleep(Duration::from_millis(16)).await; + tell!(ctx.actor_ref(), Tick)?; + Ok(()) } } diff --git a/crates/cli/src/tui/actor/mod.rs b/crates/cli/src/tui/actor/mod.rs index 6369c76..f64db4e 100644 --- a/crates/cli/src/tui/actor/mod.rs +++ b/crates/cli/src/tui/actor/mod.rs @@ -6,61 +6,85 @@ mod model; #[cfg(test)] mod tests; -use std::sync::mpsc::Sender; - -use actix::{Actor, ActorContext as _, Context}; - -pub use model::*; - +use color_eyre::Result; +use kameo::{ + actor::{ActorRef, WeakActorRef}, + error::{ActorStopReason, BoxError, PanicError}, + mailbox::unbounded::UnboundedMailbox, + Actor, +}; use ratatui::{ crossterm::event::{self, KeyCode, KeyEventKind}, DefaultTerminal, }; use tui_scrollview::ScrollViewState; +pub use model::*; + +use crate::tell; + +use super::Tick; + #[derive(Debug)] pub struct Tui { terminal: Option, - signal_shutdown: Sender, pub state: State, scroll_view_state: ScrollViewState, } impl Actor for Tui { - type Context = Context; - fn started(&mut self, _ctx: &mut Self::Context) { + type Mailbox = UnboundedMailbox; + + async fn on_start(&mut self, actor_ref: ActorRef) -> Result<(), BoxError> { self.terminal.replace(ratatui::init()); + tell!(actor_ref, Tick)?; + Ok(()) } - fn stopped(&mut self, _ctx: &mut Self::Context) { + + async fn on_stop( + &mut self, + _actor_ref: WeakActorRef, + _reason: ActorStopReason, + ) -> Result<(), BoxError> { self.terminal.take(); ratatui::restore(); + Ok(()) + } + + async fn on_panic( + &mut self, + _actor_ref: WeakActorRef, + err: PanicError, + ) -> Result, BoxError> { + self.terminal.take(); + ratatui::restore(); + Ok(Some(ActorStopReason::Panicked(err))) } } impl Tui { - pub fn new(signal_shutdown: Sender) -> Self { + pub fn new() -> Self { Self { terminal: None, - signal_shutdown, state: State::initial(), scroll_view_state: ScrollViewState::default(), } } fn draw(&mut self) -> std::io::Result<()> { - let t = self.terminal.take(); - let scroll_view_state = &mut self.scroll_view_state; - let state = &self.state; - if let Some(mut terminal) = t { + if let Some(terminal) = &mut self.terminal { terminal.draw(|frame| { - frame.render_stateful_widget(state, frame.area(), scroll_view_state); + frame.render_stateful_widget( + &self.state, + frame.area(), + &mut self.scroll_view_state, + ); })?; - self.terminal = Some(terminal); } else { eprintln!("No terminal setup"); } Ok(()) } - fn handle_input(&mut self, ctx: &mut ::Context) -> std::io::Result<()> { + fn handle_input(&mut self, actor_tui: &ActorRef) -> Result<()> { if event::poll(std::time::Duration::from_millis(16))? { let event::Event::Key(key) = event::read()? else { return Ok(()); @@ -70,10 +94,7 @@ impl Tui { } match key.code { KeyCode::Char('q') => { - ctx.stop(); - if let Err(err) = self.signal_shutdown.send(String::new()) { - tracing::error!(?err, "Failed to signal shutdown"); - } + actor_tui.kill(); } KeyCode::Char('j') | KeyCode::Down => self.scroll_view_state.scroll_down(), KeyCode::Char('k') | KeyCode::Up => self.scroll_view_state.scroll_up(), diff --git a/crates/cli/src/tui/actor/model.rs b/crates/cli/src/tui/actor/model.rs index 2adb067..b746b0c 100644 --- a/crates/cli/src/tui/actor/model.rs +++ b/crates/cli/src/tui/actor/model.rs @@ -1,4 +1,6 @@ // +use std::{collections::BTreeMap, fmt::Display, time::Instant}; + use ratatui::{ layout::Alignment, prelude::{Buffer, Rect}, @@ -7,15 +9,13 @@ use ratatui::{ text::{Line, Span}, widgets::{Block, Paragraph, StatefulWidget, Widget}, }; +use tracing::info; +use tui_scrollview::ScrollViewState; use git_next_core::{ git::{self, graph::Log, Commit}, ForgeAlias, RepoAlias, RepoBranches, }; -use tracing::info; -use tui_scrollview::ScrollViewState; - -use std::{collections::BTreeMap, fmt::Display, time::Instant}; use crate::{server::actor::messages::ValidAppConfig, tui::components::ConfiguredAppWidget}; diff --git a/crates/cli/src/webhook/handlers/mod.rs b/crates/cli/src/webhook/handlers/mod.rs deleted file mode 100644 index 418e9f6..0000000 --- a/crates/cli/src/webhook/handlers/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod shutdown_webhook; diff --git a/crates/cli/src/webhook/handlers/shutdown_webhook.rs b/crates/cli/src/webhook/handlers/shutdown_webhook.rs deleted file mode 100644 index 8789572..0000000 --- a/crates/cli/src/webhook/handlers/shutdown_webhook.rs +++ /dev/null @@ -1,13 +0,0 @@ -// -use actix::prelude::*; - -use crate::webhook::{messages::ShutdownWebhook, WebhookActor}; - -impl Handler for WebhookActor { - type Result = (); - - fn handle(&mut self, _msg: ShutdownWebhook, ctx: &mut Self::Context) -> Self::Result { - self.spawn_handle.take(); - ctx.stop(); - } -} diff --git a/crates/cli/src/webhook/messages.rs b/crates/cli/src/webhook/messages.rs deleted file mode 100644 index 74add75..0000000 --- a/crates/cli/src/webhook/messages.rs +++ /dev/null @@ -1,4 +0,0 @@ -// -use git_next_core::message; - -message!(ShutdownWebhook, "Request to shutdown the Webhook actor"); diff --git a/crates/cli/src/webhook/mod.rs b/crates/cli/src/webhook/mod.rs index 64a580c..9f353ee 100644 --- a/crates/cli/src/webhook/mod.rs +++ b/crates/cli/src/webhook/mod.rs @@ -1,43 +1,56 @@ // -use actix::prelude::*; - -mod handlers; -pub mod messages; -pub mod router; -mod server; - -use crate::repo::messages::WebhookNotification; - use std::net::SocketAddr; -use tracing::Instrument; +use color_eyre::Result; +use kameo::{actor::ActorRef, mailbox::unbounded::UnboundedMailbox, message::Message, Actor}; +use router::WebhookRouterActor; + +use crate::{ + default_on_actor_link_died, default_on_actor_panic, default_on_actor_start, + default_on_actor_stop, +}; + +pub mod router; +mod server; #[allow(clippy::module_name_repetitions)] #[derive(Debug)] pub struct WebhookActor { socket_addr: SocketAddr, - span: tracing::Span, - spawn_handle: Option, - message_receiver: Recipient, + message_receiver: ActorRef, } impl WebhookActor { - pub fn new(socket_addr: SocketAddr, message_receiver: Recipient) -> Self { - let span = tracing::info_span!("WebhookActor"); + pub const fn new( + socket_addr: SocketAddr, + message_receiver: ActorRef, + ) -> Self { Self { socket_addr, - span, message_receiver, - spawn_handle: None, } } } + impl Actor for WebhookActor { - type Context = actix::Context; - fn started(&mut self, ctx: &mut Self::Context) { - let _gaurd = self.span.enter(); - let address: Recipient = self.message_receiver.clone(); - let server = server::start(self.socket_addr, address); - let spawn_handle = ctx.spawn(server.in_current_span().into_actor(self)); - self.spawn_handle.replace(spawn_handle); + type Mailbox = UnboundedMailbox; + + default_on_actor_start!(this, actor_ref); + default_on_actor_panic!(this, actor_ref, err); + default_on_actor_link_died!(this, actor_ref, id, reason); + default_on_actor_stop!(this, actor_ref, reason); +} + +#[derive(Debug)] +pub struct Start; +impl Message for WebhookActor { + type Reply = Result<()>; + + async fn handle( + &mut self, + _msg: Start, + _ctx: kameo::message::Context<'_, Self, Self::Reply>, + ) -> Self::Reply { + server::start(self.socket_addr, self.message_receiver.clone()).await?; + Ok(()) } } diff --git a/crates/cli/src/webhook/router.rs b/crates/cli/src/webhook/router.rs index 2d180de..ae254dc 100644 --- a/crates/cli/src/webhook/router.rs +++ b/crates/cli/src/webhook/router.rs @@ -1,26 +1,28 @@ // -use actix::prelude::*; - -use derive_more::Constructor; -use tracing::{debug, info, warn}; - use std::collections::BTreeMap; -use crate::repo::messages::WebhookNotification; +use derive_more::Constructor; +use kameo::{ + actor::ActorRef, + message::{Context, Message}, + Actor, +}; +use tracing::{debug, warn}; use git_next_core::{ForgeAlias, RepoAlias}; +use crate::{ + repo::{messages::WebhookNotification, RepoActor}, + tell, +}; + +#[derive(Actor)] pub struct WebhookRouterActor { span: tracing::Span, - recipients: BTreeMap>>, + recipients: BTreeMap>>, } impl Default for WebhookRouterActor { fn default() -> Self { - Self::new() - } -} -impl WebhookRouterActor { - pub fn new() -> Self { let span = tracing::info_span!("WebhookRouter"); Self { span, @@ -28,43 +30,45 @@ impl WebhookRouterActor { } } } -impl Actor for WebhookRouterActor { - type Context = Context; -} +impl Message for WebhookRouterActor { + type Reply = color_eyre::Result<()>; -impl Handler for WebhookRouterActor { - type Result = (); - - fn handle(&mut self, msg: WebhookNotification, _ctx: &mut Self::Context) -> Self::Result { + async fn handle( + &mut self, + msg: WebhookNotification, + _ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { let _gaurd = self.span.enter(); let forge_alias = msg.forge_alias(); let repo_alias = msg.repo_alias(); debug!(forge = %forge_alias, repo = %repo_alias, "Router..."); let Some(forge_repos) = self.recipients.get(forge_alias) else { warn!(forge = %forge_alias, "No forge repos found"); - return; + return Ok(()); }; let Some(recipient) = forge_repos.get(repo_alias) else { debug!(forge = %forge_alias, repo = %repo_alias, "No recipient found"); - return; + return Ok(()); }; - recipient.do_send(msg); + tell!(recipient, msg)?; + Ok(()) } } -#[derive(Message, Constructor)] -#[rtype(result = "()")] +#[derive(Constructor, Debug)] pub struct AddWebhookRecipient { pub forge_alias: ForgeAlias, pub repo_alias: RepoAlias, - pub recipient: Recipient, + pub recipient: ActorRef, } -impl Handler for WebhookRouterActor { - type Result = (); +impl Message for WebhookRouterActor { + type Reply = (); - fn handle(&mut self, msg: AddWebhookRecipient, _ctx: &mut Self::Context) -> Self::Result { - let _gaurd = self.span.enter(); - info!(forge = %msg.forge_alias, repo = %msg.repo_alias, "Register Recipient"); + async fn handle( + &mut self, + msg: AddWebhookRecipient, + _ctx: Context<'_, Self, Self::Reply>, + ) -> Self::Reply { if !self.recipients.contains_key(&msg.forge_alias) { self.recipients .insert(msg.forge_alias.clone(), BTreeMap::new()); diff --git a/crates/cli/src/webhook/server.rs b/crates/cli/src/webhook/server.rs index 6df3809..577db6f 100644 --- a/crates/cli/src/webhook/server.rs +++ b/crates/cli/src/webhook/server.rs @@ -1,18 +1,17 @@ // -use actix::prelude::*; - use std::{collections::BTreeMap, net::SocketAddr}; +use color_eyre::Result; +use kameo::actor::ActorRef; use tracing::{info, warn}; -use crate::repo::messages::WebhookNotification; - use git_next_core::{webhook, ForgeAlias, ForgeNotification, RepoAlias}; -pub async fn start( - socket_addr: SocketAddr, - address: actix::prelude::Recipient, -) { +use crate::{repo::messages::WebhookNotification, tell}; + +use super::router::WebhookRouterActor; + +pub async fn start(socket_addr: SocketAddr, address: ActorRef) -> Result<()> { // start webhook server use warp::Filter; // Define the Warp route to handle incoming HTTP requests @@ -23,37 +22,28 @@ pub async fn start( .and(warp::header::headers_cloned()) .and(warp::body::bytes()) .and_then( - |recipient: Recipient, + |recipient: ActorRef, forge_alias: String, repo_alias: String, // query: String, headers: warp::http::HeaderMap, body: bytes::Bytes| async move { info!("POST received"); - let forge_alias = ForgeAlias::new(forge_alias); - let repo_alias = RepoAlias::new(repo_alias); - let bytes = body.to_vec(); - let body = webhook::forge_notification::Body::new( - String::from_utf8_lossy(&bytes).to_string(), - ); - let headers = headers - .into_iter() - .filter_map(|(k, v)| { - k.map(|k| (k.to_string(), v.to_str().unwrap_or_default().to_string())) - }) - .collect::>(); - let message = WebhookNotification::new(ForgeNotification::new( - forge_alias, - repo_alias, - headers, - body, + let msg = WebhookNotification::new(ForgeNotification::new( + ForgeAlias::new(forge_alias), + RepoAlias::new(repo_alias), + headers + .into_iter() + .filter_map(|(k, v)| { + k.map(|k| (k.to_string(), v.to_str().unwrap_or_default().to_string())) + }) + .collect::>(), + webhook::forge_notification::Body::new( + String::from_utf8_lossy(&body).to_string(), + ), )); - recipient - .try_send(message) - .map(|()| { - info!("Message sent ok"); - warp::reply::with_status("OK", warp::http::StatusCode::OK) - }) + tell!(recipient, msg) + .map(|()| warp::reply::with_status("OK", warp::http::StatusCode::OK)) .map_err(|e| { warn!("Unknown error: {:?}", e); warp::reject() @@ -63,5 +53,9 @@ pub async fn start( // Start the server info!("Starting webhook server: {}", socket_addr); - warp::serve(route).run(socket_addr).await; + let (socket, f) = warp::serve(route).try_bind_ephemeral(socket_addr)?; + info!("Server bound to {socket}"); + f.await; + + Ok(()) } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index f798ea8..46e73b4 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -27,9 +27,6 @@ tracing = { workspace = true } # fs/network kxio = { workspace = true } -# Actors -actix = { workspace = true } - # TOML parsing serde = { workspace = true } toml = { workspace = true } diff --git a/crates/core/src/config/server.rs b/crates/core/src/config/server.rs index 786bd65..23164f3 100644 --- a/crates/core/src/config/server.rs +++ b/crates/core/src/config/server.rs @@ -352,3 +352,13 @@ impl SmtpConfig { &self.hostname } } + +#[cfg(test)] +mod tests { + const fn is_sendable() {} + + #[test] + const fn normal() { + is_sendable::(); + } +} diff --git a/crates/core/src/git/repository/mod.rs b/crates/core/src/git/repository/mod.rs index 399eecd..4276cfb 100644 --- a/crates/core/src/git/repository/mod.rs +++ b/crates/core/src/git/repository/mod.rs @@ -50,14 +50,11 @@ pub fn open( let open_repository = if repo_details.gitdir.exists() { info!("Local copy found - opening..."); - let repo = repository_factory.open(repo_details)?; - repo.fetch()?; - repo + repository_factory.open(repo_details)? } else { info!("Local copy not found - cloning..."); repository_factory.git_clone(repo_details)? }; - info!("Validating..."); validate_default_remotes(&*open_repository, repo_details) .map_err(|e| Error::Validation(s!(e)))?; Ok(open_repository) diff --git a/crates/core/src/git/repository/open/mod.rs b/crates/core/src/git/repository/open/mod.rs index df06d81..edacb96 100644 --- a/crates/core/src/git/repository/open/mod.rs +++ b/crates/core/src/git/repository/open/mod.rs @@ -63,7 +63,7 @@ pub(crate) fn test( #[allow(clippy::module_name_repetitions)] #[mockall::automock] -pub trait OpenRepositoryLike: std::fmt::Debug + Sync { +pub trait OpenRepositoryLike: std::fmt::Debug + Sync + Send { /// Creates a clone of the `OpenRepositoryLike`. fn duplicate(&self) -> Box; diff --git a/crates/core/src/git/validation/remotes.rs b/crates/core/src/git/validation/remotes.rs index bf76d72..8ab9a18 100644 --- a/crates/core/src/git/validation/remotes.rs +++ b/crates/core/src/git/validation/remotes.rs @@ -4,8 +4,6 @@ use crate::{ s, RemoteUrl, }; -use tracing::info; - #[tracing::instrument(skip_all)] pub fn validate_default_remotes( open_repository: &dyn OpenRepositoryLike, @@ -22,7 +20,6 @@ pub fn validate_default_remotes( "Unable to build forge url" ))); }; - info!(config = %remote_url, push = %push_remote, fetch = %fetch_remote, "Check remotes match"); if !remote_url.matches(&push_remote) { return Err(Error::MismatchDefaultPushRemote { found: push_remote, diff --git a/crates/core/src/macros/message.rs b/crates/core/src/macros/message.rs index cc3ad7c..ab17a03 100644 --- a/crates/core/src/macros/message.rs +++ b/crates/core/src/macros/message.rs @@ -2,26 +2,14 @@ macro_rules! message { ($name:ident, $value:ty, $docs:literal) => { git_next_core::newtype!($name, $value, $docs); - impl actix::prelude::Message for $name { - type Result = (); - } }; ($name:ident, $docs:literal) => { git_next_core::newtype!($name, $docs); - impl actix::prelude::Message for $name { - type Result = (); - } }; ($name:ident, $value:ty => $result:ty, $docs:literal) => { git_next_core::newtype!($name, $value, $docs); - impl actix::prelude::Message for $name { - type Result = $result; - } }; ($name:ident => $result:ty, $docs:literal) => { git_next_core::newtype!($name, $docs); - impl actix::prelude::Message for $name { - type Result = $result; - } }; } diff --git a/crates/forge-forgejo/src/webhook/register.rs b/crates/forge-forgejo/src/webhook/register.rs index b2feb7c..ee96972 100644 --- a/crates/forge-forgejo/src/webhook/register.rs +++ b/crates/forge-forgejo/src/webhook/register.rs @@ -1,3 +1,4 @@ +use git_next_core::git::forge::webhook::Error; // use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId}; @@ -9,14 +10,14 @@ use tracing::{info, instrument, warn}; use crate::webhook; use crate::webhook::Hook; -#[instrument(skip_all)] +#[instrument(skip_all, fields(forge = %repo_details.forge.forge_alias(), repo = %repo_details.repo_alias))] pub async fn register( repo_details: &git::RepoDetails, repo_listen_url: &RepoListenUrl, net: &Net, ) -> git::forge::webhook::Result { let Some(repo_config) = repo_details.repo_config.clone() else { - return Err(git::forge::webhook::Error::NoRepoConfig); + return Err(Error::NoRepoConfig); }; // remove any lingering webhooks for the same URL @@ -51,17 +52,17 @@ pub async fn register( let Ok(hook) = response.json::().await else { #[cfg(not(tarpaulin_include))] // request response is Json so response_body never returns None - return Err(git::forge::webhook::Error::NetworkResponseEmpty); + return Err(Error::NetworkResponseEmpty); }; - info!(webhook_id = %hook.id, "Webhook registered"); + info!(webhook_id = %hook.id, "ok"); Ok(RegisteredWebhook::new( WebhookId::new(format!("{}", hook.id)), authorisation, )) } Err(e) => { - warn!("Failed to register webhook"); - Err(git::forge::webhook::Error::FailedToRegister(e.to_string())) + warn!(?e, "failed"); + Err(Error::FailedToRegister(e.to_string())) } } // Ok(()) diff --git a/justfile b/justfile index d7cdda2..bed34a5 100644 --- a/justfile +++ b/justfile @@ -3,6 +3,7 @@ build: set -e cargo fmt cargo fmt --check + cargo machete cargo hack clippy cargo hack build cargo hack test diff --git a/quickfix-564 b/quickfix-564 new file mode 100644 index 0000000..cb48a03 --- /dev/null +++ b/quickfix-564 @@ -0,0 +1,3 @@ +crates/cli/src/server/actor/mod.rs ┃74┃pub fn new( +crates/cli/src/server/actor/tests/receive_app_config.rs ┃31┃let server = ServerActor::new( +crates/cli/src/server/mod.rs ┃83┃let server = kameo::spqawn(ServerActor::new(