mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 22:39:46 +00:00
add error status (#2746)
* add error status * update types * ṗ̶̰̙̓͒̈́ͅü̵̢̙̫̣ŗ̷̪̺̺͛g̴̲͉͎̬̒̇e̵̪̎̅͌ ̶̡̜̘͐͛t̶͎͍̣̿̍̐h̴͕̩͗̈́̎̑e̵͚͒̂͝ ̸̛͙̦͈͝v̶̱͙̬̽̔ọ̶̧̡̒̓i̸̬̲͍̋̈́d̴͉̀ * fix some extra voids * add `package.rebuild` * introduce error status and pkg rebuild and fix mocks * minor fixes * fix build --------- Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
@@ -173,7 +173,7 @@ export function makeEffects(context: EffectContext): Effects {
|
|||||||
T.Effects["subcontainer"]["createFs"]
|
T.Effects["subcontainer"]["createFs"]
|
||||||
>
|
>
|
||||||
},
|
},
|
||||||
destroyFs(options: { guid: string }): Promise<void> {
|
destroyFs(options: { guid: string }): Promise<null> {
|
||||||
return rpcRound("subcontainer.destroy-fs", options) as ReturnType<
|
return rpcRound("subcontainer.destroy-fs", options) as ReturnType<
|
||||||
T.Effects["subcontainer"]["destroyFs"]
|
T.Effects["subcontainer"]["destroyFs"]
|
||||||
>
|
>
|
||||||
@@ -284,7 +284,7 @@ export function makeEffects(context: EffectContext): Effects {
|
|||||||
>
|
>
|
||||||
},
|
},
|
||||||
|
|
||||||
setMainStatus(o: { status: "running" | "stopped" }): Promise<void> {
|
setMainStatus(o: { status: "running" | "stopped" }): Promise<null> {
|
||||||
return rpcRound("set-main-status", o) as ReturnType<
|
return rpcRound("set-main-status", o) as ReturnType<
|
||||||
T.Effects["setHealth"]
|
T.Effects["setHealth"]
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export class SystemForStartOs implements System {
|
|||||||
const started = async (onTerm: () => Promise<void>) => {
|
const started = async (onTerm: () => Promise<void>) => {
|
||||||
await effects.setMainStatus({ status: "running" })
|
await effects.setMainStatus({ status: "running" })
|
||||||
mainOnTerm = onTerm
|
mainOnTerm = onTerm
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
const daemons = await (
|
const daemons = await (
|
||||||
await this.abi.main({
|
await this.abi.main({
|
||||||
|
|||||||
@@ -351,6 +351,14 @@ impl Debug for ErrorData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl std::error::Error for ErrorData {}
|
impl std::error::Error for ErrorData {}
|
||||||
|
impl From<Error> for ErrorData {
|
||||||
|
fn from(value: Error) -> Self {
|
||||||
|
Self {
|
||||||
|
details: value.to_string(),
|
||||||
|
debug: format!("{:?}", value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
impl From<&RpcError> for ErrorData {
|
impl From<&RpcError> for ErrorData {
|
||||||
fn from(value: &RpcError) -> Self {
|
fn from(value: &RpcError) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -5,6 +5,16 @@ use tracing::instrument;
|
|||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
|
pub async fn is_mountpoint(path: impl AsRef<Path>) -> Result<bool, Error> {
|
||||||
|
let is_mountpoint = tokio::process::Command::new("mountpoint")
|
||||||
|
.arg(path.as_ref())
|
||||||
|
.stdout(std::process::Stdio::null())
|
||||||
|
.stderr(std::process::Stdio::null())
|
||||||
|
.status()
|
||||||
|
.await?;
|
||||||
|
Ok(is_mountpoint.success())
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
|
pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
|
||||||
src: P0,
|
src: P0,
|
||||||
@@ -16,13 +26,7 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
|
|||||||
src.as_ref().display(),
|
src.as_ref().display(),
|
||||||
dst.as_ref().display()
|
dst.as_ref().display()
|
||||||
);
|
);
|
||||||
let is_mountpoint = tokio::process::Command::new("mountpoint")
|
if is_mountpoint(&dst).await? {
|
||||||
.arg(dst.as_ref())
|
|
||||||
.stdout(std::process::Stdio::null())
|
|
||||||
.stderr(std::process::Stdio::null())
|
|
||||||
.status()
|
|
||||||
.await?;
|
|
||||||
if is_mountpoint.success() {
|
|
||||||
unmount(dst.as_ref(), true).await?;
|
unmount(dst.as_ref(), true).await?;
|
||||||
}
|
}
|
||||||
tokio::fs::create_dir_all(&src).await?;
|
tokio::fs::create_dir_all(&src).await?;
|
||||||
|
|||||||
@@ -292,6 +292,13 @@ pub fn package<C: Context>() -> ParentHandler<C> {
|
|||||||
.no_display()
|
.no_display()
|
||||||
.with_call_remote::<CliContext>(),
|
.with_call_remote::<CliContext>(),
|
||||||
)
|
)
|
||||||
|
.subcommand(
|
||||||
|
"rebuild",
|
||||||
|
from_fn_async(service::rebuild)
|
||||||
|
.with_metadata("sync_db", Value::Bool(true))
|
||||||
|
.no_display()
|
||||||
|
.with_call_remote::<CliContext>(),
|
||||||
|
)
|
||||||
.subcommand("logs", logs::package_logs())
|
.subcommand("logs", logs::package_logs())
|
||||||
.subcommand(
|
.subcommand(
|
||||||
"logs",
|
"logs",
|
||||||
|
|||||||
@@ -126,7 +126,8 @@ impl LxcManager {
|
|||||||
Path::new(LXC_CONTAINER_DIR).join(container).join("rootfs"),
|
Path::new(LXC_CONTAINER_DIR).join(container).join("rootfs"),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
.log_err();
|
||||||
if tokio_stream::wrappers::ReadDirStream::new(
|
if tokio_stream::wrappers::ReadDirStream::new(
|
||||||
tokio::fs::read_dir(&rootfs_path).await?,
|
tokio::fs::read_dir(&rootfs_path).await?,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -589,6 +589,15 @@ impl ServiceActorSeed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||||
|
pub struct RebuildParams {
|
||||||
|
pub id: PackageId,
|
||||||
|
}
|
||||||
|
pub async fn rebuild(ctx: RpcContext, RebuildParams { id }: RebuildParams) -> Result<(), Error> {
|
||||||
|
ctx.services.load(&ctx, &id, LoadDisposition::Retry).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||||
pub struct ConnectParams {
|
pub struct ConnectParams {
|
||||||
pub id: PackageId,
|
pub id: PackageId,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use futures::{Future, FutureExt};
|
|||||||
use helpers::NonDetachingJoinHandle;
|
use helpers::NonDetachingJoinHandle;
|
||||||
use imbl::OrdMap;
|
use imbl::OrdMap;
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
|
use models::ErrorData;
|
||||||
use tokio::sync::{Mutex, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
|
use tokio::sync::{Mutex, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle, ProgressT
|
|||||||
use crate::s9pk::manifest::PackageId;
|
use crate::s9pk::manifest::PackageId;
|
||||||
use crate::s9pk::merkle_archive::source::FileSource;
|
use crate::s9pk::merkle_archive::source::FileSource;
|
||||||
use crate::s9pk::S9pk;
|
use crate::s9pk::S9pk;
|
||||||
|
use crate::service::start_stop::StartStop;
|
||||||
use crate::service::{LoadDisposition, Service, ServiceRef};
|
use crate::service::{LoadDisposition, Service, ServiceRef};
|
||||||
use crate::status::MainStatus;
|
use crate::status::MainStatus;
|
||||||
use crate::util::serde::Pem;
|
use crate::util::serde::Pem;
|
||||||
@@ -87,8 +89,30 @@ impl ServiceMap {
|
|||||||
if let Some(service) = service.take() {
|
if let Some(service) = service.take() {
|
||||||
shutdown_err = service.shutdown().await;
|
shutdown_err = service.shutdown().await;
|
||||||
}
|
}
|
||||||
// TODO: retry on error?
|
match Service::load(ctx, id, disposition).await {
|
||||||
*service = Service::load(ctx, id, disposition).await?.map(From::from);
|
Ok(s) => *service = s.into(),
|
||||||
|
Err(e) => {
|
||||||
|
let e = ErrorData::from(e);
|
||||||
|
ctx.db
|
||||||
|
.mutate(|db| {
|
||||||
|
if let Some(pde) = db.as_public_mut().as_package_data_mut().as_idx_mut(id) {
|
||||||
|
pde.as_status_mut().map_mutate(|s| {
|
||||||
|
Ok(MainStatus::Error {
|
||||||
|
on_rebuild: if s.running() {
|
||||||
|
StartStop::Start
|
||||||
|
} else {
|
||||||
|
StartStop::Stop
|
||||||
|
},
|
||||||
|
message: e.details,
|
||||||
|
debug: Some(e.debug),
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
shutdown_err?;
|
shutdown_err?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ pub mod health_check;
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[serde(rename_all_fields = "camelCase")]
|
#[serde(rename_all_fields = "camelCase")]
|
||||||
pub enum MainStatus {
|
pub enum MainStatus {
|
||||||
|
Error {
|
||||||
|
on_rebuild: StartStop,
|
||||||
|
message: String,
|
||||||
|
debug: Option<String>,
|
||||||
|
},
|
||||||
Stopped,
|
Stopped,
|
||||||
Restarting,
|
Restarting,
|
||||||
Restoring,
|
Restoring,
|
||||||
@@ -43,12 +48,20 @@ impl MainStatus {
|
|||||||
| MainStatus::Restarting
|
| MainStatus::Restarting
|
||||||
| MainStatus::BackingUp {
|
| MainStatus::BackingUp {
|
||||||
on_complete: StartStop::Start,
|
on_complete: StartStop::Start,
|
||||||
|
}
|
||||||
|
| MainStatus::Error {
|
||||||
|
on_rebuild: StartStop::Start,
|
||||||
|
..
|
||||||
} => true,
|
} => true,
|
||||||
MainStatus::Stopped
|
MainStatus::Stopped
|
||||||
| MainStatus::Restoring
|
| MainStatus::Restoring
|
||||||
| MainStatus::Stopping { .. }
|
| MainStatus::Stopping { .. }
|
||||||
| MainStatus::BackingUp {
|
| MainStatus::BackingUp {
|
||||||
on_complete: StartStop::Stop,
|
on_complete: StartStop::Stop,
|
||||||
|
}
|
||||||
|
| MainStatus::Error {
|
||||||
|
on_rebuild: StartStop::Stop,
|
||||||
|
..
|
||||||
} => false,
|
} => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,7 +83,8 @@ impl MainStatus {
|
|||||||
| MainStatus::Stopped
|
| MainStatus::Stopped
|
||||||
| MainStatus::Restoring
|
| MainStatus::Restoring
|
||||||
| MainStatus::Stopping { .. }
|
| MainStatus::Stopping { .. }
|
||||||
| MainStatus::Restarting => None,
|
| MainStatus::Restarting
|
||||||
|
| MainStatus::Error { .. } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,14 @@ export type Effects = {
|
|||||||
constRetry: () => void
|
constRetry: () => void
|
||||||
clearCallbacks: (
|
clearCallbacks: (
|
||||||
options: { only: number[] } | { except: number[] },
|
options: { only: number[] } | { except: number[] },
|
||||||
) => Promise<void>
|
) => Promise<null>
|
||||||
|
|
||||||
// action
|
// action
|
||||||
action: {
|
action: {
|
||||||
/** Define an action that can be invoked by a user or service */
|
/** Define an action that can be invoked by a user or service */
|
||||||
export(options: { id: ActionId; metadata: ActionMetadata }): Promise<void>
|
export(options: { id: ActionId; metadata: ActionMetadata }): Promise<null>
|
||||||
/** Remove all exported actions */
|
/** Remove all exported actions */
|
||||||
clear(options: { except: ActionId[] }): Promise<void>
|
clear(options: { except: ActionId[] }): Promise<null>
|
||||||
getInput(options: {
|
getInput(options: {
|
||||||
packageId?: PackageId
|
packageId?: PackageId
|
||||||
actionId: ActionId
|
actionId: ActionId
|
||||||
@@ -50,23 +50,23 @@ export type Effects = {
|
|||||||
}): Promise<ActionResult | null>
|
}): Promise<ActionResult | null>
|
||||||
request<Input extends Record<string, unknown>>(
|
request<Input extends Record<string, unknown>>(
|
||||||
options: RequestActionParams,
|
options: RequestActionParams,
|
||||||
): Promise<void>
|
): Promise<null>
|
||||||
clearRequests(
|
clearRequests(
|
||||||
options: { only: ActionId[] } | { except: ActionId[] },
|
options: { only: ActionId[] } | { except: ActionId[] },
|
||||||
): Promise<void>
|
): Promise<null>
|
||||||
}
|
}
|
||||||
|
|
||||||
// control
|
// control
|
||||||
/** restart this service's main function */
|
/** restart this service's main function */
|
||||||
restart(): Promise<void>
|
restart(): Promise<null>
|
||||||
/** stop this service's main function */
|
/** stop this service's main function */
|
||||||
shutdown(): Promise<void>
|
shutdown(): Promise<null>
|
||||||
/** indicate to the host os what runstate the service is in */
|
/** indicate to the host os what runstate the service is in */
|
||||||
setMainStatus(options: SetMainStatus): Promise<void>
|
setMainStatus(options: SetMainStatus): Promise<null>
|
||||||
|
|
||||||
// dependency
|
// dependency
|
||||||
/** Set the dependencies of what the service needs, usually run during the inputSpec action as a best practice */
|
/** Set the dependencies of what the service needs, usually run during the inputSpec action as a best practice */
|
||||||
setDependencies(options: { dependencies: Dependencies }): Promise<void>
|
setDependencies(options: { dependencies: Dependencies }): Promise<null>
|
||||||
/** Get the list of the dependencies, both the dynamic set by the effect of setDependencies and the end result any required in the manifest */
|
/** Get the list of the dependencies, both the dynamic set by the effect of setDependencies and the end result any required in the manifest */
|
||||||
getDependencies(): Promise<DependencyRequirement[]>
|
getDependencies(): Promise<DependencyRequirement[]>
|
||||||
/** Test whether current dependency requirements are satisfied */
|
/** Test whether current dependency requirements are satisfied */
|
||||||
@@ -86,11 +86,11 @@ export type Effects = {
|
|||||||
/** Returns a list of the ids of all installed packages */
|
/** Returns a list of the ids of all installed packages */
|
||||||
getInstalledPackages(): Promise<string[]>
|
getInstalledPackages(): Promise<string[]>
|
||||||
/** grants access to certain paths in the store to dependents */
|
/** grants access to certain paths in the store to dependents */
|
||||||
exposeForDependents(options: { paths: string[] }): Promise<void>
|
exposeForDependents(options: { paths: string[] }): Promise<null>
|
||||||
|
|
||||||
// health
|
// health
|
||||||
/** sets the result of a health check */
|
/** sets the result of a health check */
|
||||||
setHealth(o: SetHealth): Promise<void>
|
setHealth(o: SetHealth): Promise<null>
|
||||||
|
|
||||||
// subcontainer
|
// subcontainer
|
||||||
subcontainer: {
|
subcontainer: {
|
||||||
@@ -100,13 +100,13 @@ export type Effects = {
|
|||||||
name: string | null
|
name: string | null
|
||||||
}): Promise<[string, string]>
|
}): Promise<[string, string]>
|
||||||
/** A low level api used by SubContainer */
|
/** A low level api used by SubContainer */
|
||||||
destroyFs(options: { guid: string }): Promise<void>
|
destroyFs(options: { guid: string }): Promise<null>
|
||||||
}
|
}
|
||||||
|
|
||||||
// net
|
// net
|
||||||
// bind
|
// bind
|
||||||
/** Creates a host connected to the specified port with the provided options */
|
/** Creates a host connected to the specified port with the provided options */
|
||||||
bind(options: BindParams): Promise<void>
|
bind(options: BindParams): Promise<null>
|
||||||
/** Get the port address for a service */
|
/** Get the port address for a service */
|
||||||
getServicePortForward(options: {
|
getServicePortForward(options: {
|
||||||
packageId?: PackageId
|
packageId?: PackageId
|
||||||
@@ -116,7 +116,7 @@ export type Effects = {
|
|||||||
/** Removes all network bindings, called in the setupInputSpec */
|
/** Removes all network bindings, called in the setupInputSpec */
|
||||||
clearBindings(options: {
|
clearBindings(options: {
|
||||||
except: { id: HostId; internalPort: number }[]
|
except: { id: HostId; internalPort: number }[]
|
||||||
}): Promise<void>
|
}): Promise<null>
|
||||||
// host
|
// host
|
||||||
/** Returns information about the specified host, if it exists */
|
/** Returns information about the specified host, if it exists */
|
||||||
getHostInfo(options: {
|
getHostInfo(options: {
|
||||||
@@ -134,7 +134,7 @@ export type Effects = {
|
|||||||
getContainerIp(): Promise<string>
|
getContainerIp(): Promise<string>
|
||||||
// interface
|
// interface
|
||||||
/** Creates an interface bound to a specific host and port to show to the user */
|
/** Creates an interface bound to a specific host and port to show to the user */
|
||||||
exportServiceInterface(options: ExportServiceInterfaceParams): Promise<void>
|
exportServiceInterface(options: ExportServiceInterfaceParams): Promise<null>
|
||||||
/** Returns an exported service interface */
|
/** Returns an exported service interface */
|
||||||
getServiceInterface(options: {
|
getServiceInterface(options: {
|
||||||
packageId?: PackageId
|
packageId?: PackageId
|
||||||
@@ -149,7 +149,7 @@ export type Effects = {
|
|||||||
/** Removes all service interfaces */
|
/** Removes all service interfaces */
|
||||||
clearServiceInterfaces(options: {
|
clearServiceInterfaces(options: {
|
||||||
except: ServiceInterfaceId[]
|
except: ServiceInterfaceId[]
|
||||||
}): Promise<void>
|
}): Promise<null>
|
||||||
// ssl
|
// ssl
|
||||||
/** Returns a PEM encoded fullchain for the hostnames specified */
|
/** Returns a PEM encoded fullchain for the hostnames specified */
|
||||||
getSslCertificate: (options: {
|
getSslCertificate: (options: {
|
||||||
@@ -178,10 +178,10 @@ export type Effects = {
|
|||||||
/** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */
|
/** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */
|
||||||
path: StorePath
|
path: StorePath
|
||||||
value: ExtractStore
|
value: ExtractStore
|
||||||
}): Promise<void>
|
}): Promise<null>
|
||||||
}
|
}
|
||||||
/** sets the version that this service's data has been migrated to */
|
/** sets the version that this service's data has been migrated to */
|
||||||
setDataVersion(options: { version: string }): Promise<void>
|
setDataVersion(options: { version: string }): Promise<null>
|
||||||
/** returns the version that this service's data has been migrated to */
|
/** returns the version that this service's data has been migrated to */
|
||||||
getDataVersion(): Promise<string | null>
|
getDataVersion(): Promise<string | null>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { InputSpec } from "./input/builder"
|
import { InputSpec } from "./input/builder"
|
||||||
import { ExtractInputSpecType } from "./input/builder/inputSpec"
|
import { ExtractInputSpecType } from "./input/builder/inputSpec"
|
||||||
import * as T from "../types"
|
import * as T from "../types"
|
||||||
|
import { once } from "../util"
|
||||||
|
|
||||||
export type Run<
|
export type Run<
|
||||||
A extends
|
A extends
|
||||||
@@ -130,21 +131,19 @@ export class Actions<
|
|||||||
): Actions<Store, AllActions & { [id in A["id"]]: A }> {
|
): Actions<Store, AllActions & { [id in A["id"]]: A }> {
|
||||||
return new Actions({ ...this.actions, [action.id]: action })
|
return new Actions({ ...this.actions, [action.id]: action })
|
||||||
}
|
}
|
||||||
update(options: { effects: T.Effects }): Promise<void> {
|
async update(options: { effects: T.Effects }): Promise<null> {
|
||||||
const updater = async (options: { effects: T.Effects }) => {
|
options.effects = {
|
||||||
for (let action of Object.values(this.actions)) {
|
...options.effects,
|
||||||
await action.exportMetadata(options)
|
constRetry: once(() => {
|
||||||
}
|
this.update(options) // yes, this reuses the options object, but the const retry function will be overwritten each time, so the once-ness is not a problem
|
||||||
await options.effects.action.clear({ except: Object.keys(this.actions) })
|
}),
|
||||||
}
|
}
|
||||||
const updaterCtx = { options }
|
for (let action of Object.values(this.actions)) {
|
||||||
updaterCtx.options = {
|
await action.exportMetadata(options)
|
||||||
effects: {
|
|
||||||
...options.effects,
|
|
||||||
constRetry: () => updater(updaterCtx.options),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
return updater(updaterCtx.options)
|
await options.effects.action.clear({ except: Object.keys(this.actions) })
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
get<Id extends T.ActionId>(actionId: Id): AllActions[Id] {
|
get<Id extends T.ActionId>(actionId: Id): AllActions[Id] {
|
||||||
return this.actions[actionId]
|
return this.actions[actionId]
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ export type CheckDependencies<DependencyId extends PackageId = PackageId> = {
|
|||||||
) => boolean
|
) => boolean
|
||||||
satisfied: () => boolean
|
satisfied: () => boolean
|
||||||
|
|
||||||
throwIfInstalledNotSatisfied: (packageId: DependencyId) => void
|
throwIfInstalledNotSatisfied: (packageId: DependencyId) => null
|
||||||
throwIfInstalledVersionNotSatisfied: (packageId: DependencyId) => void
|
throwIfInstalledVersionNotSatisfied: (packageId: DependencyId) => null
|
||||||
throwIfRunningNotSatisfied: (packageId: DependencyId) => void
|
throwIfRunningNotSatisfied: (packageId: DependencyId) => null
|
||||||
throwIfActionsNotSatisfied: (packageId: DependencyId) => void
|
throwIfActionsNotSatisfied: (packageId: DependencyId) => null
|
||||||
throwIfHealthNotSatisfied: (
|
throwIfHealthNotSatisfied: (
|
||||||
packageId: DependencyId,
|
packageId: DependencyId,
|
||||||
healthCheckId?: HealthCheckId,
|
healthCheckId?: HealthCheckId,
|
||||||
) => void
|
) => null
|
||||||
throwIfNotSatisfied: (packageId?: DependencyId) => void
|
throwIfNotSatisfied: (packageId?: DependencyId) => null
|
||||||
}
|
}
|
||||||
export async function checkDependencies<
|
export async function checkDependencies<
|
||||||
DependencyId extends PackageId = PackageId,
|
DependencyId extends PackageId = PackageId,
|
||||||
@@ -100,6 +100,7 @@ export async function checkDependencies<
|
|||||||
if (!dep.result.installedVersion) {
|
if (!dep.result.installedVersion) {
|
||||||
throw new Error(`${dep.result.title || packageId} is not installed`)
|
throw new Error(`${dep.result.title || packageId} is not installed`)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
const throwIfInstalledVersionNotSatisfied = (packageId: DependencyId) => {
|
const throwIfInstalledVersionNotSatisfied = (packageId: DependencyId) => {
|
||||||
const dep = find(packageId)
|
const dep = find(packageId)
|
||||||
@@ -117,12 +118,14 @@ export async function checkDependencies<
|
|||||||
`Installed version ${dep.result.installedVersion} of ${dep.result.title || packageId} does not match expected version range ${dep.requirement.versionRange}`,
|
`Installed version ${dep.result.installedVersion} of ${dep.result.title || packageId} does not match expected version range ${dep.requirement.versionRange}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
const throwIfRunningNotSatisfied = (packageId: DependencyId) => {
|
const throwIfRunningNotSatisfied = (packageId: DependencyId) => {
|
||||||
const dep = find(packageId)
|
const dep = find(packageId)
|
||||||
if (dep.requirement.kind === "running" && !dep.result.isRunning) {
|
if (dep.requirement.kind === "running" && !dep.result.isRunning) {
|
||||||
throw new Error(`${dep.result.title || packageId} is not running`)
|
throw new Error(`${dep.result.title || packageId} is not running`)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
const throwIfActionsNotSatisfied = (packageId: DependencyId) => {
|
const throwIfActionsNotSatisfied = (packageId: DependencyId) => {
|
||||||
const dep = find(packageId)
|
const dep = find(packageId)
|
||||||
@@ -132,6 +135,7 @@ export async function checkDependencies<
|
|||||||
`The following action requests have not been fulfilled: ${reqs.join(", ")}`,
|
`The following action requests have not been fulfilled: ${reqs.join(", ")}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
const throwIfHealthNotSatisfied = (
|
const throwIfHealthNotSatisfied = (
|
||||||
packageId: DependencyId,
|
packageId: DependencyId,
|
||||||
@@ -158,6 +162,7 @@ export async function checkDependencies<
|
|||||||
.join("; "),
|
.join("; "),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
const throwIfPkgNotSatisfied = (packageId: DependencyId) => {
|
const throwIfPkgNotSatisfied = (packageId: DependencyId) => {
|
||||||
throwIfInstalledNotSatisfied(packageId)
|
throwIfInstalledNotSatisfied(packageId)
|
||||||
@@ -165,6 +170,7 @@ export async function checkDependencies<
|
|||||||
throwIfRunningNotSatisfied(packageId)
|
throwIfRunningNotSatisfied(packageId)
|
||||||
throwIfActionsNotSatisfied(packageId)
|
throwIfActionsNotSatisfied(packageId)
|
||||||
throwIfHealthNotSatisfied(packageId)
|
throwIfHealthNotSatisfied(packageId)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
const throwIfNotSatisfied = (packageId?: DependencyId) =>
|
const throwIfNotSatisfied = (packageId?: DependencyId) =>
|
||||||
packageId
|
packageId
|
||||||
@@ -182,6 +188,7 @@ export async function checkDependencies<
|
|||||||
if (err.length) {
|
if (err.length) {
|
||||||
throw new Error(err.join("; "))
|
throw new Error(err.join("; "))
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
})()
|
})()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as T from "../types"
|
import * as T from "../types"
|
||||||
|
import { once } from "../util"
|
||||||
import { Dependency } from "./Dependency"
|
import { Dependency } from "./Dependency"
|
||||||
|
|
||||||
type DependencyType<Manifest extends T.Manifest> = {
|
type DependencyType<Manifest extends T.Manifest> = {
|
||||||
@@ -17,40 +18,38 @@ type DependencyType<Manifest extends T.Manifest> = {
|
|||||||
|
|
||||||
export function setupDependencies<Manifest extends T.Manifest>(
|
export function setupDependencies<Manifest extends T.Manifest>(
|
||||||
fn: (options: { effects: T.Effects }) => Promise<DependencyType<Manifest>>,
|
fn: (options: { effects: T.Effects }) => Promise<DependencyType<Manifest>>,
|
||||||
): (options: { effects: T.Effects }) => Promise<void> {
|
): (options: { effects: T.Effects }) => Promise<null> {
|
||||||
return (options: { effects: T.Effects }) => {
|
const cell = { updater: async (_: { effects: T.Effects }) => null }
|
||||||
const updater = async (options: { effects: T.Effects }) => {
|
cell.updater = async (options: { effects: T.Effects }) => {
|
||||||
const dependencyType = await fn(options)
|
options.effects = {
|
||||||
return await options.effects.setDependencies({
|
...options.effects,
|
||||||
dependencies: Object.entries(dependencyType).map(
|
constRetry: once(() => {
|
||||||
([
|
cell.updater(options)
|
||||||
id,
|
}),
|
||||||
{
|
|
||||||
data: { versionRange, ...x },
|
|
||||||
},
|
|
||||||
]) => ({
|
|
||||||
id,
|
|
||||||
...x,
|
|
||||||
...(x.type === "running"
|
|
||||||
? {
|
|
||||||
kind: "running",
|
|
||||||
healthChecks: x.healthChecks,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
kind: "exists",
|
|
||||||
}),
|
|
||||||
versionRange: versionRange.toString(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const updaterCtx = { options }
|
const dependencyType = await fn(options)
|
||||||
updaterCtx.options = {
|
return await options.effects.setDependencies({
|
||||||
effects: {
|
dependencies: Object.entries(dependencyType).map(
|
||||||
...options.effects,
|
([
|
||||||
constRetry: () => updater(updaterCtx.options),
|
id,
|
||||||
},
|
{
|
||||||
}
|
data: { versionRange, ...x },
|
||||||
return updater(updaterCtx.options)
|
},
|
||||||
|
]) => ({
|
||||||
|
id,
|
||||||
|
...x,
|
||||||
|
...(x.type === "running"
|
||||||
|
? {
|
||||||
|
kind: "running",
|
||||||
|
healthChecks: x.healthChecks,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
kind: "exists",
|
||||||
|
}),
|
||||||
|
versionRange: versionRange.toString(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
return cell.updater
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as T from "../types"
|
import * as T from "../types"
|
||||||
|
import { once } from "../util"
|
||||||
import { AddressReceipt } from "./AddressReceipt"
|
import { AddressReceipt } from "./AddressReceipt"
|
||||||
|
|
||||||
declare const UpdateServiceInterfacesProof: unique symbol
|
declare const UpdateServiceInterfacesProof: unique symbol
|
||||||
@@ -21,34 +22,36 @@ export const setupServiceInterfaces: SetupServiceInterfaces = <
|
|||||||
Output extends ServiceInterfacesReceipt,
|
Output extends ServiceInterfacesReceipt,
|
||||||
>(
|
>(
|
||||||
fn: SetServiceInterfaces<Output>,
|
fn: SetServiceInterfaces<Output>,
|
||||||
) =>
|
) => {
|
||||||
((options: { effects: T.Effects }) => {
|
const cell = {
|
||||||
const updater = async (options: { effects: T.Effects }) => {
|
updater: (async (options: { effects: T.Effects }) =>
|
||||||
const bindings: T.BindId[] = []
|
[] as any as Output) as UpdateServiceInterfaces<Output>,
|
||||||
const interfaces: T.ServiceInterfaceId[] = []
|
}
|
||||||
const res = await fn({
|
cell.updater = (async (options: { effects: T.Effects }) => {
|
||||||
effects: {
|
options.effects = {
|
||||||
...options.effects,
|
...options.effects,
|
||||||
bind: (params: T.BindParams) => {
|
constRetry: once(() => {
|
||||||
bindings.push({ id: params.id, internalPort: params.internalPort })
|
cell.updater(options)
|
||||||
return options.effects.bind(params)
|
}),
|
||||||
},
|
|
||||||
exportServiceInterface: (params: T.ExportServiceInterfaceParams) => {
|
|
||||||
interfaces.push(params.id)
|
|
||||||
return options.effects.exportServiceInterface(params)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await options.effects.clearBindings({ except: bindings })
|
|
||||||
await options.effects.clearServiceInterfaces({ except: interfaces })
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
const updaterCtx = { options }
|
const bindings: T.BindId[] = []
|
||||||
updaterCtx.options = {
|
const interfaces: T.ServiceInterfaceId[] = []
|
||||||
|
const res = await fn({
|
||||||
effects: {
|
effects: {
|
||||||
...options.effects,
|
...options.effects,
|
||||||
constRetry: () => updater(updaterCtx.options),
|
bind: (params: T.BindParams) => {
|
||||||
|
bindings.push({ id: params.id, internalPort: params.internalPort })
|
||||||
|
return options.effects.bind(params)
|
||||||
|
},
|
||||||
|
exportServiceInterface: (params: T.ExportServiceInterfaceParams) => {
|
||||||
|
interfaces.push(params.id)
|
||||||
|
return options.effects.exportServiceInterface(params)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
return updater(updaterCtx.options)
|
await options.effects.clearBindings({ except: bindings })
|
||||||
|
await options.effects.clearServiceInterfaces({ except: interfaces })
|
||||||
|
return res
|
||||||
}) as UpdateServiceInterfaces<Output>
|
}) as UpdateServiceInterfaces<Output>
|
||||||
|
return cell.updater
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ import type { NamedHealthCheckResult } from "./NamedHealthCheckResult"
|
|||||||
import type { StartStop } from "./StartStop"
|
import type { StartStop } from "./StartStop"
|
||||||
|
|
||||||
export type MainStatus =
|
export type MainStatus =
|
||||||
|
| {
|
||||||
|
main: "error"
|
||||||
|
onRebuild: StartStop
|
||||||
|
message: string
|
||||||
|
debug: string | null
|
||||||
|
}
|
||||||
| { main: "stopped" }
|
| { main: "stopped" }
|
||||||
| { main: "restarting" }
|
| { main: "restarting" }
|
||||||
| { main: "restoring" }
|
| { main: "restoring" }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DataUrl, Manifest, MerkleArchiveCommitment } from "../osBindings"
|
import { DataUrl, Manifest, MerkleArchiveCommitment } from "../osBindings"
|
||||||
import { ArrayBufferReader, MerkleArchive } from "./merkleArchive"
|
import { ArrayBufferReader, MerkleArchive } from "./merkleArchive"
|
||||||
import mime from "mime"
|
import mime from "mime-types"
|
||||||
|
|
||||||
const magicAndVersion = new Uint8Array([59, 59, 2])
|
const magicAndVersion = new Uint8Array([59, 59, 2])
|
||||||
|
|
||||||
@@ -52,13 +52,14 @@ export class S9pk {
|
|||||||
async icon(): Promise<DataUrl> {
|
async icon(): Promise<DataUrl> {
|
||||||
const iconName = Object.keys(this.archive.contents.contents).find(
|
const iconName = Object.keys(this.archive.contents.contents).find(
|
||||||
(name) =>
|
(name) =>
|
||||||
name.startsWith("icon.") && mime.getType(name)?.startsWith("image/"),
|
name.startsWith("icon.") &&
|
||||||
|
(mime.contentType(name) || null)?.startsWith("image/"),
|
||||||
)
|
)
|
||||||
if (!iconName) {
|
if (!iconName) {
|
||||||
throw new Error("no icon found in archive")
|
throw new Error("no icon found in archive")
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
`data:${mime.getType(iconName)};base64,` +
|
`data:${mime.contentType(iconName)};base64,` +
|
||||||
Buffer.from(
|
Buffer.from(
|
||||||
await this.archive.contents.getPath([iconName])!.verifiedFileContents(),
|
await this.archive.contents.getPath([iconName])!.verifiedFileContents(),
|
||||||
).toString("base64")
|
).toString("base64")
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export namespace ExpectedExports {
|
|||||||
*/
|
*/
|
||||||
export type main = (options: {
|
export type main = (options: {
|
||||||
effects: Effects
|
effects: Effects
|
||||||
started(onTerm: () => PromiseLike<void>): PromiseLike<void>
|
started(onTerm: () => PromiseLike<void>): PromiseLike<null>
|
||||||
}) => Promise<DaemonBuildable>
|
}) => Promise<DaemonBuildable>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,7 +118,7 @@ export type DaemonReceipt = {
|
|||||||
}
|
}
|
||||||
export type Daemon = {
|
export type Daemon = {
|
||||||
wait(): Promise<string>
|
wait(): Promise<string>
|
||||||
term(): Promise<void>
|
term(): Promise<null>
|
||||||
[DaemonProof]: never
|
[DaemonProof]: never
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ export type CommandType = string | [string, ...string[]]
|
|||||||
|
|
||||||
export type DaemonReturned = {
|
export type DaemonReturned = {
|
||||||
wait(): Promise<unknown>
|
wait(): Promise<unknown>
|
||||||
term(options?: { signal?: Signals; timeout?: number }): Promise<void>
|
term(options?: { signal?: Signals; timeout?: number }): Promise<null>
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare const hostName: unique symbol
|
export declare const hostName: unique symbol
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { boolean } from "ts-matches"
|
import { boolean } from "ts-matches"
|
||||||
|
|
||||||
export type Vertex<VMetadata = void, EMetadata = void> = {
|
export type Vertex<VMetadata = null, EMetadata = null> = {
|
||||||
metadata: VMetadata
|
metadata: VMetadata
|
||||||
edges: Array<Edge<EMetadata, VMetadata>>
|
edges: Array<Edge<EMetadata, VMetadata>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Edge<EMetadata = void, VMetadata = void> = {
|
export type Edge<EMetadata = null, VMetadata = null> = {
|
||||||
metadata: EMetadata
|
metadata: EMetadata
|
||||||
from: Vertex<VMetadata, EMetadata>
|
from: Vertex<VMetadata, EMetadata>
|
||||||
to: Vertex<VMetadata, EMetadata>
|
to: Vertex<VMetadata, EMetadata>
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Graph<VMetadata = void, EMetadata = void> {
|
export class Graph<VMetadata = null, EMetadata = null> {
|
||||||
private readonly vertices: Array<Vertex<VMetadata, EMetadata>> = []
|
private readonly vertices: Array<Vertex<VMetadata, EMetadata>> = []
|
||||||
constructor() {}
|
constructor() {}
|
||||||
addVertex(
|
addVertex(
|
||||||
@@ -46,7 +46,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
|
|||||||
}
|
}
|
||||||
findVertex(
|
findVertex(
|
||||||
predicate: (vertex: Vertex<VMetadata, EMetadata>) => boolean,
|
predicate: (vertex: Vertex<VMetadata, EMetadata>) => boolean,
|
||||||
): Generator<Vertex<VMetadata, EMetadata>, void> {
|
): Generator<Vertex<VMetadata, EMetadata>, null> {
|
||||||
const veritces = this.vertices
|
const veritces = this.vertices
|
||||||
function* gen() {
|
function* gen() {
|
||||||
for (let vertex of veritces) {
|
for (let vertex of veritces) {
|
||||||
@@ -54,6 +54,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
|
|||||||
yield vertex
|
yield vertex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
return gen()
|
return gen()
|
||||||
}
|
}
|
||||||
@@ -75,13 +76,13 @@ export class Graph<VMetadata = void, EMetadata = void> {
|
|||||||
from:
|
from:
|
||||||
| Vertex<VMetadata, EMetadata>
|
| Vertex<VMetadata, EMetadata>
|
||||||
| ((vertex: Vertex<VMetadata, EMetadata>) => boolean),
|
| ((vertex: Vertex<VMetadata, EMetadata>) => boolean),
|
||||||
): Generator<Vertex<VMetadata, EMetadata>, void> {
|
): Generator<Vertex<VMetadata, EMetadata>, null> {
|
||||||
const visited: Array<Vertex<VMetadata, EMetadata>> = []
|
const visited: Array<Vertex<VMetadata, EMetadata>> = []
|
||||||
function* rec(
|
function* rec(
|
||||||
vertex: Vertex<VMetadata, EMetadata>,
|
vertex: Vertex<VMetadata, EMetadata>,
|
||||||
): Generator<Vertex<VMetadata, EMetadata>, void> {
|
): Generator<Vertex<VMetadata, EMetadata>, null> {
|
||||||
if (visited.includes(vertex)) {
|
if (visited.includes(vertex)) {
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
visited.push(vertex)
|
visited.push(vertex)
|
||||||
yield vertex
|
yield vertex
|
||||||
@@ -99,6 +100,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (from instanceof Function) {
|
if (from instanceof Function) {
|
||||||
@@ -115,6 +117,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
})()
|
})()
|
||||||
} else {
|
} else {
|
||||||
return rec(from)
|
return rec(from)
|
||||||
@@ -124,13 +127,13 @@ export class Graph<VMetadata = void, EMetadata = void> {
|
|||||||
to:
|
to:
|
||||||
| Vertex<VMetadata, EMetadata>
|
| Vertex<VMetadata, EMetadata>
|
||||||
| ((vertex: Vertex<VMetadata, EMetadata>) => boolean),
|
| ((vertex: Vertex<VMetadata, EMetadata>) => boolean),
|
||||||
): Generator<Vertex<VMetadata, EMetadata>, void> {
|
): Generator<Vertex<VMetadata, EMetadata>, null> {
|
||||||
const visited: Array<Vertex<VMetadata, EMetadata>> = []
|
const visited: Array<Vertex<VMetadata, EMetadata>> = []
|
||||||
function* rec(
|
function* rec(
|
||||||
vertex: Vertex<VMetadata, EMetadata>,
|
vertex: Vertex<VMetadata, EMetadata>,
|
||||||
): Generator<Vertex<VMetadata, EMetadata>, void> {
|
): Generator<Vertex<VMetadata, EMetadata>, null> {
|
||||||
if (visited.includes(vertex)) {
|
if (visited.includes(vertex)) {
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
visited.push(vertex)
|
visited.push(vertex)
|
||||||
yield vertex
|
yield vertex
|
||||||
@@ -148,6 +151,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (to instanceof Function) {
|
if (to instanceof Function) {
|
||||||
@@ -164,6 +168,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
})()
|
})()
|
||||||
} else {
|
} else {
|
||||||
return rec(to)
|
return rec(to)
|
||||||
@@ -176,7 +181,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
|
|||||||
to:
|
to:
|
||||||
| Vertex<VMetadata, EMetadata>
|
| Vertex<VMetadata, EMetadata>
|
||||||
| ((vertex: Vertex<VMetadata, EMetadata>) => boolean),
|
| ((vertex: Vertex<VMetadata, EMetadata>) => boolean),
|
||||||
): Array<Edge<EMetadata, VMetadata>> | void {
|
): Array<Edge<EMetadata, VMetadata>> | null {
|
||||||
const isDone =
|
const isDone =
|
||||||
to instanceof Function
|
to instanceof Function
|
||||||
? to
|
? to
|
||||||
@@ -186,12 +191,12 @@ export class Graph<VMetadata = void, EMetadata = void> {
|
|||||||
function* check(
|
function* check(
|
||||||
vertex: Vertex<VMetadata, EMetadata>,
|
vertex: Vertex<VMetadata, EMetadata>,
|
||||||
path: Array<Edge<EMetadata, VMetadata>>,
|
path: Array<Edge<EMetadata, VMetadata>>,
|
||||||
): Generator<undefined, Array<Edge<EMetadata, VMetadata>> | undefined> {
|
): Generator<undefined, Array<Edge<EMetadata, VMetadata>> | null> {
|
||||||
if (isDone(vertex)) {
|
if (isDone(vertex)) {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
if (visited.includes(vertex)) {
|
if (visited.includes(vertex)) {
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
visited.push(vertex)
|
visited.push(vertex)
|
||||||
yield
|
yield
|
||||||
@@ -213,6 +218,7 @@ export class Graph<VMetadata = void, EMetadata = void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (from instanceof Function) {
|
if (from instanceof Function) {
|
||||||
@@ -240,5 +246,6 @@ export class Graph<VMetadata = void, EMetadata = void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
sdk/base/package-lock.json
generated
37
sdk/base/package-lock.json
generated
@@ -13,13 +13,14 @@
|
|||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
"isomorphic-fetch": "^3.0.0",
|
"isomorphic-fetch": "^3.0.0",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
"mime": "^4.0.3",
|
"mime-types": "^2.1.35",
|
||||||
"ts-matches": "^5.5.1",
|
"ts-matches": "^5.5.1",
|
||||||
"yaml": "^2.2.2"
|
"yaml": "^2.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "^29.4.0",
|
||||||
"@types/lodash.merge": "^4.6.2",
|
"@types/lodash.merge": "^4.6.2",
|
||||||
|
"@types/mime-types": "^2.1.4",
|
||||||
"jest": "^29.4.3",
|
"jest": "^29.4.3",
|
||||||
"peggy": "^3.0.2",
|
"peggy": "^3.0.2",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
@@ -1249,6 +1250,13 @@
|
|||||||
"@types/lodash": "*"
|
"@types/lodash": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/mime-types": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "18.15.10",
|
"version": "18.15.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz",
|
||||||
@@ -3106,18 +3114,25 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mime": {
|
"node_modules/mime-db": {
|
||||||
"version": "4.0.3",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
"integrity": "sha512-KgUb15Oorc0NEKPbvfa0wRU+PItIEZmiv+pyAO2i0oTIVTJhlzMclU7w4RXWQrSOVH5ax/p/CkIO7KI4OyFJTQ==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
"funding": [
|
"license": "MIT",
|
||||||
"https://github.com/sponsors/broofa"
|
"engines": {
|
||||||
],
|
"node": ">= 0.6"
|
||||||
"bin": {
|
}
|
||||||
"mime": "bin/cli.js"
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mimic-fn": {
|
"node_modules/mimic-fn": {
|
||||||
|
|||||||
@@ -21,14 +21,14 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/Start9Labs/start-sdk#readme",
|
"homepage": "https://github.com/Start9Labs/start-sdk#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"isomorphic-fetch": "^3.0.0",
|
|
||||||
"lodash.merge": "^4.6.2",
|
|
||||||
"mime": "^4.0.3",
|
|
||||||
"ts-matches": "^5.5.1",
|
|
||||||
"yaml": "^2.2.2",
|
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@noble/curves": "^1.4.0",
|
"@noble/curves": "^1.4.0",
|
||||||
"@noble/hashes": "^1.4.0"
|
"@noble/hashes": "^1.4.0",
|
||||||
|
"isomorphic-fetch": "^3.0.0",
|
||||||
|
"lodash.merge": "^4.6.2",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
|
"ts-matches": "^5.5.1",
|
||||||
|
"yaml": "^2.2.2"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "^29.4.0",
|
||||||
"@types/lodash.merge": "^4.6.2",
|
"@types/lodash.merge": "^4.6.2",
|
||||||
|
"@types/mime-types": "^2.1.4",
|
||||||
"jest": "^29.4.3",
|
"jest": "^29.4.3",
|
||||||
"peggy": "^3.0.2",
|
"peggy": "^3.0.2",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
|
|||||||
@@ -563,7 +563,7 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
setupMain: (
|
setupMain: (
|
||||||
fn: (o: {
|
fn: (o: {
|
||||||
effects: Effects
|
effects: Effects
|
||||||
started(onTerm: () => PromiseLike<void>): PromiseLike<void>
|
started(onTerm: () => PromiseLike<void>): PromiseLike<null>
|
||||||
}) => Promise<Daemons<Manifest, any>>,
|
}) => Promise<Daemons<Manifest, any>>,
|
||||||
) => setupMain<Manifest, Store>(fn),
|
) => setupMain<Manifest, Store>(fn),
|
||||||
/**
|
/**
|
||||||
@@ -657,12 +657,12 @@ export class StartSdk<Manifest extends T.Manifest, Store> {
|
|||||||
) => InputSpec.of<Spec, Store>(spec),
|
) => InputSpec.of<Spec, Store>(spec),
|
||||||
},
|
},
|
||||||
Daemons: {
|
Daemons: {
|
||||||
of(inputSpec: {
|
of(options: {
|
||||||
effects: Effects
|
effects: Effects
|
||||||
started: (onTerm: () => PromiseLike<void>) => PromiseLike<void>
|
started: (onTerm: () => PromiseLike<void>) => PromiseLike<null>
|
||||||
healthReceipts: HealthReceipt[]
|
healthReceipts: HealthReceipt[]
|
||||||
}) {
|
}) {
|
||||||
return Daemons.of<Manifest>(inputSpec)
|
return Daemons.of<Manifest>(options)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
List: {
|
List: {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function setupInit<Manifest extends T.Manifest, Store>(
|
|||||||
install: Install<Manifest, Store>,
|
install: Install<Manifest, Store>,
|
||||||
uninstall: Uninstall<Manifest, Store>,
|
uninstall: Uninstall<Manifest, Store>,
|
||||||
setServiceInterfaces: UpdateServiceInterfaces<any>,
|
setServiceInterfaces: UpdateServiceInterfaces<any>,
|
||||||
setDependencies: (options: { effects: T.Effects }) => Promise<void>,
|
setDependencies: (options: { effects: T.Effects }) => Promise<null>,
|
||||||
actions: Actions<Store, any>,
|
actions: Actions<Store, any>,
|
||||||
exposedStore: ExposedStorePaths,
|
exposedStore: ExposedStorePaths,
|
||||||
): {
|
): {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as T from "../../../base/lib/types"
|
|||||||
|
|
||||||
export type InstallFn<Manifest extends T.Manifest, Store> = (opts: {
|
export type InstallFn<Manifest extends T.Manifest, Store> = (opts: {
|
||||||
effects: T.Effects
|
effects: T.Effects
|
||||||
}) => Promise<void>
|
}) => Promise<null>
|
||||||
export class Install<Manifest extends T.Manifest, Store> {
|
export class Install<Manifest extends T.Manifest, Store> {
|
||||||
private constructor(readonly fn: InstallFn<Manifest, Store>) {}
|
private constructor(readonly fn: InstallFn<Manifest, Store>) {}
|
||||||
static of<Manifest extends T.Manifest, Store>(
|
static of<Manifest extends T.Manifest, Store>(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as T from "../../../base/lib/types"
|
|||||||
|
|
||||||
export type UninstallFn<Manifest extends T.Manifest, Store> = (opts: {
|
export type UninstallFn<Manifest extends T.Manifest, Store> = (opts: {
|
||||||
effects: T.Effects
|
effects: T.Effects
|
||||||
}) => Promise<void>
|
}) => Promise<null>
|
||||||
export class Uninstall<Manifest extends T.Manifest, Store> {
|
export class Uninstall<Manifest extends T.Manifest, Store> {
|
||||||
private constructor(readonly fn: UninstallFn<Manifest, Store>) {}
|
private constructor(readonly fn: UninstallFn<Manifest, Store>) {}
|
||||||
static of<Manifest extends T.Manifest, Store>(
|
static of<Manifest extends T.Manifest, Store>(
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ export class CommandController {
|
|||||||
| undefined
|
| undefined
|
||||||
cwd?: string | undefined
|
cwd?: string | undefined
|
||||||
user?: string | undefined
|
user?: string | undefined
|
||||||
onStdout?: (x: Buffer) => void
|
onStdout?: (x: Buffer) => null
|
||||||
onStderr?: (x: Buffer) => void
|
onStderr?: (x: Buffer) => null
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const commands = splitCommand(command)
|
const commands = splitCommand(command)
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export class Daemon {
|
|||||||
| undefined
|
| undefined
|
||||||
cwd?: string | undefined
|
cwd?: string | undefined
|
||||||
user?: string | undefined
|
user?: string | undefined
|
||||||
onStdout?: (x: Buffer) => void
|
onStdout?: (x: Buffer) => null
|
||||||
onStderr?: (x: Buffer) => void
|
onStderr?: (x: Buffer) => null
|
||||||
sigtermTimeout?: number
|
sigtermTimeout?: number
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export class Daemons<Manifest extends T.Manifest, Ids extends string>
|
|||||||
{
|
{
|
||||||
private constructor(
|
private constructor(
|
||||||
readonly effects: T.Effects,
|
readonly effects: T.Effects,
|
||||||
readonly started: (onTerm: () => PromiseLike<void>) => PromiseLike<void>,
|
readonly started: (onTerm: () => PromiseLike<void>) => PromiseLike<null>,
|
||||||
readonly daemons: Promise<Daemon>[],
|
readonly daemons: Promise<Daemon>[],
|
||||||
readonly ids: Ids[],
|
readonly ids: Ids[],
|
||||||
readonly healthDaemons: HealthDaemon[],
|
readonly healthDaemons: HealthDaemon[],
|
||||||
@@ -86,17 +86,17 @@ export class Daemons<Manifest extends T.Manifest, Ids extends string>
|
|||||||
*
|
*
|
||||||
* Daemons run in the order they are defined, with latter daemons being capable of
|
* Daemons run in the order they are defined, with latter daemons being capable of
|
||||||
* depending on prior daemons
|
* depending on prior daemons
|
||||||
* @param inputSpec
|
* @param options
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static of<Manifest extends T.Manifest>(inputSpec: {
|
static of<Manifest extends T.Manifest>(options: {
|
||||||
effects: T.Effects
|
effects: T.Effects
|
||||||
started: (onTerm: () => PromiseLike<void>) => PromiseLike<void>
|
started: (onTerm: () => PromiseLike<void>) => PromiseLike<null>
|
||||||
healthReceipts: HealthReceipt[]
|
healthReceipts: HealthReceipt[]
|
||||||
}) {
|
}) {
|
||||||
return new Daemons<Manifest, never>(
|
return new Daemons<Manifest, never>(
|
||||||
inputSpec.effects,
|
options.effects,
|
||||||
inputSpec.started,
|
options.started,
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export class HealthDaemon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private healthCheckCleanup: (() => void) | null = null
|
private healthCheckCleanup: (() => null) | null = null
|
||||||
private turnOffHealthCheck() {
|
private turnOffHealthCheck() {
|
||||||
this.healthCheckCleanup?.()
|
this.healthCheckCleanup?.()
|
||||||
}
|
}
|
||||||
@@ -125,6 +125,7 @@ export class HealthDaemon {
|
|||||||
this.healthCheckCleanup = () => {
|
this.healthCheckCleanup = () => {
|
||||||
setStatus({ done: true })
|
setStatus({ done: true })
|
||||||
this.healthCheckCleanup = null
|
this.healthCheckCleanup = null
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const DEFAULT_SIGTERM_TIMEOUT = 30_000
|
|||||||
export const setupMain = <Manifest extends T.Manifest, Store>(
|
export const setupMain = <Manifest extends T.Manifest, Store>(
|
||||||
fn: (o: {
|
fn: (o: {
|
||||||
effects: T.Effects
|
effects: T.Effects
|
||||||
started(onTerm: () => PromiseLike<void>): PromiseLike<void>
|
started(onTerm: () => PromiseLike<void>): PromiseLike<null>
|
||||||
}) => Promise<Daemons<Manifest, any>>,
|
}) => Promise<Daemons<Manifest, any>>,
|
||||||
): T.ExpectedExports.main => {
|
): T.ExpectedExports.main => {
|
||||||
return async (options) => {
|
return async (options) => {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const TIMES_TO_WAIT_FOR_PROC = 100
|
|||||||
* case where the subcontainer isn't owned by the process, the subcontainer shouldn't be destroyed.
|
* case where the subcontainer isn't owned by the process, the subcontainer shouldn't be destroyed.
|
||||||
*/
|
*/
|
||||||
export interface ExecSpawnable {
|
export interface ExecSpawnable {
|
||||||
get destroy(): undefined | (() => Promise<void>)
|
get destroy(): undefined | (() => Promise<null>)
|
||||||
exec(
|
exec(
|
||||||
command: string[],
|
command: string[],
|
||||||
options?: CommandOptions & ExecOptions,
|
options?: CommandOptions & ExecOptions,
|
||||||
@@ -47,7 +47,7 @@ export interface ExecSpawnable {
|
|||||||
export class SubContainer implements ExecSpawnable {
|
export class SubContainer implements ExecSpawnable {
|
||||||
private leader: cp.ChildProcess
|
private leader: cp.ChildProcess
|
||||||
private leaderExited: boolean = false
|
private leaderExited: boolean = false
|
||||||
private waitProc: () => Promise<void>
|
private waitProc: () => Promise<null>
|
||||||
private constructor(
|
private constructor(
|
||||||
readonly effects: T.Effects,
|
readonly effects: T.Effects,
|
||||||
readonly imageId: T.ImageId,
|
readonly imageId: T.ImageId,
|
||||||
@@ -79,7 +79,7 @@ export class SubContainer implements ExecSpawnable {
|
|||||||
}
|
}
|
||||||
await wait(1)
|
await wait(1)
|
||||||
}
|
}
|
||||||
resolve()
|
resolve(null)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -180,12 +180,12 @@ export class SubContainer implements ExecSpawnable {
|
|||||||
if (this.leaderExited) {
|
if (this.leaderExited) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<null>((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
let timeout = setTimeout(() => this.leader.kill("SIGKILL"), 30000)
|
let timeout = setTimeout(() => this.leader.kill("SIGKILL"), 30000)
|
||||||
this.leader.on("exit", () => {
|
this.leader.on("exit", () => {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
resolve()
|
resolve(null)
|
||||||
})
|
})
|
||||||
if (!this.leader.kill("SIGTERM")) {
|
if (!this.leader.kill("SIGTERM")) {
|
||||||
reject(new Error("kill(2) failed"))
|
reject(new Error("kill(2) failed"))
|
||||||
@@ -201,6 +201,7 @@ export class SubContainer implements ExecSpawnable {
|
|||||||
const guid = this.guid
|
const guid = this.guid
|
||||||
await this.killLeader()
|
await this.killLeader()
|
||||||
await this.effects.subcontainer.destroyFs({ guid })
|
await this.effects.subcontainer.destroyFs({ guid })
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,16 +246,16 @@ export class SubContainer implements ExecSpawnable {
|
|||||||
options || {},
|
options || {},
|
||||||
)
|
)
|
||||||
if (options?.input) {
|
if (options?.input) {
|
||||||
await new Promise<void>((resolve, reject) =>
|
await new Promise<null>((resolve, reject) =>
|
||||||
child.stdin.write(options.input, (e) => {
|
child.stdin.write(options.input, (e) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
reject(e)
|
reject(e)
|
||||||
} else {
|
} else {
|
||||||
resolve()
|
resolve(null)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
await new Promise<void>((resolve) => child.stdin.end(resolve))
|
await new Promise<null>((resolve) => child.stdin.end(resolve))
|
||||||
}
|
}
|
||||||
const pid = child.pid
|
const pid = child.pid
|
||||||
const stdout = { data: "" as string | Buffer }
|
const stdout = { data: "" as string | Buffer }
|
||||||
|
|||||||
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@@ -57,6 +57,7 @@
|
|||||||
"ng-qrcode": "^7.0.0",
|
"ng-qrcode": "^7.0.0",
|
||||||
"node-jose": "^2.2.0",
|
"node-jose": "^2.2.0",
|
||||||
"patch-db-client": "file:../patch-db/client",
|
"patch-db-client": "file:../patch-db/client",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
"pbkdf2": "^3.1.2",
|
"pbkdf2": "^3.1.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"swiper": "^8.2.4",
|
"swiper": "^8.2.4",
|
||||||
@@ -123,13 +124,14 @@
|
|||||||
"@noble/hashes": "^1.4.0",
|
"@noble/hashes": "^1.4.0",
|
||||||
"isomorphic-fetch": "^3.0.0",
|
"isomorphic-fetch": "^3.0.0",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
"mime": "^4.0.3",
|
"mime-types": "^2.1.35",
|
||||||
"ts-matches": "^5.5.1",
|
"ts-matches": "^5.5.1",
|
||||||
"yaml": "^2.2.2"
|
"yaml": "^2.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "^29.4.0",
|
||||||
"@types/lodash.merge": "^4.6.2",
|
"@types/lodash.merge": "^4.6.2",
|
||||||
|
"@types/mime-types": "^2.1.4",
|
||||||
"jest": "^29.4.3",
|
"jest": "^29.4.3",
|
||||||
"peggy": "^3.0.2",
|
"peggy": "^3.0.2",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
@@ -11731,7 +11733,7 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||||
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||||||
"optional": true
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/path-exists": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
"ng-qrcode": "^7.0.0",
|
"ng-qrcode": "^7.0.0",
|
||||||
"node-jose": "^2.2.0",
|
"node-jose": "^2.2.0",
|
||||||
"patch-db-client": "file:../patch-db/client",
|
"patch-db-client": "file:../patch-db/client",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
"pbkdf2": "^3.1.2",
|
"pbkdf2": "^3.1.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"swiper": "^8.2.4",
|
"swiper": "^8.2.4",
|
||||||
|
|||||||
@@ -11,10 +11,19 @@
|
|||||||
<ion-item-group *ngIf="pkg$ | async as pkg">
|
<ion-item-group *ngIf="pkg$ | async as pkg">
|
||||||
<!-- ** standard actions ** -->
|
<!-- ** standard actions ** -->
|
||||||
<ion-item-divider>Standard Actions</ion-item-divider>
|
<ion-item-divider>Standard Actions</ion-item-divider>
|
||||||
|
<app-actions-item
|
||||||
|
[action]="{
|
||||||
|
name: 'Rebuild Service',
|
||||||
|
description: 'Rebuilds the service container. It is harmless and only takes a few seconds to complete, but it should only be necessary if a StartOS bug is preventing dependencies, interfaces, or actions from synchronizing.',
|
||||||
|
visibility: 'enabled'
|
||||||
|
}"
|
||||||
|
icon="construct-outline"
|
||||||
|
(click)="rebuild(pkg.manifest.id)"
|
||||||
|
></app-actions-item>
|
||||||
<app-actions-item
|
<app-actions-item
|
||||||
[action]="{
|
[action]="{
|
||||||
name: 'Uninstall',
|
name: 'Uninstall',
|
||||||
description: 'This will uninstall the service from StartOS and delete all data permanently.',
|
description: 'Uninstalls this service from StartOS and delete all data permanently.',
|
||||||
visibility: 'enabled'
|
visibility: 'enabled'
|
||||||
}"
|
}"
|
||||||
icon="trash-outline"
|
icon="trash-outline"
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { AlertController, NavController } from '@ionic/angular'
|
import { getPkgId } from '@start9labs/shared'
|
||||||
import { ErrorService, getPkgId, LoadingService } from '@start9labs/shared'
|
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|
||||||
import { ActionService } from 'src/app/services/action.service'
|
import { ActionService } from 'src/app/services/action.service'
|
||||||
|
import { StandardActionsService } from 'src/app/services/standard-actions.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { getAllPackages, getManifest } from 'src/app/util/get-package-data'
|
import { getManifest } from 'src/app/util/get-package-data'
|
||||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
|
||||||
import { filter, map } from 'rxjs'
|
import { filter, map } from 'rxjs'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -35,13 +33,9 @@ export class AppActionsPage {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
private readonly api: ApiService,
|
|
||||||
private readonly alertCtrl: AlertController,
|
|
||||||
private readonly errorService: ErrorService,
|
|
||||||
private readonly loader: LoadingService,
|
|
||||||
private readonly navCtrl: NavController,
|
|
||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
private readonly actionService: ActionService,
|
private readonly actionService: ActionService,
|
||||||
|
private readonly standardActionsService: StandardActionsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handleAction(
|
async handleAction(
|
||||||
@@ -55,51 +49,12 @@ export class AppActionsPage {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryUninstall(manifest: T.Manifest): Promise<void> {
|
async rebuild(id: string) {
|
||||||
let message =
|
return this.standardActionsService.rebuild(id)
|
||||||
manifest.alerts.uninstall ||
|
|
||||||
`Uninstalling ${manifest.title} will permanently delete its data`
|
|
||||||
|
|
||||||
if (hasCurrentDeps(this.pkgId, await getAllPackages(this.patch))) {
|
|
||||||
message = `${message}. Services that depend on ${manifest.title} will no longer work properly and may crash`
|
|
||||||
}
|
|
||||||
|
|
||||||
const alert = await this.alertCtrl.create({
|
|
||||||
header: 'Warning',
|
|
||||||
message,
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
text: 'Cancel',
|
|
||||||
role: 'cancel',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Uninstall',
|
|
||||||
handler: () => {
|
|
||||||
this.uninstall()
|
|
||||||
},
|
|
||||||
cssClass: 'enter-click',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
cssClass: 'alert-warning-message',
|
|
||||||
})
|
|
||||||
|
|
||||||
await alert.present()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async uninstall() {
|
async tryUninstall(manifest: T.Manifest) {
|
||||||
const loader = this.loader.open(`Beginning uninstall...`).subscribe()
|
return this.standardActionsService.tryUninstall(manifest)
|
||||||
|
|
||||||
try {
|
|
||||||
await this.api.uninstallPackage({ id: this.pkgId })
|
|
||||||
this.api
|
|
||||||
.setDbValue<boolean>(['ackInstructions', this.pkgId], false)
|
|
||||||
.catch(e => console.error('Failed to mark instructions as unseen', e))
|
|
||||||
this.navCtrl.navigateRoot('/services')
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errorService.handleError(e)
|
|
||||||
} finally {
|
|
||||||
loader.unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { AppShowDependenciesComponent } from './components/app-show-dependencies
|
|||||||
import { AppShowMenuComponent } from './components/app-show-menu/app-show-menu.component'
|
import { AppShowMenuComponent } from './components/app-show-menu/app-show-menu.component'
|
||||||
import { AppShowHealthChecksComponent } from './components/app-show-health-checks/app-show-health-checks.component'
|
import { AppShowHealthChecksComponent } from './components/app-show-health-checks/app-show-health-checks.component'
|
||||||
import { AppShowAdditionalComponent } from './components/app-show-additional/app-show-additional.component'
|
import { AppShowAdditionalComponent } from './components/app-show-additional/app-show-additional.component'
|
||||||
|
import { AppShowErrorComponent } from './components/app-show-error/app-show-error.component'
|
||||||
import { HealthColorPipe } from './pipes/health-color.pipe'
|
import { HealthColorPipe } from './pipes/health-color.pipe'
|
||||||
import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe'
|
import { ToHealthChecksPipe } from './pipes/to-health-checks.pipe'
|
||||||
import { ToButtonsPipe } from './pipes/to-buttons.pipe'
|
import { ToButtonsPipe } from './pipes/to-buttons.pipe'
|
||||||
@@ -43,6 +44,7 @@ const routes: Routes = [
|
|||||||
AppShowMenuComponent,
|
AppShowMenuComponent,
|
||||||
AppShowHealthChecksComponent,
|
AppShowHealthChecksComponent,
|
||||||
AppShowAdditionalComponent,
|
AppShowAdditionalComponent,
|
||||||
|
AppShowErrorComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
<ion-item-group *ngIf="pkgPlus.status as status">
|
<ion-item-group *ngIf="pkgPlus.status as status">
|
||||||
<!-- ** status ** -->
|
<!-- ** status ** -->
|
||||||
<app-show-status [pkg]="pkg" [status]="status"></app-show-status>
|
<app-show-status [pkg]="pkg" [status]="status"></app-show-status>
|
||||||
<!-- ** installed && !backingUp ** -->
|
<!-- ** installed && !backingUp && !error ** -->
|
||||||
<ng-container
|
<ng-container
|
||||||
*ngIf="isInstalled(pkg) && status.primary !== 'backingUp'"
|
*ngIf="isInstalled(pkg) && status.primary !== 'backingUp' && status.primary !== 'error'"
|
||||||
>
|
>
|
||||||
<!-- ** health checks ** -->
|
<!-- ** health checks ** -->
|
||||||
<app-show-health-checks
|
<app-show-health-checks
|
||||||
@@ -35,6 +35,11 @@
|
|||||||
<!-- ** additional ** -->
|
<!-- ** additional ** -->
|
||||||
<app-show-additional [pkg]="pkg"></app-show-additional>
|
<app-show-additional [pkg]="pkg"></app-show-additional>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<app-show-error
|
||||||
|
*ngIf="pkg.status.main === 'error'"
|
||||||
|
[manifest]="pkgPlus.manifest"
|
||||||
|
[error]="pkg.status"
|
||||||
|
></app-show-error>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export interface DependencyInfo {
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class AppShowPage {
|
export class AppShowPage {
|
||||||
private readonly pkgId = getPkgId(this.route)
|
readonly pkgId = getPkgId(this.route)
|
||||||
|
|
||||||
readonly pkgPlus$ = combineLatest([
|
readonly pkgPlus$ = combineLatest([
|
||||||
this.patch.watch$('packageData'),
|
this.patch.watch$('packageData'),
|
||||||
@@ -58,9 +58,11 @@ export class AppShowPage {
|
|||||||
}),
|
}),
|
||||||
map(([allPkgs, depErrors]) => {
|
map(([allPkgs, depErrors]) => {
|
||||||
const pkg = allPkgs[this.pkgId]
|
const pkg = allPkgs[this.pkgId]
|
||||||
|
const manifest = getManifest(pkg)
|
||||||
return {
|
return {
|
||||||
pkg,
|
pkg,
|
||||||
dependencies: this.getDepInfo(pkg, allPkgs, depErrors),
|
manifest,
|
||||||
|
dependencies: this.getDepInfo(pkg, manifest, allPkgs, depErrors),
|
||||||
status: renderPkgStatus(pkg, depErrors),
|
status: renderPkgStatus(pkg, depErrors),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -84,11 +86,10 @@ export class AppShowPage {
|
|||||||
|
|
||||||
private getDepInfo(
|
private getDepInfo(
|
||||||
pkg: PackageDataEntry,
|
pkg: PackageDataEntry,
|
||||||
|
manifest: T.Manifest,
|
||||||
allPkgs: AllPackageData,
|
allPkgs: AllPackageData,
|
||||||
depErrors: PkgDependencyErrors,
|
depErrors: PkgDependencyErrors,
|
||||||
): DependencyInfo[] {
|
): DependencyInfo[] {
|
||||||
const manifest = getManifest(pkg)
|
|
||||||
|
|
||||||
return Object.keys(pkg.currentDependencies).map(id =>
|
return Object.keys(pkg.currentDependencies).map(id =>
|
||||||
this.getDepValues(pkg, allPkgs, manifest, id, depErrors),
|
this.getDepValues(pkg, allPkgs, manifest, id, depErrors),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<ion-item-divider>Message</ion-item-divider>
|
||||||
|
<div class="code-block ion-margin">
|
||||||
|
<code>
|
||||||
|
<ion-text color="warning">{{ error.message }}</ion-text>
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-item-divider>Actions</ion-item-divider>
|
||||||
|
<div class="ion-margin">
|
||||||
|
<p>
|
||||||
|
<b>Rebuild Container</b>
|
||||||
|
is harmless action that and only takes a few seconds to complete. It will
|
||||||
|
likely resolve this issue.
|
||||||
|
<b>Uninstall Service</b>
|
||||||
|
is a dangerous action that will remove the service from StartOS and wipe all
|
||||||
|
its data.
|
||||||
|
</p>
|
||||||
|
<ion-button class="ion-margin-end" (click)="rebuild()">
|
||||||
|
Rebuild Container
|
||||||
|
</ion-button>
|
||||||
|
<ion-button (click)="tryUninstall()" color="danger">
|
||||||
|
Uninstall Service
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="error.debug">
|
||||||
|
<ion-item-divider>Full Stack Trace</ion-item-divider>
|
||||||
|
<div class="code-block ion-margin">
|
||||||
|
<code>{{ error.message }}</code>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||||
|
import { ToastController } from '@ionic/angular'
|
||||||
|
import { copyToClipboard } from '@start9labs/shared'
|
||||||
|
import { T } from '@start9labs/start-sdk'
|
||||||
|
import { StandardActionsService } from 'src/app/services/standard-actions.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-show-error',
|
||||||
|
templateUrl: 'app-show-error.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AppShowErrorComponent {
|
||||||
|
@Input()
|
||||||
|
manifest!: T.Manifest
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
error!: T.MainStatus & { main: 'error' }
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly toastCtrl: ToastController,
|
||||||
|
private readonly standardActionsService: StandardActionsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async copy(text: string): Promise<void> {
|
||||||
|
const success = await copyToClipboard(text)
|
||||||
|
const message = success
|
||||||
|
? 'Copied to clipboard!'
|
||||||
|
: 'Failed to copy to clipboard.'
|
||||||
|
|
||||||
|
const toast = await this.toastCtrl.create({
|
||||||
|
header: message,
|
||||||
|
position: 'bottom',
|
||||||
|
duration: 1000,
|
||||||
|
})
|
||||||
|
await toast.present()
|
||||||
|
}
|
||||||
|
|
||||||
|
async rebuild() {
|
||||||
|
return this.standardActionsService.rebuild(this.manifest.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async tryUninstall() {
|
||||||
|
return this.standardActionsService.tryUninstall(this.manifest)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,14 @@
|
|||||||
</ion-label>
|
</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
<ng-container *ngIf="isInstalled(pkg) && (connection$ | async)">
|
<ng-container
|
||||||
|
*ngIf="
|
||||||
|
isInstalled(pkg) &&
|
||||||
|
pkg.status.main !== 'backingUp' &&
|
||||||
|
pkg.status.main !== 'error' &&
|
||||||
|
(connection$ | async)
|
||||||
|
"
|
||||||
|
>
|
||||||
<ion-grid>
|
<ion-grid>
|
||||||
<ion-row style="padding-left: 12px">
|
<ion-row style="padding-left: 12px">
|
||||||
<ion-col>
|
<ion-col>
|
||||||
|
|||||||
@@ -257,6 +257,9 @@ export module RR {
|
|||||||
export type StopPackageReq = { id: string } // package.stop
|
export type StopPackageReq = { id: string } // package.stop
|
||||||
export type StopPackageRes = null
|
export type StopPackageRes = null
|
||||||
|
|
||||||
|
export type RebuildPackageReq = { id: string } // package.rebuild
|
||||||
|
export type RebuildPackageRes = null
|
||||||
|
|
||||||
export type UninstallPackageReq = { id: string } // package.uninstall
|
export type UninstallPackageReq = { id: string } // package.uninstall
|
||||||
export type UninstallPackageRes = null
|
export type UninstallPackageRes = null
|
||||||
|
|
||||||
|
|||||||
@@ -249,6 +249,10 @@ export abstract class ApiService {
|
|||||||
|
|
||||||
abstract stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes>
|
abstract stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes>
|
||||||
|
|
||||||
|
abstract rebuildPackage(
|
||||||
|
params: RR.RebuildPackageReq,
|
||||||
|
): Promise<RR.RebuildPackageRes>
|
||||||
|
|
||||||
abstract uninstallPackage(
|
abstract uninstallPackage(
|
||||||
params: RR.UninstallPackageReq,
|
params: RR.UninstallPackageReq,
|
||||||
): Promise<RR.UninstallPackageRes>
|
): Promise<RR.UninstallPackageRes>
|
||||||
|
|||||||
@@ -498,6 +498,12 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.rpcRequest({ method: 'package.stop', params })
|
return this.rpcRequest({ method: 'package.stop', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async rebuildPackage(
|
||||||
|
params: RR.RebuildPackageReq,
|
||||||
|
): Promise<RR.RebuildPackageRes> {
|
||||||
|
return this.rpcRequest({ method: 'package.rebuild', params })
|
||||||
|
}
|
||||||
|
|
||||||
async uninstallPackage(
|
async uninstallPackage(
|
||||||
params: RR.UninstallPackageReq,
|
params: RR.UninstallPackageReq,
|
||||||
): Promise<RR.UninstallPackageRes> {
|
): Promise<RR.UninstallPackageRes> {
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { Injectable } from '@angular/core'
|
|||||||
import { Log, RPCErrorDetails, RPCOptions, pauseFor } from '@start9labs/shared'
|
import { Log, RPCErrorDetails, RPCOptions, pauseFor } from '@start9labs/shared'
|
||||||
import { ApiService } from './embassy-api.service'
|
import { ApiService } from './embassy-api.service'
|
||||||
import {
|
import {
|
||||||
|
AddOperation,
|
||||||
Operation,
|
Operation,
|
||||||
PatchOp,
|
PatchOp,
|
||||||
pathFromArray,
|
pathFromArray,
|
||||||
RemoveOperation,
|
RemoveOperation,
|
||||||
|
ReplaceOperation,
|
||||||
Revision,
|
Revision,
|
||||||
} from 'patch-db-client'
|
} from 'patch-db-client'
|
||||||
import {
|
import {
|
||||||
@@ -636,14 +638,14 @@ export class MockApiService extends ApiService {
|
|||||||
|
|
||||||
async createBackup(params: RR.CreateBackupReq): Promise<RR.CreateBackupRes> {
|
async createBackup(params: RR.CreateBackupReq): Promise<RR.CreateBackupRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
const path = '/serverInfo/statusInfo/backupProgress'
|
const serverPath = '/serverInfo/statusInfo/backupProgress'
|
||||||
const ids = params.packageIds
|
const ids = params.packageIds
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
for (let i = 0; i < ids.length; i++) {
|
for (let i = 0; i < ids.length; i++) {
|
||||||
const id = ids[i]
|
const id = ids[i]
|
||||||
const appPath = `/packageData/${id}/status/main/status`
|
const appPath = `/packageData/${id}/status/main/`
|
||||||
const appPatch = [
|
const appPatch: ReplaceOperation<T.MainStatus['main']>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: appPath,
|
path: appPath,
|
||||||
@@ -660,40 +662,43 @@ export class MockApiService extends ApiService {
|
|||||||
value: 'stopped',
|
value: 'stopped',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
this.mockRevision([
|
|
||||||
|
const serverPatch: ReplaceOperation<T.BackupProgress['complete']>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: `${path}/${id}/complete`,
|
path: `${serverPath}/${id}/complete`,
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
])
|
]
|
||||||
|
this.mockRevision(serverPatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
|
|
||||||
// set server back to running
|
// remove backupProgress
|
||||||
const lastPatch = [
|
const lastPatch: ReplaceOperation<T.ServerStatus['backupProgress']>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path,
|
path: serverPath,
|
||||||
value: null,
|
value: null,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.mockRevision(lastPatch)
|
this.mockRevision(lastPatch)
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|
||||||
const originalPatch = [
|
const originalPatch: ReplaceOperation<T.ServerStatus['backupProgress']>[] =
|
||||||
{
|
[
|
||||||
op: PatchOp.REPLACE,
|
{
|
||||||
path,
|
op: PatchOp.REPLACE,
|
||||||
value: ids.reduce((acc, val) => {
|
path: serverPath,
|
||||||
return {
|
value: ids.reduce((acc, val) => {
|
||||||
...acc,
|
return {
|
||||||
[val]: { complete: false },
|
...acc,
|
||||||
}
|
[val]: { complete: false },
|
||||||
}, {}),
|
}
|
||||||
},
|
}, {}),
|
||||||
]
|
},
|
||||||
|
]
|
||||||
|
|
||||||
this.mockRevision(originalPatch)
|
this.mockRevision(originalPatch)
|
||||||
|
|
||||||
@@ -750,7 +755,7 @@ export class MockApiService extends ApiService {
|
|||||||
this.installProgress(params.id)
|
this.installProgress(params.id)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
const patch: Operation<
|
const patch: AddOperation<
|
||||||
PackageDataEntry<InstallingState | UpdatingState>
|
PackageDataEntry<InstallingState | UpdatingState>
|
||||||
>[] = [
|
>[] = [
|
||||||
{
|
{
|
||||||
@@ -799,7 +804,7 @@ export class MockApiService extends ApiService {
|
|||||||
params: RR.RestorePackagesReq,
|
params: RR.RestorePackagesReq,
|
||||||
): Promise<RR.RestorePackagesRes> {
|
): Promise<RR.RestorePackagesRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
const patch: Operation<PackageDataEntry>[] = params.ids.map(id => {
|
const patch: AddOperation<PackageDataEntry>[] = params.ids.map(id => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
this.installProgress(id)
|
this.installProgress(id)
|
||||||
}, 2000)
|
}, 2000)
|
||||||
@@ -826,76 +831,61 @@ export class MockApiService extends ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
|
async startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
|
||||||
const path = `/packageData/${params.id}/status/main`
|
const path = `/packageData/${params.id}/status`
|
||||||
|
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const patch2 = [
|
const patch2: ReplaceOperation<T.MainStatus & { main: 'running' }>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: path + '/status',
|
path,
|
||||||
value: 'running',
|
value: {
|
||||||
},
|
main: 'running',
|
||||||
{
|
started: new Date().toISOString(),
|
||||||
op: PatchOp.REPLACE,
|
health: {
|
||||||
path: path + '/started',
|
'ephemeral-health-check': {
|
||||||
value: new Date().toISOString(),
|
name: 'Ephemeral Health Check',
|
||||||
|
result: 'starting',
|
||||||
|
message: null,
|
||||||
|
},
|
||||||
|
'unnecessary-health-check': {
|
||||||
|
name: 'Unnecessary Health Check',
|
||||||
|
result: 'disabled',
|
||||||
|
message: 'Custom disabled message',
|
||||||
|
},
|
||||||
|
'chain-state': {
|
||||||
|
name: 'Chain State',
|
||||||
|
result: 'loading',
|
||||||
|
message: 'Bitcoin is syncing from genesis',
|
||||||
|
},
|
||||||
|
'p2p-interface': {
|
||||||
|
name: 'P2P Interface',
|
||||||
|
result: 'success',
|
||||||
|
message: null,
|
||||||
|
},
|
||||||
|
'rpc-interface': {
|
||||||
|
name: 'RPC Interface',
|
||||||
|
result: 'failure',
|
||||||
|
message: 'Custom failure message',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.mockRevision(patch2)
|
this.mockRevision(patch2)
|
||||||
|
|
||||||
const patch3 = [
|
|
||||||
{
|
|
||||||
op: PatchOp.REPLACE,
|
|
||||||
path: path + '/health',
|
|
||||||
value: {
|
|
||||||
'ephemeral-health-check': {
|
|
||||||
result: 'starting',
|
|
||||||
},
|
|
||||||
'unnecessary-health-check': {
|
|
||||||
result: 'disabled',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
this.mockRevision(patch3)
|
|
||||||
|
|
||||||
await pauseFor(2000)
|
|
||||||
|
|
||||||
const patch4 = [
|
|
||||||
{
|
|
||||||
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.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
this.mockRevision(patch4)
|
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
|
||||||
const originalPatch = [
|
const originalPatch: ReplaceOperation<
|
||||||
|
T.MainStatus & { main: 'starting' }
|
||||||
|
>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: path + '/status',
|
path,
|
||||||
value: 'starting',
|
value: {
|
||||||
|
main: 'starting',
|
||||||
|
health: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -907,74 +897,57 @@ export class MockApiService extends ApiService {
|
|||||||
async restartPackage(
|
async restartPackage(
|
||||||
params: RR.RestartPackageReq,
|
params: RR.RestartPackageReq,
|
||||||
): Promise<RR.RestartPackageRes> {
|
): Promise<RR.RestartPackageRes> {
|
||||||
// first enact stop
|
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
const path = `/packageData/${params.id}/status/main`
|
const path = `/packageData/${params.id}/status`
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const patch2: Operation<any>[] = [
|
const patch2: ReplaceOperation<T.MainStatus & { main: 'running' }>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: path + '/status',
|
path,
|
||||||
value: 'starting',
|
value: {
|
||||||
},
|
main: 'running',
|
||||||
{
|
started: new Date().toISOString(),
|
||||||
op: PatchOp.ADD,
|
health: {
|
||||||
path: path + '/restarting',
|
'ephemeral-health-check': {
|
||||||
value: true,
|
name: 'Ephemeral Health Check',
|
||||||
|
result: 'starting',
|
||||||
|
message: null,
|
||||||
|
},
|
||||||
|
'unnecessary-health-check': {
|
||||||
|
name: 'Unnecessary Health Check',
|
||||||
|
result: 'disabled',
|
||||||
|
message: 'Custom disabled message',
|
||||||
|
},
|
||||||
|
'chain-state': {
|
||||||
|
name: 'Chain State',
|
||||||
|
result: 'loading',
|
||||||
|
message: 'Bitcoin is syncing from genesis',
|
||||||
|
},
|
||||||
|
'p2p-interface': {
|
||||||
|
name: 'P2P Interface',
|
||||||
|
result: 'success',
|
||||||
|
message: null,
|
||||||
|
},
|
||||||
|
'rpc-interface': {
|
||||||
|
name: 'RPC Interface',
|
||||||
|
result: 'failure',
|
||||||
|
message: 'Custom failure message',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.mockRevision(patch2)
|
this.mockRevision(patch2)
|
||||||
|
|
||||||
await pauseFor(2000)
|
|
||||||
|
|
||||||
const patch3: Operation<any>[] = [
|
|
||||||
{
|
|
||||||
op: PatchOp.REPLACE,
|
|
||||||
path: path + '/status',
|
|
||||||
value: 'running',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
op: PatchOp.REMOVE,
|
|
||||||
path: path + '/restarting',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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.mockRevision(patch3)
|
|
||||||
}, this.revertTime)
|
}, this.revertTime)
|
||||||
|
|
||||||
const patch = [
|
const patch: ReplaceOperation<T.MainStatus & { main: 'restarting' }>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: path + '/status',
|
path,
|
||||||
value: 'restarting',
|
value: {
|
||||||
},
|
main: 'restarting',
|
||||||
{
|
},
|
||||||
op: PatchOp.REPLACE,
|
|
||||||
path: path + '/health',
|
|
||||||
value: {},
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -985,29 +958,24 @@ export class MockApiService extends ApiService {
|
|||||||
|
|
||||||
async stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
|
async stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
const path = `/packageData/${params.id}/status/main`
|
const path = `/packageData/${params.id}/status`
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const patch2 = [
|
const patch2: ReplaceOperation<T.MainStatus & { main: 'stopped' }>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: path,
|
path: path,
|
||||||
value: {
|
value: { main: 'stopped' },
|
||||||
status: 'stopped',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.mockRevision(patch2)
|
this.mockRevision(patch2)
|
||||||
}, this.revertTime)
|
}, this.revertTime)
|
||||||
|
|
||||||
const patch = [
|
const patch: ReplaceOperation<T.MainStatus & { main: 'stopping' }>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: path,
|
path: path,
|
||||||
value: {
|
value: { main: 'stopping' },
|
||||||
status: 'stopping',
|
|
||||||
timeout: '35s',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1016,6 +984,12 @@ export class MockApiService extends ApiService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async rebuildPackage(
|
||||||
|
params: RR.RebuildPackageReq,
|
||||||
|
): Promise<RR.RebuildPackageRes> {
|
||||||
|
return this.restartPackage(params)
|
||||||
|
}
|
||||||
|
|
||||||
async uninstallPackage(
|
async uninstallPackage(
|
||||||
params: RR.UninstallPackageReq,
|
params: RR.UninstallPackageReq,
|
||||||
): Promise<RR.UninstallPackageRes> {
|
): Promise<RR.UninstallPackageRes> {
|
||||||
@@ -1031,7 +1005,7 @@ export class MockApiService extends ApiService {
|
|||||||
this.mockRevision(patch2)
|
this.mockRevision(patch2)
|
||||||
}, this.revertTime)
|
}, this.revertTime)
|
||||||
|
|
||||||
const patch = [
|
const patch: ReplaceOperation<T.PackageState['state']>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: `/packageData/${params.id}/stateInfo/state`,
|
path: `/packageData/${params.id}/stateInfo/state`,
|
||||||
|
|||||||
@@ -96,36 +96,14 @@ export const mockPatchData: DataModel = {
|
|||||||
icon: '/assets/img/service-icons/bitcoind.svg',
|
icon: '/assets/img/service-icons/bitcoind.svg',
|
||||||
lastBackup: null,
|
lastBackup: null,
|
||||||
status: {
|
status: {
|
||||||
main: 'running',
|
main: 'stopped',
|
||||||
started: '2021-06-14T20:49:17.774Z',
|
|
||||||
health: {
|
|
||||||
'ephemeral-health-check': {
|
|
||||||
name: 'Ephemeral Health Check',
|
|
||||||
result: 'starting',
|
|
||||||
message: null,
|
|
||||||
},
|
|
||||||
'chain-state': {
|
|
||||||
name: 'Chain State',
|
|
||||||
result: 'loading',
|
|
||||||
message: 'Bitcoin is syncing from genesis',
|
|
||||||
},
|
|
||||||
'p2p-interface': {
|
|
||||||
name: 'P2P',
|
|
||||||
result: 'success',
|
|
||||||
message: 'Health check successful',
|
|
||||||
},
|
|
||||||
'rpc-interface': {
|
|
||||||
name: 'RPC',
|
|
||||||
result: 'failure',
|
|
||||||
message: 'RPC interface unreachable.',
|
|
||||||
},
|
|
||||||
'unnecessary-health-check': {
|
|
||||||
name: 'Unnecessary Health Check',
|
|
||||||
result: 'disabled',
|
|
||||||
message: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
// status: {
|
||||||
|
// main: 'error',
|
||||||
|
// message: 'Bitcoin is erroring out',
|
||||||
|
// debug: 'This is a complete stack trace for bitcoin',
|
||||||
|
// onRebuild: 'start',
|
||||||
|
// },
|
||||||
actions: {
|
actions: {
|
||||||
config: {
|
config: {
|
||||||
name: 'Bitcoin Config',
|
name: 'Bitcoin Config',
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export type PrimaryStatus =
|
|||||||
| 'stopped'
|
| 'stopped'
|
||||||
| 'backingUp'
|
| 'backingUp'
|
||||||
| 'needsConfig'
|
| 'needsConfig'
|
||||||
|
| 'error'
|
||||||
|
|
||||||
export type DependencyStatus = 'warning' | 'satisfied'
|
export type DependencyStatus = 'warning' | 'satisfied'
|
||||||
|
|
||||||
@@ -139,6 +140,11 @@ export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
|
|||||||
color: 'warning',
|
color: 'warning',
|
||||||
showDots: false,
|
showDots: false,
|
||||||
},
|
},
|
||||||
|
error: {
|
||||||
|
display: 'Service Launch Error',
|
||||||
|
color: 'danger',
|
||||||
|
showDots: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DependencyRendering: Record<DependencyStatus, StatusRendering> = {
|
export const DependencyRendering: Record<DependencyStatus, StatusRendering> = {
|
||||||
|
|||||||
85
web/projects/ui/src/app/services/standard-actions.service.ts
Normal file
85
web/projects/ui/src/app/services/standard-actions.service.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { T } from '@start9labs/start-sdk'
|
||||||
|
import { hasCurrentDeps } from '../util/has-deps'
|
||||||
|
import { getAllPackages } from '../util/get-package-data'
|
||||||
|
import { PatchDB } from 'patch-db-client'
|
||||||
|
import { DataModel } from './patch-db/data-model'
|
||||||
|
import { AlertController, NavController } from '@ionic/angular'
|
||||||
|
import { ApiService } from './api/embassy-api.service'
|
||||||
|
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class StandardActionsService {
|
||||||
|
constructor(
|
||||||
|
private readonly patch: PatchDB<DataModel>,
|
||||||
|
private readonly api: ApiService,
|
||||||
|
private readonly alertCtrl: AlertController,
|
||||||
|
private readonly errorService: ErrorService,
|
||||||
|
private readonly loader: LoadingService,
|
||||||
|
private readonly navCtrl: NavController,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async rebuild(id: string) {
|
||||||
|
const loader = this.loader.open(`Rebuilding Container...`).subscribe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api.rebuildPackage({ id })
|
||||||
|
this.navCtrl.navigateBack('/services/' + id)
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async tryUninstall(manifest: T.Manifest): Promise<void> {
|
||||||
|
const { id, title, alerts } = manifest
|
||||||
|
|
||||||
|
let message =
|
||||||
|
alerts.uninstall ||
|
||||||
|
`Uninstalling ${title} will permanently delete its data`
|
||||||
|
|
||||||
|
if (hasCurrentDeps(id, await getAllPackages(this.patch))) {
|
||||||
|
message = `${message}. Services that depend on ${title} will no longer work properly and may crash`
|
||||||
|
}
|
||||||
|
|
||||||
|
const alert = await this.alertCtrl.create({
|
||||||
|
header: 'Warning',
|
||||||
|
message,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Cancel',
|
||||||
|
role: 'cancel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Uninstall',
|
||||||
|
handler: () => {
|
||||||
|
this.uninstall(id)
|
||||||
|
},
|
||||||
|
cssClass: 'enter-click',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
cssClass: 'alert-warning-message',
|
||||||
|
})
|
||||||
|
|
||||||
|
await alert.present()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async uninstall(id: string) {
|
||||||
|
const loader = this.loader.open(`Beginning uninstall...`).subscribe()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.api.uninstallPackage({ id })
|
||||||
|
this.api
|
||||||
|
.setDbValue<boolean>(['ackInstructions', id], false)
|
||||||
|
.catch(e => console.error('Failed to mark instructions as unseen', e))
|
||||||
|
this.navCtrl.navigateRoot('/services')
|
||||||
|
} catch (e: any) {
|
||||||
|
this.errorService.handleError(e)
|
||||||
|
} finally {
|
||||||
|
loader.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,6 +110,12 @@ $subheader-height: 48px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
background-color: rgb(69, 69, 69);
|
||||||
|
padding: 12px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
.center {
|
.center {
|
||||||
display: block;
|
display: block;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
/* These paths are relative to each app base folder */
|
/* These paths are relative to each app base folder */
|
||||||
"@start9labs/marketplace": ["../marketplace/src/public-api"],
|
"@start9labs/marketplace": ["../marketplace/src/public-api"],
|
||||||
"@start9labs/shared": ["../shared/src/public-api"]
|
"@start9labs/shared": ["../shared/src/public-api"],
|
||||||
|
"path": ["../../node_modules/path-browserify"]
|
||||||
},
|
},
|
||||||
"typeRoots": ["node_modules/@types"],
|
"typeRoots": ["node_modules/@types"],
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
|
|||||||
Reference in New Issue
Block a user