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 <me@drbonez.dev>

* check if service is restarting

* filter package when restarting to avoid glitch

Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Lucy C
2022-06-17 20:11:15 -06:00
parent 889cf03c1c
commit a4a8f33df0
18 changed files with 212 additions and 64 deletions

View File

@@ -268,9 +268,11 @@ async fn perform_backup<Db: DbHandle>(
main_status_model.lock(&mut tx, LockType::Write).await?; main_status_model.lock(&mut tx, LockType::Write).await?;
let (started, health) = match main_status_model.get(&mut tx, true).await?.into_owned() { 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::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 { .. } => { MainStatus::BackingUp { .. } => {
backup_report.insert( backup_report.insert(
package_id, package_id,

View File

@@ -335,12 +335,14 @@ impl RpcContext {
let main = match status.main { let main = match status.main {
MainStatus::BackingUp { started, .. } => { MainStatus::BackingUp { started, .. } => {
if let Some(_) = started { if let Some(_) = started {
MainStatus::Starting MainStatus::Starting { restarting: false }
} else { } else {
MainStatus::Stopped MainStatus::Stopped
} }
} }
MainStatus::Running { .. } => MainStatus::Starting, MainStatus::Running { .. } => {
MainStatus::Starting { restarting: false }
}
a => a.clone(), a => a.clone(),
}; };
let new_package = PackageDataEntry::Installed { let new_package = PackageDataEntry::Installed {

View File

@@ -71,7 +71,10 @@ pub async fn start(
let mut tx = db.begin().await?; let mut tx = db.begin().await?;
let receipts = StartReceipts::new(&mut tx, &id).await?; let receipts = StartReceipts::new(&mut tx, &id).await?;
let version = receipts.version.get(&mut tx).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?; heal_all_dependents_transitive(&ctx, &mut tx, &id, &receipts.dependency_receipt).await?;
let revision = tx.commit(None).await?; let revision = tx.commit(None).await?;
@@ -181,3 +184,33 @@ pub async fn stop_impl(ctx: RpcContext, id: PackageId) -> Result<WithRevision<()
response: (), response: (),
}) })
} }
#[command(display(display_none))]
pub async fn restart(
#[context] ctx: RpcContext,
#[arg] id: PackageId,
) -> Result<WithRevision<()>, 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: (),
})
}

View File

@@ -14,11 +14,12 @@ use rpc_toolkit::command;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::instrument; use tracing::instrument;
use crate::config::action::{ConfigActions, ConfigRes};
use crate::config::spec::PackagePointerSpec; use crate::config::spec::PackagePointerSpec;
use crate::config::{not_found, Config, ConfigReceipts, ConfigSpec}; use crate::config::{not_found, Config, ConfigReceipts, ConfigSpec};
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::db::model::{CurrentDependencies, CurrentDependents, InstalledPackageDataEntry}; 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::s9pk::manifest::{Manifest, PackageId};
use crate::status::health_check::{HealthCheckId, HealthCheckResult}; use crate::status::health_check::{HealthCheckId, HealthCheckResult};
use crate::status::{MainStatus, Status}; use crate::status::{MainStatus, Status};
@@ -26,10 +27,6 @@ use crate::util::serde::display_serializable;
use crate::util::{display_none, Version}; use crate::util::{display_none, Version};
use crate::volume::Volumes; use crate::volume::Volumes;
use crate::Error; use crate::Error;
use crate::{
config::action::{ConfigActions, ConfigRes},
procedure::ProcedureName,
};
#[command(subcommands(configure))] #[command(subcommands(configure))]
pub fn dependency() -> Result<(), Error> { pub fn dependency() -> Result<(), Error> {
@@ -339,7 +336,7 @@ impl DependencyError {
.await? .await?
} }
} }
MainStatus::Starting => { MainStatus::Starting { .. } | MainStatus::Restarting => {
DependencyError::Transitive DependencyError::Transitive
.try_heal( .try_heal(
ctx, ctx,

View File

@@ -99,6 +99,7 @@ pub fn server() -> Result<(), RpcError> {
config::config, config::config,
control::start, control::start,
control::stop, control::stop,
control::restart,
logs::logs, logs::logs,
properties::properties, properties::properties,
dependencies::dependency, dependencies::dependency,

View File

@@ -31,7 +31,10 @@ async fn synchronize_once(shared: &ManagerSharedState) -> Result<Status, Error>
MainStatus::Stopping => { MainStatus::Stopping => {
*status = MainStatus::Stopped; *status = MainStatus::Stopped;
} }
MainStatus::Starting => { MainStatus::Restarting => {
*status = MainStatus::Starting { restarting: true };
}
MainStatus::Starting { .. } => {
start(shared).await?; start(shared).await?;
} }
MainStatus::Running { started, .. } => { MainStatus::Running { started, .. } => {
@@ -41,19 +44,19 @@ async fn synchronize_once(shared: &ManagerSharedState) -> Result<Status, Error>
MainStatus::BackingUp { .. } => (), MainStatus::BackingUp { .. } => (),
}, },
Status::Starting => match *status { Status::Starting => match *status {
MainStatus::Stopped | MainStatus::Stopping => { MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => {
stop(shared).await?; stop(shared).await?;
} }
MainStatus::Starting | MainStatus::Running { .. } => (), MainStatus::Starting { .. } | MainStatus::Running { .. } => (),
MainStatus::BackingUp { .. } => { MainStatus::BackingUp { .. } => {
pause(shared).await?; pause(shared).await?;
} }
}, },
Status::Running => match *status { Status::Running => match *status {
MainStatus::Stopped | MainStatus::Stopping => { MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => {
stop(shared).await?; stop(shared).await?;
} }
MainStatus::Starting => { MainStatus::Starting { .. } => {
*status = MainStatus::Running { *status = MainStatus::Running {
started: Utc::now(), started: Utc::now(),
health: BTreeMap::new(), health: BTreeMap::new(),
@@ -65,10 +68,10 @@ async fn synchronize_once(shared: &ManagerSharedState) -> Result<Status, Error>
} }
}, },
Status::Paused => match *status { Status::Paused => match *status {
MainStatus::Stopped | MainStatus::Stopping => { MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => {
stop(shared).await?; stop(shared).await?;
} }
MainStatus::Starting | MainStatus::Running { .. } => { MainStatus::Starting { .. } | MainStatus::Running { .. } => {
resume(shared).await?; resume(shared).await?;
} }
MainStatus::BackingUp { .. } => (), MainStatus::BackingUp { .. } => (),

View File

@@ -24,8 +24,11 @@ pub struct Status {
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum MainStatus { pub enum MainStatus {
Stopped, Stopped,
Restarting,
Stopping, Stopping,
Starting, Starting {
restarting: bool,
},
Running { Running {
started: DateTime<Utc>, started: DateTime<Utc>,
health: BTreeMap<HealthCheckId, HealthCheckResult>, health: BTreeMap<HealthCheckId, HealthCheckResult>,
@@ -38,25 +41,26 @@ pub enum MainStatus {
impl MainStatus { impl MainStatus {
pub fn running(&self) -> bool { pub fn running(&self) -> bool {
match self { match self {
MainStatus::Starting MainStatus::Starting { .. }
| MainStatus::Running { .. } | MainStatus::Running { .. }
| MainStatus::BackingUp { | MainStatus::BackingUp {
started: Some(_), .. started: Some(_), ..
} => true, } => true,
MainStatus::Stopped MainStatus::Stopped
| MainStatus::Stopping | MainStatus::Stopping
| MainStatus::Restarting
| MainStatus::BackingUp { started: None, .. } => false, | MainStatus::BackingUp { started: None, .. } => false,
} }
} }
pub fn stop(&mut self) { pub fn stop(&mut self) {
match self { match self {
MainStatus::Starting | MainStatus::Running { .. } => { MainStatus::Starting { .. } | MainStatus::Running { .. } => {
*self = MainStatus::Stopping; *self = MainStatus::Stopping;
} }
MainStatus::BackingUp { started, .. } => { MainStatus::BackingUp { started, .. } => {
*started = None; *started = None;
} }
MainStatus::Stopped | MainStatus::Stopping => (), MainStatus::Stopped | MainStatus::Stopping | MainStatus::Restarting => (),
} }
} }
} }

View File

@@ -3,6 +3,7 @@ import { NavController } from '@ionic/angular'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { import {
PackageDataEntry, PackageDataEntry,
PackageMainStatus,
PackageState, PackageState,
} from 'src/app/services/patch-db/data-model' } from 'src/app/services/patch-db/data-model'
import { import {
@@ -13,7 +14,7 @@ import {
ConnectionFailure, ConnectionFailure,
ConnectionService, ConnectionService,
} from 'src/app/services/connection.service' } 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 { ActivatedRoute } from '@angular/router'
import { getPkgId } from '@start9labs/shared' import { getPkgId } from '@start9labs/shared'
@@ -32,6 +33,13 @@ export class AppShowPage {
private readonly pkgId = getPkgId(this.route) private readonly pkgId = getPkgId(this.route)
readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe( 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 => { map(pkg => {
// if package disappears, navigate to list page // if package disappears, navigate to list page
if (!pkg) { if (!pkg) {

View File

@@ -21,6 +21,14 @@
<ion-icon slot="start" name="stop-outline"></ion-icon> <ion-icon slot="start" name="stop-outline"></ion-icon>
Stop Stop
</ion-button> </ion-button>
<ion-button
class="action-button"
color="warning"
(click)="tryRestart()"
>
<ion-icon slot="start" name="refresh"></ion-icon>
Restart
</ion-button>
</ng-container> </ng-container>
<ion-button <ion-button
@@ -58,4 +66,4 @@
</ion-col> </ion-col>
</ion-row> </ion-row>
</ion-grid> </ion-grid>
</ng-container> </ng-container>

View File

@@ -3,7 +3,7 @@
} }
.action-button { .action-button {
margin: 20px 20px 10px 0; margin: 12px 20px 10px 0;
min-height: 42px; min-height: 42px;
min-width: 140px; min-width: 140px;
} }

View File

@@ -137,8 +137,6 @@ export class AppShowStatusComponent {
} }
} }
<<<<<<< HEAD
=======
async tryRestart(): Promise<void> { async tryRestart(): Promise<void> {
if (hasCurrentDeps(this.pkg)) { if (hasCurrentDeps(this.pkg)) {
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({
@@ -165,7 +163,28 @@ export class AppShowStatusComponent {
} }
} }
>>>>>>> 918a1907... Remove app wiz and dry calls (#1541) async presentAlertRestart(): Promise<void> {
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<void> { private async start(): Promise<void> {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
message: `Starting...`, message: `Starting...`,
@@ -181,8 +200,6 @@ export class AppShowStatusComponent {
} }
} }
<<<<<<< HEAD
=======
private async stop(): Promise<void> { private async stop(): Promise<void> {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
message: 'Stopping...', message: 'Stopping...',
@@ -212,8 +229,6 @@ export class AppShowStatusComponent {
loader.dismiss() loader.dismiss()
} }
} }
>>>>>>> 918a1907... Remove app wiz and dry calls (#1541)
private async presentAlertStart(message: string): Promise<boolean> { private async presentAlertStart(message: string): Promise<boolean> {
return new Promise(async resolve => { return new Promise(async resolve => {
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({

View File

@@ -215,6 +215,9 @@ export module RR {
export type StartPackageReq = WithExpire<{ id: string }> // package.start export type StartPackageReq = WithExpire<{ id: string }> // package.start
export type StartPackageRes = WithRevision<null> export type StartPackageRes = WithRevision<null>
export type RestartPackageReq = WithExpire<{ id: string }> // package.restart
export type RestartPackageRes = WithRevision<null>
export type StopPackageReq = WithExpire<{ id: string }> // package.stop export type StopPackageReq = WithExpire<{ id: string }> // package.stop
export type StopPackageRes = WithRevision<null> export type StopPackageRes = WithRevision<null>

View File

@@ -233,6 +233,12 @@ export abstract class ApiService implements Source<DataModel>, Http<DataModel> {
startPackage = (params: RR.StartPackageReq) => startPackage = (params: RR.StartPackageReq) =>
this.syncResponse(() => this.startPackageRaw(params))() this.syncResponse(() => this.startPackageRaw(params))()
protected abstract restartPackageRaw(
params: RR.RestartPackageReq,
): Promise<RR.RestartPackageRes>
restartPackage = (params: RR.RestartPackageReq) =>
this.syncResponse(() => this.restartPackageRaw(params))()
protected abstract stopPackageRaw( protected abstract stopPackageRaw(
params: RR.StopPackageReq, params: RR.StopPackageReq,
): Promise<RR.StopPackageRes> ): Promise<RR.StopPackageRes>

View File

@@ -306,6 +306,12 @@ export class LiveApiService extends ApiService {
return this.http.rpcRequest({ method: 'package.start', params }) return this.http.rpcRequest({ method: 'package.start', params })
} }
async restartPackageRaw(
params: RR.RestartPackageReq,
): Promise<RR.RestartPackageRes> {
return this.http.rpcRequest({ method: 'package.restart', params })
}
async stopPackageRaw(params: RR.StopPackageReq): Promise<RR.StopPackageRes> { async stopPackageRaw(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
return this.http.rpcRequest({ method: 'package.stop', params }) return this.http.rpcRequest({ method: 'package.stop', params })
} }

View File

@@ -36,7 +36,7 @@ export class MockApiService extends ApiService {
value: mockPatchData, value: mockPatchData,
expireId: null, expireId: null,
}) })
private readonly revertTime = 4000 private readonly revertTime = 2000
sequence: number sequence: number
constructor(private readonly bootstrapper: LocalStorageBootstrap) { constructor(private readonly bootstrapper: LocalStorageBootstrap) {
@@ -654,6 +654,63 @@ export class MockApiService extends ApiService {
return this.withRevision(originalPatch) return this.withRevision(originalPatch)
} }
async restartPackageRaw(
params: RR.RestartPackageReq,
): Promise<RR.RestartPackageRes> {
// 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<RR.StopPackageRes> { async stopPackageRaw(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
await pauseFor(2000) await pauseFor(2000)
const path = `/package-data/${params.id}/installed/status/main` const path = `/package-data/${params.id}/installed/status/main`

View File

@@ -250,6 +250,7 @@ export type MainStatus =
| MainStatusStarting | MainStatusStarting
| MainStatusRunning | MainStatusRunning
| MainStatusBackingUp | MainStatusBackingUp
| MainStatusRestarting
export interface MainStatusStopped { export interface MainStatusStopped {
status: PackageMainStatus.Stopped status: PackageMainStatus.Stopped
@@ -261,6 +262,7 @@ export interface MainStatusStopping {
export interface MainStatusStarting { export interface MainStatusStarting {
status: PackageMainStatus.Starting status: PackageMainStatus.Starting
restarting: boolean
} }
export interface MainStatusRunning { export interface MainStatusRunning {
@@ -274,12 +276,17 @@ export interface MainStatusBackingUp {
started: string | null // UTC date string started: string | null // UTC date string
} }
export interface MainStatusRestarting {
status: PackageMainStatus.Restarting
}
export enum PackageMainStatus { export enum PackageMainStatus {
Starting = 'starting', Starting = 'starting',
Running = 'running', Running = 'running',
Stopping = 'stopping', Stopping = 'stopping',
Stopped = 'stopped', Stopped = 'stopped',
BackingUp = 'backing-up', BackingUp = 'backing-up',
Restarting = 'restarting',
} }
export type HealthCheckResult = export type HealthCheckResult =

View File

@@ -1,5 +1,6 @@
import { isEmptyObject } from '@start9labs/shared' import { isEmptyObject } from '@start9labs/shared'
import { import {
MainStatusStarting,
PackageDataEntry, PackageDataEntry,
PackageMainStatus, PackageMainStatus,
PackageState, PackageState,
@@ -32,6 +33,8 @@ export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
function getPrimaryStatus(status: Status): PrimaryStatus { function getPrimaryStatus(status: Status): PrimaryStatus {
if (!status.configured) { if (!status.configured) {
return PrimaryStatus.NeedsConfig return PrimaryStatus.NeedsConfig
} else if ((status.main as MainStatusStarting).restarting) {
return PrimaryStatus.Restarting
} else { } else {
return status.main.status as any as PrimaryStatus return status.main.status as any as PrimaryStatus
} }
@@ -57,7 +60,6 @@ function getHealthStatus(
} }
const values = Object.values(status.main.health) const values = Object.values(status.main.health)
console.log('HEALTH CHECKS', values)
if (values.some(h => h.result === 'failure')) { if (values.some(h => h.result === 'failure')) {
return HealthStatus.Failure return HealthStatus.Failure
@@ -94,6 +96,7 @@ export enum PrimaryStatus {
Starting = 'starting', Starting = 'starting',
Running = 'running', Running = 'running',
Stopping = 'stopping', Stopping = 'stopping',
Restarting = 'restarting',
Stopped = 'stopped', Stopped = 'stopped',
BackingUp = 'backing-up', BackingUp = 'backing-up',
// config // config
@@ -139,6 +142,11 @@ export const PrimaryRendering: Record<string, StatusRendering> = {
color: 'dark-shade', color: 'dark-shade',
showDots: true, showDots: true,
}, },
[PrimaryStatus.Restarting]: {
display: 'Restarting',
color: 'warning',
showDots: true,
},
[PrimaryStatus.Stopped]: { [PrimaryStatus.Stopped]: {
display: 'Stopped', display: 'Stopped',
color: 'dark-shade', color: 'dark-shade',

View File

@@ -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::anyhow::{anyhow, bail};
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::resolve_import; use deno_core::{
use deno_core::JsRuntime; resolve_import, Extension, JsRuntime, ModuleLoader, ModuleSource, ModuleSourceFuture,
use deno_core::ModuleLoader; ModuleSpecifier, ModuleType, OpDecl, RuntimeOptions, Snapshot,
use deno_core::ModuleSource; };
use deno_core::ModuleSourceFuture; use helpers::{script_dir, NonDetachingJoinHandle};
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 models::{PackageId, ProcedureName, Version, VolumeId}; use models::{PackageId, ProcedureName, Version, VolumeId};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::time::SystemTime; use std::time::SystemTime;
use std::{path::Path, sync::Arc};
use std::{path::PathBuf, pin::Pin};
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
pub trait PathForVolumeId: Send + Sync { 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 /// 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 { mod fns {
use deno_core::{ use std::cell::RefCell;
anyhow::{anyhow, bail}, use std::convert::TryFrom;
error::AnyError, 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 serde_json::Value;
use std::os::unix::fs::MetadataExt; 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 crate::{system_time_as_unix_ms, MetadataJs};
use super::{AnswerState, JsContext}; use super::{AnswerState, JsContext};
@@ -645,4 +633,4 @@ fn system_time_as_unix_ms(system_time: &SystemTime) -> Option<u64> {
.as_millis() .as_millis()
.try_into() .try_into()
.ok() .ok()
} }