Feature/server status restarting (#2503)

* extend `server-info`

* add restarting, shutting down to FE status bar

* fix build

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Matt Hill
2023-11-08 02:31:18 -07:00
committed by GitHub
parent 753fbc0c5c
commit 871f78b570
11 changed files with 117 additions and 37 deletions

View File

@@ -57,6 +57,8 @@ impl Database {
backup_progress: None, backup_progress: None,
updated: false, updated: false,
update_progress: None, update_progress: None,
shutting_down: false,
restarting: false,
}, },
wifi: WifiInfo { wifi: WifiInfo {
ssids: Vec::new(), ssids: Vec::new(),
@@ -166,6 +168,10 @@ pub struct ServerStatus {
pub backup_progress: Option<BTreeMap<PackageId, BackupProgress>>, pub backup_progress: Option<BTreeMap<PackageId, BackupProgress>>,
pub updated: bool, pub updated: bool,
pub update_progress: Option<UpdateProgress>, pub update_progress: Option<UpdateProgress>,
#[serde(default)]
pub shutting_down: bool,
#[serde(default)]
pub restarting: bool,
} }
#[derive(Debug, Deserialize, Serialize, HasModel)] #[derive(Debug, Deserialize, Serialize, HasModel)]

View File

@@ -406,6 +406,8 @@ pub async fn init(cfg: &RpcContextConfig) -> Result<InitResult, Error> {
updated: false, updated: false,
update_progress: None, update_progress: None,
backup_progress: None, backup_progress: None,
shutting_down: false,
restarting: false,
}; };
server_info.ntp_synced = if time_not_synced { server_info.ntp_synced = if time_not_synced {

View File

@@ -6,10 +6,11 @@ use rpc_toolkit::command;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::disk::main::export; use crate::disk::main::export;
use crate::init::{STANDBY_MODE_PATH, SYSTEM_REBUILD_PATH}; use crate::init::{STANDBY_MODE_PATH, SYSTEM_REBUILD_PATH};
use crate::prelude::*;
use crate::sound::SHUTDOWN; use crate::sound::SHUTDOWN;
use crate::util::docker::CONTAINER_TOOL; use crate::util::docker::CONTAINER_TOOL;
use crate::util::{display_none, Invoke}; use crate::util::{display_none, Invoke};
use crate::{Error, PLATFORM}; use crate::PLATFORM;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Shutdown { pub struct Shutdown {
@@ -90,6 +91,14 @@ impl Shutdown {
#[command(display(display_none))] #[command(display(display_none))]
pub async fn shutdown(#[context] ctx: RpcContext) -> Result<(), Error> { pub async fn shutdown(#[context] ctx: RpcContext) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_server_info_mut()
.as_status_info_mut()
.as_shutting_down_mut()
.ser(&true)
})
.await?;
ctx.shutdown ctx.shutdown
.send(Some(Shutdown { .send(Some(Shutdown {
export_args: Some((ctx.disk_guid.clone(), ctx.datadir.clone())), export_args: Some((ctx.disk_guid.clone(), ctx.datadir.clone())),
@@ -102,6 +111,14 @@ pub async fn shutdown(#[context] ctx: RpcContext) -> Result<(), Error> {
#[command(display(display_none))] #[command(display(display_none))]
pub async fn restart(#[context] ctx: RpcContext) -> Result<(), Error> { pub async fn restart(#[context] ctx: RpcContext) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_server_info_mut()
.as_status_info_mut()
.as_restarting_mut()
.ser(&true)
})
.await?;
ctx.shutdown ctx.shutdown
.send(Some(Shutdown { .send(Some(Shutdown {
export_args: Some((ctx.disk_guid.clone(), ctx.datadir.clone())), export_args: Some((ctx.disk_guid.clone(), ctx.datadir.clone())),

View File

@@ -26,10 +26,7 @@
type="overlay" type="overlay"
side="end" side="end"
class="right-menu container" class="right-menu container"
[class.container_offline]=" [class.container_offline]="offline$ | async"
(authService.isVerified$ | async) &&
!(connection.connected$ | async)
"
[class.right-menu_hidden]="!drawer.open" [class.right-menu_hidden]="!drawer.open"
[style.--side-width.px]="drawer.width" [style.--side-width.px]="drawer.width"
> >
@@ -47,10 +44,7 @@
[responsiveColViewport]="viewport" [responsiveColViewport]="viewport"
id="main-content" id="main-content"
class="container" class="container"
[class.container_offline]=" [class.container_offline]="offline$ | async"
(authService.isVerified$ | async) &&
!(connection.connected$ | async)
"
> >
<ion-content <ion-content
#viewport="viewport" #viewport="viewport"

View File

@@ -1,5 +1,5 @@
import { Component, inject, OnDestroy } from '@angular/core' import { Component, inject, OnDestroy } from '@angular/core'
import { merge } from 'rxjs' import { combineLatest, map, merge, startWith } from 'rxjs'
import { AuthService } from './services/auth.service' import { AuthService } from './services/auth.service'
import { SplitPaneTracker } from './services/split-pane.service' import { SplitPaneTracker } from './services/split-pane.service'
import { PatchDataService } from './services/patch-data.service' import { PatchDataService } from './services/patch-data.service'
@@ -25,6 +25,19 @@ export class AppComponent implements OnDestroy {
readonly sidebarOpen$ = this.splitPane.sidebarOpen$ readonly sidebarOpen$ = this.splitPane.sidebarOpen$
readonly widgetDrawer$ = this.clientStorageService.widgetDrawer$ readonly widgetDrawer$ = this.clientStorageService.widgetDrawer$
readonly theme$ = inject(THEME) readonly theme$ = inject(THEME)
readonly offline$ = combineLatest([
this.authService.isVerified$,
this.connection.connected$,
this.patch
.watch$('server-info', 'status-info')
.pipe(startWith({ restarting: false, 'shutting-down': false })),
]).pipe(
map(
([verified, connected, status]) =>
verified &&
(!connected || status.restarting || status['shutting-down']),
),
)
constructor( constructor(
private readonly titleService: Title, private readonly titleService: Title,

View File

@@ -1,6 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core' import { ChangeDetectionStrategy, Component } from '@angular/core'
import { PatchDB } from 'patch-db-client'
import { combineLatest, map, Observable, startWith } from 'rxjs' import { combineLatest, map, Observable, startWith } from 'rxjs'
import { ConnectionService } from 'src/app/services/connection.service' import { ConnectionService } from 'src/app/services/connection.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({ @Component({
selector: 'connection-bar', selector: 'connection-bar',
@@ -19,8 +21,11 @@ export class ConnectionBarComponent {
}> = combineLatest([ }> = combineLatest([
this.connectionService.networkConnected$, this.connectionService.networkConnected$,
this.websocket$.pipe(startWith(false)), this.websocket$.pipe(startWith(false)),
this.patch
.watch$('server-info', 'status-info')
.pipe(startWith({ restarting: false, 'shutting-down': false })),
]).pipe( ]).pipe(
map(([network, websocket]) => { map(([network, websocket, status]) => {
if (!network) if (!network)
return { return {
message: 'No Internet', message: 'No Internet',
@@ -35,6 +40,20 @@ export class ConnectionBarComponent {
icon: 'cloud-offline-outline', icon: 'cloud-offline-outline',
dots: true, dots: true,
} }
if (status['shutting-down'])
return {
message: 'Shutting Down',
color: 'dark',
icon: 'power',
dots: true,
}
if (status.restarting)
return {
message: 'Restarting',
color: 'dark',
icon: 'power',
dots: true,
}
return { return {
message: 'Connected', message: 'Connected',
@@ -45,5 +64,8 @@ export class ConnectionBarComponent {
}), }),
) )
constructor(private readonly connectionService: ConnectionService) {} constructor(
private readonly connectionService: ConnectionService,
private readonly patch: PatchDB<DataModel>,
) {}
} }

View File

@@ -352,7 +352,6 @@ export class ServerShowPage {
try { try {
await this.embassyApi.restartServer({}) await this.embassyApi.restartServer({})
this.presentAlertInProgress(action, ` until ${action} completes.`)
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {
@@ -370,10 +369,6 @@ export class ServerShowPage {
try { try {
await this.embassyApi.shutdownServer({}) await this.embassyApi.shutdownServer({})
this.presentAlertInProgress(
action,
'.<br /><br /><b>You will need to physically power cycle the device to regain connectivity.</b>',
)
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {
@@ -391,7 +386,6 @@ export class ServerShowPage {
try { try {
await this.embassyApi.systemRebuild({}) await this.embassyApi.systemRebuild({})
this.presentAlertInProgress(action, ` until ${action} completes.`)
} catch (e: any) { } catch (e: any) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {
@@ -437,21 +431,6 @@ export class ServerShowPage {
alert.present() alert.present()
} }
private async presentAlertInProgress(verb: string, message: string) {
const alert = await this.alertCtrl.create({
header: `${verb} In Progress...`,
message: `Stopping all services gracefully. This can take a while.<br /><br />If you have a speaker, your server will <b>♫ play a melody ♫</b> before shutting down. Your server will then become unreachable${message}`,
buttons: [
{
text: 'OK',
role: 'cancel',
cssClass: 'enter-click',
},
],
})
alert.present()
}
settings: ServerSettings = { settings: ServerSettings = {
Backups: [ Backups: [
{ {

View File

@@ -17,6 +17,8 @@ export module Mock {
'backup-progress': null, 'backup-progress': null,
'update-progress': null, 'update-progress': null,
updated: true, updated: true,
restarting: false,
'shutting-down': false,
} }
export const MarketplaceEos: RR.GetMarketplaceEosRes = { export const MarketplaceEos: RR.GetMarketplaceEosRes = {
version: '0.3.5', version: '0.3.5',

View File

@@ -302,6 +302,27 @@ export class MockApiService extends ApiService {
params: RR.RestartServerReq, params: RR.RestartServerReq,
): Promise<RR.RestartServerRes> { ): Promise<RR.RestartServerRes> {
await pauseFor(2000) await pauseFor(2000)
const patch = [
{
op: PatchOp.REPLACE,
path: '/server-info/status-info/restarting',
value: true,
},
]
this.mockRevision(patch)
setTimeout(() => {
const patch2 = [
{
op: PatchOp.REPLACE,
path: '/server-info/status-info/restarting',
value: false,
},
]
this.mockRevision(patch2)
}, 2000)
return null return null
} }
@@ -309,14 +330,34 @@ export class MockApiService extends ApiService {
params: RR.ShutdownServerReq, params: RR.ShutdownServerReq,
): Promise<RR.ShutdownServerRes> { ): Promise<RR.ShutdownServerRes> {
await pauseFor(2000) await pauseFor(2000)
const patch = [
{
op: PatchOp.REPLACE,
path: '/server-info/status-info/shutting-down',
value: true,
},
]
this.mockRevision(patch)
setTimeout(() => {
const patch2 = [
{
op: PatchOp.REPLACE,
path: '/server-info/status-info/shutting-down',
value: false,
},
]
this.mockRevision(patch2)
}, 2000)
return null return null
} }
async systemRebuild( async systemRebuild(
params: RR.RestartServerReq, params: RR.SystemRebuildReq,
): Promise<RR.RestartServerRes> { ): Promise<RR.SystemRebuildRes> {
await pauseFor(2000) return this.restartServer(params)
return null
} }
async repairDisk(params: RR.RestartServerReq): Promise<RR.RestartServerRes> { async repairDisk(params: RR.RestartServerReq): Promise<RR.RestartServerRes> {

View File

@@ -66,6 +66,8 @@ export const mockPatchData: DataModel = {
'backup-progress': null, 'backup-progress': null,
updated: false, updated: false,
'update-progress': null, 'update-progress': null,
restarting: false,
'shutting-down': false,
}, },
hostname: 'random-words', hostname: 'random-words',
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m', pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',

View File

@@ -96,6 +96,8 @@ export interface ServerStatusInfo {
} }
updated: boolean updated: boolean
'update-progress': { size: number | null; downloaded: number } | null 'update-progress': { size: number | null; downloaded: number } | null
restarting: boolean
'shutting-down': boolean
} }
export enum ServerStatus { export enum ServerStatus {