diff --git a/Cargo.lock b/Cargo.lock index 38fb4d8..0ab7043 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3787,7 +3787,6 @@ version = "0.0.1" dependencies = [ "age", "argon2", - "async-recursion", "async-stream", "async-trait", "diesel", diff --git a/Cargo.toml b/Cargo.toml index ac67514..bf9c642 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,6 @@ actix-session = { version = "0.9", features = ["cookie-session"] } actix-web = "4.6" age = { version = "0.10", features = ["armor"] } argon2 = "0.5" -async-recursion = "1.1" async-stream = "0.3" async-trait = "0.1" clap = { version = "4.5", features = ["derive", "env"] } diff --git a/typhon-core/Cargo.toml b/typhon-core/Cargo.toml index 05ccaa2..b7192e3 100644 --- a/typhon-core/Cargo.toml +++ b/typhon-core/Cargo.toml @@ -7,7 +7,6 @@ edition.workspace = true typhon-types.workspace = true age.workspace = true argon2.workspace = true -async-recursion.workspace = true async-stream.workspace = true async-trait.workspace = true diesel.workspace = true diff --git a/typhon-core/src/actions.rs b/typhon-core/src/actions.rs index dd92c8b..89d1016 100644 --- a/typhon-core/src/actions.rs +++ b/typhon-core/src/actions.rs @@ -179,45 +179,41 @@ impl Action { ) -> Result<(), error::Error> { use crate::log_event; - let run = { - let self_ = self.clone(); - move |sender| async move { - action( - &projects::Project { - refresh_task: None, // FIXME? - project: self_.project.clone(), - }, - &self_.action.path, - &self_.action.name, - &Value::from_str(&self_.action.input).unwrap(), - sender, - ) - .await - .map_err(|e| e.into()) - } - }; - - let finish = { - let handle = self.handle(); - move |res: Option>| { - let status = match res { - Some(Err(_)) => { - let _ = finish(None); - TaskStatusKind::Failure - } - Some(Ok(stdout)) => finish(Some(stdout)), - None => { - let _ = finish(None); - TaskStatusKind::Canceled - } - }; - (status, Event::ActionFinished(handle)) - } - }; - log_event(Event::ActionNew(self.handle())); - self.task.run(conn, run, finish)?; + let action_handle = self.handle(); + let self_ = self.clone(); + self.task.run(conn, |handle, sender| async move { + let res = handle + .spawn(async move { + action( + &projects::Project { + refresh_task: None, // FIXME? + project: self_.project.clone(), + }, + &self_.action.path, + &self_.action.name, + &Value::from_str(&self_.action.input).unwrap(), + sender, + ) + .await + .map_err(Into::::into) + }) + .await; + let status_kind = tokio::task::spawn_blocking(|| match res { + Some(Err(_)) => { + let _ = finish(None); + TaskStatusKind::Failure + } + Some(Ok(stdout)) => finish(Some(stdout)), + None => { + let _ = finish(None); + TaskStatusKind::Canceled + } + }) + .await?; + Ok((status_kind, Some(Event::ActionFinished(action_handle)))) + })?; Ok(()) } diff --git a/typhon-core/src/build_manager.rs b/typhon-core/src/build_manager.rs index a883a9d..e11f4ab 100644 --- a/typhon-core/src/build_manager.rs +++ b/typhon-core/src/build_manager.rs @@ -111,35 +111,28 @@ impl State { }; self.join_set.spawn(abort); - let run = { - let drv = drv.clone(); - let sender = sender.clone(); - move |sender_log| run_build(drv, sender, sender_log) - }; - let finish = { - let drv = drv.clone(); - let handle = build.handle(); - let sender = sender.clone(); - |res| { - let status = finish_build(drv, sender, res); - (status, Event::BuildFinished(handle)) - } - }; - build.task.run(&mut self.conn, run, finish)?; + let build_handle = build.handle(); + let sender = sender.clone(); + build + .task + .run(&mut self.conn, move |handle, sender_log| async move { + let drv_bis = drv.clone(); + let res = handle + .spawn(run_build(drv_bis, sender.clone(), sender_log)) + .await; + let _ = sender.send(Msg::Finished(drv, res.clone())); + let status_kind = match res { + Some(Some(())) => TaskStatusKind::Success, + Some(None) => TaskStatusKind::Failure, + None => TaskStatusKind::Canceled, + }; + Ok((status_kind, Some(Event::BuildFinished(build_handle)))) + })?; Ok(build.build.id) } } -fn finish_build(drv: DrvPath, sender: mpsc::UnboundedSender, res: Output) -> TaskStatusKind { - let _ = sender.send(Msg::Finished(drv, res.clone())); - match res { - Some(Some(())) => TaskStatusKind::Success, - Some(None) => TaskStatusKind::Failure, - None => TaskStatusKind::Canceled, - } -} - async fn run_build( drv: DrvPath, sender: mpsc::UnboundedSender, diff --git a/typhon-core/src/error.rs b/typhon-core/src/error.rs index 05b0b44..ea7d9bc 100644 --- a/typhon-core/src/error.rs +++ b/typhon-core/src/error.rs @@ -27,6 +27,7 @@ pub enum Error { LoginError, TaskError(task_manager::Error), BadWebhookOutput, + TokioError, } impl Error { @@ -83,6 +84,7 @@ impl std::fmt::Display for Error { UnexpectedTimeError(e) => write!(f, "Time error: {}", e), TaskError(e) => write!(f, "Task error: {}", e), BadWebhookOutput => write!(f, "Bad webhook output"), + TokioError => write!(f, "Tokio error"), } } } @@ -117,6 +119,12 @@ impl From for Error { } } +impl From for Error { + fn from(_: tokio::task::JoinError) -> Error { + Error::TokioError + } +} + impl Into for Error { fn into(self) -> typhon_types::responses::ResponseError { use {typhon_types::responses::ResponseError::*, Error::*}; @@ -125,6 +133,7 @@ impl Into for Error { | UnexpectedDatabaseError(_) | UnexpectedTimeError(_) | TaskError(_) + | TokioError | Todo => InternalError, EvaluationNotFound(_) | JobNotFound(_) diff --git a/typhon-core/src/evaluations.rs b/typhon-core/src/evaluations.rs index 3641ba2..e316084 100644 --- a/typhon-core/src/evaluations.rs +++ b/typhon-core/src/evaluations.rs @@ -4,6 +4,7 @@ use crate::models; use crate::nix; use crate::responses; use crate::schema; +use crate::task_manager::TaskHandle; use crate::tasks; use crate::Conn; use crate::POOL; @@ -94,18 +95,6 @@ impl Evaluation { self.task.cancel() } - pub fn finish(self, r: Option>) -> TaskStatusKind { - let mut conn = POOL.get().unwrap(); - match r { - Some(Ok(new_jobs)) => match self.create_new_jobs(&mut conn, new_jobs) { - Ok(()) => TaskStatusKind::Success, - Err(_) => TaskStatusKind::Failure, - }, - Some(Err(_)) => TaskStatusKind::Failure, - None => TaskStatusKind::Canceled, - } - } - pub fn get(conn: &mut Conn, handle: &handles::Evaluation) -> Result { let (evaluation, project, task) = schema::evaluations::table .inner_join(schema::projects::table) @@ -249,20 +238,40 @@ impl Evaluation { pub async fn run( self, + handle: TaskHandle, sender: mpsc::UnboundedSender, - ) -> Result { - let res = nix::eval_jobs(&self.evaluation.url, self.evaluation.flake).await; - match &res { - Err(e) => { - for line in e.to_string().split("\n") { - // TODO: hide internal error messages? - // TODO: error management - let _ = sender.send(line.to_string()); + ) -> Result { + let url = self.evaluation.url.clone(); + let flake = self.evaluation.flake; + let res = handle + .spawn(async move { + let res = nix::eval_jobs(&url, flake).await; + match &res { + Err(e) => { + for line in e.to_string().split("\n") { + // TODO: hide internal error messages? + // TODO: error management + let _ = sender.send(line.to_string()); + } + } + _ => (), } + res + }) + .await; + let status_kind = tokio::task::spawn_blocking(move || { + let mut conn = POOL.get().unwrap(); + match res { + Some(Ok(new_jobs)) => match self.create_new_jobs(&mut conn, new_jobs) { + Ok(()) => TaskStatusKind::Success, + Err(_) => TaskStatusKind::Failure, + }, + Some(Err(_)) => TaskStatusKind::Failure, + None => TaskStatusKind::Canceled, } - _ => (), - } - res + }) + .await?; + Ok(status_kind) } fn create_new_jobs(&self, conn: &mut Conn, new_jobs: nix::NewJobs) -> Result<(), Error> { diff --git a/typhon-core/src/jobsets.rs b/typhon-core/src/jobsets.rs index 486d726..da1ed25 100644 --- a/typhon-core/src/jobsets.rs +++ b/typhon-core/src/jobsets.rs @@ -125,23 +125,17 @@ impl Jobset { }) })?; - let run = { - let evaluation = evaluation.clone(); - move |sender| evaluation.run(sender) - }; - - let finish = { - let evaluation = evaluation.clone(); - move |r| { - let handle = evaluation.handle(); - let status = evaluation.finish(r); - (status, Event::EvaluationFinished(handle)) - } - }; - log_event(Event::EvaluationNew(evaluation.handle())); - evaluation.task.run(conn, run, finish)?; + let evaluation_ = evaluation.clone(); + let evaluation_handle = evaluation.handle(); + evaluation.task.run(conn, |handle, sender| async move { + let status_kind = evaluation_.run(handle, sender).await?; + Ok(( + status_kind, + Some(Event::EvaluationFinished(evaluation_handle)), + )) + })?; gcroots::update(conn); diff --git a/typhon-core/src/projects.rs b/typhon-core/src/projects.rs index 31d3859..ab14eb5 100644 --- a/typhon-core/src/projects.rs +++ b/typhon-core/src/projects.rs @@ -157,50 +157,6 @@ impl Project { meta: ProjectMetadata, } - let run = { - let url = self.project.url.clone(); - let flake = self.project.flake; - move |sender| async move { - let url_locked = nix::lock(&url)?; - - let TyphonProject { actions, meta } = - serde_json::from_value(nix::eval(&url_locked, &"typhonProject", flake).await?) - .map_err(|_| Error::BadProjectDecl)?; - - let actions: Option<&String> = - actions.as_ref().map(|m| m.get(&*CURRENT_SYSTEM)).flatten(); - - let actions_path = if let Some(x) = actions { - let drv = nix::derivation(nix::Expr::Path(x.clone())).await?; - // FIXME: this should spawn a build - Some(nix::build(&drv.path, sender).await?["out"].clone()) - // TODO: check public key used to encrypt secrets - } else { - None - }; - - Ok((url_locked, meta, actions_path)) - } - }; - - let finish = { - let self_ = self.clone(); - move |res: Option), Error>>| { - let status = match res { - Some(Ok(x)) => self_.finish_refresh(x), - Some(Err(e)) => { - tracing::warn!("refresh error for project {}: {}", self_.handle(), e); - Ok(TaskStatusKind::Failure) - } - None => Ok(TaskStatusKind::Canceled), - }; - ( - status.unwrap_or(TaskStatusKind::Failure), - Event::ProjectUpdated(self_.handle()), - ) - } - }; - let task = tasks::Task::new(conn)?; diesel::update(&self.project) .set(schema::projects::last_refresh_task_id.eq(task.task.id)) @@ -208,7 +164,50 @@ impl Project { log_event(Event::ProjectUpdated(self.handle())); - task.run(conn, run, finish)?; + let url = self.project.url.clone(); + let flake = self.project.flake; + let self_ = self.clone(); + task.run(conn, move |handle, sender| async move { + let res = handle + .spawn(async move { + let url_locked = nix::lock(&url)?; + + let TyphonProject { actions, meta } = serde_json::from_value( + nix::eval(&url_locked, &"typhonProject", flake).await?, + ) + .map_err(|_| Error::BadProjectDecl)?; + + let actions: Option<&String> = + actions.as_ref().map(|m| m.get(&*CURRENT_SYSTEM)).flatten(); + + let actions_path = if let Some(x) = actions { + let drv = nix::derivation(nix::Expr::Path(x.clone())).await?; + // FIXME: this should spawn a build + Some(nix::build(&drv.path, sender).await?["out"].clone()) + // TODO: check public key used to encrypt secrets + } else { + None + }; + + Ok::<_, Error>((url_locked, meta, actions_path)) + }) + .await; + let status_kind = match res { + Some(Ok(x)) => { + let self_ = self_.clone(); + tokio::task::spawn_blocking(move || self_.finish_refresh(x)).await? + } + Some(Err(e)) => { + tracing::warn!("refresh error for project {}: {}", self_.handle(), e); + Ok(TaskStatusKind::Failure) + } + None => Ok(TaskStatusKind::Canceled), + }; + Ok(( + status_kind.unwrap_or(TaskStatusKind::Failure), + Some(Event::ProjectUpdated(self_.handle())), + )) + })?; Ok(()) } diff --git a/typhon-core/src/runs.rs b/typhon-core/src/runs.rs index c102a48..58cb2a3 100644 --- a/typhon-core/src/runs.rs +++ b/typhon-core/src/runs.rs @@ -182,38 +182,32 @@ impl Run { .execute(conn)?; log_event(Event::RunUpdated(self.handle())); - // a waiter task - let run_run = async move { + let self_ = self.clone(); + RUNS.run(self.run.id, move |_| async move { + // wait for the `begin` action TASKS.wait(&action_begin.task.task.id).await; + + // wait for the build let res = build_handle.wait().await; - match res { + let status_kind = match res { Some(Some(())) => TaskStatusKind::Success, Some(None) => TaskStatusKind::Failure, None => TaskStatusKind::Canceled, - } - }; - - // run the 'end' action - let finish_run = { - let self_ = self.clone(); - let finish_err = move |status| { - if let Some(status) = status { - let mut conn = POOL.get().unwrap(); - let action_end = self_.spawn_action(&mut conn, "end", status)?; - diesel::update(&self_.run) - .set((schema::runs::end_id.eq(action_end.action.id),)) - .execute(&mut conn)?; - log_event(Event::RunUpdated(self_.handle())); - } - Ok::<_, Error>(()) }; - move |status| { - finish_err(status).unwrap(); // FIXME - None::<()> - } - }; - RUNS.run(self.run.id, (run_run, finish_run)); + // run the `end` action + let _ = tokio::task::spawn_blocking(move || { + let mut conn = POOL.get().unwrap(); + let action_end = self_.spawn_action(&mut conn, "end", status_kind)?; + diesel::update(&self_.run) + .set((schema::runs::end_id.eq(action_end.action.id),)) + .execute(&mut conn)?; + log_event(Event::RunUpdated(self_.handle())); + Ok::<(), Error>(()) + }) + .await + .unwrap(); + }); Ok(()) } diff --git a/typhon-core/src/task_manager.rs b/typhon-core/src/task_manager.rs index 91df41d..810d6a8 100644 --- a/typhon-core/src/task_manager.rs +++ b/typhon-core/src/task_manager.rs @@ -24,7 +24,7 @@ enum Msg { Wait(Id, oneshot::Sender<()>), } -struct TaskHandle { +struct TaskCtx { canceler: Option>, waiters: Vec>, } @@ -34,45 +34,25 @@ pub struct TaskManager { watch: watch::Receiver<()>, } -pub trait Task { - type T: Send + 'static; - fn get( - self, - ) -> ( - impl Future + Send + 'static, - impl FnOnce(Option) -> Option + Send + 'static, - ); +pub struct TaskHandle { + cancel: mpsc::UnboundedSender>, } -#[allow(refining_impl_trait)] -impl Task for () { - type T = (); - fn get( - self, - ) -> ( - impl Future + Send + 'static, - impl FnOnce(Option) -> Option<()> + Send + 'static, - ) { - (async move {}, move |_| None) - } -} - -#[allow(refining_impl_trait)] -impl< - T: Send + 'static, - C: Task + Send + 'static, - F: Future + Send + 'static, - Fn: FnOnce(Option) -> Option + Send + 'static, - > Task for (F, Fn) -{ - type T = T; - fn get( - self, - ) -> ( - impl Future + Send + 'static, - impl FnOnce(Option) -> Option + Send + 'static, - ) { - self +impl TaskHandle { + pub async fn spawn + Send + 'static>( + &self, + f: F, + ) -> Option { + let (cancel_send, cancel_recv) = oneshot::channel(); + let res = tokio::spawn(async move { + tokio::select! { + _ = cancel_recv => None, + r = f => Some(r), + } + }) + .await; + let _ = self.cancel.send(cancel_send); + res.unwrap() } } @@ -83,7 +63,7 @@ impl = HashMap::new(); + let mut tasks: HashMap = HashMap::new(); let mut shutdown = false; while let Some(msg) = msg_recv.recv().await { match (shutdown, msg) { @@ -103,7 +83,7 @@ impl { - let task = TaskHandle { + let task = TaskCtx { canceler: Some(cancel_send), waiters: Vec::new(), }; @@ -143,10 +123,14 @@ impl(&self, id: Id, task: T) { - use tokio::task::spawn_blocking; - + pub fn run< + O: Future + Send + 'static, + T: FnOnce(TaskHandle) -> O + Send + 'static, + >( + &self, + id: Id, + task: T, + ) { let (cancel_send, cancel_recv) = oneshot::channel::<()>(); let sender_self = self.msg_send.clone(); let id_bis = id.clone(); @@ -160,25 +144,11 @@ impl>, - task: impl Task + Send + 'static, - ) { - let (run, finish) = task.get(); - let (cancel_step_send, cancel_step_recv) = oneshot::channel(); - let _ = cancel_thread_send.send(cancel_step_send); - let r = tokio::select! { - _ = cancel_step_recv => None, - r = run => Some(r), - }; - let maybe_task = spawn_blocking(move || finish(r)).await.unwrap_or(None); - if let Some(task) = maybe_task { - aux(cancel_thread_send, task).await; - } - } - aux(cancel_thread_send, task).await; + task(handle).await; cancel_thread.abort(); let _ = cancel_thread.await; let _ = sender_self.send(Msg::Finish(id_bis)); diff --git a/typhon-core/src/tasks.rs b/typhon-core/src/tasks.rs index 057e74c..de0d355 100644 --- a/typhon-core/src/tasks.rs +++ b/typhon-core/src/tasks.rs @@ -2,6 +2,7 @@ use crate::error::Error; use crate::log_event; use crate::models; use crate::schema; +use crate::task_manager::TaskHandle; use crate::Conn; use crate::POOL; use crate::{LOGS, TASKS}; @@ -73,15 +74,12 @@ impl Task { } pub fn run< - T: Send + 'static, - O: Future + Send + 'static, - F: (FnOnce(mpsc::UnboundedSender) -> O) + Send + 'static, - G: (FnOnce(Option) -> (TaskStatusKind, Event)) + Send + Sync + 'static, + O: Future), Error>> + Send + 'static, + F: (FnOnce(TaskHandle, mpsc::UnboundedSender) -> O) + Send + 'static, >( &self, conn: &mut Conn, - run: F, - finish: G, + task: F, ) -> Result<(), Error> { let start = Some(OffsetDateTime::now_utc()); let id = self.task.id; @@ -89,34 +87,40 @@ impl Task { self.set_status(conn, TaskStatus::Pending { start })?; let (sender, mut receiver) = mpsc::unbounded_channel(); - let run = async move { + + let self_ = self.clone(); + TASKS.run(id, move |handle| async move { LOGS.init(&id); - let (res, ()) = tokio::join!(run(sender), async move { + let (res, ()) = tokio::join!(task(handle, sender), async move { while let Some(line) = receiver.recv().await { LOGS.send_line(&id, line); } },); - res - }; - let finish = { - let task = self.clone(); - move |res: Option| { + if let Err(e) = &res { + tracing::error!("error when running task: {}", e); + } + let (status_kind, event) = res.unwrap_or((TaskStatusKind::Error, None)); + let res = tokio::task::spawn_blocking(move || { let mut conn = POOL.get().unwrap(); - let (status_kind, event) = finish(res); let time_finished = OffsetDateTime::now_utc(); let stderr = LOGS.remove(&id).unwrap_or(String::new()); // FIXME let status = status_kind.into_task_status(start, Some(time_finished)); - task.set_status(&mut conn, status).unwrap(); - diesel::update(schema::logs::table.filter(schema::logs::id.eq(task.task.log_id))) + self_.set_status(&mut conn, status).unwrap(); + diesel::update(schema::logs::table.filter(schema::logs::id.eq(self_.task.log_id))) .set(schema::logs::stderr.eq(stderr)) - .execute(&mut conn) - .unwrap(); // TODO: handle error properly - log_event(event); - None::<()> + .execute(&mut conn)?; + if let Some(event) = event { + log_event(event); + } + Ok::<_, Error>(()) + }) + .await; + match res { + Ok(Err(e)) => tracing::error!("error when finishing task: {}", e), + Err(e) => tracing::error!("error when finishing task: {}", e), + _ => (), } - }; - - TASKS.run(id, (run, finish)); + }); Ok(()) } diff --git a/typhon-types/src/task_status.rs b/typhon-types/src/task_status.rs index 2795659..c982839 100644 --- a/typhon-types/src/task_status.rs +++ b/typhon-types/src/task_status.rs @@ -19,6 +19,8 @@ pub enum TaskStatus { * is a `Some(TimeRange {start,end})`) or before running. */ // TODO: we should have either a TimeRange or a {end}, right? Canceled(Option), + /** The task failed because of an internal error */ + Error(TimeRange), } impl Default for TaskStatus { @@ -40,6 +42,7 @@ pub enum TaskStatusKind { Success = 1, Failure = 2, Canceled = 3, + Error = 4, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -63,6 +66,7 @@ impl From<&TaskStatus> for TaskStatusKind { TaskStatus::Success(..) => Self::Success, TaskStatus::Failure(..) => Self::Failure, TaskStatus::Canceled(..) => Self::Canceled, + TaskStatus::Error(..) => Self::Error, } } } @@ -77,6 +81,7 @@ const SUCCESS_TIME_INVARIANT: &str = "a `TaskStatus::Success` requires a start time and an end time"; const FAILURE_TIME_INVARIANT: &str = "a `TaskStatus::Failure` requires a start time and an end time"; +const ERROR_TIME_INVARIANT: &str = "a `TaskStatus::Error` requires a start time and an end time"; impl TaskStatusKind { /** Promotes a `TaskStatusKind` to a `TaskStatus`, given a start * time and a finish time. Note those are optional: a success task @@ -93,6 +98,7 @@ impl TaskStatusKind { Self::Success => TaskStatus::Success(range.expect(SUCCESS_TIME_INVARIANT)), Self::Failure => TaskStatus::Failure(range.expect(FAILURE_TIME_INVARIANT)), Self::Canceled => TaskStatus::Canceled(range), + Self::Error => TaskStatus::Error(range.expect(ERROR_TIME_INVARIANT)), } } } @@ -102,9 +108,10 @@ impl TaskStatus { pub fn times(self) -> (Option, Option) { match self { Self::Pending { start } => (start, None), - Self::Success(range) | Self::Failure(range) | Self::Canceled(Some(range)) => { - (Some(range.start), Some(range.end)) - } + Self::Success(range) + | Self::Failure(range) + | Self::Error(range) + | Self::Canceled(Some(range)) => (Some(range.start), Some(range.end)), Self::Canceled(None) => (None, None), } } @@ -121,6 +128,7 @@ impl TaskStatus { TaskStatusKind::Pending => Self::Pending { start }, TaskStatusKind::Canceled => Self::Canceled(range), TaskStatusKind::Success => Self::Success(range.expect(SUCCESS_TIME_INVARIANT)), + TaskStatusKind::Error => Self::Error(range.expect(ERROR_TIME_INVARIANT)), } } } @@ -145,6 +153,7 @@ impl std::fmt::Display for TaskStatusKind { Self::Success => write!(f, "success"), Self::Failure => write!(f, "failure"), Self::Canceled => write!(f, "canceled"), + Self::Error => write!(f, "error"), } } } @@ -161,6 +170,8 @@ impl core::cmp::Ord for TaskStatusKind { return Ordering::Equal; } match (self, rhs) { + (TaskStatusKind::Error, _) => Ordering::Greater, + (_, TaskStatusKind::Error) => Ordering::Less, (TaskStatusKind::Failure, _) => Ordering::Greater, (_, TaskStatusKind::Failure) => Ordering::Less, (TaskStatusKind::Pending, _) => Ordering::Greater, diff --git a/typhon-webapp/src/components/evaluations.rs b/typhon-webapp/src/components/evaluations.rs index ac8b251..e580643 100644 --- a/typhon-webapp/src/components/evaluations.rs +++ b/typhon-webapp/src/components/evaluations.rs @@ -95,7 +95,9 @@ impl EvalStatus { TaskStatusKind::Success => HybridStatusKind::EvalSucceeded { build: self.jobs.unwrap_or_default().into(), }, - TaskStatusKind::Failure | TaskStatusKind::Canceled => HybridStatusKind::EvalStopped, + TaskStatusKind::Failure | TaskStatusKind::Canceled | TaskStatusKind::Error => { + HybridStatusKind::EvalStopped + } } } pub fn summary(&self) -> TaskStatus { diff --git a/typhon-webapp/src/components/status.rs b/typhon-webapp/src/components/status.rs index c54d3d0..f102986 100644 --- a/typhon-webapp/src/components/status.rs +++ b/typhon-webapp/src/components/status.rs @@ -63,6 +63,7 @@ pub fn HybridStatus(#[prop(into)] status: Signal) -> impl Into TaskStatusKind::Pending => BiLoaderAltRegular, TaskStatusKind::Failure => BiXCircleSolid, TaskStatusKind::Canceled => BiStopCircleRegular, + TaskStatusKind::Error => BiRadiationSolid, } } }; diff --git a/typhon-webapp/src/pages/evaluation.rs b/typhon-webapp/src/pages/evaluation.rs index de49c01..491f607 100644 --- a/typhon-webapp/src/pages/evaluation.rs +++ b/typhon-webapp/src/pages/evaluation.rs @@ -226,6 +226,7 @@ pub fn JobSubpage( TaskStatus::Failure(..) => make("failed"), TaskStatus::Canceled(Some(..)) => make("canceled"), TaskStatus::Canceled(None) => view! { <>canceled }, + TaskStatus::Error(..) => view! { <>error }, } }