From bf2f4887cb4743a8b7edc78d7316c5f0281d981c Mon Sep 17 00:00:00 2001 From: Lucy C <12953208+elvece@users.noreply.github.com> Date: Fri, 17 Jun 2022 20:11:15 -0600 Subject: [PATCH] Feature/restart service (#1554) * add restart button to service show page and restart rpc api * Feature/restart rpc (#1555) * add restart rpc and status * wire up rpc * add restarting bool Co-authored-by: Aiden McClelland * check if service is restarting * filter package when restarting to avoid glitch Co-authored-by: Aiden McClelland --- backend/src/backup/backup_bulk.rs | 6 +- backend/src/context/rpc.rs | 6 +- backend/src/control.rs | 35 ++++++++++- backend/src/dependencies.rs | 9 +-- backend/src/lib.rs | 1 + backend/src/manager/sync.rs | 17 +++--- backend/src/status/mod.rs | 12 ++-- .../apps-routes/app-show/app-show.page.ts | 10 +++- .../app-show-status.component.html | 10 +++- .../app-show-status.component.scss | 4 +- .../app-show-status.component.ts | 29 ++++++--- .../ui/src/app/services/api/api.types.ts | 3 + .../app/services/api/embassy-api.service.ts | 6 ++ .../services/api/embassy-live-api.service.ts | 6 ++ .../services/api/embassy-mock-api.service.ts | 59 ++++++++++++++++++- .../src/app/services/patch-db/data-model.ts | 7 +++ .../services/pkg-status-rendering.service.ts | 10 +++- libs/js_engine/src/lib.rs | 46 ++++++--------- 18 files changed, 212 insertions(+), 64 deletions(-) diff --git a/backend/src/backup/backup_bulk.rs b/backend/src/backup/backup_bulk.rs index f8e1281b4..d49d2cd4c 100644 --- a/backend/src/backup/backup_bulk.rs +++ b/backend/src/backup/backup_bulk.rs @@ -268,9 +268,11 @@ async fn perform_backup( main_status_model.lock(&mut tx, LockType::Write).await?; let (started, health) = match main_status_model.get(&mut tx, true).await?.into_owned() { - MainStatus::Starting => (Some(Utc::now()), Default::default()), + MainStatus::Starting { .. } => (Some(Utc::now()), Default::default()), MainStatus::Running { started, health } => (Some(started), health.clone()), - MainStatus::Stopped | MainStatus::Stopping => (None, Default::default()), + MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => { + (None, Default::default()) + } MainStatus::BackingUp { .. } => { backup_report.insert( package_id, diff --git a/backend/src/context/rpc.rs b/backend/src/context/rpc.rs index a5e99b9e3..e576fe864 100644 --- a/backend/src/context/rpc.rs +++ b/backend/src/context/rpc.rs @@ -335,12 +335,14 @@ impl RpcContext { let main = match status.main { MainStatus::BackingUp { started, .. } => { if let Some(_) = started { - MainStatus::Starting + MainStatus::Starting { restarting: false } } else { MainStatus::Stopped } } - MainStatus::Running { .. } => MainStatus::Starting, + MainStatus::Running { .. } => { + MainStatus::Starting { restarting: false } + } a => a.clone(), }; let new_package = PackageDataEntry::Installed { diff --git a/backend/src/control.rs b/backend/src/control.rs index 3b8fb5df6..4235b03ca 100644 --- a/backend/src/control.rs +++ b/backend/src/control.rs @@ -71,7 +71,10 @@ pub async fn start( let mut tx = db.begin().await?; let receipts = StartReceipts::new(&mut tx, &id).await?; let version = receipts.version.get(&mut tx).await?; - receipts.status.set(&mut tx, MainStatus::Starting).await?; + receipts + .status + .set(&mut tx, MainStatus::Starting { restarting: false }) + .await?; heal_all_dependents_transitive(&ctx, &mut tx, &id, &receipts.dependency_receipt).await?; let revision = tx.commit(None).await?; @@ -181,3 +184,33 @@ pub async fn stop_impl(ctx: RpcContext, id: PackageId) -> Result Result, Error> { + let mut db = ctx.db.handle(); + let mut tx = db.begin().await?; + + let mut status = crate::db::DatabaseModel::new() + .package_data() + .idx_model(&id) + .and_then(|pde| pde.installed()) + .map(|i| i.status().main()) + .get_mut(&mut tx) + .await?; + if !matches!(&*status, Some(MainStatus::Running { .. })) { + return Err(Error::new( + eyre!("{} is not running", id), + crate::ErrorKind::InvalidRequest, + )); + } + *status = Some(MainStatus::Restarting); + status.save(&mut tx).await?; + + Ok(WithRevision { + revision: tx.commit(None).await?, + response: (), + }) +} diff --git a/backend/src/dependencies.rs b/backend/src/dependencies.rs index 7e5499e67..90248f290 100644 --- a/backend/src/dependencies.rs +++ b/backend/src/dependencies.rs @@ -14,11 +14,12 @@ use rpc_toolkit::command; use serde::{Deserialize, Serialize}; use tracing::instrument; +use crate::config::action::{ConfigActions, ConfigRes}; use crate::config::spec::PackagePointerSpec; use crate::config::{not_found, Config, ConfigReceipts, ConfigSpec}; use crate::context::RpcContext; use crate::db::model::{CurrentDependencies, CurrentDependents, InstalledPackageDataEntry}; -use crate::procedure::{NoOutput, PackageProcedure}; +use crate::procedure::{NoOutput, PackageProcedure, ProcedureName}; use crate::s9pk::manifest::{Manifest, PackageId}; use crate::status::health_check::{HealthCheckId, HealthCheckResult}; use crate::status::{MainStatus, Status}; @@ -26,10 +27,6 @@ use crate::util::serde::display_serializable; use crate::util::{display_none, Version}; use crate::volume::Volumes; use crate::Error; -use crate::{ - config::action::{ConfigActions, ConfigRes}, - procedure::ProcedureName, -}; #[command(subcommands(configure))] pub fn dependency() -> Result<(), Error> { @@ -339,7 +336,7 @@ impl DependencyError { .await? } } - MainStatus::Starting => { + MainStatus::Starting { .. } | MainStatus::Restarting => { DependencyError::Transitive .try_heal( ctx, diff --git a/backend/src/lib.rs b/backend/src/lib.rs index b4e61bb5a..1eeefd0e2 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -99,6 +99,7 @@ pub fn server() -> Result<(), RpcError> { config::config, control::start, control::stop, + control::restart, logs::logs, properties::properties, dependencies::dependency, diff --git a/backend/src/manager/sync.rs b/backend/src/manager/sync.rs index 841e52f5f..5cb17aeb4 100644 --- a/backend/src/manager/sync.rs +++ b/backend/src/manager/sync.rs @@ -31,7 +31,10 @@ async fn synchronize_once(shared: &ManagerSharedState) -> Result MainStatus::Stopping => { *status = MainStatus::Stopped; } - MainStatus::Starting => { + MainStatus::Restarting => { + *status = MainStatus::Starting { restarting: true }; + } + MainStatus::Starting { .. } => { start(shared).await?; } MainStatus::Running { started, .. } => { @@ -41,19 +44,19 @@ async fn synchronize_once(shared: &ManagerSharedState) -> Result MainStatus::BackingUp { .. } => (), }, Status::Starting => match *status { - MainStatus::Stopped | MainStatus::Stopping => { + MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => { stop(shared).await?; } - MainStatus::Starting | MainStatus::Running { .. } => (), + MainStatus::Starting { .. } | MainStatus::Running { .. } => (), MainStatus::BackingUp { .. } => { pause(shared).await?; } }, Status::Running => match *status { - MainStatus::Stopped | MainStatus::Stopping => { + MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => { stop(shared).await?; } - MainStatus::Starting => { + MainStatus::Starting { .. } => { *status = MainStatus::Running { started: Utc::now(), health: BTreeMap::new(), @@ -65,10 +68,10 @@ async fn synchronize_once(shared: &ManagerSharedState) -> Result } }, Status::Paused => match *status { - MainStatus::Stopped | MainStatus::Stopping => { + MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => { stop(shared).await?; } - MainStatus::Starting | MainStatus::Running { .. } => { + MainStatus::Starting { .. } | MainStatus::Running { .. } => { resume(shared).await?; } MainStatus::BackingUp { .. } => (), diff --git a/backend/src/status/mod.rs b/backend/src/status/mod.rs index 4167a0020..e793665df 100644 --- a/backend/src/status/mod.rs +++ b/backend/src/status/mod.rs @@ -24,8 +24,11 @@ pub struct Status { #[serde(rename_all = "kebab-case")] pub enum MainStatus { Stopped, + Restarting, Stopping, - Starting, + Starting { + restarting: bool, + }, Running { started: DateTime, health: BTreeMap, @@ -38,25 +41,26 @@ pub enum MainStatus { impl MainStatus { pub fn running(&self) -> bool { match self { - MainStatus::Starting + MainStatus::Starting { .. } | MainStatus::Running { .. } | MainStatus::BackingUp { started: Some(_), .. } => true, MainStatus::Stopped | MainStatus::Stopping + | MainStatus::Restarting | MainStatus::BackingUp { started: None, .. } => false, } } pub fn stop(&mut self) { match self { - MainStatus::Starting | MainStatus::Running { .. } => { + MainStatus::Starting { .. } | MainStatus::Running { .. } => { *self = MainStatus::Stopping; } MainStatus::BackingUp { started, .. } => { *started = None; } - MainStatus::Stopped | MainStatus::Stopping => (), + MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => (), } } } diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index 4557a68ab..b0254ef13 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -3,6 +3,7 @@ import { NavController } from '@ionic/angular' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PackageDataEntry, + PackageMainStatus, PackageState, } from 'src/app/services/patch-db/data-model' import { @@ -13,7 +14,7 @@ import { ConnectionFailure, ConnectionService, } from 'src/app/services/connection.service' -import { map, startWith } from 'rxjs/operators' +import { map, startWith, filter } from 'rxjs/operators' import { ActivatedRoute } from '@angular/router' import { getPkgId } from '@start9labs/shared' @@ -32,6 +33,13 @@ export class AppShowPage { private readonly pkgId = getPkgId(this.route) readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe( + filter( + (p: PackageDataEntry) => + !( + p.installed?.status.main.status === PackageMainStatus.Starting && + p.installed?.status.main.restarting + ), + ), map(pkg => { // if package disappears, navigate to list page if (!pkg) { diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html index 5e28b0cba..ac90fc240 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html @@ -21,6 +21,14 @@ Stop + + + Restart + - \ No newline at end of file + diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.scss b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.scss index 0b534fd9f..d6b8b47fb 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.scss +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.scss @@ -3,7 +3,7 @@ } .action-button { - margin: 20px 20px 10px 0; + margin: 12px 20px 10px 0; min-height: 42px; min-width: 140px; -} +} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts index 9bf3c1e57..c80938845 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts @@ -137,8 +137,6 @@ export class AppShowStatusComponent { } } -<<<<<<< HEAD -======= async tryRestart(): Promise { if (hasCurrentDeps(this.pkg)) { const alert = await this.alertCtrl.create({ @@ -165,7 +163,28 @@ export class AppShowStatusComponent { } } ->>>>>>> 918a1907... Remove app wiz and dry calls (#1541) + async presentAlertRestart(): Promise { + const alert = await this.alertCtrl.create({ + header: 'Confirm', + message: 'Are you sure you want to restart this service?', + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Restart', + handler: () => { + this.restart() + }, + cssClass: 'enter-click', + }, + ], + }) + + await alert.present() + } + private async start(): Promise { const loader = await this.loadingCtrl.create({ message: `Starting...`, @@ -181,8 +200,6 @@ export class AppShowStatusComponent { } } -<<<<<<< HEAD -======= private async stop(): Promise { const loader = await this.loadingCtrl.create({ message: 'Stopping...', @@ -212,8 +229,6 @@ export class AppShowStatusComponent { loader.dismiss() } } - ->>>>>>> 918a1907... Remove app wiz and dry calls (#1541) private async presentAlertStart(message: string): Promise { return new Promise(async resolve => { const alert = await this.alertCtrl.create({ diff --git a/frontend/projects/ui/src/app/services/api/api.types.ts b/frontend/projects/ui/src/app/services/api/api.types.ts index e3b036a44..4f23af82d 100644 --- a/frontend/projects/ui/src/app/services/api/api.types.ts +++ b/frontend/projects/ui/src/app/services/api/api.types.ts @@ -215,6 +215,9 @@ export module RR { export type StartPackageReq = WithExpire<{ id: string }> // package.start export type StartPackageRes = WithRevision + export type RestartPackageReq = WithExpire<{ id: string }> // package.restart + export type RestartPackageRes = WithRevision + export type StopPackageReq = WithExpire<{ id: string }> // package.stop export type StopPackageRes = WithRevision diff --git a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts index 495e80f9b..fd103bb3c 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts @@ -233,6 +233,12 @@ export abstract class ApiService implements Source, Http { startPackage = (params: RR.StartPackageReq) => this.syncResponse(() => this.startPackageRaw(params))() + protected abstract restartPackageRaw( + params: RR.RestartPackageReq, + ): Promise + restartPackage = (params: RR.RestartPackageReq) => + this.syncResponse(() => this.restartPackageRaw(params))() + protected abstract stopPackageRaw( params: RR.StopPackageReq, ): Promise diff --git a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts index 2ee89804d..db10e8144 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -306,6 +306,12 @@ export class LiveApiService extends ApiService { return this.http.rpcRequest({ method: 'package.start', params }) } + async restartPackageRaw( + params: RR.RestartPackageReq, + ): Promise { + return this.http.rpcRequest({ method: 'package.restart', params }) + } + async stopPackageRaw(params: RR.StopPackageReq): Promise { return this.http.rpcRequest({ method: 'package.stop', params }) } diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts index dad557746..0c859a621 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -36,7 +36,7 @@ export class MockApiService extends ApiService { value: mockPatchData, expireId: null, }) - private readonly revertTime = 4000 + private readonly revertTime = 2000 sequence: number constructor(private readonly bootstrapper: LocalStorageBootstrap) { @@ -654,6 +654,63 @@ export class MockApiService extends ApiService { return this.withRevision(originalPatch) } + async restartPackageRaw( + params: RR.RestartPackageReq, + ): Promise { + // first enact stop + await pauseFor(2000) + const path = `/package-data/${params.id}/installed/status/main` + + setTimeout(() => { + const patch2 = [ + { + op: PatchOp.REPLACE, + path: path + '/status', + value: PackageMainStatus.Running, + }, + { + op: PatchOp.REPLACE, + path: path + '/health', + value: { + 'ephemeral-health-check': { + result: 'starting', + }, + 'unnecessary-health-check': { + result: 'disabled', + }, + 'chain-state': { + result: 'loading', + message: 'Bitcoin is syncing from genesis', + }, + 'p2p-interface': { + result: 'success', + }, + 'rpc-interface': { + result: 'failure', + error: 'RPC interface unreachable.', + }, + }, + } as any, + ] + this.updateMock(patch2) + }, this.revertTime) + + const patch = [ + { + op: PatchOp.REPLACE, + path: path + '/status', + value: PackageMainStatus.Restarting, + }, + { + op: PatchOp.REPLACE, + path: path + '/health', + value: {}, + }, + ] + + return this.withRevision(patch) + } + async stopPackageRaw(params: RR.StopPackageReq): Promise { await pauseFor(2000) const path = `/package-data/${params.id}/installed/status/main` diff --git a/frontend/projects/ui/src/app/services/patch-db/data-model.ts b/frontend/projects/ui/src/app/services/patch-db/data-model.ts index 8eca23da2..2079a4392 100644 --- a/frontend/projects/ui/src/app/services/patch-db/data-model.ts +++ b/frontend/projects/ui/src/app/services/patch-db/data-model.ts @@ -250,6 +250,7 @@ export type MainStatus = | MainStatusStarting | MainStatusRunning | MainStatusBackingUp + | MainStatusRestarting export interface MainStatusStopped { status: PackageMainStatus.Stopped @@ -261,6 +262,7 @@ export interface MainStatusStopping { export interface MainStatusStarting { status: PackageMainStatus.Starting + restarting: boolean } export interface MainStatusRunning { @@ -274,12 +276,17 @@ export interface MainStatusBackingUp { started: string | null // UTC date string } +export interface MainStatusRestarting { + status: PackageMainStatus.Restarting +} + export enum PackageMainStatus { Starting = 'starting', Running = 'running', Stopping = 'stopping', Stopped = 'stopped', BackingUp = 'backing-up', + Restarting = 'restarting', } export type HealthCheckResult = diff --git a/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts b/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts index b6ce47650..264d73d1c 100644 --- a/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts +++ b/frontend/projects/ui/src/app/services/pkg-status-rendering.service.ts @@ -1,5 +1,6 @@ import { isEmptyObject } from '@start9labs/shared' import { + MainStatusStarting, PackageDataEntry, PackageMainStatus, PackageState, @@ -32,6 +33,8 @@ export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus { function getPrimaryStatus(status: Status): PrimaryStatus { if (!status.configured) { return PrimaryStatus.NeedsConfig + } else if ((status.main as MainStatusStarting).restarting) { + return PrimaryStatus.Restarting } else { return status.main.status as any as PrimaryStatus } @@ -57,7 +60,6 @@ function getHealthStatus( } const values = Object.values(status.main.health) - console.log('HEALTH CHECKS', values) if (values.some(h => h.result === 'failure')) { return HealthStatus.Failure @@ -94,6 +96,7 @@ export enum PrimaryStatus { Starting = 'starting', Running = 'running', Stopping = 'stopping', + Restarting = 'restarting', Stopped = 'stopped', BackingUp = 'backing-up', // config @@ -139,6 +142,11 @@ export const PrimaryRendering: Record = { color: 'dark-shade', showDots: true, }, + [PrimaryStatus.Restarting]: { + display: 'Restarting', + color: 'warning', + showDots: true, + }, [PrimaryStatus.Stopped]: { display: 'Stopped', color: 'dark-shade', diff --git a/libs/js_engine/src/lib.rs b/libs/js_engine/src/lib.rs index 6998dff58..d256e16c7 100644 --- a/libs/js_engine/src/lib.rs +++ b/libs/js_engine/src/lib.rs @@ -1,23 +1,17 @@ +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::sync::Arc; use deno_core::anyhow::{anyhow, bail}; use deno_core::error::AnyError; -use deno_core::resolve_import; -use deno_core::JsRuntime; -use deno_core::ModuleLoader; -use deno_core::ModuleSource; -use deno_core::ModuleSourceFuture; -use deno_core::ModuleSpecifier; -use deno_core::ModuleType; -use deno_core::RuntimeOptions; -use deno_core::Snapshot; -use deno_core::{Extension, OpDecl}; -use helpers::script_dir; -use helpers::NonDetachingJoinHandle; +use deno_core::{ + resolve_import, Extension, JsRuntime, ModuleLoader, ModuleSource, ModuleSourceFuture, + ModuleSpecifier, ModuleType, OpDecl, RuntimeOptions, Snapshot, +}; +use helpers::{script_dir, NonDetachingJoinHandle}; use models::{PackageId, ProcedureName, Version, VolumeId}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::time::SystemTime; -use std::{path::Path, sync::Arc}; -use std::{path::PathBuf, pin::Pin}; use tokio::io::AsyncReadExt; pub trait PathForVolumeId: Send + Sync { @@ -335,23 +329,17 @@ impl JsExecutionEnvironment { /// Note: Make sure that we have the assumption that all these methods are callable at any time, and all call restrictions should be in rust mod fns { - use deno_core::{ - anyhow::{anyhow, bail}, - error::AnyError, - *, - }; + use std::cell::RefCell; + use std::convert::TryFrom; + use std::path::{Path, PathBuf}; + use std::rc::Rc; + use deno_core::anyhow::{anyhow, bail}; + use deno_core::error::AnyError; + use deno_core::*; + use models::VolumeId; use serde_json::Value; use std::os::unix::fs::MetadataExt; - use std::{ - cell::RefCell, - convert::TryFrom, - path::{Path, PathBuf}, - rc::Rc, - }; - - use models::VolumeId; - use crate::{system_time_as_unix_ms, MetadataJs}; use super::{AnswerState, JsContext}; @@ -645,4 +633,4 @@ fn system_time_as_unix_ms(system_time: &SystemTime) -> Option { .as_millis() .try_into() .ok() -} +} \ No newline at end of file