diff --git a/Makefile b/Makefile index 286f79da5..48a28c388 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client GZIP_BIN := $(shell which pigz || which gzip) TAR_BIN := $(shell which gtar || which tar) COMPILED_TARGETS := $(BINS) system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar -ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; fi') $(PLATFORM_FILE) +ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; fi') $(PLATFORM_FILE) sdk/lib/test ifeq ($(REMOTE),) mkdir = mkdir -p $1 @@ -87,7 +87,7 @@ clean: format: cd core && cargo +nightly fmt -test: $(CORE_SRC) $(ENVIRONMENT_FILE) +test: $(CORE_SRC) $(ENVIRONMENT_FILE) cd core && cargo build && cargo test cli: @@ -109,7 +109,7 @@ results/$(BASENAME).$(IMAGE_TYPE) results/$(BASENAME).squashfs: $(IMAGE_RECIPE_S ./image-recipe/run-local-build.sh "results/$(BASENAME).deb" # For creating os images. DO NOT USE -install: $(ALL_TARGETS) +install: $(ALL_TARGETS) $(call mkdir,$(DESTDIR)/usr/bin) $(call cp,core/target/$(ARCH)-unknown-linux-musl/release/startbox,$(DESTDIR)/usr/bin/startbox) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd) @@ -173,11 +173,14 @@ container-runtime/node_modules: container-runtime/package.json container-runtime npm --prefix container-runtime ci touch container-runtime/node_modules -core/startos/bindings: $(CORE_SRC) $(ENVIRONMENT_FILE) $(PLATFORM_FILE) +core/startos/bindings: $(shell git ls-files core) $(ENVIRONMENT_FILE) $(PLATFORM_FILE) (cd core/ && cargo test) touch core/startos/bindings -sdk/dist: $(shell git ls-files sdk) core/startos/bindings +sdk/lib/test: $(shell git ls-files sdk) core/startos/bindings + (cd sdk && make test) + +sdk/dist: $(shell git ls-files sdk) (cd sdk && make bundle) container-runtime/dist: container-runtime/node_modules $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json @@ -209,7 +212,8 @@ $(BINS): $(CORE_SRC) $(ENVIRONMENT_FILE) cd core && ARCH=$(ARCH) ./build-prod.sh touch $(BINS) -web/node_modules: web/package.json +web/node_modules: web/package.json sdk/dist + (cd sdk && make bundle) npm --prefix web ci web/dist/raw/ui: $(WEB_UI_SRC) $(WEB_SHARED_SRC) diff --git a/container-runtime/src/Adapters/HostSystemStartOs.ts b/container-runtime/src/Adapters/HostSystemStartOs.ts index ec4fac796..f173e4e62 100644 --- a/container-runtime/src/Adapters/HostSystemStartOs.ts +++ b/container-runtime/src/Adapters/HostSystemStartOs.ts @@ -117,10 +117,7 @@ export class HostSystemStartOs implements Effects { T.Effects["createOverlayedImage"] > } - destroyOverlayedImage(options: { - imageId: string - guid: string - }): Promise { + destroyOverlayedImage(options: { guid: string }): Promise { return this.rpcRound("destroyOverlayedImage", options) as ReturnType< T.Effects["destroyOverlayedImage"] > @@ -196,16 +193,13 @@ export class HostSystemStartOs implements Effects { T.Effects["getServicePortForward"] > } - getSslCertificate( - ...[packageId, algorithm]: Parameters - ) { - return this.rpcRound("getSslCertificate", { - packageId, - algorithm, - }) as ReturnType + getSslCertificate(options: Parameters[0]) { + return this.rpcRound("getSslCertificate", options) as ReturnType< + T.Effects["getSslCertificate"] + > } - getSslKey(...[packageId, algorithm]: Parameters) { - return this.rpcRound("getSslKey", { packageId, algorithm }) as ReturnType< + getSslKey(options: Parameters[0]) { + return this.rpcRound("getSslKey", options) as ReturnType< T.Effects["getSslKey"] > } diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts index 3506575bc..7852a1ec7 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts @@ -33,11 +33,16 @@ export class DockerProcedureContainer { await overlay.mount({ type: "assets", id: mount }, mounts[mount]) } else if (volumeMount.type === "certificate") { volumeMount - const certChain = await effects.getSslCertificate( - null, - volumeMount["interface-id"], - ) - const key = await effects.getSslKey(null, volumeMount["interface-id"]) + const certChain = await effects.getSslCertificate({ + packageId: null, + hostId: volumeMount["interface-id"], + algorithm: null, + }) + const key = await effects.getSslKey({ + packageId: null, + hostId: volumeMount["interface-id"], + algorithm: null, + }) await fs.writeFile( `${path}/${volumeMount["interface-id"]}.cert.pem`, certChain.join("\n"), diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts index c5e6644af..9a64fd33d 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -186,6 +186,7 @@ export class MainLoop { if ("result" in result) { await effects.setHealth({ + message: null, name: healthId, status: "passing", }) diff --git a/core/models/src/id/health_check.rs b/core/models/src/id/health_check.rs index 72c2947e7..c416ab1e6 100644 --- a/core/models/src/id/health_check.rs +++ b/core/models/src/id/health_check.rs @@ -1,8 +1,9 @@ use std::path::Path; +use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; -use crate::Id; +use crate::{Id, InvalidId}; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, ts_rs::TS)] pub struct HealthCheckId(Id); @@ -11,6 +12,12 @@ impl std::fmt::Display for HealthCheckId { write!(f, "{}", &self.0) } } +impl FromStr for HealthCheckId { + type Err = InvalidId; + fn from_str(s: &str) -> Result { + Id::from_str(s).map(HealthCheckId) + } +} impl AsRef for HealthCheckId { fn as_ref(&self) -> &str { self.0.as_ref() diff --git a/core/models/src/id/mod.rs b/core/models/src/id/mod.rs index ef38ece9d..29825f628 100644 --- a/core/models/src/id/mod.rs +++ b/core/models/src/id/mod.rs @@ -1,4 +1,5 @@ use std::borrow::Borrow; +use std::str::FromStr; use regex::Regex; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -59,6 +60,12 @@ impl TryFrom<&str> for Id { } } } +impl FromStr for Id { + type Err = InvalidId; + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} impl From for InternedString { fn from(value: Id) -> Self { value.0 diff --git a/core/startos/bindings/AddressInfo.ts b/core/startos/bindings/AddressInfo.ts new file mode 100644 index 000000000..9d4c8ee6e --- /dev/null +++ b/core/startos/bindings/AddressInfo.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BindOptions } from "./BindOptions"; + +export interface AddressInfo { username: string | null, hostId: string, bindOptions: BindOptions, suffix: string, } \ No newline at end of file diff --git a/core/startos/bindings/AllowedStatuses.ts b/core/startos/bindings/AllowedStatuses.ts new file mode 100644 index 000000000..87d122f70 --- /dev/null +++ b/core/startos/bindings/AllowedStatuses.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AllowedStatuses = "only-running" | "only-stopped" | "any" | "disabled"; \ No newline at end of file diff --git a/core/startos/bindings/BindOptions.ts b/core/startos/bindings/BindOptions.ts new file mode 100644 index 000000000..029be6a16 --- /dev/null +++ b/core/startos/bindings/BindOptions.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddSslOptions } from "./AddSslOptions"; +import type { BindOptionsSecure } from "./BindOptionsSecure"; + +export interface BindOptions { scheme: string | null, preferredExternalPort: number, addSsl: AddSslOptions | null, secure: BindOptionsSecure | null, } \ No newline at end of file diff --git a/core/startos/bindings/BindOptionsSecure.ts b/core/startos/bindings/BindOptionsSecure.ts new file mode 100644 index 000000000..00bfd7dd2 --- /dev/null +++ b/core/startos/bindings/BindOptionsSecure.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface BindOptionsSecure { ssl: boolean, } \ No newline at end of file diff --git a/core/startos/bindings/BindParams.ts b/core/startos/bindings/BindParams.ts index 8d8eef6b4..f48f848b8 100644 --- a/core/startos/bindings/BindParams.ts +++ b/core/startos/bindings/BindParams.ts @@ -1,5 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AddSslOptions } from "./AddSslOptions"; import type { BindKind } from "./BindKind"; +import type { BindOptionsSecure } from "./BindOptionsSecure"; -export interface BindParams { kind: BindKind, id: string, internalPort: number, scheme: string, preferredExternalPort: number, addSsl: AddSslOptions | null, secure: boolean, ssl: boolean, } \ No newline at end of file +export interface BindParams { kind: BindKind, id: string, internalPort: number, scheme: string, preferredExternalPort: number, addSsl: AddSslOptions | null, secure: BindOptionsSecure | null, } \ No newline at end of file diff --git a/core/startos/bindings/CreateOverlayedImageParams.ts b/core/startos/bindings/CreateOverlayedImageParams.ts index 98a72b497..34924cb1a 100644 --- a/core/startos/bindings/CreateOverlayedImageParams.ts +++ b/core/startos/bindings/CreateOverlayedImageParams.ts @@ -1,4 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ImageId } from "./ImageId"; -export interface CreateOverlayedImageParams { imageId: ImageId, } \ No newline at end of file +export interface CreateOverlayedImageParams { imageId: string, } \ No newline at end of file diff --git a/core/startos/bindings/DependencyKind.ts b/core/startos/bindings/DependencyKind.ts new file mode 100644 index 000000000..018562971 --- /dev/null +++ b/core/startos/bindings/DependencyKind.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DependencyKind = "exists" | "running"; \ No newline at end of file diff --git a/core/startos/bindings/DependencyRequirement.ts b/core/startos/bindings/DependencyRequirement.ts new file mode 100644 index 000000000..0179c0e84 --- /dev/null +++ b/core/startos/bindings/DependencyRequirement.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DependencyKind } from "./DependencyKind"; + +export interface DependencyRequirement { id: string, kind: DependencyKind, healthChecks: string[], } \ No newline at end of file diff --git a/core/startos/bindings/DestroyOverlayedImageParams.ts b/core/startos/bindings/DestroyOverlayedImageParams.ts index aef910544..b875e45bd 100644 --- a/core/startos/bindings/DestroyOverlayedImageParams.ts +++ b/core/startos/bindings/DestroyOverlayedImageParams.ts @@ -1,4 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ImageId } from "./ImageId"; -export interface DestroyOverlayedImageParams { imageId: ImageId, guid: string, } \ No newline at end of file +export interface DestroyOverlayedImageParams { guid: string, } \ No newline at end of file diff --git a/core/startos/bindings/ExecuteAction.ts b/core/startos/bindings/ExecuteAction.ts index a92852147..d31ae9279 100644 --- a/core/startos/bindings/ExecuteAction.ts +++ b/core/startos/bindings/ExecuteAction.ts @@ -1,5 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ActionId } from "./ActionId"; -import type { PackageId } from "./PackageId"; -export interface ExecuteAction { serviceId: PackageId | null, actionId: ActionId, input: any, } \ No newline at end of file +export interface ExecuteAction { serviceId: string | null, actionId: string, input: any, } \ No newline at end of file diff --git a/core/startos/bindings/ExportActionParams.ts b/core/startos/bindings/ExportActionParams.ts new file mode 100644 index 000000000..413a63183 --- /dev/null +++ b/core/startos/bindings/ExportActionParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AllowedStatuses } from "./AllowedStatuses"; + +export interface ExportActionParams { name: string, description: string, id: string, input: {[key: string]: any}, allowedStatuses: AllowedStatuses, group: string | null, } \ No newline at end of file diff --git a/core/startos/bindings/ExportServiceInterfaceParams.ts b/core/startos/bindings/ExportServiceInterfaceParams.ts new file mode 100644 index 000000000..d30cce9db --- /dev/null +++ b/core/startos/bindings/ExportServiceInterfaceParams.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddressInfo } from "./AddressInfo"; +import type { ServiceInterfaceType } from "./ServiceInterfaceType"; + +export interface ExportServiceInterfaceParams { id: string, name: string, description: string, hasPrimary: boolean, disabled: boolean, masked: boolean, addressInfo: AddressInfo, type: ServiceInterfaceType, } \ No newline at end of file diff --git a/core/startos/bindings/ExposeUiParams.ts b/core/startos/bindings/ExposeUiParams.ts index dcf649285..3c2230060 100644 --- a/core/startos/bindings/ExposeUiParams.ts +++ b/core/startos/bindings/ExposeUiParams.ts @@ -1,4 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExposedUI } from "./ExposedUI"; -export interface ExposeUiParams { paths: Array, } \ No newline at end of file +export type ExposeUiParams = { "type": "object", value: {[key: string]: ExposeUiParams}, } | { "type": "string", path: string, description: string | null, masked: boolean, copyable: boolean | null, qr: boolean | null, }; \ No newline at end of file diff --git a/core/startos/bindings/ExposedUI.ts b/core/startos/bindings/ExposedUI.ts new file mode 100644 index 000000000..1354bb576 --- /dev/null +++ b/core/startos/bindings/ExposedUI.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface ExposedUI { path: string, title: string, description: string | null, masked: boolean | null, copyable: boolean | null, qr: boolean | null, } \ No newline at end of file diff --git a/core/startos/bindings/GetPrimaryUrlParams.ts b/core/startos/bindings/GetPrimaryUrlParams.ts new file mode 100644 index 000000000..40fe7f154 --- /dev/null +++ b/core/startos/bindings/GetPrimaryUrlParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Callback } from "./Callback"; + +export interface GetPrimaryUrlParams { packageId: string | null, serviceInterfaceId: string, callback: Callback, } \ No newline at end of file diff --git a/core/startos/bindings/GetServiceInterfaceParams.ts b/core/startos/bindings/GetServiceInterfaceParams.ts index e0e4d2477..fb0f96791 100644 --- a/core/startos/bindings/GetServiceInterfaceParams.ts +++ b/core/startos/bindings/GetServiceInterfaceParams.ts @@ -1,5 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Callback } from "./Callback"; -import type { PackageId } from "./PackageId"; -export interface GetServiceInterfaceParams { packageId: PackageId | null, serviceInterfaceId: string, callback: Callback, } \ No newline at end of file +export interface GetServiceInterfaceParams { packageId: string | null, serviceInterfaceId: string, callback: Callback, } \ No newline at end of file diff --git a/core/startos/bindings/GetServicePortForwardParams.ts b/core/startos/bindings/GetServicePortForwardParams.ts new file mode 100644 index 000000000..270fcb709 --- /dev/null +++ b/core/startos/bindings/GetServicePortForwardParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface GetServicePortForwardParams { packageId: string | null, internalPort: number, } \ No newline at end of file diff --git a/core/startos/bindings/GetStoreParams.ts b/core/startos/bindings/GetStoreParams.ts index f2087832a..65703be7a 100644 --- a/core/startos/bindings/GetStoreParams.ts +++ b/core/startos/bindings/GetStoreParams.ts @@ -1,4 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PackageId } from "./PackageId"; -export interface GetStoreParams { packageId: PackageId | null, path: string, } \ No newline at end of file +export interface GetStoreParams { packageId: string | null, path: string, } \ No newline at end of file diff --git a/core/startos/bindings/GetSystemSmtpParams.ts b/core/startos/bindings/GetSystemSmtpParams.ts new file mode 100644 index 000000000..617fd7fa0 --- /dev/null +++ b/core/startos/bindings/GetSystemSmtpParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Callback } from "./Callback"; + +export interface GetSystemSmtpParams { callback: Callback, } \ No newline at end of file diff --git a/core/startos/bindings/HealthCheckString.ts b/core/startos/bindings/HealthCheckString.ts new file mode 100644 index 000000000..efe9f42bd --- /dev/null +++ b/core/startos/bindings/HealthCheckString.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HealthCheckString = "passing" | "disabled" | "starting" | "warning" | "failure"; \ No newline at end of file diff --git a/core/startos/bindings/ListServiceInterfacesParams.ts b/core/startos/bindings/ListServiceInterfacesParams.ts new file mode 100644 index 000000000..33db3f129 --- /dev/null +++ b/core/startos/bindings/ListServiceInterfacesParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Callback } from "./Callback"; + +export interface ListServiceInterfacesParams { packageId: string | null, callback: Callback, } \ No newline at end of file diff --git a/core/startos/bindings/MountParams.ts b/core/startos/bindings/MountParams.ts new file mode 100644 index 000000000..26e8373c7 --- /dev/null +++ b/core/startos/bindings/MountParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { MountTarget } from "./MountTarget"; + +export interface MountParams { location: string, target: MountTarget, } \ No newline at end of file diff --git a/core/startos/bindings/MountTarget.ts b/core/startos/bindings/MountTarget.ts new file mode 100644 index 000000000..41c026deb --- /dev/null +++ b/core/startos/bindings/MountTarget.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface MountTarget { packageId: string, volumeId: string, path: string, readonly: boolean, } \ No newline at end of file diff --git a/core/startos/bindings/ParamsMaybePackageId.ts b/core/startos/bindings/ParamsMaybePackageId.ts index 9a2e46c0b..80bdd9a55 100644 --- a/core/startos/bindings/ParamsMaybePackageId.ts +++ b/core/startos/bindings/ParamsMaybePackageId.ts @@ -1,4 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PackageId } from "./PackageId"; -export interface ParamsMaybePackageId { packageId: PackageId | null, } \ No newline at end of file +export interface ParamsMaybePackageId { packageId: string | null, } \ No newline at end of file diff --git a/core/startos/bindings/ParamsPackageId.ts b/core/startos/bindings/ParamsPackageId.ts index 784f268f5..7631cfb11 100644 --- a/core/startos/bindings/ParamsPackageId.ts +++ b/core/startos/bindings/ParamsPackageId.ts @@ -1,4 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PackageId } from "./PackageId"; -export interface ParamsPackageId { packageId: PackageId, } \ No newline at end of file +export interface ParamsPackageId { packageId: string, } \ No newline at end of file diff --git a/core/startos/bindings/RemoveActionParams.ts b/core/startos/bindings/RemoveActionParams.ts new file mode 100644 index 000000000..3e0c3c48d --- /dev/null +++ b/core/startos/bindings/RemoveActionParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface RemoveActionParams { id: string, } \ No newline at end of file diff --git a/core/startos/bindings/RemoveAddressParams.ts b/core/startos/bindings/RemoveAddressParams.ts new file mode 100644 index 000000000..1bc483f89 --- /dev/null +++ b/core/startos/bindings/RemoveAddressParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface RemoveAddressParams { id: string, } \ No newline at end of file diff --git a/core/startos/bindings/ReverseProxyBind.ts b/core/startos/bindings/ReverseProxyBind.ts new file mode 100644 index 000000000..3f9e0dc25 --- /dev/null +++ b/core/startos/bindings/ReverseProxyBind.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface ReverseProxyBind { ip: string | null, port: number, ssl: boolean, } \ No newline at end of file diff --git a/core/startos/bindings/ReverseProxyDestination.ts b/core/startos/bindings/ReverseProxyDestination.ts new file mode 100644 index 000000000..a6003e189 --- /dev/null +++ b/core/startos/bindings/ReverseProxyDestination.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface ReverseProxyDestination { ip: string | null, port: number, ssl: boolean, } \ No newline at end of file diff --git a/core/startos/bindings/ReverseProxyHttp.ts b/core/startos/bindings/ReverseProxyHttp.ts new file mode 100644 index 000000000..5c1d0aa1b --- /dev/null +++ b/core/startos/bindings/ReverseProxyHttp.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export interface ReverseProxyHttp { headers: null | {[key: string]: string}, } \ No newline at end of file diff --git a/core/startos/bindings/ReverseProxyParams.ts b/core/startos/bindings/ReverseProxyParams.ts new file mode 100644 index 000000000..bde3034e9 --- /dev/null +++ b/core/startos/bindings/ReverseProxyParams.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReverseProxyBind } from "./ReverseProxyBind"; +import type { ReverseProxyDestination } from "./ReverseProxyDestination"; +import type { ReverseProxyHttp } from "./ReverseProxyHttp"; + +export interface ReverseProxyParams { bind: ReverseProxyBind, dst: ReverseProxyDestination, http: ReverseProxyHttp, } \ No newline at end of file diff --git a/core/startos/bindings/ServiceInterfaceType.ts b/core/startos/bindings/ServiceInterfaceType.ts new file mode 100644 index 000000000..74372d4ad --- /dev/null +++ b/core/startos/bindings/ServiceInterfaceType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ServiceInterfaceType = "ui" | "p2p" | "api"; \ No newline at end of file diff --git a/core/startos/bindings/SetDependenciesParams.ts b/core/startos/bindings/SetDependenciesParams.ts new file mode 100644 index 000000000..7741cddde --- /dev/null +++ b/core/startos/bindings/SetDependenciesParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DependencyRequirement } from "./DependencyRequirement"; + +export interface SetDependenciesParams { dependencies: Array, } \ No newline at end of file diff --git a/core/startos/bindings/SetHealth.ts b/core/startos/bindings/SetHealth.ts index d4c202067..3635ec9c5 100644 --- a/core/startos/bindings/SetHealth.ts +++ b/core/startos/bindings/SetHealth.ts @@ -1,5 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HealthCheckId } from "./HealthCheckId"; import type { HealthCheckString } from "./HealthCheckString"; -export interface SetHealth { name: HealthCheckId, status: HealthCheckString, message: string | null, } \ No newline at end of file +export interface SetHealth { name: string, status: HealthCheckString, message: string | null, } \ No newline at end of file diff --git a/core/startos/src/backup/backup_bulk.rs b/core/startos/src/backup/backup_bulk.rs index 4f633d7ae..8c609f672 100644 --- a/core/startos/src/backup/backup_bulk.rs +++ b/core/startos/src/backup/backup_bulk.rs @@ -18,7 +18,8 @@ use crate::auth::check_password_against_db; use crate::backup::os::OsBackup; use crate::backup::{BackupReport, ServerBackupReport}; use crate::context::RpcContext; -use crate::db::model::{BackupProgress, DatabaseModel}; +use crate::db::model::public::BackupProgress; +use crate::db::model::DatabaseModel; use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; @@ -174,7 +175,7 @@ pub async fn backup_all( .as_package_data() .as_entries()? .into_iter() - .filter(|(_, m)| m.expect_as_installed().is_ok()) + .filter(|(_, m)| m.as_state_info().expect_installed().is_ok()) .map(|(id, _)| id) .collect() }; diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index 0f11bf63f..f987ffbf4 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -19,7 +19,7 @@ use super::setup::CURRENT_SECRET; use crate::account::AccountInfo; use crate::context::config::ServerConfig; use crate::core::rpc_continuations::{RequestGuid, RestHandler, RpcContinuation, WebSocketHandler}; -use crate::db::model::CurrentDependents; +use crate::db::model::package::CurrentDependents; use crate::db::prelude::PatchDbExt; use crate::dependencies::compute_dependency_config_errs; use crate::disk::OsPartitionInfo; @@ -219,12 +219,7 @@ impl RpcContext { for (package_id, package) in f.as_public_mut().as_package_data_mut().as_entries_mut()? { - for (k, v) in package - .as_installed_mut() - .into_iter() - .flat_map(|i| i.clone().into_current_dependencies().into_entries()) - .flatten() - { + for (k, v) in package.clone().into_current_dependencies().into_entries()? { let mut entry: BTreeMap<_, _> = current_dependents.remove(&k).unwrap_or_default(); entry.insert(package_id.clone(), v.de()?); @@ -236,16 +231,7 @@ impl RpcContext { .as_public_mut() .as_package_data_mut() .as_idx_mut(&package_id) - .and_then(|pde| pde.expect_as_installed_mut().ok()) - .map(|i| i.as_installed_mut().as_current_dependents_mut()) - { - deps.ser(&CurrentDependents(current_dependents))?; - } else if let Some(deps) = f - .as_public_mut() - .as_package_data_mut() - .as_idx_mut(&package_id) - .and_then(|pde| pde.expect_as_removing_mut().ok()) - .map(|i| i.as_removing_mut().as_current_dependents_mut()) + .map(|i| i.as_current_dependents_mut()) { deps.ser(&CurrentDependents(current_dependents))?; } @@ -261,23 +247,18 @@ impl RpcContext { let peek = self.db.peek().await; for (package_id, package) in peek.as_public().as_package_data().as_entries()?.into_iter() { let package = package.clone(); - if let Some(current_dependencies) = package - .as_installed() - .and_then(|x| x.as_current_dependencies().de().ok()) - { - let manifest = package.as_manifest().de()?; - all_dependency_config_errs.insert( - package_id.clone(), - compute_dependency_config_errs( - self, - &peek, - &manifest, - ¤t_dependencies, - &Default::default(), - ) - .await?, - ); - } + let current_dependencies = package.as_current_dependencies().de()?; + all_dependency_config_errs.insert( + package_id.clone(), + compute_dependency_config_errs( + self, + &peek, + &package_id, + ¤t_dependencies, + &Default::default(), + ) + .await?, + ); } self.db .mutate(|v| { @@ -286,7 +267,6 @@ impl RpcContext { .as_public_mut() .as_package_data_mut() .as_idx_mut(&package_id) - .and_then(|pde| pde.as_installed_mut()) .map(|i| i.as_status_mut().as_dependency_config_errors_mut()) { config_errors.ser(&errs)?; diff --git a/core/startos/src/db/model.rs b/core/startos/src/db/model.rs deleted file mode 100644 index d3dd9db74..000000000 --- a/core/startos/src/db/model.rs +++ /dev/null @@ -1,626 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::net::{Ipv4Addr, Ipv6Addr}; - -use chrono::{DateTime, Utc}; -use emver::VersionRange; -use imbl_value::InternedString; -use ipnet::{Ipv4Net, Ipv6Net}; -use isocountry::CountryCode; -use itertools::Itertools; -use models::{DataUrl, HealthCheckId, HostId, PackageId}; -use openssl::hash::MessageDigest; -use patch_db::json_ptr::JsonPointer; -use patch_db::{HasModel, Value}; -use reqwest::Url; -use serde::{Deserialize, Serialize}; -use torut::onion::OnionAddressV3; - -use crate::account::AccountInfo; -use crate::auth::Sessions; -use crate::backup::target::cifs::CifsTargets; -use crate::net::forward::AvailablePorts; -use crate::net::host::HostInfo; -use crate::net::keys::KeyStore; -use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr}; -use crate::notifications::Notifications; -use crate::prelude::*; -use crate::progress::FullProgress; -use crate::s9pk::manifest::Manifest; -use crate::ssh::SshKeys; -use crate::status::Status; -use crate::util::cpupower::Governor; -use crate::util::serde::Pem; -use crate::util::Version; -use crate::version::{Current, VersionT}; -use crate::{ARCH, PLATFORM}; - -fn get_arch() -> InternedString { - (*ARCH).into() -} - -fn get_platform() -> InternedString { - (&*PLATFORM).into() -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct Database { - pub public: Public, - pub private: Private, -} -impl Database { - pub fn init(account: &AccountInfo) -> Result { - let lan_address = account.hostname.lan_address().parse().unwrap(); - Ok(Database { - public: Public { - server_info: ServerInfo { - arch: get_arch(), - platform: get_platform(), - id: account.server_id.clone(), - version: Current::new().semver().into(), - hostname: account.hostname.no_dot_host_name(), - last_backup: None, - last_wifi_region: None, - eos_version_compat: Current::new().compat().clone(), - lan_address, - onion_address: account.tor_key.public().get_onion_address(), - tor_address: format!( - "https://{}", - account.tor_key.public().get_onion_address() - ) - .parse() - .unwrap(), - ip_info: BTreeMap::new(), - status_info: ServerStatus { - backup_progress: None, - updated: false, - update_progress: None, - shutting_down: false, - restarting: false, - }, - wifi: WifiInfo { - ssids: Vec::new(), - connected: None, - selected: None, - }, - unread_notification_count: 0, - connection_addresses: ConnectionAddresses { - tor: Vec::new(), - clearnet: Vec::new(), - }, - password_hash: account.password.clone(), - pubkey: ssh_key::PublicKey::from(&account.ssh_key) - .to_openssh() - .unwrap(), - ca_fingerprint: account - .root_ca_cert - .digest(MessageDigest::sha256()) - .unwrap() - .iter() - .map(|x| format!("{x:X}")) - .join(":"), - ntp_synced: false, - zram: true, - governor: None, - }, - package_data: AllPackageData::default(), - ui: serde_json::from_str(include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../../web/patchdb-ui-seed.json" - ))) - .unwrap(), - }, - private: Private { - key_store: KeyStore::new(account)?, - password: account.password.clone(), - ssh_privkey: Pem(account.ssh_key.clone()), - ssh_pubkeys: SshKeys::new(), - available_ports: AvailablePorts::new(), - sessions: Sessions::new(), - notifications: Notifications::new(), - cifs: CifsTargets::new(), - package_stores: BTreeMap::new(), - }, // TODO - }) - } -} - -pub type DatabaseModel = Model; - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -// #[macro_debug] -pub struct Public { - pub server_info: ServerInfo, - pub package_data: AllPackageData, - pub ui: Value, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct Private { - pub key_store: KeyStore, - pub password: String, // argon2 hash - pub ssh_privkey: Pem, - pub ssh_pubkeys: SshKeys, - pub available_ports: AvailablePorts, - pub sessions: Sessions, - pub notifications: Notifications, - pub cifs: CifsTargets, - #[serde(default)] - pub package_stores: BTreeMap, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct ServerInfo { - #[serde(default = "get_arch")] - pub arch: InternedString, - #[serde(default = "get_platform")] - pub platform: InternedString, - pub id: String, - pub hostname: String, - pub version: Version, - pub last_backup: Option>, - /// Used in the wifi to determine the region to set the system to - pub last_wifi_region: Option, - pub eos_version_compat: VersionRange, - pub lan_address: Url, - pub onion_address: OnionAddressV3, - /// for backwards compatibility - pub tor_address: Url, - pub ip_info: BTreeMap, - #[serde(default)] - pub status_info: ServerStatus, - pub wifi: WifiInfo, - pub unread_notification_count: u64, - pub connection_addresses: ConnectionAddresses, - pub password_hash: String, - pub pubkey: String, - pub ca_fingerprint: String, - #[serde(default)] - pub ntp_synced: bool, - #[serde(default)] - pub zram: bool, - pub governor: Option, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct IpInfo { - pub ipv4_range: Option, - pub ipv4: Option, - pub ipv6_range: Option, - pub ipv6: Option, -} -impl IpInfo { - pub async fn for_interface(iface: &str) -> Result { - let (ipv4, ipv4_range) = get_iface_ipv4_addr(iface).await?.unzip(); - let (ipv6, ipv6_range) = get_iface_ipv6_addr(iface).await?.unzip(); - Ok(Self { - ipv4_range, - ipv4, - ipv6_range, - ipv6, - }) - } -} - -#[derive(Debug, Default, Deserialize, Serialize, HasModel)] -#[model = "Model"] -pub struct BackupProgress { - pub complete: bool, -} - -#[derive(Debug, Default, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct ServerStatus { - pub backup_progress: Option>, - pub updated: bool, - pub update_progress: Option, - #[serde(default)] - pub shutting_down: bool, - #[serde(default)] - pub restarting: bool, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct UpdateProgress { - pub size: Option, - pub downloaded: u64, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct WifiInfo { - pub ssids: Vec, - pub selected: Option, - pub connected: Option, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct ServerSpecs { - pub cpu: String, - pub disk: String, - pub memory: String, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct ConnectionAddresses { - pub tor: Vec, - pub clearnet: Vec, -} - -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct AllPackageData(pub BTreeMap); -impl Map for AllPackageData { - type Key = PackageId; - type Value = PackageDataEntry; - fn key_str(key: &Self::Key) -> Result, Error> { - Ok(key) - } - fn key_string(key: &Self::Key) -> Result { - Ok(key.clone().into()) - } -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct StaticFiles { - license: String, - instructions: String, - icon: DataUrl<'static>, -} -impl StaticFiles { - pub fn local(id: &PackageId, version: &Version, icon: DataUrl<'static>) -> Self { - StaticFiles { - license: format!("/public/package-data/{}/{}/LICENSE.md", id, version), - instructions: format!("/public/package-data/{}/{}/INSTRUCTIONS.md", id, version), - icon, - } - } -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryInstalling { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub install_progress: FullProgress, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryUpdating { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub installed: InstalledPackageInfo, - pub install_progress: FullProgress, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryRestoring { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub install_progress: FullProgress, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryRemoving { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub removing: InstalledPackageInfo, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct PackageDataEntryInstalled { - pub static_files: StaticFiles, - pub manifest: Manifest, - pub installed: InstalledPackageInfo, -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(tag = "state")] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -// #[macro_debug] -pub enum PackageDataEntry { - Installing(PackageDataEntryInstalling), - Updating(PackageDataEntryUpdating), - Restoring(PackageDataEntryRestoring), - Removing(PackageDataEntryRemoving), - Installed(PackageDataEntryInstalled), -} -impl Model { - pub fn expect_into_installed(self) -> Result, Error> { - if let PackageDataEntryMatchModel::Installed(a) = self.into_match() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in installed state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_installed(&self) -> Result<&Model, Error> { - if let PackageDataEntryMatchModelRef::Installed(a) = self.as_match() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in installed state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_installed_mut( - &mut self, - ) -> Result<&mut Model, Error> { - if let PackageDataEntryMatchModelMut::Installed(a) = self.as_match_mut() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in installed state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_into_removing(self) -> Result, Error> { - if let PackageDataEntryMatchModel::Removing(a) = self.into_match() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in removing state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_removing(&self) -> Result<&Model, Error> { - if let PackageDataEntryMatchModelRef::Removing(a) = self.as_match() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in removing state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_removing_mut( - &mut self, - ) -> Result<&mut Model, Error> { - if let PackageDataEntryMatchModelMut::Removing(a) = self.as_match_mut() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in removing state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn expect_as_installing_mut( - &mut self, - ) -> Result<&mut Model, Error> { - if let PackageDataEntryMatchModelMut::Installing(a) = self.as_match_mut() { - Ok(a) - } else { - Err(Error::new( - eyre!("package is not in installing state"), - ErrorKind::InvalidRequest, - )) - } - } - pub fn into_manifest(self) -> Model { - match self.into_match() { - PackageDataEntryMatchModel::Installing(a) => a.into_manifest(), - PackageDataEntryMatchModel::Updating(a) => a.into_installed().into_manifest(), - PackageDataEntryMatchModel::Restoring(a) => a.into_manifest(), - PackageDataEntryMatchModel::Removing(a) => a.into_manifest(), - PackageDataEntryMatchModel::Installed(a) => a.into_manifest(), - PackageDataEntryMatchModel::Error(_) => Model::from(Value::Null), - } - } - pub fn as_manifest(&self) -> &Model { - match self.as_match() { - PackageDataEntryMatchModelRef::Installing(a) => a.as_manifest(), - PackageDataEntryMatchModelRef::Updating(a) => a.as_installed().as_manifest(), - PackageDataEntryMatchModelRef::Restoring(a) => a.as_manifest(), - PackageDataEntryMatchModelRef::Removing(a) => a.as_manifest(), - PackageDataEntryMatchModelRef::Installed(a) => a.as_manifest(), - PackageDataEntryMatchModelRef::Error(_) => (&Value::Null).into(), - } - } - pub fn into_installed(self) -> Option> { - match self.into_match() { - PackageDataEntryMatchModel::Installing(_) => None, - PackageDataEntryMatchModel::Updating(a) => Some(a.into_installed()), - PackageDataEntryMatchModel::Restoring(_) => None, - PackageDataEntryMatchModel::Removing(_) => None, - PackageDataEntryMatchModel::Installed(a) => Some(a.into_installed()), - PackageDataEntryMatchModel::Error(_) => None, - } - } - pub fn as_installed(&self) -> Option<&Model> { - match self.as_match() { - PackageDataEntryMatchModelRef::Installing(_) => None, - PackageDataEntryMatchModelRef::Updating(a) => Some(a.as_installed()), - PackageDataEntryMatchModelRef::Restoring(_) => None, - PackageDataEntryMatchModelRef::Removing(_) => None, - PackageDataEntryMatchModelRef::Installed(a) => Some(a.as_installed()), - PackageDataEntryMatchModelRef::Error(_) => None, - } - } - pub fn as_installed_mut(&mut self) -> Option<&mut Model> { - match self.as_match_mut() { - PackageDataEntryMatchModelMut::Installing(_) => None, - PackageDataEntryMatchModelMut::Updating(a) => Some(a.as_installed_mut()), - PackageDataEntryMatchModelMut::Restoring(_) => None, - PackageDataEntryMatchModelMut::Removing(_) => None, - PackageDataEntryMatchModelMut::Installed(a) => Some(a.as_installed_mut()), - PackageDataEntryMatchModelMut::Error(_) => None, - } - } - pub fn as_install_progress(&self) -> Option<&Model> { - match self.as_match() { - PackageDataEntryMatchModelRef::Installing(a) => Some(a.as_install_progress()), - PackageDataEntryMatchModelRef::Updating(a) => Some(a.as_install_progress()), - PackageDataEntryMatchModelRef::Restoring(a) => Some(a.as_install_progress()), - PackageDataEntryMatchModelRef::Removing(_) => None, - PackageDataEntryMatchModelRef::Installed(_) => None, - PackageDataEntryMatchModelRef::Error(_) => None, - } - } - pub fn as_install_progress_mut(&mut self) -> Option<&mut Model> { - match self.as_match_mut() { - PackageDataEntryMatchModelMut::Installing(a) => Some(a.as_install_progress_mut()), - PackageDataEntryMatchModelMut::Updating(a) => Some(a.as_install_progress_mut()), - PackageDataEntryMatchModelMut::Restoring(a) => Some(a.as_install_progress_mut()), - PackageDataEntryMatchModelMut::Removing(_) => None, - PackageDataEntryMatchModelMut::Installed(_) => None, - PackageDataEntryMatchModelMut::Error(_) => None, - } - } -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct InstalledPackageInfo { - pub status: Status, - pub marketplace_url: Option, - #[serde(default)] - #[serde(with = "crate::util::serde::ed25519_pubkey")] - pub developer_key: ed25519_dalek::VerifyingKey, - pub manifest: Manifest, - pub last_backup: Option>, - pub dependency_info: BTreeMap, - pub current_dependents: CurrentDependents, - pub current_dependencies: CurrentDependencies, - pub interface_addresses: InterfaceAddressMap, - pub hosts: HostInfo, - pub store_exposed_ui: Vec, - pub store_exposed_dependents: Vec, -} -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[model = "Model"] -pub struct ExposedDependent { - path: String, - title: String, - description: Option, - masked: Option, - copyable: Option, - qr: Option, -} -#[derive(Clone, Debug, Deserialize, Serialize, HasModel, ts_rs::TS)] -#[model = "Model"] -pub struct ExposedUI { - #[ts(type = "string")] - pub path: JsonPointer, - pub title: String, - pub description: Option, - pub masked: Option, - pub copyable: Option, - pub qr: Option, -} - -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -pub struct CurrentDependents(pub BTreeMap); -impl CurrentDependents { - pub fn map( - mut self, - transform: impl Fn( - BTreeMap, - ) -> BTreeMap, - ) -> Self { - self.0 = transform(self.0); - self - } -} -impl Map for CurrentDependents { - type Key = PackageId; - type Value = CurrentDependencyInfo; - fn key_str(key: &Self::Key) -> Result, Error> { - Ok(key) - } - fn key_string(key: &Self::Key) -> Result { - Ok(key.clone().into()) - } -} -#[derive(Debug, Clone, Default, Deserialize, Serialize)] -pub struct CurrentDependencies(pub BTreeMap); -impl CurrentDependencies { - pub fn map( - mut self, - transform: impl Fn( - BTreeMap, - ) -> BTreeMap, - ) -> Self { - self.0 = transform(self.0); - self - } -} -impl Map for CurrentDependencies { - type Key = PackageId; - type Value = CurrentDependencyInfo; - fn key_str(key: &Self::Key) -> Result, Error> { - Ok(key) - } - fn key_string(key: &Self::Key) -> Result { - Ok(key.clone().into()) - } -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct StaticDependencyInfo { - pub title: String, - pub icon: DataUrl<'static>, -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct CurrentDependencyInfo { - #[serde(default)] - pub health_checks: BTreeSet, -} - -#[derive(Debug, Default, Deserialize, Serialize)] -pub struct InterfaceAddressMap(pub BTreeMap); -impl Map for InterfaceAddressMap { - type Key = HostId; - type Value = InterfaceAddresses; - fn key_str(key: &Self::Key) -> Result, Error> { - Ok(key) - } - fn key_string(key: &Self::Key) -> Result { - Ok(key.clone().into()) - } -} - -#[derive(Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct InterfaceAddresses { - pub tor_address: Option, - pub lan_address: Option, -} diff --git a/core/startos/src/db/model/mod.rs b/core/startos/src/db/model/mod.rs new file mode 100644 index 000000000..24497ed4e --- /dev/null +++ b/core/startos/src/db/model/mod.rs @@ -0,0 +1,48 @@ +use std::collections::BTreeMap; + +use patch_db::HasModel; +use serde::{Deserialize, Serialize}; + +use crate::account::AccountInfo; +use crate::auth::Sessions; +use crate::backup::target::cifs::CifsTargets; +use crate::db::model::private::Private; +use crate::db::model::public::Public; +use crate::net::forward::AvailablePorts; +use crate::net::keys::KeyStore; +use crate::notifications::Notifications; +use crate::prelude::*; +use crate::ssh::SshKeys; +use crate::util::serde::Pem; + +pub mod package; +pub mod private; +pub mod public; + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct Database { + pub public: Public, + pub private: Private, +} +impl Database { + pub fn init(account: &AccountInfo) -> Result { + Ok(Self { + public: Public::init(account)?, + private: Private { + key_store: KeyStore::new(account)?, + password: account.password.clone(), + ssh_privkey: Pem(account.ssh_key.clone()), + ssh_pubkeys: SshKeys::new(), + available_ports: AvailablePorts::new(), + sessions: Sessions::new(), + notifications: Notifications::new(), + cifs: CifsTargets::new(), + package_stores: BTreeMap::new(), + }, // TODO + }) + } +} + +pub type DatabaseModel = Model; diff --git a/core/startos/src/db/model/package.rs b/core/startos/src/db/model/package.rs new file mode 100644 index 000000000..cb4e3a255 --- /dev/null +++ b/core/startos/src/db/model/package.rs @@ -0,0 +1,424 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use chrono::{DateTime, Utc}; +use imbl_value::InternedString; +use models::{DataUrl, HealthCheckId, HostId, PackageId}; +use patch_db::json_ptr::JsonPointer; +use patch_db::HasModel; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::net::host::HostInfo; +use crate::prelude::*; +use crate::progress::FullProgress; +use crate::s9pk::manifest::Manifest; +use crate::status::Status; + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct AllPackageData(pub BTreeMap); +impl Map for AllPackageData { + type Key = PackageId; + type Value = PackageDataEntry; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone().into()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum ManifestPreference { + Old, + New, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "state")] +#[model = "Model"] +pub enum PackageState { + Installing(InstallingState), + Restoring(InstallingState), + Updating(UpdatingState), + Installed(InstalledState), + Removing(InstalledState), +} +impl PackageState { + pub fn expect_installed(&self) -> Result<&InstalledState, Error> { + match self { + Self::Installed(a) => Ok(a), + a => Err(Error::new( + eyre!( + "Package {} is not in installed state", + self.as_manifest(ManifestPreference::Old).id + ), + ErrorKind::InvalidRequest, + )), + } + } + pub fn into_installing_info(self) -> Option { + match self { + Self::Installing(InstallingState { installing_info }) + | Self::Restoring(InstallingState { installing_info }) => Some(installing_info), + Self::Updating(UpdatingState { + installing_info, .. + }) => Some(installing_info), + Self::Installed(_) | Self::Removing(_) => None, + } + } + pub fn as_installing_info(&self) -> Option<&InstallingInfo> { + match self { + Self::Installing(InstallingState { installing_info }) + | Self::Restoring(InstallingState { installing_info }) => Some(installing_info), + Self::Updating(UpdatingState { + installing_info, .. + }) => Some(installing_info), + Self::Installed(_) | Self::Removing(_) => None, + } + } + pub fn as_installing_info_mut(&mut self) -> Option<&mut InstallingInfo> { + match self { + Self::Installing(InstallingState { installing_info }) + | Self::Restoring(InstallingState { installing_info }) => Some(installing_info), + Self::Updating(UpdatingState { + installing_info, .. + }) => Some(installing_info), + Self::Installed(_) | Self::Removing(_) => None, + } + } + pub fn into_manifest(self, preference: ManifestPreference) -> Manifest { + match self { + Self::Installing(InstallingState { + installing_info: InstallingInfo { new_manifest, .. }, + }) + | Self::Restoring(InstallingState { + installing_info: InstallingInfo { new_manifest, .. }, + }) => new_manifest, + Self::Updating(UpdatingState { manifest, .. }) + if preference == ManifestPreference::Old => + { + manifest + } + Self::Updating(UpdatingState { + installing_info: InstallingInfo { new_manifest, .. }, + .. + }) => new_manifest, + Self::Installed(InstalledState { manifest }) + | Self::Removing(InstalledState { manifest }) => manifest, + } + } + pub fn as_manifest(&self, preference: ManifestPreference) -> &Manifest { + match self { + Self::Installing(InstallingState { + installing_info: InstallingInfo { new_manifest, .. }, + }) + | Self::Restoring(InstallingState { + installing_info: InstallingInfo { new_manifest, .. }, + }) => new_manifest, + Self::Updating(UpdatingState { manifest, .. }) + if preference == ManifestPreference::Old => + { + manifest + } + Self::Updating(UpdatingState { + installing_info: InstallingInfo { new_manifest, .. }, + .. + }) => new_manifest, + Self::Installed(InstalledState { manifest }) + | Self::Removing(InstalledState { manifest }) => manifest, + } + } + pub fn as_manifest_mut(&mut self, preference: ManifestPreference) -> &mut Manifest { + match self { + Self::Installing(InstallingState { + installing_info: InstallingInfo { new_manifest, .. }, + }) + | Self::Restoring(InstallingState { + installing_info: InstallingInfo { new_manifest, .. }, + }) => new_manifest, + Self::Updating(UpdatingState { manifest, .. }) + if preference == ManifestPreference::Old => + { + manifest + } + Self::Updating(UpdatingState { + installing_info: InstallingInfo { new_manifest, .. }, + .. + }) => new_manifest, + Self::Installed(InstalledState { manifest }) + | Self::Removing(InstalledState { manifest }) => manifest, + } + } +} +impl Model { + pub fn expect_installed(&self) -> Result<&Model, Error> { + match self.as_match() { + PackageStateMatchModelRef::Installed(a) => Ok(a), + a => Err(Error::new( + eyre!( + "Package {} is not in installed state", + self.as_manifest(ManifestPreference::Old).as_id().de()? + ), + ErrorKind::InvalidRequest, + )), + } + } + pub fn into_installing_info(self) -> Option> { + match self.into_match() { + PackageStateMatchModel::Installing(s) | PackageStateMatchModel::Restoring(s) => { + Some(s.into_installing_info()) + } + PackageStateMatchModel::Updating(s) => Some(s.into_installing_info()), + PackageStateMatchModel::Installed(_) | PackageStateMatchModel::Removing(_) => None, + PackageStateMatchModel::Error(_) => None, + } + } + pub fn as_installing_info(&self) -> Option<&Model> { + match self.as_match() { + PackageStateMatchModelRef::Installing(s) | PackageStateMatchModelRef::Restoring(s) => { + Some(s.as_installing_info()) + } + PackageStateMatchModelRef::Updating(s) => Some(s.as_installing_info()), + PackageStateMatchModelRef::Installed(_) | PackageStateMatchModelRef::Removing(_) => { + None + } + PackageStateMatchModelRef::Error(_) => None, + } + } + pub fn as_installing_info_mut(&mut self) -> Option<&mut Model> { + match self.as_match_mut() { + PackageStateMatchModelMut::Installing(s) | PackageStateMatchModelMut::Restoring(s) => { + Some(s.as_installing_info_mut()) + } + PackageStateMatchModelMut::Updating(s) => Some(s.as_installing_info_mut()), + PackageStateMatchModelMut::Installed(_) | PackageStateMatchModelMut::Removing(_) => { + None + } + PackageStateMatchModelMut::Error(_) => None, + } + } + pub fn into_manifest(self, preference: ManifestPreference) -> Model { + match self.into_match() { + PackageStateMatchModel::Installing(s) | PackageStateMatchModel::Restoring(s) => { + s.into_installing_info().into_new_manifest() + } + PackageStateMatchModel::Updating(s) if preference == ManifestPreference::Old => { + s.into_manifest() + } + PackageStateMatchModel::Updating(s) => s.into_installing_info().into_new_manifest(), + PackageStateMatchModel::Installed(s) | PackageStateMatchModel::Removing(s) => { + s.into_manifest() + } + PackageStateMatchModel::Error(_) => Value::Null.into(), + } + } + pub fn as_manifest(&self, preference: ManifestPreference) -> &Model { + match self.as_match() { + PackageStateMatchModelRef::Installing(s) | PackageStateMatchModelRef::Restoring(s) => { + s.as_installing_info().as_new_manifest() + } + PackageStateMatchModelRef::Updating(s) if preference == ManifestPreference::Old => { + s.as_manifest() + } + PackageStateMatchModelRef::Updating(s) => s.as_installing_info().as_new_manifest(), + PackageStateMatchModelRef::Installed(s) | PackageStateMatchModelRef::Removing(s) => { + s.as_manifest() + } + PackageStateMatchModelRef::Error(_) => (&Value::Null).into(), + } + } + pub fn as_manifest_mut( + &mut self, + preference: ManifestPreference, + ) -> Result<&mut Model, Error> { + Ok(match self.as_match_mut() { + PackageStateMatchModelMut::Installing(s) | PackageStateMatchModelMut::Restoring(s) => { + s.as_installing_info_mut().as_new_manifest_mut() + } + PackageStateMatchModelMut::Updating(s) if preference == ManifestPreference::Old => { + s.as_manifest_mut() + } + PackageStateMatchModelMut::Updating(s) => { + s.as_installing_info_mut().as_new_manifest_mut() + } + PackageStateMatchModelMut::Installed(s) | PackageStateMatchModelMut::Removing(s) => { + s.as_manifest_mut() + } + PackageStateMatchModelMut::Error(s) => { + return Err(Error::new( + eyre!("could not determine package state to get manifest"), + ErrorKind::Database, + )) + } + }) + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct InstallingState { + pub installing_info: InstallingInfo, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct UpdatingState { + pub manifest: Manifest, + pub installing_info: InstallingInfo, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct InstalledState { + pub manifest: Manifest, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct InstallingInfo { + pub new_manifest: Manifest, + pub progress: FullProgress, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct PackageDataEntry { + pub state_info: PackageState, + pub status: Status, + pub marketplace_url: Option, + #[serde(default)] + #[serde(with = "crate::util::serde::ed25519_pubkey")] + pub developer_key: ed25519_dalek::VerifyingKey, + pub icon: DataUrl<'static>, + pub last_backup: Option>, + pub dependency_info: BTreeMap, + pub current_dependents: CurrentDependents, + pub current_dependencies: CurrentDependencies, + pub interface_addresses: InterfaceAddressMap, + pub hosts: HostInfo, + pub store_exposed_ui: Vec, + pub store_exposed_dependents: Vec, +} +impl AsRef for PackageDataEntry { + fn as_ref(&self) -> &PackageDataEntry { + self + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[model = "Model"] +pub struct ExposedDependent { + path: String, + title: String, + description: Option, + masked: Option, + copyable: Option, + qr: Option, +} +#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)] +#[model = "Model"] +#[ts(export)] +pub struct ExposedUI { + #[ts(type = "string")] + pub path: JsonPointer, + pub title: String, + pub description: Option, + pub masked: Option, + pub copyable: Option, + pub qr: Option, +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct CurrentDependents(pub BTreeMap); +impl CurrentDependents { + pub fn map( + mut self, + transform: impl Fn( + BTreeMap, + ) -> BTreeMap, + ) -> Self { + self.0 = transform(self.0); + self + } +} +impl Map for CurrentDependents { + type Key = PackageId; + type Value = CurrentDependencyInfo; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone().into()) + } +} +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct CurrentDependencies(pub BTreeMap); +impl CurrentDependencies { + pub fn map( + mut self, + transform: impl Fn( + BTreeMap, + ) -> BTreeMap, + ) -> Self { + self.0 = transform(self.0); + self + } +} +impl Map for CurrentDependencies { + type Key = PackageId; + type Value = CurrentDependencyInfo; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone().into()) + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct StaticDependencyInfo { + pub title: String, + pub icon: DataUrl<'static>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "kind")] +pub enum CurrentDependencyInfo { + Exists, + #[serde(rename_all = "kebab-case")] + Running { + #[serde(default)] + health_checks: BTreeSet, + }, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct InterfaceAddressMap(pub BTreeMap); +impl Map for InterfaceAddressMap { + type Key = HostId; + type Value = InterfaceAddresses; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(key) + } + fn key_string(key: &Self::Key) -> Result { + Ok(key.clone().into()) + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct InterfaceAddresses { + pub tor_address: Option, + pub lan_address: Option, +} diff --git a/core/startos/src/db/model/private.rs b/core/startos/src/db/model/private.rs new file mode 100644 index 000000000..b9bc51e63 --- /dev/null +++ b/core/startos/src/db/model/private.rs @@ -0,0 +1,30 @@ +use std::collections::BTreeMap; + +use models::PackageId; +use patch_db::{HasModel, Value}; +use serde::{Deserialize, Serialize}; + +use crate::auth::Sessions; +use crate::backup::target::cifs::CifsTargets; +use crate::net::forward::AvailablePorts; +use crate::net::keys::KeyStore; +use crate::notifications::Notifications; +use crate::prelude::*; +use crate::ssh::SshKeys; +use crate::util::serde::Pem; + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct Private { + pub key_store: KeyStore, + pub password: String, // argon2 hash + pub ssh_privkey: Pem, + pub ssh_pubkeys: SshKeys, + pub available_ports: AvailablePorts, + pub sessions: Sessions, + pub notifications: Notifications, + pub cifs: CifsTargets, + #[serde(default)] + pub package_stores: BTreeMap, +} diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs new file mode 100644 index 000000000..a9cf8d7e2 --- /dev/null +++ b/core/startos/src/db/model/public.rs @@ -0,0 +1,210 @@ +use std::collections::BTreeMap; +use std::net::{Ipv4Addr, Ipv6Addr}; + +use chrono::{DateTime, Utc}; +use emver::VersionRange; +use imbl_value::InternedString; +use ipnet::{Ipv4Net, Ipv6Net}; +use isocountry::CountryCode; +use itertools::Itertools; +use models::PackageId; +use openssl::hash::MessageDigest; +use patch_db::{HasModel, Value}; +use reqwest::Url; +use serde::{Deserialize, Serialize}; +use torut::onion::OnionAddressV3; + +use crate::account::AccountInfo; +use crate::db::model::package::AllPackageData; +use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr}; +use crate::prelude::*; +use crate::util::cpupower::Governor; +use crate::util::Version; +use crate::version::{Current, VersionT}; +use crate::{ARCH, PLATFORM}; + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +// #[macro_debug] +pub struct Public { + pub server_info: ServerInfo, + pub package_data: AllPackageData, + pub ui: Value, +} +impl Public { + pub fn init(account: &AccountInfo) -> Result { + let lan_address = account.hostname.lan_address().parse().unwrap(); + Ok(Self { + server_info: ServerInfo { + arch: get_arch(), + platform: get_platform(), + id: account.server_id.clone(), + version: Current::new().semver().into(), + hostname: account.hostname.no_dot_host_name(), + last_backup: None, + last_wifi_region: None, + eos_version_compat: Current::new().compat().clone(), + lan_address, + onion_address: account.tor_key.public().get_onion_address(), + tor_address: format!("https://{}", account.tor_key.public().get_onion_address()) + .parse() + .unwrap(), + ip_info: BTreeMap::new(), + status_info: ServerStatus { + backup_progress: None, + updated: false, + update_progress: None, + shutting_down: false, + restarting: false, + }, + wifi: WifiInfo { + ssids: Vec::new(), + connected: None, + selected: None, + }, + unread_notification_count: 0, + connection_addresses: ConnectionAddresses { + tor: Vec::new(), + clearnet: Vec::new(), + }, + password_hash: account.password.clone(), + pubkey: ssh_key::PublicKey::from(&account.ssh_key) + .to_openssh() + .unwrap(), + ca_fingerprint: account + .root_ca_cert + .digest(MessageDigest::sha256()) + .unwrap() + .iter() + .map(|x| format!("{x:X}")) + .join(":"), + ntp_synced: false, + zram: true, + governor: None, + }, + package_data: AllPackageData::default(), + ui: serde_json::from_str(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../web/patchdb-ui-seed.json" + ))) + .with_kind(ErrorKind::Deserialization)?, + }) + } +} + +fn get_arch() -> InternedString { + (*ARCH).into() +} + +fn get_platform() -> InternedString { + (&*PLATFORM).into() +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct ServerInfo { + #[serde(default = "get_arch")] + pub arch: InternedString, + #[serde(default = "get_platform")] + pub platform: InternedString, + pub id: String, + pub hostname: String, + pub version: Version, + pub last_backup: Option>, + /// Used in the wifi to determine the region to set the system to + pub last_wifi_region: Option, + pub eos_version_compat: VersionRange, + pub lan_address: Url, + pub onion_address: OnionAddressV3, + /// for backwards compatibility + pub tor_address: Url, + pub ip_info: BTreeMap, + #[serde(default)] + pub status_info: ServerStatus, + pub wifi: WifiInfo, + pub unread_notification_count: u64, + pub connection_addresses: ConnectionAddresses, + pub password_hash: String, + pub pubkey: String, + pub ca_fingerprint: String, + #[serde(default)] + pub ntp_synced: bool, + #[serde(default)] + pub zram: bool, + pub governor: Option, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct IpInfo { + pub ipv4_range: Option, + pub ipv4: Option, + pub ipv6_range: Option, + pub ipv6: Option, +} +impl IpInfo { + pub async fn for_interface(iface: &str) -> Result { + let (ipv4, ipv4_range) = get_iface_ipv4_addr(iface).await?.unzip(); + let (ipv6, ipv6_range) = get_iface_ipv6_addr(iface).await?.unzip(); + Ok(Self { + ipv4_range, + ipv4, + ipv6_range, + ipv6, + }) + } +} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel)] +#[model = "Model"] +pub struct BackupProgress { + pub complete: bool, +} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct ServerStatus { + pub backup_progress: Option>, + pub updated: bool, + pub update_progress: Option, + #[serde(default)] + pub shutting_down: bool, + #[serde(default)] + pub restarting: bool, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct UpdateProgress { + pub size: Option, + pub downloaded: u64, +} + +#[derive(Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +pub struct WifiInfo { + pub ssids: Vec, + pub selected: Option, + pub connected: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct ServerSpecs { + pub cpu: String, + pub disk: String, + pub memory: String, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct ConnectionAddresses { + pub tor: Vec, + pub clearnet: Vec, +} diff --git a/core/startos/src/db/prelude.rs b/core/startos/src/db/prelude.rs index 0e90e2c88..43dd59002 100644 --- a/core/startos/src/db/prelude.rs +++ b/core/startos/src/db/prelude.rs @@ -124,6 +124,12 @@ impl Model { self.ser(&orig)?; Ok(res) } + pub fn map_mutate(&mut self, f: impl FnOnce(T) -> Result) -> Result { + let mut orig = self.de()?; + let res = f(orig)?; + self.ser(&res)?; + Ok(res) + } } impl Clone for Model { fn clone(&self) -> Self { diff --git a/core/startos/src/dependencies.rs b/core/startos/src/dependencies.rs index 4e96a3db3..a22411244 100644 --- a/core/startos/src/dependencies.rs +++ b/core/startos/src/dependencies.rs @@ -10,7 +10,8 @@ use tracing::instrument; use crate::config::{Config, ConfigSpec, ConfigureContext}; use crate::context::RpcContext; -use crate::db::model::{CurrentDependencies, Database}; +use crate::db::model::package::CurrentDependencies; +use crate::db::model::Database; use crate::prelude::*; use crate::s9pk::manifest::Manifest; use crate::status::DependencyConfigErrors; @@ -195,52 +196,19 @@ pub async fn configure_logic( todo!() } -#[instrument(skip_all)] -pub fn add_dependent_to_current_dependents_lists( - db: &mut Model, - dependent_id: &PackageId, - current_dependencies: &CurrentDependencies, -) -> Result<(), Error> { - for (dependency, dep_info) in ¤t_dependencies.0 { - if let Some(dependency_dependents) = db - .as_public_mut() - .as_package_data_mut() - .as_idx_mut(dependency) - .and_then(|pde| pde.as_installed_mut()) - .map(|i| i.as_current_dependents_mut()) - { - dependency_dependents.insert(dependent_id, dep_info)?; - } - } - Ok(()) -} - #[instrument(skip_all)] pub async fn compute_dependency_config_errs( ctx: &RpcContext, db: &Peeked, - manifest: &Manifest, + id: &PackageId, current_dependencies: &CurrentDependencies, dependency_config: &BTreeMap, ) -> Result { let mut dependency_config_errs = BTreeMap::new(); - for (dependency, _dep_info) in current_dependencies - .0 - .iter() - .filter(|(dep_id, _)| dep_id != &&manifest.id) - { + for (dependency, _dep_info) in current_dependencies.0.iter() { // check if config passes dependency check - if let Some(cfg) = &manifest - .dependencies - .0 - .get(dependency) - .or_not_found(dependency)? - .config - { - let error = todo!(); - { - dependency_config_errs.insert(dependency.clone(), error); - } + if let Some(error) = todo!() { + dependency_config_errs.insert(dependency.clone(), error); } } Ok(DependencyConfigErrors(dependency_config_errs)) diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index 62bbdc58b..d1d5a9943 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -11,7 +11,7 @@ use tracing::instrument; use crate::account::AccountInfo; use crate::context::config::ServerConfig; -use crate::db::model::ServerStatus; +use crate::db::model::public::ServerStatus; use crate::disk::mount::util::unmount; use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; use crate::prelude::*; diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 6ea4a7129..f4c415915 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -18,10 +18,7 @@ use tracing::instrument; use crate::context::{CliContext, RpcContext}; use crate::core::rpc_continuations::{RequestGuid, RpcContinuation}; -use crate::db::model::{ - PackageDataEntry, PackageDataEntryInstalled, PackageDataEntryMatchModelRef, - PackageDataEntryRemoving, -}; +use crate::db::model::package::{ManifestPreference, PackageState, PackageStateMatchModelRef}; use crate::prelude::*; use crate::progress::{FullProgress, PhasedProgressBar}; use crate::s9pk::manifest::PackageId; @@ -40,27 +37,27 @@ pub async fn list(ctx: RpcContext) -> Result { Ok(ctx.db.peek().await.as_public().as_package_data().as_entries()? .iter() .filter_map(|(id, pde)| { - let status = match pde.as_match() { - PackageDataEntryMatchModelRef::Installed(_) => { + let status = match pde.as_state_info().as_match() { + PackageStateMatchModelRef::Installed(_) => { "installed" } - PackageDataEntryMatchModelRef::Installing(_) => { + PackageStateMatchModelRef::Installing(_) => { "installing" } - PackageDataEntryMatchModelRef::Updating(_) => { + PackageStateMatchModelRef::Updating(_) => { "updating" } - PackageDataEntryMatchModelRef::Restoring(_) => { + PackageStateMatchModelRef::Restoring(_) => { "restoring" } - PackageDataEntryMatchModelRef::Removing(_) => { + PackageStateMatchModelRef::Removing(_) => { "removing" } - PackageDataEntryMatchModelRef::Error(_) => { + PackageStateMatchModelRef::Error(_) => { "error" } }; - serde_json::to_value(json!({ "status":status, "id": id.clone(), "version": pde.as_manifest().as_version().de().ok()?})) + serde_json::to_value(json!({ "status": status, "id": id.clone(), "version": pde.as_state_info().as_manifest(ManifestPreference::Old).as_version().de().ok()?})) .ok() }) .collect()) @@ -212,7 +209,7 @@ pub async fn sideload(ctx: RpcContext) -> Result { .as_public() .as_package_data() .as_idx(&id) - .and_then(|e| e.as_install_progress()) + .and_then(|e| e.as_state_info().as_installing_info()).map(|i| i.as_progress()) { Ok::<_, ()>(p.de()?) } else { @@ -407,31 +404,18 @@ pub async fn uninstall( ) -> Result { ctx.db .mutate(|db| { - let (manifest, static_files, installed) = match db - .as_public() - .as_package_data() - .as_idx(&id) - .or_not_found(&id)? - .de()? - { - PackageDataEntry::Installed(PackageDataEntryInstalled { - manifest, - static_files, - installed, - }) => (manifest, static_files, installed), - _ => { - return Err(Error::new( - eyre!("Package is not installed."), - crate::ErrorKind::NotFound, - )); - } - }; - let pde = PackageDataEntry::Removing(PackageDataEntryRemoving { - manifest, - static_files, - removing: installed, - }); - db.as_public_mut().as_package_data_mut().insert(&id, &pde) + let entry = db + .as_public_mut() + .as_package_data_mut() + .as_idx_mut(&id) + .or_not_found(&id)?; + entry.as_state_info_mut().map_mutate(|s| match s { + PackageState::Installed(s) => Ok(PackageState::Removing(s)), + _ => Err(Error::new( + eyre!("Package {id} is not installed."), + crate::ErrorKind::NotFound, + )), + }) }) .await?; diff --git a/core/startos/src/net/dhcp.rs b/core/startos/src/net/dhcp.rs index a8dbcabb0..d66448406 100644 --- a/core/startos/src/net/dhcp.rs +++ b/core/startos/src/net/dhcp.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use crate::context::{CliContext, RpcContext}; -use crate::db::model::IpInfo; +use crate::db::model::public::IpInfo; use crate::net::utils::{iface_is_physical, list_interfaces}; use crate::prelude::*; use crate::Error; diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index d9d7a5d76..5fa2a30ec 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -205,8 +205,6 @@ impl NetService { .as_package_data_mut() .as_idx_mut(pkg_id) .or_not_found(pkg_id)? - .as_installed_mut() - .or_not_found(pkg_id)? .as_hosts_mut(); hosts.add_binding(&mut ports, kind, &id, internal_port, options)?; let host = hosts diff --git a/core/startos/src/properties.rs b/core/startos/src/properties.rs index 1a8315cc3..4f59d4303 100644 --- a/core/startos/src/properties.rs +++ b/core/startos/src/properties.rs @@ -4,9 +4,10 @@ use models::PackageId; use rpc_toolkit::command; use serde::{Deserialize, Serialize}; +use crate::context::RpcContext; +use crate::db::model::package::ExposedUI; use crate::prelude::*; use crate::Error; -use crate::{context::RpcContext, db::model::ExposedUI}; pub fn display_properties(response: Value) { println!("{}", response); @@ -59,8 +60,6 @@ pub async fn properties( .as_package_data() .as_idx(&id) .or_not_found(&id)? - .as_installed() - .or_not_found(&id)? .as_store_exposed_ui() .de()? .into_properties(&data)) diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 83f03fc4b..fb521ca34 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -16,9 +16,8 @@ use crate::action::ActionResult; use crate::config::action::ConfigRes; use crate::context::{CliContext, RpcContext}; use crate::core::rpc_continuations::RequestGuid; -use crate::db::model::{ - InstalledPackageInfo, PackageDataEntry, PackageDataEntryInstalled, PackageDataEntryMatchModel, - StaticFiles, +use crate::db::model::package::{ + InstalledState, PackageDataEntry, PackageState, PackageStateMatchModelRef, UpdatingState, }; use crate::disk::mount::guard::GenericMountGuard; use crate::install::PKG_ARCHIVE_DIR; @@ -28,7 +27,7 @@ use crate::s9pk::S9pk; use crate::service::service_map::InstallProgressHandles; use crate::service::transition::TransitionKind; use crate::status::health_check::HealthCheckResult; -use crate::status::{MainStatus, Status}; +use crate::status::MainStatus; use crate::util::actor::{Actor, BackgroundJobs, SimpleActor}; use crate::volume::data_dir; @@ -100,7 +99,7 @@ impl Service { ) -> Result, Error> { let handle_installed = { let ctx = ctx.clone(); - move |s9pk: S9pk, i: Model| async move { + move |s9pk: S9pk, i: Model| async move { for volume_id in &s9pk.as_manifest().volumes { let tmp_path = data_dir(&ctx.datadir, &s9pk.as_manifest().id.clone(), volume_id); @@ -118,16 +117,18 @@ impl Service { }; let s9pk_dir = ctx.datadir.join(PKG_ARCHIVE_DIR).join("installed"); // TODO: make this based on hash let s9pk_path = s9pk_dir.join(id).with_extension("s9pk"); - match ctx + let Some(entry) = ctx .db .peek() .await .into_public() .into_package_data() .into_idx(id) - .map(|pde| pde.into_match()) - { - Some(PackageDataEntryMatchModel::Installing(_)) => { + else { + return Ok(None); + }; + match entry.as_state_info().as_match() { + PackageStateMatchModelRef::Installing(_) => { if disposition == LoadDisposition::Retry { if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| { tracing::error!("Error opening s9pk for install: {e}"); @@ -150,14 +151,17 @@ impl Service { .await?; Ok(None) } - Some(PackageDataEntryMatchModel::Updating(e)) => { + PackageStateMatchModelRef::Updating(s) => { if disposition == LoadDisposition::Retry - && e.as_install_progress().de()?.phases.iter().any( - |NamedProgress { name, progress }| { + && s.as_installing_info() + .as_progress() + .de()? + .phases + .iter() + .any(|NamedProgress { name, progress }| { name.eq_ignore_ascii_case("download") && progress == &Progress::Complete(true) - }, - ) + }) { if let Ok(s9pk) = S9pk::open(&s9pk_path, Some(id)).await.map_err(|e| { tracing::error!("Error opening s9pk for update: {e}"); @@ -166,7 +170,7 @@ impl Service { if let Ok(service) = Self::install( ctx.clone(), s9pk, - Some(e.as_installed().as_manifest().as_version().de()?), + Some(s.as_manifest().as_version().de()?), None, ) .await @@ -181,24 +185,28 @@ impl Service { let s9pk = S9pk::open(s9pk_path, Some(id)).await?; ctx.db .mutate({ - let manifest = s9pk.as_manifest().clone(); |db| { db.as_public_mut() .as_package_data_mut() - .as_idx_mut(&manifest.id) - .or_not_found(&manifest.id)? - .ser(&PackageDataEntry::Installed(PackageDataEntryInstalled { - static_files: e.as_static_files().de()?, - manifest, - installed: e.as_installed().de()?, - })) + .as_idx_mut(&id) + .or_not_found(&id)? + .as_state_info_mut() + .map_mutate(|s| { + if let PackageState::Updating(UpdatingState { + manifest, .. + }) = s + { + Ok(PackageState::Installed(InstalledState { manifest })) + } else { + Err(Error::new(eyre!("Race condition detected - package state changed during load"), ErrorKind::Database)) + } + }) } }) .await?; - handle_installed(s9pk, e.as_installed().clone()).await + handle_installed(s9pk, entry).await } - Some(PackageDataEntryMatchModel::Removing(_)) - | Some(PackageDataEntryMatchModel::Restoring(_)) => { + PackageStateMatchModelRef::Removing(_) | PackageStateMatchModelRef::Restoring(_) => { if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| { tracing::error!("Error opening s9pk for removal: {e}"); tracing::debug!("{e:?}") @@ -230,18 +238,13 @@ impl Service { Ok(None) } - Some(PackageDataEntryMatchModel::Installed(i)) => { - handle_installed( - S9pk::open(s9pk_path, Some(id)).await?, - i.as_installed().clone(), - ) - .await + PackageStateMatchModelRef::Installed(_) => { + handle_installed(S9pk::open(s9pk_path, Some(id)).await?, entry).await } - Some(PackageDataEntryMatchModel::Error(e)) => Err(Error::new( + PackageStateMatchModelRef::Error(e) => Err(Error::new( eyre!("Failed to parse PackageDataEntry, found {e:?}"), ErrorKind::Deserialization, )), - None => Ok(None), } } @@ -255,7 +258,6 @@ impl Service { let manifest = s9pk.as_manifest().clone(); let developer_key = s9pk.as_archive().signer(); let icon = s9pk.icon_data_url().await?; - let static_files = StaticFiles::local(&manifest.id, &manifest.version, icon); let service = Self::new(ctx.clone(), s9pk, StartStop::Stop).await?; service .seed @@ -270,32 +272,19 @@ impl Service { } ctx.db .mutate(|d| { - d.as_public_mut() + let entry = d + .as_public_mut() .as_package_data_mut() .as_idx_mut(&manifest.id) - .or_not_found(&manifest.id)? - .ser(&PackageDataEntry::Installed(PackageDataEntryInstalled { - installed: InstalledPackageInfo { - current_dependencies: Default::default(), // TODO - current_dependents: Default::default(), // TODO - dependency_info: Default::default(), // TODO - developer_key, - status: Status { - configured: false, // TODO - main: MainStatus::Stopped, // TODO - dependency_config_errors: Default::default(), // TODO - }, - interface_addresses: Default::default(), // TODO - marketplace_url: None, // TODO - manifest: manifest.clone(), - last_backup: None, // TODO - hosts: Default::default(), // TODO - store_exposed_dependents: Default::default(), // TODO - store_exposed_ui: Default::default(), // TODO - }, - manifest, - static_files, - })) + .or_not_found(&manifest.id)?; + entry + .as_state_info_mut() + .ser(&PackageState::Installed(InstalledState { manifest }))?; + entry.as_developer_key_mut().ser(&developer_key)?; + entry.as_icon_mut().ser(&icon)?; + // TODO: marketplace url + // TODO: dependency info + Ok(()) }) .await?; Ok(service) @@ -466,11 +455,7 @@ impl Actor for ServiceActor { seed.ctx .db .mutate(|d| { - if let Some(i) = d - .as_public_mut() - .as_package_data_mut() - .as_idx_mut(&id) - .and_then(|p| p.as_installed_mut()) + if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) { i.as_status_mut().as_main_mut().ser(&main_status)?; } diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 4524eb0ee..e67177998 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::ffi::OsString; use std::os::unix::process::CommandExt; use std::path::{Path, PathBuf}; @@ -6,14 +7,16 @@ use std::sync::{Arc, Weak}; use clap::builder::ValueParserFactory; use clap::Parser; +use imbl::OrdMap; use imbl_value::{json, InternedString}; -use models::{ActionId, HealthCheckId, ImageId, PackageId}; +use models::{ActionId, HealthCheckId, ImageId, InvalidId, PackageId}; use patch_db::json_ptr::JsonPointer; use rpc_toolkit::{from_fn, from_fn_async, AnyContext, Context, Empty, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; use tokio::process::Command; use ts_rs::TS; -use crate::db::model::ExposedUI; +use crate::db::model::package::{CurrentDependencies, CurrentDependencyInfo, ExposedUI}; use crate::disk::mount::filesystem::idmapped::IdMapped; use crate::disk::mount::filesystem::loop_dev::LoopDev; use crate::disk::mount::filesystem::overlayfs::OverlayGuard; @@ -131,29 +134,250 @@ pub fn service_effect_handler() -> ParentHandler { .subcommand("clearBindings", from_fn_async(clear_bindings).no_cli()) .subcommand("bind", from_fn_async(bind).no_cli()) .subcommand("getHostInfo", from_fn_async(get_host_info).no_cli()) - // TODO @DrBonez when we get the new api for 4.0 - // .subcommand("setDependencies",from_fn_async(set_dependencies).no_cli()) - // .subcommand("embassyGetInterface",from_fn_async(embassy_get_interface).no_cli()) - // .subcommand("mount",from_fn_async(mount).no_cli()) - // .subcommand("removeAction",from_fn_async(remove_action).no_cli()) - // .subcommand("removeAddress",from_fn_async(remove_address).no_cli()) - // .subcommand("exportAction",from_fn_async(export_action).no_cli()) - // .subcommand("clearServiceInterfaces",from_fn_async(clear_network_interfaces).no_cli()) - // .subcommand("exportServiceInterface",from_fn_async(export_network_interface).no_cli()) - // .subcommand("getHostnames",from_fn_async(get_hostnames).no_cli()) - // .subcommand("getInterface",from_fn_async(get_interface).no_cli()) - // .subcommand("listInterface",from_fn_async(list_interface).no_cli()) - // .subcommand("getIPHostname",from_fn_async(get_ip_hostname).no_cli()) - // .subcommand("getContainerIp",from_fn_async(get_container_ip).no_cli()) - // .subcommand("getLocalHostname",from_fn_async(get_local_hostname).no_cli()) - // .subcommand("getPrimaryUrl",from_fn_async(get_primary_url).no_cli()) - // .subcommand("getServicePortForward",from_fn_async(get_service_port_forward).no_cli()) - // .subcommand("getServiceTorHostname",from_fn_async(get_service_tor_hostname).no_cli()) - // .subcommand("getSystemSmtp",from_fn_async(get_system_smtp).no_cli()) - // .subcommand("reverseProxy",from_fn_async(reverse_proxy).no_cli()) + .subcommand( + "setDependencies", + from_fn_async(set_dependencies) + .no_display() + .with_remote_cli::(), + ) + .subcommand("getSystemSmtp", from_fn_async(get_system_smtp).no_cli()) + .subcommand("getContainerIp", from_fn_async(get_container_ip).no_cli()) + .subcommand( + "getServicePortForward", + from_fn_async(get_service_port_forward).no_cli(), + ) + .subcommand( + "clearServiceInterfaces", + from_fn_async(clear_network_interfaces).no_cli(), + ) + .subcommand( + "exportServiceInterface", + from_fn_async(export_service_interface).no_cli(), + ) + .subcommand("getPrimaryUrl", from_fn_async(get_primary_url).no_cli()) + .subcommand( + "listServiceInterfaces", + from_fn_async(list_service_interfaces).no_cli(), + ) + .subcommand("removeAddress", from_fn_async(remove_address).no_cli()) + .subcommand("exportAction", from_fn_async(export_action).no_cli()) + .subcommand("removeAction", from_fn_async(remove_action).no_cli()) + .subcommand("reverseProxy", from_fn_async(reverse_proxy).no_cli()) + .subcommand("mount", from_fn_async(mount).no_cli()) // TODO Callbacks } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct GetSystemSmtpParams { + callback: Callback, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct GetServicePortForwardParams { + #[ts(type = "string | null")] + package_id: Option, + internal_port: u32, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct BindOptionsSecure { + ssl: bool, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct BindOptions { + scheme: Option, + preferred_external_port: u32, + add_ssl: Option, + secure: Option, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct AddressInfo { + username: Option, + host_id: String, + bind_options: BindOptions, + suffix: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +enum ServiceInterfaceType { + Ui, + P2p, + Api, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct ExportServiceInterfaceParams { + id: String, + name: String, + description: String, + has_primary: bool, + disabled: bool, + masked: bool, + address_info: AddressInfo, + r#type: ServiceInterfaceType, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct GetPrimaryUrlParams { + #[ts(type = "string | null")] + package_id: Option, + service_interface_id: String, + callback: Callback, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct ListServiceInterfacesParams { + #[ts(type = "string | null")] + package_id: Option, + callback: Callback, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct RemoveAddressParams { + id: String, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "kebab-case")] +enum AllowedStatuses { + OnlyRunning, + OnlyStopped, + Any, + Disabled, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct ExportActionParams { + name: String, + description: String, + id: String, + #[ts(type = "{[key: string]: any}")] + input: Value, + allowed_statuses: AllowedStatuses, + group: Option, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct RemoveActionParams { + id: String, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct ReverseProxyBind { + ip: Option, + port: u32, + ssl: bool, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct ReverseProxyDestination { + ip: Option, + port: u32, + ssl: bool, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct ReverseProxyHttp { + #[ts(type = "null | {[key: string]: string}")] + headers: Option>, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct ReverseProxyParams { + bind: ReverseProxyBind, + dst: ReverseProxyDestination, + http: ReverseProxyHttp, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct MountTarget { + #[ts(type = "string")] + package_id: PackageId, + volume_id: String, + path: String, + readonly: bool, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct MountParams { + location: String, + target: MountTarget, +} +async fn get_system_smtp( + context: EffectContext, + data: GetSystemSmtpParams, +) -> Result { + todo!() +} +async fn get_container_ip(context: EffectContext, _: Empty) -> Result { + todo!() +} +async fn get_service_port_forward( + context: EffectContext, + data: GetServicePortForwardParams, +) -> Result { + todo!() +} +async fn clear_network_interfaces(context: EffectContext, _: Empty) -> Result { + todo!() +} +async fn export_service_interface( + context: EffectContext, + data: ExportServiceInterfaceParams, +) -> Result { + todo!() +} +async fn get_primary_url( + context: EffectContext, + data: GetPrimaryUrlParams, +) -> Result { + todo!() +} +async fn list_service_interfaces( + context: EffectContext, + data: ListServiceInterfacesParams, +) -> Result { + todo!() +} +async fn remove_address(context: EffectContext, data: RemoveAddressParams) -> Result { + todo!() +} +async fn export_action(context: EffectContext, data: ExportActionParams) -> Result { + todo!() +} +async fn remove_action(context: EffectContext, data: RemoveActionParams) -> Result { + todo!() +} +async fn reverse_proxy(context: EffectContext, data: ReverseProxyParams) -> Result { + todo!() +} +async fn mount(context: EffectContext, data: MountParams) -> Result { + todo!() +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] #[ts(export)] struct Callback(#[ts(type = "() => void")] i64); @@ -170,7 +394,8 @@ enum GetHostInfoParamsKind { struct GetHostInfoParams { kind: Option, service_interface_id: String, - package_id: Option, + #[ts(type = "string | null")] + package_id: Option, callback: Callback, } async fn get_host_info( @@ -211,8 +436,7 @@ struct BindParams { scheme: String, preferred_external_port: u32, add_ssl: Option, - secure: bool, - ssl: bool, + secure: Option, } async fn bind(_: AnyContext, BindParams { .. }: BindParams) -> Result { todo!() @@ -222,6 +446,7 @@ async fn bind(_: AnyContext, BindParams { .. }: BindParams) -> Result, service_interface_id: String, callback: Callback, @@ -375,6 +600,7 @@ async fn get_ssl_key( #[serde(rename_all = "camelCase")] #[ts(export)] struct GetStoreParams { + #[ts(type = "string | null")] package_id: Option, #[ts(type = "string")] path: JsonPointer, @@ -457,8 +683,6 @@ async fn expose_for_dependents( .as_package_data_mut() .as_idx_mut(&package_id) .or_not_found(&package_id)? - .as_installed_mut() - .or_not_found(&package_id)? .as_store_exposed_dependents_mut() .ser(&paths) }) @@ -467,37 +691,45 @@ async fn expose_for_dependents( } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] #[serde(rename_all = "camelCase")] +#[serde(tag = "type")] #[ts(export)] -struct ExposeUiParams { - paths: Vec, +enum ExposeUiParams { + Object { + #[ts(type = "{[key: string]: ExposeUiParams}")] + value: OrdMap, + }, + String { + path: String, + description: Option, + masked: bool, + copyable: Option, + qr: Option, + }, } -async fn expose_ui( - context: EffectContext, - ExposeUiParams { paths }: ExposeUiParams, -) -> Result<(), Error> { - let context = context.deref()?; - let package_id = context.id.clone(); - context - .ctx - .db - .mutate(|db| { - db.as_public_mut() - .as_package_data_mut() - .as_idx_mut(&package_id) - .or_not_found(&package_id)? - .as_installed_mut() - .or_not_found(&package_id)? - .as_store_exposed_ui_mut() - .ser(&paths) - }) - .await?; - Ok(()) +async fn expose_ui(context: EffectContext, params: ExposeUiParams) -> Result<(), Error> { + todo!() + // let context = context.deref()?; + // let package_id = context.id.clone(); + // context + // .ctx + // .db + // .mutate(|db| { + // db.as_public_mut() + // .as_package_data_mut() + // .as_idx_mut(&package_id) + // .or_not_found(&package_id)? + // .as_store_exposed_ui_mut() + // .ser(&paths) + // }) + // .await?; + // Ok(()) } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] struct ParamsPackageId { + #[ts(type = "string")] package_id: PackageId, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] @@ -505,6 +737,7 @@ struct ParamsPackageId { #[command(rename_all = "camelCase")] #[ts(export)] struct ParamsMaybePackageId { + #[ts(type = "string | null")] package_id: Option, } @@ -523,7 +756,9 @@ async fn exists(context: EffectContext, params: ParamsPackageId) -> Result, + #[ts(type = "string")] action_id: ActionId, #[ts(type = "any")] input: Value, @@ -560,8 +795,6 @@ async fn get_configured(context: EffectContext, _: Empty) -> Result Result .as_package_data() .as_idx(&package_id) .or_not_found(&package_id)? - .as_installed() - .or_not_found(&package_id)? .as_status() .as_main() .de()?; Ok(json!(matches!(package, MainStatus::Stopped))) } -async fn running(context: EffectContext, params: ParamsMaybePackageId) -> Result { +async fn running(context: EffectContext, params: ParamsPackageId) -> Result { dbg!("Starting the running {params:?}"); let context = context.deref()?; let peeked = context.ctx.db.peek().await; - let package_id = params.package_id.unwrap_or_else(|| context.id.clone()); + let package_id = params.package_id; let package = peeked .as_public() .as_package_data() .as_idx(&package_id) .or_not_found(&package_id)? - .as_installed() - .or_not_found(&package_id)? .as_status() .as_main() .de()?; @@ -646,8 +875,6 @@ async fn set_configured(context: EffectContext, params: SetConfigured) -> Result .as_package_data_mut() .as_idx_mut(package_id) .or_not_found(package_id)? - .as_installed_mut() - .or_not_found(package_id)? .as_status_mut() .as_configured_mut() .ser(¶ms.configured) @@ -701,6 +928,7 @@ async fn set_main_status(context: EffectContext, params: SetMainStatus) -> Resul #[serde(rename_all = "camelCase")] #[ts(export)] struct SetHealth { + #[ts(type = "string")] name: HealthCheckId, status: HealthCheckString, message: Option, @@ -726,8 +954,6 @@ async fn set_health( .as_package_data() .as_idx(package_id) .or_not_found(package_id)? - .as_installed() - .or_not_found(package_id)? .as_status() .as_main() .de()?; @@ -757,8 +983,6 @@ async fn set_health( .as_package_data_mut() .as_idx_mut(package_id) .or_not_found(package_id)? - .as_installed_mut() - .or_not_found(package_id)? .as_status_mut() .as_main_mut() .ser(&main) @@ -771,7 +995,6 @@ async fn set_health( #[command(rename_all = "camelCase")] #[ts(export)] pub struct DestroyOverlayedImageParams { - image_id: ImageId, #[ts(type = "string")] guid: InternedString, } @@ -779,7 +1002,7 @@ pub struct DestroyOverlayedImageParams { #[instrument(skip_all)] pub async fn destroy_overlayed_image( ctx: EffectContext, - DestroyOverlayedImageParams { image_id, guid }: DestroyOverlayedImageParams, + DestroyOverlayedImageParams { guid }: DestroyOverlayedImageParams, ) -> Result<(), Error> { let ctx = ctx.deref()?; if ctx @@ -799,6 +1022,7 @@ pub async fn destroy_overlayed_image( #[command(rename_all = "camelCase")] #[ts(export)] pub struct CreateOverlayedImageParams { + #[ts(type = "string")] image_id: ImageId, } @@ -864,3 +1088,127 @@ pub async fn create_overlayed_image( )) } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +enum DependencyKind { + Exists, + Running, +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +struct DependencyRequirement { + #[ts(type = "string")] + id: PackageId, + kind: DependencyKind, + #[serde(default)] + #[ts(type = "string[]")] + health_checks: BTreeSet, +} +// filebrowser:exists,bitcoind:running:foo+bar+baz +impl FromStr for DependencyRequirement { + type Err = Error; + fn from_str(s: &str) -> Result { + match s.split_once(':') { + Some((id, "e")) | Some((id, "exists")) => Ok(Self { + id: id.parse()?, + kind: DependencyKind::Exists, + health_checks: BTreeSet::new(), + }), + Some((id, rest)) => { + let health_checks = match rest.split_once(":") { + Some(("r", rest)) | Some(("running", rest)) => rest + .split('+') + .map(|id| id.parse().map_err(Error::from)) + .collect(), + Some((kind, _)) => Err(Error::new( + eyre!("unknown dependency kind {kind}"), + ErrorKind::InvalidRequest, + )), + None => match rest { + "r" | "running" => Ok(BTreeSet::new()), + kind => Err(Error::new( + eyre!("unknown dependency kind {kind}"), + ErrorKind::InvalidRequest, + )), + }, + }?; + Ok(Self { + id: id.parse()?, + kind: DependencyKind::Running, + health_checks, + }) + } + None => Ok(Self { + id: s.parse()?, + kind: DependencyKind::Running, + health_checks: BTreeSet::new(), + }), + } + } +} +impl ValueParserFactory for DependencyRequirement { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "camelCase")] +#[ts(export)] +struct SetDependenciesParams { + dependencies: Vec, +} + +async fn set_dependencies( + ctx: EffectContext, + SetDependenciesParams { dependencies }: SetDependenciesParams, +) -> Result<(), Error> { + let ctx = ctx.deref()?; + let id = &ctx.id; + ctx.ctx + .db + .mutate(|db| { + let dependencies = CurrentDependencies( + dependencies + .into_iter() + .map( + |DependencyRequirement { + id, + kind, + health_checks, + }| { + ( + id, + match kind { + DependencyKind::Exists => CurrentDependencyInfo::Exists, + DependencyKind::Running => { + CurrentDependencyInfo::Running { health_checks } + } + }, + ) + }, + ) + .collect(), + ); + for (dep, entry) in db.as_public_mut().as_package_data_mut().as_entries_mut()? { + if let Some(info) = dependencies.0.get(&dep) { + entry.as_current_dependents_mut().insert(id, info)?; + } else { + entry.as_current_dependents_mut().remove(id)?; + } + } + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(id) + .or_not_found(id)? + .as_current_dependencies_mut() + .ser(&dependencies) + }) + .await +} diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index f555be531..23e1bb540 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -11,9 +11,8 @@ use tokio::sync::{Mutex, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock}; use tracing::instrument; use crate::context::RpcContext; -use crate::db::model::{ - PackageDataEntry, PackageDataEntryInstalled, PackageDataEntryInstalling, - PackageDataEntryRestoring, PackageDataEntryUpdating, StaticFiles, +use crate::db::model::package::{ + InstallingInfo, InstallingState, PackageDataEntry, PackageState, UpdatingState, }; use crate::disk::mount::guard::GenericMountGuard; use crate::install::PKG_ARCHIVE_DIR; @@ -27,6 +26,7 @@ use crate::s9pk::manifest::PackageId; use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::S9pk; use crate::service::{LoadDisposition, Service}; +use crate::status::{MainStatus, Status}; pub type DownloadInstallFuture = BoxFuture<'static, Result>; pub type InstallFuture = BoxFuture<'static, Result<(), Error>>; @@ -95,9 +95,10 @@ impl ServiceMap { mut s9pk: S9pk, recovery_source: Option, ) -> Result { - let manifest = Arc::new(s9pk.as_manifest().clone()); + let manifest = s9pk.as_manifest().clone(); let id = manifest.id.clone(); let icon = s9pk.icon_data_url().await?; + let developer_key = s9pk.as_archive().signer(); let mut service = self.get_mut(&id).await; let op_name = if recovery_source.is_none() { @@ -135,49 +136,51 @@ impl ServiceMap { let id = id.clone(); let install_progress = progress.snapshot(); move |db| { - let pde = match db - .as_public() - .as_package_data() - .as_idx(&id) - .map(|x| x.de()) - .transpose()? - { - Some(PackageDataEntry::Installed(PackageDataEntryInstalled { - installed, - static_files, - .. - })) => PackageDataEntry::Updating(PackageDataEntryUpdating { - install_progress, - installed, - manifest: (*manifest).clone(), - static_files, - }), - None if restoring => { - PackageDataEntry::Restoring(PackageDataEntryRestoring { - install_progress, - static_files: StaticFiles::local( - &manifest.id, - &manifest.version, - icon, - ), - manifest: (*manifest).clone(), - }) - } - None => PackageDataEntry::Installing(PackageDataEntryInstalling { - install_progress, - static_files: StaticFiles::local(&manifest.id, &manifest.version, icon), - manifest: (*manifest).clone(), - }), - _ => { - return Err(Error::new( - eyre!("Cannot install over a package in a transient state"), - crate::ErrorKind::InvalidRequest, - )) - } + if let Some(pde) = db.as_public_mut().as_package_data_mut().as_idx_mut(&id) { + let prev = pde.as_state_info().expect_installed()?.de()?; + pde.as_state_info_mut() + .ser(&PackageState::Updating(UpdatingState { + manifest: prev.manifest, + installing_info: InstallingInfo { + new_manifest: manifest, + progress: install_progress, + }, + }))?; + } else { + let installing = InstallingState { + installing_info: InstallingInfo { + new_manifest: manifest, + progress: install_progress, + }, + }; + db.as_public_mut().as_package_data_mut().insert( + &id, + &PackageDataEntry { + state_info: if restoring { + PackageState::Restoring(installing) + } else { + PackageState::Installing(installing) + }, + status: Status { + configured: false, + main: MainStatus::Stopped, + dependency_config_errors: Default::default(), + }, + marketplace_url: None, + developer_key, + icon, + last_backup: None, + dependency_info: Default::default(), + current_dependents: Default::default(), // TODO: initialize + current_dependencies: Default::default(), + interface_addresses: Default::default(), + hosts: Default::default(), + store_exposed_ui: Default::default(), + store_exposed_dependents: Default::default(), + }, + )?; }; - db.as_public_mut() - .as_package_data_mut() - .insert(&manifest.id, &pde) + Ok(()) } })) .await?; @@ -200,7 +203,8 @@ impl ServiceMap { v.as_public_mut() .as_package_data_mut() .as_idx_mut(&deref_id) - .and_then(|e| e.as_install_progress_mut()) + .and_then(|e| e.as_state_info_mut().as_installing_info_mut()) + .map(|i| i.as_progress_mut()) }, Some(Duration::from_millis(100)), ))); diff --git a/core/startos/src/status/health_check.rs b/core/startos/src/status/health_check.rs index fd3a89683..39434115c 100644 --- a/core/startos/src/status/health_check.rs +++ b/core/startos/src/status/health_check.rs @@ -25,6 +25,7 @@ impl std::fmt::Display for HealthCheckResult { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)] #[serde(rename_all = "camelCase")] +#[ts(export)] pub enum HealthCheckString { Passing, Disabled, diff --git a/core/startos/src/update/mod.rs b/core/startos/src/update/mod.rs index 9f2b58135..c327bbb61 100644 --- a/core/startos/src/update/mod.rs +++ b/core/startos/src/update/mod.rs @@ -14,7 +14,7 @@ use tokio_stream::StreamExt; use tracing::instrument; use crate::context::RpcContext; -use crate::db::model::UpdateProgress; +use crate::db::model::public::UpdateProgress; use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::guard::MountGuard; diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index 938de5fbb..1847ed508 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -19,9 +19,14 @@ import { BackupOptions, DeepPartial, MaybePromise, + ServiceInterfaceId, + PackageId, + EnsureStorePath, + ExtractStore, + DaemonReturned, + ValidIfNoStupidEscape, } from "./types" import * as patterns from "./util/patterns" -import { Utils } from "./util/utils" import { DependencyConfig, Update } from "./dependencyConfig/DependencyConfig" import { BackupSet, Backups } from "./backup/Backups" import { smtpConfig } from "./config/configConstants" @@ -53,6 +58,19 @@ import { } from "./interfaces/setupInterfaces" import { successFailure } from "./trigger/successFailure" import { SetupExports } from "./inits/setupExports" +import { HealthReceipt } from "./health/HealthReceipt" +import { MultiHost, Scheme, SingleHost, StaticHost } from "./interfaces/Host" +import { ServiceInterfaceBuilder } from "./interfaces/ServiceInterfaceBuilder" +import { GetSystemSmtp } from "./util/GetSystemSmtp" +import nullIfEmpty from "./util/nullIfEmpty" +import { + GetServiceInterface, + getServiceInterface, +} from "./util/getServiceInterface" +import { getServiceInterfaces } from "./util/getServiceInterfaces" +import { getStore } from "./store/getStore" +import { CommandOptions, MountOptions, Overlay } from "./util/Overlay" +import { splitCommand } from "./util/splitCommand" // prettier-ignore type AnyNeverCond = @@ -61,6 +79,17 @@ type AnyNeverCond = T extends [any, ...infer U] ? AnyNeverCond : never +export type ServiceInterfaceType = "ui" | "p2p" | "api" +export type MainEffects = Effects & { _type: "main" } +export type Signals = NodeJS.Signals +export const SIGTERM: Signals = "SIGTERM" +export const SIGKILL: Signals = "SIGTERM" +export const NO_TIMEOUT = -1 + +function removeConstType() { + return (t: T) => t as T & (E extends MainEffects ? {} : { const: never }) +} + export class StartSdk { private constructor(readonly manifest: Manifest) {} static of() { @@ -75,7 +104,78 @@ export class StartSdk { build(isReady: AnyNeverCond<[Manifest, Store], "Build not ready", true>) { return { + serviceInterface: { + getOwn: (effects: E, id: ServiceInterfaceId) => + removeConstType()( + getServiceInterface(effects, { + id, + packageId: null, + }), + ), + get: ( + effects: E, + opts: { id: ServiceInterfaceId; packageId: PackageId }, + ) => removeConstType()(getServiceInterface(effects, opts)), + getAllOwn: (effects: E) => + removeConstType()( + getServiceInterfaces(effects, { + packageId: null, + }), + ), + getAll: ( + effects: E, + opts: { packageId: PackageId }, + ) => removeConstType()(getServiceInterfaces(effects, opts)), + }, + + store: { + get: ( + effects: E, + packageId: string, + path: EnsureStorePath, + ) => + removeConstType()( + getStore(effects, path as any, { + packageId, + }), + ), + getOwn: ( + effects: E, + path: EnsureStorePath, + ) => removeConstType()(getStore(effects, path as any)), + setOwn: ( + effects: E, + path: EnsureStorePath, + value: ExtractStore, + ) => effects.store.set({ value, path: path as any }), + }, + + host: { + static: (effects: Effects, id: string) => + new StaticHost({ id, effects }), + single: (effects: Effects, id: string) => + new SingleHost({ id, effects }), + multi: (effects: Effects, id: string) => new MultiHost({ id, effects }), + }, + nullIfEmpty, + configConstants: { smtpConfig }, + createInterface: ( + effects: Effects, + options: { + name: string + id: string + description: string + hasPrimary: boolean + disabled: boolean + type: ServiceInterfaceType + username: null | string + path: string + search: Record + schemeOverride: { ssl: Scheme; noSsl: Scheme } | null + masked: boolean + }, + ) => new ServiceInterfaceBuilder({ ...options, effects }), createAction: < ConfigType extends | Record @@ -88,13 +188,34 @@ export class StartSdk { }, fn: (options: { effects: Effects - utils: Utils input: Type }) => Promise, ) => { const { input, ...rest } = metaData return createAction(rest, fn, input) }, + getSystemSmtp: (effects: E) => + removeConstType()(new GetSystemSmtp(effects)), + runCommand: async ( + effects: Effects, + imageId: Manifest["images"][number], + command: ValidIfNoStupidEscape | [string, ...string[]], + options: CommandOptions & { + mounts?: { path: string; options: MountOptions }[] + }, + ): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> => { + const commands = splitCommand(command) + const overlay = await Overlay.of(effects, imageId) + try { + for (let mount of options.mounts || []) { + await overlay.mount(mount.options, mount.path) + } + return await overlay.exec(commands) + } finally { + await overlay.destroy() + } + }, + createDynamicAction: < ConfigType extends | Record @@ -104,11 +225,9 @@ export class StartSdk { >( metaData: (options: { effects: Effects - utils: Utils }) => MaybePromise>, fn: (options: { effects: Effects - utils: Utils input: Type }) => Promise, input: Config | Config, @@ -193,9 +312,8 @@ export class StartSdk { ) => setupInterfaces(config, fn), setupMain: ( fn: (o: { - effects: Effects + effects: MainEffects started(onTerm: () => PromiseLike): PromiseLike - utils: Utils }) => Promise>, ) => setupMain(fn), setupMigrations: < @@ -232,7 +350,15 @@ export class StartSdk { spec: Spec, ) => Config.of(spec), }, - Daemons: { of: Daemons.of }, + Daemons: { + of(config: { + effects: Effects + started: (onTerm: () => PromiseLike) => PromiseLike + healthReceipts: HealthReceipt[] + }) { + return Daemons.of(config) + }, + }, DependencyConfig: { of< LocalConfig extends Record, @@ -248,7 +374,6 @@ export class StartSdk { dependencyConfig: (options: { effects: Effects localConfig: LocalConfig - utils: Utils }) => Promise> update?: Update, RemoteConfig> }) { @@ -332,14 +457,8 @@ export class StartSdk { Migration: { of: (options: { version: Version - up: (opts: { - effects: Effects - utils: Utils - }) => Promise - down: (opts: { - effects: Effects - utils: Utils - }) => Promise + up: (opts: { effects: Effects }) => Promise + down: (opts: { effects: Effects }) => Promise }) => Migration.of(options), }, Value: { diff --git a/sdk/lib/actions/createAction.ts b/sdk/lib/actions/createAction.ts index d14b7ce0d..dc52a658a 100644 --- a/sdk/lib/actions/createAction.ts +++ b/sdk/lib/actions/createAction.ts @@ -1,15 +1,10 @@ import { Config, ExtractConfigType } from "../config/builder/config" import { SDKManifest } from "../manifest/ManifestTypes" import { ActionMetadata, ActionResult, Effects, ExportedAction } from "../types" -import { createUtils } from "../util" -import { Utils } from "../util/utils" export type MaybeFn = | Value - | ((options: { - effects: Effects - utils: Utils - }) => Promise | Value) + | ((options: { effects: Effects }) => Promise | Value) export class CreatedAction< Manifest extends SDKManifest, Store, @@ -27,7 +22,6 @@ export class CreatedAction< >, readonly fn: (options: { effects: Effects - utils: Utils input: Type }) => Promise, readonly input: Config, @@ -44,11 +38,7 @@ export class CreatedAction< Type extends Record = ExtractConfigType, >( metaData: MaybeFn>, - fn: (options: { - effects: Effects - utils: Utils - input: Type - }) => Promise, + fn: (options: { effects: Effects; input: Type }) => Promise, inputConfig: Config | Config, ) { return new CreatedAction( @@ -61,7 +51,6 @@ export class CreatedAction< exportedAction: ExportedAction = ({ effects, input }) => { return this.fn({ effects, - utils: createUtils(effects), input: this.validator.unsafeCast(input), }) } @@ -69,21 +58,17 @@ export class CreatedAction< run = async ({ effects, input }: { effects: Effects; input?: Type }) => { return this.fn({ effects, - utils: createUtils(effects), input: this.validator.unsafeCast(input), }) } - async metaData(options: { effects: Effects; utils: Utils }) { + async metaData(options: { effects: Effects }) { if (this.myMetaData instanceof Function) return await this.myMetaData(options) return this.myMetaData } - async ActionMetadata(options: { - effects: Effects - utils: Utils - }): Promise { + async ActionMetadata(options: { effects: Effects }): Promise { return { ...(await this.metaData(options)), input: await this.input.build(options), @@ -93,7 +78,6 @@ export class CreatedAction< async getConfig({ effects }: { effects: Effects }) { return this.input.build({ effects, - utils: createUtils(effects) as any, }) } } diff --git a/sdk/lib/actions/setupActions.ts b/sdk/lib/actions/setupActions.ts index 84a0e4345..035f8dafa 100644 --- a/sdk/lib/actions/setupActions.ts +++ b/sdk/lib/actions/setupActions.ts @@ -1,17 +1,12 @@ import { SDKManifest } from "../manifest/ManifestTypes" import { Effects, ExpectedExports } from "../types" -import { createUtils } from "../util" import { once } from "../util/once" -import { Utils } from "../util/utils" import { CreatedAction } from "./createAction" export function setupActions( ...createdActions: CreatedAction[] ) { - const myActions = async (options: { - effects: Effects - utils: Utils - }) => { + const myActions = async (options: { effects: Effects }) => { const actions: Record> = {} for (const action of createdActions) { const actionMetadata = await action.metaData(options) @@ -24,17 +19,11 @@ export function setupActions( actionsMetadata: ExpectedExports.actionsMetadata } = { actions(options: { effects: Effects }) { - const utils = createUtils(options.effects) - - return myActions({ - ...options, - utils, - }) + return myActions(options) }, async actionsMetadata({ effects }: { effects: Effects }) { - const utils = createUtils(effects) return Promise.all( - createdActions.map((x) => x.ActionMetadata({ effects, utils })), + createdActions.map((x) => x.ActionMetadata({ effects })), ) }, } diff --git a/sdk/lib/backup/Backups.ts b/sdk/lib/backup/Backups.ts index 659da0ec7..20099c86d 100644 --- a/sdk/lib/backup/Backups.ts +++ b/sdk/lib/backup/Backups.ts @@ -42,7 +42,7 @@ export class Backups { private constructor( private options = DEFAULT_OPTIONS, - private backupSet = [] as BackupSet[], + private backupSet = [] as BackupSet[], ) {} static volumes( ...volumeNames: Array diff --git a/sdk/lib/backup/setupBackups.ts b/sdk/lib/backup/setupBackups.ts index d171a4aa7..af2d08410 100644 --- a/sdk/lib/backup/setupBackups.ts +++ b/sdk/lib/backup/setupBackups.ts @@ -4,7 +4,7 @@ import { ExpectedExports } from "../types" import { _ } from "../util" export type SetupBackupsParams = Array< - M["volumes"][0] | Backups + M["volumes"][number] | Backups > export function setupBackups( diff --git a/sdk/lib/config/builder/config.ts b/sdk/lib/config/builder/config.ts index 81009abaa..c30f37890 100644 --- a/sdk/lib/config/builder/config.ts +++ b/sdk/lib/config/builder/config.ts @@ -1,5 +1,4 @@ import { ValueSpec } from "../configTypes" -import { Utils } from "../../util/utils" import { Value } from "./value" import { _ } from "../../util" import { Effects } from "../../types" @@ -7,7 +6,6 @@ import { Parser, object } from "ts-matches" export type LazyBuildOptions = { effects: Effects - utils: Utils } export type LazyBuild = ( options: LazyBuildOptions, diff --git a/sdk/lib/config/configConstants.ts b/sdk/lib/config/configConstants.ts index 13cfe32b9..aa0e024c9 100644 --- a/sdk/lib/config/configConstants.ts +++ b/sdk/lib/config/configConstants.ts @@ -1,4 +1,5 @@ import { SmtpValue } from "../types" +import { GetSystemSmtp } from "../util/GetSystemSmtp" import { email } from "../util/patterns" import { Config, ConfigSpecOf } from "./builder/config" import { Value } from "./builder/value" @@ -47,8 +48,8 @@ export const customSmtp = Config.of, never>({ * For service config. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings */ export const smtpConfig = Value.filteredUnion( - async ({ effects, utils }) => { - const smtp = await utils.getSystemSmtp().once() + async ({ effects }) => { + const smtp = await new GetSystemSmtp(effects).once() return smtp ? [] : ["system"] }, { diff --git a/sdk/lib/config/configDependencies.ts b/sdk/lib/config/configDependencies.ts index 6b31abc81..be0475b0f 100644 --- a/sdk/lib/config/configDependencies.ts +++ b/sdk/lib/config/configDependencies.ts @@ -3,7 +3,7 @@ import { Dependency } from "../types" export type ConfigDependencies = { exists(id: keyof T["dependencies"]): Dependency - running(id: keyof T["dependencies"]): Dependency + running(id: keyof T["dependencies"], healthChecks: string[]): Dependency } export const configDependenciesSet = < @@ -16,10 +16,11 @@ export const configDependenciesSet = < } as Dependency }, - running(id: keyof T["dependencies"]) { + running(id: keyof T["dependencies"], healthChecks: string[]) { return { id, kind: "running", + healthChecks, } as Dependency }, }) diff --git a/sdk/lib/config/setupConfig.ts b/sdk/lib/config/setupConfig.ts index 95f9fd1ac..8519eb358 100644 --- a/sdk/lib/config/setupConfig.ts +++ b/sdk/lib/config/setupConfig.ts @@ -2,7 +2,6 @@ import { Effects, ExpectedExports } from "../types" import { SDKManifest } from "../manifest/ManifestTypes" import * as D from "./configDependencies" import { Config, ExtractConfigType } from "./builder/config" -import { Utils, createUtils } from "../util/utils" import nullIfEmpty from "../util/nullIfEmpty" import { InterfaceReceipt } from "../interfaces/interfaceReceipt" import { InterfacesReceipt as InterfacesReceipt } from "../interfaces/setupInterfaces" @@ -22,7 +21,6 @@ export type Save< > = (options: { effects: Effects input: ExtractConfigType & Record - utils: Utils dependencies: D.ConfigDependencies }) => Promise<{ dependenciesReceipt: DependenciesReceipt @@ -38,7 +36,6 @@ export type Read< | Config, never>, > = (options: { effects: Effects - utils: Utils }) => Promise & Record)> /** * We want to setup a config export with a get and set, this @@ -72,7 +69,6 @@ export function setupConfig< const { restart } = await write({ input: JSON.parse(JSON.stringify(input)), effects, - utils: createUtils(effects), dependencies: D.configDependenciesSet(), }) if (restart) { @@ -80,14 +76,10 @@ export function setupConfig< } }) as ExpectedExports.setConfig, getConfig: (async ({ effects }) => { - const myUtils = createUtils(effects) - const configValue = nullIfEmpty( - (await read({ effects, utils: myUtils })) || null, - ) + const configValue = nullIfEmpty((await read({ effects })) || null) return { spec: await spec.build({ effects, - utils: myUtils as any, }), config: configValue, } diff --git a/sdk/lib/dependencyConfig/DependencyConfig.ts b/sdk/lib/dependencyConfig/DependencyConfig.ts index 10dcb4bd8..d7ce435ad 100644 --- a/sdk/lib/dependencyConfig/DependencyConfig.ts +++ b/sdk/lib/dependencyConfig/DependencyConfig.ts @@ -3,7 +3,6 @@ import { DeepPartial, Effects, } from "../types" -import { Utils, createUtils } from "../util/utils" import { deepEqual } from "../util/deepEqual" import { deepMerge } from "../util/deepMerge" import { SDKManifest } from "../manifest/ManifestTypes" @@ -29,7 +28,6 @@ export class DependencyConfig< readonly dependencyConfig: (options: { effects: Effects localConfig: Input - utils: Utils }) => Promise>, readonly update: Update< void | DeepPartial, @@ -41,7 +39,6 @@ export class DependencyConfig< return this.dependencyConfig({ localConfig: options.localConfig as Input, effects: options.effects, - utils: createUtils(options.effects), }) } } diff --git a/sdk/lib/health/HealthCheck.ts b/sdk/lib/health/HealthCheck.ts index 652aed94e..488ecdcf5 100644 --- a/sdk/lib/health/HealthCheck.ts +++ b/sdk/lib/health/HealthCheck.ts @@ -6,52 +6,59 @@ import { Trigger } from "../trigger" import { TriggerInput } from "../trigger/TriggerInput" import { defaultTrigger } from "../trigger/defaultTrigger" import { once } from "../util/once" +import { Overlay } from "../util/Overlay" export function healthCheck(o: { effects: Effects name: string + imageId: string trigger?: Trigger - fn(): Promise | CheckResult + fn(overlay: Overlay): Promise | CheckResult onFirstSuccess?: () => unknown | Promise }) { new Promise(async () => { - let currentValue: TriggerInput = { - hadSuccess: false, - } - const getCurrentValue = () => currentValue - const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue) - const triggerFirstSuccess = once(() => - Promise.resolve( - "onFirstSuccess" in o && o.onFirstSuccess - ? o.onFirstSuccess() - : undefined, - ), - ) - for ( - let res = await trigger.next(); - !res.done; - res = await trigger.next() - ) { - try { - const { status, message } = await o.fn() - await o.effects.setHealth({ - name: o.name, - status, - message, - }) - currentValue.hadSuccess = true - currentValue.lastResult = "passing" - await triggerFirstSuccess().catch((err) => { - console.error(err) - }) - } catch (e) { - await o.effects.setHealth({ - name: o.name, - status: "failure", - message: asMessage(e), - }) - currentValue.lastResult = "failure" + const overlay = await Overlay.of(o.effects, o.imageId) + try { + let currentValue: TriggerInput = { + hadSuccess: false, } + const getCurrentValue = () => currentValue + const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue) + const triggerFirstSuccess = once(() => + Promise.resolve( + "onFirstSuccess" in o && o.onFirstSuccess + ? o.onFirstSuccess() + : undefined, + ), + ) + for ( + let res = await trigger.next(); + !res.done; + res = await trigger.next() + ) { + try { + const { status, message } = await o.fn(overlay) + await o.effects.setHealth({ + name: o.name, + status, + message, + }) + currentValue.hadSuccess = true + currentValue.lastResult = "passing" + await triggerFirstSuccess().catch((err) => { + console.error(err) + }) + } catch (e) { + await o.effects.setHealth({ + name: o.name, + status: "failure", + message: asMessage(e), + }) + currentValue.lastResult = "failure" + } + } + } finally { + await overlay.destroy() } }) return {} as HealthReceipt diff --git a/sdk/lib/health/checkFns/checkPortListening.ts b/sdk/lib/health/checkFns/checkPortListening.ts index a82b75fd4..89dcf89b3 100644 --- a/sdk/lib/health/checkFns/checkPortListening.ts +++ b/sdk/lib/health/checkFns/checkPortListening.ts @@ -1,7 +1,12 @@ import { Effects } from "../../types" -import { createUtils } from "../../util" import { stringFromStdErrOut } from "../../util/stringFromStdErrOut" import { CheckResult } from "./CheckResult" + +import { promisify } from "node:util" +import * as CP from "node:child_process" + +const cpExec = promisify(CP.exec) +const cpExecFile = promisify(CP.execFile) export function containsAddress(x: string, port: number) { const readPorts = x .split("\n") @@ -28,20 +33,15 @@ export async function checkPortListening( timeout?: number }, ): Promise { - const utils = createUtils(effects) return Promise.race([ Promise.resolve().then(async () => { const hasAddress = containsAddress( - await utils.childProcess - .exec(`cat /proc/net/tcp`, {}) - .then(stringFromStdErrOut), + await cpExec(`cat /proc/net/tcp`, {}).then(stringFromStdErrOut), port, ) || containsAddress( - await utils.childProcess - .exec("cat /proc/net/udp", {}) - .then(stringFromStdErrOut), + await cpExec("cat /proc/net/udp", {}).then(stringFromStdErrOut), port, ) if (hasAddress) { diff --git a/sdk/lib/health/checkFns/runHealthScript.ts b/sdk/lib/health/checkFns/runHealthScript.ts index 659c787f8..5d69f5e17 100644 --- a/sdk/lib/health/checkFns/runHealthScript.ts +++ b/sdk/lib/health/checkFns/runHealthScript.ts @@ -1,5 +1,5 @@ -import { CommandType, Effects } from "../../types" -import { createUtils } from "../../util" +import { Effects } from "../../types" +import { Overlay } from "../../util/Overlay" import { stringFromStdErrOut } from "../../util/stringFromStdErrOut" import { CheckResult } from "./CheckResult" import { timeoutPromise } from "./index" @@ -13,7 +13,8 @@ import { timeoutPromise } from "./index" */ export const runHealthScript = async ( effects: Effects, - runCommand: string, + runCommand: string[], + overlay: Overlay, { timeout = 30000, errorMessage = `Error while running command: ${runCommand}`, @@ -21,9 +22,8 @@ export const runHealthScript = async ( `Have ran script ${runCommand} and the result: ${res}`, } = {}, ): Promise => { - const utils = createUtils(effects) const res = await Promise.race([ - utils.childProcess.exec(runCommand, { timeout }).then(stringFromStdErrOut), + overlay.exec(runCommand), timeoutPromise(timeout), ]).catch((e) => { console.warn(errorMessage) @@ -33,6 +33,6 @@ export const runHealthScript = async ( }) return { status: "passing", - message: message(res), + message: message(res.stdout.toString()), } as CheckResult } diff --git a/sdk/lib/index.ts b/sdk/lib/index.ts index aff2ef7ed..2b22e2c83 100644 --- a/sdk/lib/index.ts +++ b/sdk/lib/index.ts @@ -1,9 +1,9 @@ export { Daemons } from "./mainFn/Daemons" export { EmVer } from "./emverLite/mod" export { Overlay } from "./util/Overlay" -export { Utils } from "./util/utils" export { StartSdk } from "./StartSdk" export { setupManifest } from "./manifest/setupManifest" +export { FileHelper } from "./util/fileHelper" export * as actions from "./actions" export * as backup from "./backup" export * as config from "./config" diff --git a/sdk/lib/inits/migrations/Migration.ts b/sdk/lib/inits/migrations/Migration.ts index 06e8e6e39..119271aea 100644 --- a/sdk/lib/inits/migrations/Migration.ts +++ b/sdk/lib/inits/migrations/Migration.ts @@ -1,6 +1,5 @@ import { ManifestVersion, SDKManifest } from "../../manifest/ManifestTypes" import { Effects } from "../../types" -import { Utils } from "../../util/utils" export class Migration< Manifest extends SDKManifest, @@ -10,14 +9,8 @@ export class Migration< constructor( readonly options: { version: Version - up: (opts: { - effects: Effects - utils: Utils - }) => Promise - down: (opts: { - effects: Effects - utils: Utils - }) => Promise + up: (opts: { effects: Effects }) => Promise + down: (opts: { effects: Effects }) => Promise }, ) {} static of< @@ -26,23 +19,17 @@ export class Migration< Version extends ManifestVersion, >(options: { version: Version - up: (opts: { - effects: Effects - utils: Utils - }) => Promise - down: (opts: { - effects: Effects - utils: Utils - }) => Promise + up: (opts: { effects: Effects }) => Promise + down: (opts: { effects: Effects }) => Promise }) { return new Migration(options) } - async up(opts: { effects: Effects; utils: Utils }) { + async up(opts: { effects: Effects }) { this.up(opts) } - async down(opts: { effects: Effects; utils: Utils }) { + async down(opts: { effects: Effects }) { this.down(opts) } } diff --git a/sdk/lib/inits/migrations/setupMigrations.ts b/sdk/lib/inits/migrations/setupMigrations.ts index dabe3122c..288b2b9d7 100644 --- a/sdk/lib/inits/migrations/setupMigrations.ts +++ b/sdk/lib/inits/migrations/setupMigrations.ts @@ -1,7 +1,6 @@ import { EmVer } from "../../emverLite/mod" import { SDKManifest } from "../../manifest/ManifestTypes" import { ExpectedExports } from "../../types" -import { createUtils } from "../../util" import { once } from "../../util/once" import { Migration } from "./Migration" @@ -32,13 +31,12 @@ export class Migrations { effects, previousVersion, }: Parameters[0]) { - const utils = createUtils(effects) if (!!previousVersion) { const previousVersionEmVer = EmVer.parse(previousVersion) for (const [_, migration] of this.sortedMigrations() .filter((x) => x[0].greaterThan(previousVersionEmVer)) .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { - await migration.up({ effects, utils }) + await migration.up({ effects }) } } } @@ -46,14 +44,13 @@ export class Migrations { effects, nextVersion, }: Parameters[0]) { - const utils = createUtils(effects) if (!!nextVersion) { const nextVersionEmVer = EmVer.parse(nextVersion) const reversed = [...this.sortedMigrations()].reverse() for (const [_, migration] of reversed .filter((x) => x[0].greaterThan(nextVersionEmVer)) .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { - await migration.down({ effects, utils }) + await migration.down({ effects }) } } } diff --git a/sdk/lib/inits/setupExports.ts b/sdk/lib/inits/setupExports.ts index bad6dd9ad..5f7c2b23f 100644 --- a/sdk/lib/inits/setupExports.ts +++ b/sdk/lib/inits/setupExports.ts @@ -1,16 +1,12 @@ import { Effects, ExposeServicePaths, ExposeUiPaths } from "../types" -import { Utils } from "../util/utils" -export type SetupExports = (opts: { - effects: Effects - utils: Utils -}) => +export type SetupExports = (opts: { effects: Effects }) => | { - ui: ExposeUiPaths + ui: { [k: string]: ExposeUiPaths } services: ExposeServicePaths } | Promise<{ - ui: ExposeUiPaths + ui: { [k: string]: ExposeUiPaths } services: ExposeServicePaths }> diff --git a/sdk/lib/inits/setupInit.ts b/sdk/lib/inits/setupInit.ts index 9139de23b..2df688f18 100644 --- a/sdk/lib/inits/setupInit.ts +++ b/sdk/lib/inits/setupInit.ts @@ -1,7 +1,6 @@ import { SetInterfaces } from "../interfaces/setupInterfaces" import { SDKManifest } from "../manifest/ManifestTypes" -import { ExpectedExports } from "../types" -import { createUtils } from "../util" +import { ExpectedExports, ExposeUiPaths, ExposeUiPathsAll } from "../types" import { Migrations } from "./migrations/setupMigrations" import { SetupExports } from "./setupExports" import { Install } from "./setupInstall" @@ -19,20 +18,20 @@ export function setupInit( } { return { init: async (opts) => { - const utils = createUtils(opts.effects) await migrations.init(opts) await install.init(opts) await setInterfaces({ ...opts, input: null, - utils, - }) - const { services, ui } = await setupExports({ - ...opts, - utils, }) + const { services, ui } = await setupExports(opts) await opts.effects.exposeForDependents(services) - await opts.effects.exposeUi({ paths: ui }) + await opts.effects.exposeUi( + forExpose({ + type: "object", + value: ui, + }), + ) }, uninit: async (opts) => { await migrations.uninit(opts) @@ -40,3 +39,21 @@ export function setupInit( }, } } + +function forExpose(ui: ExposeUiPaths): ExposeUiPathsAll { + if (ui.type === ("object" as const)) { + return { + type: "object" as const, + value: Object.fromEntries( + Object.entries(ui.value).map(([key, value]) => [key, forExpose(value)]), + ), + } + } + return { + description: null, + + copyable: null, + qr: null, + ...ui, + } +} diff --git a/sdk/lib/inits/setupInstall.ts b/sdk/lib/inits/setupInstall.ts index e49c0b545..3990be0ca 100644 --- a/sdk/lib/inits/setupInstall.ts +++ b/sdk/lib/inits/setupInstall.ts @@ -1,10 +1,8 @@ import { SDKManifest } from "../manifest/ManifestTypes" import { Effects, ExpectedExports } from "../types" -import { Utils, createUtils } from "../util/utils" export type InstallFn = (opts: { effects: Effects - utils: Utils }) => Promise export class Install { private constructor(readonly fn: InstallFn) {} @@ -21,7 +19,6 @@ export class Install { if (!previousVersion) await this.fn({ effects, - utils: createUtils(effects), }) } } diff --git a/sdk/lib/inits/setupUninstall.ts b/sdk/lib/inits/setupUninstall.ts index b411d2fc7..812848c8f 100644 --- a/sdk/lib/inits/setupUninstall.ts +++ b/sdk/lib/inits/setupUninstall.ts @@ -1,10 +1,8 @@ import { SDKManifest } from "../manifest/ManifestTypes" import { Effects, ExpectedExports } from "../types" -import { Utils, createUtils } from "../util/utils" export type UninstallFn = (opts: { effects: Effects - utils: Utils }) => Promise export class Uninstall { private constructor(readonly fn: UninstallFn) {} @@ -21,7 +19,6 @@ export class Uninstall { if (!nextVersion) await this.fn({ effects, - utils: createUtils(effects), }) } } diff --git a/sdk/lib/interfaces/Host.ts b/sdk/lib/interfaces/Host.ts index 4c3f43634..250f42075 100644 --- a/sdk/lib/interfaces/Host.ts +++ b/sdk/lib/interfaces/Host.ts @@ -59,12 +59,13 @@ type AddSslOptions = { preferredExternalPort: number addXForwardedHeaders: boolean | null /** default: false */ } -type Security = { secure: false; ssl: false } | { secure: true; ssl: boolean } +type Security = { ssl: boolean } export type BindOptions = { scheme: Scheme preferredExternalPort: number addSsl: AddSslOptions | null -} & Security + secure: Security | null +} type KnownProtocols = typeof knownProtocols type ProtocolsWithSslVariants = { [K in keyof KnownProtocols]: KnownProtocols[K] extends { @@ -79,16 +80,17 @@ type NotProtocolsWithSslVariants = Exclude< > type BindOptionsByKnownProtocol = - | ({ + | { protocol: ProtocolsWithSslVariants preferredExternalPort?: number - scheme: Scheme | null - } & ({ noAddSsl: true } | { addSsl?: Partial })) + scheme?: Scheme + addSsl?: Partial + } | { protocol: NotProtocolsWithSslVariants preferredExternalPort?: number - scheme: Scheme | null - addSsl: AddSslOptions | null + scheme?: Scheme + addSsl?: AddSslOptions } type BindOptionsByProtocol = BindOptionsByKnownProtocol | BindOptions @@ -120,17 +122,12 @@ export class Host { private async bindPortForUnknown( internalPort: number, - options: - | ({ - scheme: Scheme - preferredExternalPort: number - addSsl: AddSslOptions | null - } & { secure: false; ssl: false }) - | ({ - scheme: Scheme - preferredExternalPort: number - addSsl: AddSslOptions | null - } & { secure: true; ssl: boolean }), + options: { + scheme: Scheme + preferredExternalPort: number + addSsl: AddSslOptions | null + secure: { ssl: boolean } | null + }, ) { await this.options.effects.bind({ kind: this.options.kind, @@ -154,18 +151,13 @@ export class Host { knownProtocols[options.protocol].defaultPort const addSsl = this.getAddSsl(options, protoInfo) - const security: Security = !protoInfo.secure - ? { - secure: protoInfo.secure, - ssl: protoInfo.ssl, - } - : { secure: false, ssl: false } + const secure: Security | null = !protoInfo.secure ? null : { ssl: false } const newOptions = { scheme, preferredExternalPort, addSsl, - ...security, + secure, } await this.options.effects.bind({ diff --git a/sdk/lib/interfaces/ServiceInterfaceBuilder.ts b/sdk/lib/interfaces/ServiceInterfaceBuilder.ts index c7b99b2d5..14eaee1d3 100644 --- a/sdk/lib/interfaces/ServiceInterfaceBuilder.ts +++ b/sdk/lib/interfaces/ServiceInterfaceBuilder.ts @@ -1,5 +1,5 @@ +import { ServiceInterfaceType } from "../StartSdk" import { Effects } from "../types" -import { ServiceInterfaceType } from "../util/utils" import { Scheme } from "./Host" /** diff --git a/sdk/lib/interfaces/setupInterfaces.ts b/sdk/lib/interfaces/setupInterfaces.ts index 1514cabf3..5ad8d8a7d 100644 --- a/sdk/lib/interfaces/setupInterfaces.ts +++ b/sdk/lib/interfaces/setupInterfaces.ts @@ -1,7 +1,6 @@ import { Config } from "../config/builder/config" import { SDKManifest } from "../manifest/ManifestTypes" import { AddressInfo, Effects } from "../types" -import { Utils } from "../util/utils" import { AddressReceipt } from "./AddressReceipt" export type InterfacesReceipt = Array @@ -10,11 +9,7 @@ export type SetInterfaces< Store, ConfigInput extends Record, Output extends InterfacesReceipt, -> = (opts: { - effects: Effects - input: null | ConfigInput - utils: Utils -}) => Promise +> = (opts: { effects: Effects; input: null | ConfigInput }) => Promise export type SetupInterfaces = < Manifest extends SDKManifest, Store, diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index 442ef7c46..300b61b83 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -1,3 +1,4 @@ +import { NO_TIMEOUT, SIGKILL, SIGTERM, Signals } from "../StartSdk" import { HealthReceipt } from "../health/HealthReceipt" import { CheckResult } from "../health/checkFns" import { SDKManifest } from "../manifest/ManifestTypes" @@ -5,9 +6,22 @@ import { Trigger } from "../trigger" import { TriggerInput } from "../trigger/TriggerInput" import { defaultTrigger } from "../trigger/defaultTrigger" import { DaemonReturned, Effects, ValidIfNoStupidEscape } from "../types" -import { createUtils } from "../util" -import { Signals } from "../util/utils" import { Mounts } from "./Mounts" +import { CommandOptions, MountOptions, Overlay } from "../util/Overlay" +import { splitCommand } from "../util/splitCommand" + +import { promisify } from "node:util" +import * as CP from "node:child_process" + +const cpExec = promisify(CP.exec) +const cpExecFile = promisify(CP.execFile) +async function psTree(pid: number, overlay: Overlay): Promise { + const { stdout } = await cpExec(`pstree -p ${pid}`) + const regex: RegExp = /\((\d+)\)/g + return [...stdout.toString().matchAll(regex)].map(([_all, pid]) => + parseInt(pid), + ) +} type Daemon< Manifest extends SDKManifest, Ids extends string, @@ -28,6 +42,89 @@ type Daemon< } type ErrorDuplicateId = `The id '${Id}' is already used` + +const runDaemon = + () => + async ( + effects: Effects, + imageId: Manifest["images"][number], + command: ValidIfNoStupidEscape | [string, ...string[]], + options: CommandOptions & { + mounts?: { path: string; options: MountOptions }[] + overlay?: Overlay + }, + ): Promise => { + const commands = splitCommand(command) + const overlay = options.overlay || (await Overlay.of(effects, imageId)) + for (let mount of options.mounts || []) { + await overlay.mount(mount.options, mount.path) + } + const childProcess = await overlay.spawn(commands, { + env: options.env, + }) + const answer = new Promise((resolve, reject) => { + childProcess.stdout.on("data", (data: any) => { + console.log(data.toString()) + }) + childProcess.stderr.on("data", (data: any) => { + console.error(data.toString()) + }) + + childProcess.on("exit", (code: any) => { + if (code === 0) { + return resolve(null) + } + return reject(new Error(`${commands[0]} exited with code ${code}`)) + }) + }) + + const pid = childProcess.pid + return { + async wait() { + const pids = pid ? await psTree(pid, overlay) : [] + try { + return await answer + } finally { + for (const process of pids) { + cpExecFile("kill", [`-9`, String(process)]).catch((_) => {}) + } + } + }, + async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) { + const pids = pid ? await psTree(pid, overlay) : [] + try { + childProcess.kill(signal) + + if (timeout > NO_TIMEOUT) { + const didTimeout = await Promise.race([ + new Promise((resolve) => setTimeout(resolve, timeout)).then( + () => true, + ), + answer.then(() => false), + ]) + if (didTimeout) { + childProcess.kill(SIGKILL) + } + } else { + await answer + } + } finally { + await overlay.destroy() + } + + try { + for (const process of pids) { + await cpExecFile("kill", [`-${signal}`, String(process)]) + } + } finally { + for (const process of pids) { + cpExecFile("kill", [`-9`, String(process)]).catch((_) => {}) + } + } + }, + } + } + /** * A class for defining and controlling the service daemons ```ts @@ -106,9 +203,8 @@ export class Daemons { ) daemonsStarted[daemon.id] = requiredPromise.then(async () => { const { command, imageId } = daemon - const utils = createUtils(effects) - const child = utils.runDaemon(imageId, command, { + const child = runDaemon()(effects, imageId, command, { env: daemon.env, mounts: daemon.mounts.build(), }) diff --git a/sdk/lib/mainFn/index.ts b/sdk/lib/mainFn/index.ts index 58f0228b2..3da57d32f 100644 --- a/sdk/lib/mainFn/index.ts +++ b/sdk/lib/mainFn/index.ts @@ -1,12 +1,11 @@ -import { Effects, ExpectedExports } from "../types" -import { createMainUtils } from "../util" -import { Utils, createUtils } from "../util/utils" +import { ExpectedExports } from "../types" import { Daemons } from "./Daemons" import "../interfaces/ServiceInterfaceBuilder" import "../interfaces/Origin" import "./Daemons" import { SDKManifest } from "../manifest/ManifestTypes" +import { MainEffects } from "../StartSdk" /** * Used to ensure that the main function is running with the valid proofs. @@ -20,16 +19,12 @@ import { SDKManifest } from "../manifest/ManifestTypes" */ export const setupMain = ( fn: (o: { - effects: Effects + effects: MainEffects started(onTerm: () => PromiseLike): PromiseLike - utils: Utils }) => Promise>, ): ExpectedExports.main => { return async (options) => { - const result = await fn({ - ...options, - utils: createMainUtils(options.effects), - }) + const result = await fn(options) return result } } diff --git a/sdk/lib/test/configBuilder.test.ts b/sdk/lib/test/configBuilder.test.ts index fe9d123ea..ef85ee366 100644 --- a/sdk/lib/test/configBuilder.test.ts +++ b/sdk/lib/test/configBuilder.test.ts @@ -4,6 +4,8 @@ import { List } from "../config/builder/list" import { Value } from "../config/builder/value" import { Variants } from "../config/builder/variants" import { ValueSpec } from "../config/configTypes" +import { setupManifest } from "../manifest/setupManifest" +import { StartSdk } from "../StartSdk" describe("builder tests", () => { test("text", async () => { @@ -379,17 +381,61 @@ describe("values", () => { }) }) test("datetime", async () => { - const value = Value.dynamicDatetime<{ test: "a" }>(async ({ utils }) => { - ;async () => { - ;(await utils.store.getOwn("/test").once()) satisfies "a" - } + const sdk = StartSdk.of() + .withManifest( + setupManifest({ + id: "testOutput", + title: "", + version: "1.0", + releaseNotes: "", + license: "", + replaces: [], + wrapperRepo: "", + upstreamRepo: "", + supportSite: "", + marketingSite: "", + donationUrl: null, + description: { + short: "", + long: "", + }, + containers: {}, + images: [], + volumes: [], + assets: [], + alerts: { + install: null, + update: null, + uninstall: null, + restore: null, + start: null, + stop: null, + }, + dependencies: { + remoteTest: { + description: "", + requirement: { how: "", type: "opt-in" }, + version: "1.0", + }, + }, + }), + ) + .withStore<{ test: "a" }>() + .build(true) - return { - name: "Testing", - required: { default: null }, - inputmode: "date", - } - }) + const value = Value.dynamicDatetime<{ test: "a" }>( + async ({ effects }) => { + ;async () => { + ;(await sdk.store.getOwn(effects, "/test").once()) satisfies "a" + } + + return { + name: "Testing", + required: { default: null }, + inputmode: "date", + } + }, + ) const validator = value.validator validator.unsafeCast("2021-01-01") validator.unsafeCast(null) diff --git a/sdk/lib/test/host.test.ts b/sdk/lib/test/host.test.ts index 89776cf66..82372f61b 100644 --- a/sdk/lib/test/host.test.ts +++ b/sdk/lib/test/host.test.ts @@ -1,15 +1,13 @@ import { ServiceInterfaceBuilder } from "../interfaces/ServiceInterfaceBuilder" import { Effects } from "../types" -import { createUtils } from "../util" +import { sdk } from "./output.sdk" describe("host", () => { test("Testing that the types work", () => { async function test(effects: Effects) { - const utils = createUtils(effects) - const foo = utils.host.multi("foo") + const foo = sdk.host.multi(effects, "foo") const fooOrigin = await foo.bindPort(80, { protocol: "http" as const, - scheme: null, }) const fooInterface = new ServiceInterfaceBuilder({ effects, diff --git a/sdk/lib/test/startosTypeValidation.test.ts b/sdk/lib/test/startosTypeValidation.test.ts index b07d6dd0c..5f1be60bb 100644 --- a/sdk/lib/test/startosTypeValidation.test.ts +++ b/sdk/lib/test/startosTypeValidation.test.ts @@ -13,40 +13,25 @@ import { ExposeUiParams } from "../../../core/startos/bindings/ExposeUiParams" import { GetSslCertificateParams } from "../../../core/startos/bindings/GetSslCertificateParams" import { GetSslKeyParams } from "../../../core/startos/bindings/GetSslKeyParams" import { GetServiceInterfaceParams } from "../../../core/startos/bindings/GetServiceInterfaceParams" - +import { SetDependenciesParams } from "../../../core/startos/bindings/SetDependenciesParams" +import { GetSystemSmtpParams } from "../../../core/startos/bindings/GetSystemSmtpParams" +import { GetServicePortForwardParams } from "../../../core/startos/bindings/GetServicePortForwardParams" +import { ExportServiceInterfaceParams } from "../../../core/startos/bindings/ExportServiceInterfaceParams" +import { GetPrimaryUrlParams } from "../../../core/startos/bindings/GetPrimaryUrlParams" +import { ListServiceInterfacesParams } from "../../../core/startos/bindings/ListServiceInterfacesParams" +import { RemoveAddressParams } from "../../../core/startos/bindings/RemoveAddressParams" +import { ExportActionParams } from "../../../core/startos/bindings/ExportActionParams" +import { RemoveActionParams } from "../../../core/startos/bindings/RemoveActionParams" +import { ReverseProxyParams } from "../../../core/startos/bindings/ReverseProxyParams" +import { MountParams } from "../../../core/startos/bindings/MountParams" function typeEquality(_a: ExpectedType) {} describe("startosTypeValidation ", () => { test(`checking the params match`, () => { const testInput: any = {} typeEquality<{ - [K in keyof Effects & - ( - | "gitInfo" - | "echo" - | "chroot" - | "exists" - | "executeAction" - | "getConfigured" - | "stopped" - | "running" - | "restart" - | "shutdown" - | "setConfigured" - | "setMainStatus" - | "setHealth" - | "getStore" - | "setStore" - | "exposeForDependents" - | "exposeUi" - | "createOverlayedImage" - | "destroyOverlayedImage" - | "getSslCertificate" - | "getSslKey" - | "getServiceInterface" - | "clearBindings" - | "bind" - | "getHostInfo" - )]: Effects[K] extends Function ? Parameters[0] : never + [K in keyof Effects]: Effects[K] extends (args: infer A) => any + ? A + : never }>({ executeAction: {} as ExecuteAction, createOverlayedImage: {} as CreateOverlayedImageParams, @@ -57,7 +42,7 @@ describe("startosTypeValidation ", () => { exists: {} as ParamsPackageId, getConfigured: undefined, stopped: {} as ParamsMaybePackageId, - running: {} as ParamsMaybePackageId, + running: {} as ParamsPackageId, restart: undefined, shutdown: undefined, setConfigured: {} as SetConfigured, @@ -67,6 +52,20 @@ describe("startosTypeValidation ", () => { getSslCertificate: {} as GetSslCertificateParams, getSslKey: {} as GetSslKeyParams, getServiceInterface: {} as GetServiceInterfaceParams, + setDependencies: {} as SetDependenciesParams, + store: {} as never, + getSystemSmtp: {} as GetSystemSmtpParams, + getContainerIp: undefined, + getServicePortForward: {} as GetServicePortForwardParams, + clearServiceInterfaces: undefined, + exportServiceInterface: {} as ExportServiceInterfaceParams, + getPrimaryUrl: {} as GetPrimaryUrlParams, + listServiceInterfaces: {} as ListServiceInterfacesParams, + removeAddress: {} as RemoveAddressParams, + exportAction: {} as ExportActionParams, + removeAction: {} as RemoveActionParams, + reverseProxy: {} as ReverseProxyParams, + mount: {} as MountParams, }) typeEquality[0]>( testInput as ExecuteAction, diff --git a/sdk/lib/test/store.test.ts b/sdk/lib/test/store.test.ts index 2ed8c4dfd..c41f3c85a 100644 --- a/sdk/lib/test/store.test.ts +++ b/sdk/lib/test/store.test.ts @@ -1,6 +1,5 @@ +import { MainEffects, StartSdk } from "../StartSdk" import { Effects } from "../types" -import { createMainUtils } from "../util" -import { createUtils } from "../util/utils" type Store = { config: { @@ -12,26 +11,31 @@ const todo = (): A => { throw new Error("not implemented") } const noop = () => {} + +const sdk = StartSdk.of() + .withManifest({} as Manifest) + .withStore() + .build(true) + describe("Store", () => { test("types", async () => { ;async () => { - createUtils(todo()).store.setOwn("/config", { + sdk.store.setOwn(todo(), "/config", { someValue: "a", }) - createUtils(todo()).store.setOwn( - "/config/someValue", - "b", - ) - createUtils(todo()).store.setOwn("", { + sdk.store.setOwn(todo(), "/config/someValue", "b") + sdk.store.setOwn(todo(), "", { config: { someValue: "b" }, }) - createUtils(todo()).store.setOwn( + sdk.store.setOwn( + todo(), "/config/someValue", // @ts-expect-error Type is wrong for the setting value 5, ) - createUtils(todo()).store.setOwn( + sdk.store.setOwn( + todo(), // @ts-expect-error Path is wrong "/config/someVae3lue", "someValue", @@ -52,49 +56,47 @@ describe("Store", () => { path: "/config/some2Value", value: "a", }) - ;(await createMainUtils(todo()) - .store.getOwn("/config/someValue") + ;(await sdk.store + .getOwn(todo(), "/config/someValue") .const()) satisfies string - ;(await createMainUtils(todo()) - .store.getOwn("/config") + ;(await sdk.store + .getOwn(todo(), "/config") .const()) satisfies Store["config"] - await createMainUtils(todo()) - // @ts-expect-error Path is wrong - .store.getOwn("/config/somdsfeValue") + await sdk.store // @ts-expect-error Path is wrong + .getOwn(todo(), "/config/somdsfeValue") .const() /// ----------------- ERRORS ----------------- - createUtils(todo()).store.setOwn("", { + sdk.store.setOwn(todo(), "", { // @ts-expect-error Type is wrong for the setting value config: { someValue: "notInAOrB" }, }) - createUtils(todo()).store.setOwn( + sdk.store.setOwn( + todo(), "/config/someValue", // @ts-expect-error Type is wrong for the setting value "notInAOrB", ) - ;(await createUtils(todo()) - .store.getOwn("/config/someValue") + ;(await sdk.store + .getOwn(todo(), "/config/someValue") // @ts-expect-error Const should normally not be callable .const()) satisfies string - ;(await createUtils(todo()) - .store.getOwn("/config") + ;(await sdk.store + .getOwn(todo(), "/config") // @ts-expect-error Const should normally not be callable .const()) satisfies Store["config"] - await createUtils(todo()) - // @ts-expect-error Path is wrong - .store.getOwn("/config/somdsfeValue") + await sdk.store // @ts-expect-error Path is wrong + .getOwn("/config/somdsfeValue") // @ts-expect-error Const should normally not be callable .const() /// - ;(await createUtils(todo()) - .store.getOwn("/config/someValue") + ;(await sdk.store + .getOwn(todo(), "/config/someValue") // @ts-expect-error satisfies type is wrong .const()) satisfies number - ;(await createMainUtils(todo()) - // @ts-expect-error Path is wrong - .store.getOwn("/config/") + ;(await sdk.store // @ts-expect-error Path is wrong + .getOwn(todo(), "/config/") .const()) satisfies Store["config"] ;(await todo().store.get({ path: "/config/someValue", diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index d9215cb1b..35e345618 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -1,11 +1,11 @@ export * as configTypes from "./config/configTypes" import { AddSslOptions } from "../../core/startos/bindings/AddSslOptions" +import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk" import { InputSpec } from "./config/configTypes" import { DependenciesReceipt } from "./config/setupConfig" import { BindOptions, Scheme } from "./interfaces/Host" import { Daemons } from "./mainFn/Daemons" import { UrlString } from "./util/getServiceInterface" -import { ServiceInterfaceType, Signals } from "./util/utils" export type ExportedAction = (options: { effects: Effects @@ -59,7 +59,7 @@ export namespace ExpectedExports { * package represents, like running a bitcoind in a bitcoind-wrapper. */ export type main = (options: { - effects: Effects + effects: MainEffects started(onTerm: () => PromiseLike): PromiseLike }) => Promise> @@ -167,7 +167,7 @@ export type ActionMetadata = { /** * So the ordering of the actions is by alphabetical order of the group, then followed by the alphabetical of the actions */ - group?: string + group: string | null } export declare const hostName: unique symbol // asdflkjadsf.onion | 1.2.3.4 @@ -261,24 +261,47 @@ export type ExposeServicePaths = { paths: Store extends never ? string[] : ExposeAllServicePaths[] } -export type ExposeUiPaths = Array<{ - /** The path to the value in the Store. [JsonPath](https://jsonpath.com/) */ - path: ExposeAllUiPaths - /** A human readable title for the value */ - title: string - /** A human readable description or explanation of the value */ - description?: string - /** (string/number only) Whether or not to mask the value, for example, when displaying a password */ - masked?: boolean - /** (string/number only) Whether or not to include a button for copying the value to clipboard */ - copyable?: boolean - /** (string/number only) Whether or not to include a button for displaying the value as a QR code */ - qr?: boolean -}> +export type ExposeUiPaths = + | { + type: "object" + value: { [k: string]: ExposeUiPaths } + } + | { + type: "string" + /** The path to the value in the Store. [JsonPath](https://jsonpath.com/) */ + path: ExposeAllUiPaths + /** A human readable description or explanation of the value */ + description?: string + /** (string/number only) Whether or not to mask the value, for example, when displaying a password */ + masked: boolean + /** (string/number only) Whether or not to include a button for copying the value to clipboard */ + copyable?: boolean + /** (string/number only) Whether or not to include a button for displaying the value as a QR code */ + qr?: boolean + } +export type ExposeUiPathsAll = + | { + type: "object" + value: { [k: string]: ExposeUiPathsAll } + } + | { + type: "string" + /** The path to the value in the Store. [JsonPath](https://jsonpath.com/) */ + path: string + /** A human readable description or explanation of the value */ + description: string | null + /** (string/number only) Whether or not to mask the value, for example, when displaying a password */ + masked: boolean + /** (string/number only) Whether or not to include a button for copying the value to clipboard */ + copyable: boolean | null + /** (string/number only) Whether or not to include a button for displaying the value as a QR code */ + qr: boolean | null + } + /** Used to reach out from the pure js runtime */ export type Effects = { executeAction(opts: { - serviceId?: string + serviceId: string | null input: Input }): Promise @@ -286,10 +309,7 @@ export type Effects = { createOverlayedImage(options: { imageId: string }): Promise<[string, string]> /** A low level api used by destroyOverlay + makeOverlay:destroy */ - destroyOverlayedImage(options: { - imageId: string - guid: string - }): Promise + destroyOverlayedImage(options: { guid: string }): Promise /** Removes all network bindings */ clearBindings(): Promise @@ -302,8 +322,7 @@ export type Effects = { scheme: Scheme preferredExternalPort: number addSsl: AddSslOptions | null - secure: boolean - ssl: boolean + secure: { ssl: boolean } | null }): Promise /** Retrieves the current hostname(s) associated with a host id */ // getHostInfo(options: { @@ -362,10 +381,10 @@ export type Effects = { /** * Get the port address for another service */ - getServicePortForward( - internalPort: number, - packageId?: string, - ): Promise + getServicePortForward(options: { + internalPort: number + packageId: string | null + }): Promise /** Removes all network interfaces */ clearServiceInterfaces(): Promise @@ -376,16 +395,7 @@ export type Effects = { exposeForDependents(options: { paths: string[] }): Promise - exposeUi(options: { - paths: { - path: string - title: string - description?: string | undefined - masked?: boolean | undefined - copyable?: boolean | undefined - qr?: boolean | undefined - }[] - }): Promise + exposeUi(options: ExposeUiPathsAll): Promise /** * There are times that we want to see the addresses that where exported * @param options.addressId If we want to filter the address id @@ -467,7 +477,9 @@ export type Effects = { }): Promise /** Set the dependencies of what the service needs, usually ran during the set config as a best practice */ - setDependencies(dependencies: Dependencies): Promise + setDependencies(options: { + dependencies: Dependencies + }): Promise /** Exists could be useful during the runtime to know if some service exists, option dep */ exists(options: { packageId: PackageId }): Promise /** Exists could be useful during the runtime to know if some service is running, option dep */ @@ -477,20 +489,20 @@ export type Effects = { reverseProxy(options: { bind: { /** Optional, default is 0.0.0.0 */ - ip?: string + ip: string | null port: number ssl: boolean } dst: { /** Optional: default is 127.0.0.1 */ - ip?: string // optional, default 127.0.0.1 + ip: string | null // optional, default 127.0.0.1 port: number ssl: boolean } - http?: { + http: { // optional, will do TCP layer proxy only if not present - headers?: (headers: Record) => Record - } + headers: Record | null + } | null }): Promise<{ stop(): Promise }> restart(): void shutdown(): void @@ -585,7 +597,7 @@ export type KnownError = export type Dependency = { id: PackageId kind: DependencyKind -} +} & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] }) export type Dependencies = Array export type DeepPartial = T extends {} diff --git a/sdk/lib/util/Overlay.ts b/sdk/lib/util/Overlay.ts index 7f243d7d1..f5ff0e0d1 100644 --- a/sdk/lib/util/Overlay.ts +++ b/sdk/lib/util/Overlay.ts @@ -64,7 +64,7 @@ export class Overlay { async destroy() { const imageId = this.imageId const guid = this.guid - await this.effects.destroyOverlayedImage({ imageId, guid }) + await this.effects.destroyOverlayedImage({ guid }) } async exec( diff --git a/sdk/lib/util/getServiceInterface.ts b/sdk/lib/util/getServiceInterface.ts index d5ad048e3..3b7af1c41 100644 --- a/sdk/lib/util/getServiceInterface.ts +++ b/sdk/lib/util/getServiceInterface.ts @@ -1,3 +1,4 @@ +import { ServiceInterfaceType } from "../StartSdk" import { AddressInfo, Effects, @@ -5,7 +6,6 @@ import { Hostname, HostnameInfo, } from "../types" -import { ServiceInterfaceType } from "./utils" export type UrlString = string export type HostId = string diff --git a/sdk/lib/util/index.ts b/sdk/lib/util/index.ts index 81bd88da6..b3cab7183 100644 --- a/sdk/lib/util/index.ts +++ b/sdk/lib/util/index.ts @@ -7,7 +7,6 @@ import "./deepEqual" import "./deepMerge" import "./Overlay" import "./once" -import * as utils from "./utils" import { SDKManifest } from "../manifest/ManifestTypes" // prettier-ignore @@ -23,11 +22,6 @@ export const isKnownError = (e: unknown): e is T.KnownError => declare const affine: unique symbol -export const createUtils = utils.createUtils -export const createMainUtils = ( - effects: T.Effects, -) => createUtils(effects) - type NeverPossible = { [affine]: string } export type NoAny = NeverPossible extends A ? keyof NeverPossible extends keyof A diff --git a/sdk/lib/util/utils.ts b/sdk/lib/util/utils.ts deleted file mode 100644 index d56f22305..000000000 --- a/sdk/lib/util/utils.ts +++ /dev/null @@ -1,300 +0,0 @@ -import nullIfEmpty from "./nullIfEmpty" -import { - CheckResult, - checkPortListening, - checkWebUrl, -} from "../health/checkFns" -import { - DaemonReturned, - Effects, - EnsureStorePath, - ExtractStore, - ServiceInterfaceId, - PackageId, - ValidIfNoStupidEscape, -} from "../types" -import { GetSystemSmtp } from "./GetSystemSmtp" -import { GetStore, getStore } from "../store/getStore" -import { MultiHost, Scheme, SingleHost, StaticHost } from "../interfaces/Host" -import { ServiceInterfaceBuilder } from "../interfaces/ServiceInterfaceBuilder" -import { GetServiceInterface, getServiceInterface } from "./getServiceInterface" -import { - GetServiceInterfaces, - getServiceInterfaces, -} from "./getServiceInterfaces" -import * as CP from "node:child_process" -import { promisify } from "node:util" -import { splitCommand } from "./splitCommand" -import { SDKManifest } from "../manifest/ManifestTypes" -import { MountOptions, Overlay, CommandOptions } from "./Overlay" -export type Signals = NodeJS.Signals - -export const SIGTERM: Signals = "SIGTERM" -export const SIGKILL: Signals = "SIGTERM" -export const NO_TIMEOUT = -1 - -const childProcess = { - exec: promisify(CP.exec), - execFile: promisify(CP.execFile), -} -const cp = childProcess - -export type ServiceInterfaceType = "ui" | "p2p" | "api" - -export type Utils< - Manifest extends SDKManifest, - Store, - WrapperOverWrite = { const: never }, -> = { - checkPortListening( - port: number, - options: { - errorMessage: string - successMessage: string - timeoutMessage?: string - timeout?: number - }, - ): Promise - checkWebUrl( - url: string, - options?: { - timeout?: number - successMessage?: string - errorMessage?: string - }, - ): Promise - childProcess: typeof childProcess - createInterface: (options: { - name: string - id: string - description: string - hasPrimary: boolean - disabled: boolean - type: ServiceInterfaceType - username: null | string - path: string - search: Record - schemeOverride: { ssl: Scheme; noSsl: Scheme } | null - masked: boolean - }) => ServiceInterfaceBuilder - getSystemSmtp: () => GetSystemSmtp & WrapperOverWrite - host: { - static: (id: string) => StaticHost - single: (id: string) => SingleHost - multi: (id: string) => MultiHost - } - serviceInterface: { - getOwn: (id: ServiceInterfaceId) => GetServiceInterface & WrapperOverWrite - get: (opts: { - id: ServiceInterfaceId - packageId: PackageId - }) => GetServiceInterface & WrapperOverWrite - getAllOwn: () => GetServiceInterfaces & WrapperOverWrite - getAll: (opts: { - packageId: PackageId - }) => GetServiceInterfaces & WrapperOverWrite - } - nullIfEmpty: typeof nullIfEmpty - runCommand: ( - imageId: Manifest["images"][number], - command: ValidIfNoStupidEscape | [string, ...string[]], - options: CommandOptions & { - mounts?: { path: string; options: MountOptions }[] - }, - ) => Promise<{ stdout: string | Buffer; stderr: string | Buffer }> - runDaemon: ( - imageId: Manifest["images"][number], - command: ValidIfNoStupidEscape | [string, ...string[]], - options: CommandOptions & { - mounts?: { path: string; options: MountOptions }[] - overlay?: Overlay - }, - ) => Promise - store: { - get: ( - packageId: string, - path: EnsureStorePath, - ) => GetStore & WrapperOverWrite - getOwn: ( - path: EnsureStorePath, - ) => GetStore & WrapperOverWrite - setOwn: ( - path: EnsureStorePath, - value: ExtractStore, - ) => Promise - } -} -export const createUtils = < - Manifest extends SDKManifest, - Store = never, - WrapperOverWrite = { const: never }, ->( - effects: Effects, -): Utils => { - return { - createInterface: (options: { - name: string - id: string - description: string - hasPrimary: boolean - disabled: boolean - type: ServiceInterfaceType - username: null | string - path: string - search: Record - schemeOverride: { ssl: Scheme; noSsl: Scheme } | null - masked: boolean - }) => new ServiceInterfaceBuilder({ ...options, effects }), - childProcess, - getSystemSmtp: () => - new GetSystemSmtp(effects) as GetSystemSmtp & WrapperOverWrite, - - host: { - static: (id: string) => new StaticHost({ id, effects }), - single: (id: string) => new SingleHost({ id, effects }), - multi: (id: string) => new MultiHost({ id, effects }), - }, - nullIfEmpty, - - serviceInterface: { - getOwn: (id: ServiceInterfaceId) => - getServiceInterface(effects, { - id, - packageId: null, - }) as GetServiceInterface & WrapperOverWrite, - get: (opts: { id: ServiceInterfaceId; packageId: PackageId }) => - getServiceInterface(effects, opts) as GetServiceInterface & - WrapperOverWrite, - getAllOwn: () => - getServiceInterfaces(effects, { - packageId: null, - }) as GetServiceInterfaces & WrapperOverWrite, - getAll: (opts: { packageId: PackageId }) => - getServiceInterfaces(effects, opts) as GetServiceInterfaces & - WrapperOverWrite, - }, - store: { - get: ( - packageId: string, - path: EnsureStorePath, - ) => - getStore(effects, path as any, { - packageId, - }) as any, - getOwn: (path: EnsureStorePath) => - getStore(effects, path as any) as any, - setOwn: ( - path: EnsureStorePath, - value: ExtractStore, - ) => effects.store.set({ value, path: path as any }), - }, - - runCommand: async ( - imageId: Manifest["images"][number], - command: ValidIfNoStupidEscape | [string, ...string[]], - options: CommandOptions & { - mounts?: { path: string; options: MountOptions }[] - }, - ): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> => { - const commands = splitCommand(command) - const overlay = await Overlay.of(effects, imageId) - try { - for (let mount of options.mounts || []) { - await overlay.mount(mount.options, mount.path) - } - return await overlay.exec(commands) - } finally { - await overlay.destroy() - } - }, - runDaemon: async ( - imageId: Manifest["images"][number], - command: ValidIfNoStupidEscape | [string, ...string[]], - options: CommandOptions & { - mounts?: { path: string; options: MountOptions }[] - overlay?: Overlay - }, - ): Promise => { - const commands = splitCommand(command) - const overlay = options.overlay || (await Overlay.of(effects, imageId)) - for (let mount of options.mounts || []) { - await overlay.mount(mount.options, mount.path) - } - const childProcess = await overlay.spawn(commands, { - env: options.env, - }) - const answer = new Promise((resolve, reject) => { - childProcess.stdout.on("data", (data: any) => { - console.log(data.toString()) - }) - childProcess.stderr.on("data", (data: any) => { - console.error(data.toString()) - }) - - childProcess.on("exit", (code: any) => { - if (code === 0) { - return resolve(null) - } - return reject(new Error(`${commands[0]} exited with code ${code}`)) - }) - }) - - const pid = childProcess.pid - return { - async wait() { - const pids = pid ? await psTree(pid, overlay) : [] - try { - return await answer - } finally { - for (const process of pids) { - cp.execFile("kill", [`-9`, String(process)]).catch((_) => {}) - } - } - }, - async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) { - const pids = pid ? await psTree(pid, overlay) : [] - try { - childProcess.kill(signal) - - if (timeout > NO_TIMEOUT) { - const didTimeout = await Promise.race([ - new Promise((resolve) => setTimeout(resolve, timeout)).then( - () => true, - ), - answer.then(() => false), - ]) - if (didTimeout) { - childProcess.kill(SIGKILL) - } - } else { - await answer - } - } finally { - await overlay.destroy() - } - - try { - for (const process of pids) { - await cp.execFile("kill", [`-${signal}`, String(process)]) - } - } finally { - for (const process of pids) { - cp.execFile("kill", [`-9`, String(process)]).catch((_) => {}) - } - } - }, - } - }, - checkPortListening: checkPortListening.bind(null, effects), - checkWebUrl: checkWebUrl.bind(null, effects), - } -} -function noop(): void {} - -async function psTree(pid: number, overlay: Overlay): Promise { - const { stdout } = await childProcess.exec(`pstree -p ${pid}`) - const regex: RegExp = /\((\d+)\)/g - return [...stdout.toString().matchAll(regex)].map(([_all, pid]) => - parseInt(pid), - ) -} diff --git a/sdk/package.json b/sdk/package.json index 14165ca30..539d8f90a 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -2,15 +2,15 @@ "name": "@start9labs/start-sdk", "version": "0.4.0-rev0.lib0.rc8.beta10", "description": "Software development kit to facilitate packaging services for StartOS", - "main": "./cjs/lib/index.js", - "types": "./cjs/lib/index.d.ts", - "module": "./mjs/lib/index.js", + "main": "./cjs/sdk/lib/index.js", + "types": "./cjs/sdk/lib/index.d.ts", + "module": "./mjs/sdk/lib/index.js", "sideEffects": true, "exports": { ".": { - "import": "./mjs/lib/index.js", - "require": "./cjs/lib/index.js", - "types": "./cjs/lib/index.d.ts" + "import": "./mjs/sdk/lib/index.js", + "require": "./cjs/sdk/lib/index.js", + "types": "./cjs/sdk/lib/index.d.ts" } }, "typesVersion": { @@ -56,4 +56,4 @@ "tsx": "^4.7.1", "typescript": "^5.0.4" } -} \ No newline at end of file +} diff --git a/web/projects/ui/src/app/app/preloader/preloader.component.ts b/web/projects/ui/src/app/app/preloader/preloader.component.ts index 9823ac981..0b387ac45 100644 --- a/web/projects/ui/src/app/app/preloader/preloader.component.ts +++ b/web/projects/ui/src/app/app/preloader/preloader.component.ts @@ -40,8 +40,6 @@ const ICONS = [ 'file-tray-stacked-outline', 'finger-print-outline', 'flash-outline', - 'flask-outline', - 'flash-off-outline', 'folder-open-outline', 'globe-outline', 'grid-outline', @@ -70,6 +68,7 @@ const ICONS = [ 'receipt-outline', 'refresh', 'reload', + 'reload-circle-outline', 'remove', 'remove-circle-outline', 'remove-outline', diff --git a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html index ea83aedb1..79b1a1fde 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html @@ -10,7 +10,7 @@ - User Interfaces (UI) + User Interfaces - Application Program Interfaces (API) + Application Program Interfaces - Peer-To-Peer Interfaces (P2P) + Peer-To-Peer Interfaces { - const addresses: MappedAddress[] = [] + const addresses: MappedAddress[] = [] - let name = '' - let hostname = '' + hostnames.forEach(h => { + let name = '' + let hostname = '' - if (h.kind === 'onion') { - name = 'Tor' - hostname = h.hostname.value + if (h.kind === 'onion') { + name = 'Tor' + hostname = h.hostname.value + } else { + const hostnameKind = h.hostname.kind + + if (hostnameKind === 'domain') { + name = 'Domain' + hostname = `${h.hostname.subdomain}.${h.hostname.domain}` } else { - name = h.hostname.kind - hostname = - h.hostname.kind === 'domain' - ? `${h.hostname.subdomain}.${h.hostname.domain}` - : h.hostname.value + name = + hostnameKind === 'local' + ? 'Local' + : `${h.networkInterfaceId} (${hostnameKind})` + hostname = h.hostname.value } + } - if (h.hostname.sslPort) { - const port = h.hostname.sslPort === 443 ? '' : `:${h.hostname.sslPort}` - const scheme = addressInfo.bindOptions.addSsl?.scheme - ? `${addressInfo.bindOptions.addSsl.scheme}://` - : '' + if (h.hostname.sslPort) { + const port = h.hostname.sslPort === 443 ? '' : `:${h.hostname.sslPort}` + const scheme = addressInfo.bindOptions.addSsl?.scheme + ? `${addressInfo.bindOptions.addSsl.scheme}://` + : '' - addresses.push({ - name, - url: `${scheme}${username}${hostname}${port}${suffix}`, - }) - } + addresses.push({ + name: name === 'Tor' ? 'Tor (HTTPS)' : name, + url: `${scheme}${username}${hostname}${port}${suffix}`, + }) + } - if (h.hostname.port) { - const port = h.hostname.port === 80 ? '' : `:${h.hostname.port}` - const scheme = addressInfo.bindOptions.scheme - ? `${addressInfo.bindOptions.scheme}://` - : '' + if (h.hostname.port) { + const port = h.hostname.port === 80 ? '' : `:${h.hostname.port}` + const scheme = addressInfo.bindOptions.scheme + ? `${addressInfo.bindOptions.scheme}://` + : '' - addresses.push({ - name, - url: `${scheme}${username}${hostname}${port}${suffix}`, - }) - } + addresses.push({ + name: name === 'Tor' ? 'Tor (HTTP)' : name, + url: `${scheme}${username}${hostname}${port}${suffix}`, + }) + } + }) - return addresses - }) - .flat() + return addresses } diff --git a/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.module.ts b/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.module.ts deleted file mode 100644 index 86e374b17..000000000 --- a/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { Routes, RouterModule } from '@angular/router' -import { IonicModule } from '@ionic/angular' -import { ExperimentalFeaturesPage } from './experimental-features.page' -import { EmverPipesModule } from '@start9labs/shared' - -const routes: Routes = [ - { - path: '', - component: ExperimentalFeaturesPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - EmverPipesModule, - ], - declarations: [ExperimentalFeaturesPage], -}) -export class ExperimentalFeaturesPageModule {} diff --git a/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.html b/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.html deleted file mode 100644 index 0ca8c7d8e..000000000 --- a/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - Experimental Features - - - - - - - - -

Reset Tor

-

- Resetting the Tor daemon on your server may resolve Tor connectivity - issues. -

-
-
- - - -

{{ server.zram ? 'Disable' : 'Enable' }} zram

-

- Zram creates compressed swap in memory, resulting in faster I/O for - low RAM devices -

-
-
-
-
diff --git a/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.scss b/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.ts b/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.ts deleted file mode 100644 index bf445250a..000000000 --- a/web/projects/ui/src/app/pages/server-routes/experimental-features/experimental-features.page.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' -import { - AlertController, - LoadingController, - ToastController, -} from '@ionic/angular' -import { PatchDB } from 'patch-db-client' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ConfigService } from 'src/app/services/config.service' -import { DataModel } from 'src/app/services/patch-db/data-model' -import { ErrorToastService } from '@start9labs/shared' - -@Component({ - selector: 'experimental-features', - templateUrl: './experimental-features.page.html', - styleUrls: ['./experimental-features.page.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ExperimentalFeaturesPage { - readonly server$ = this.patch.watch$('server-info') - - constructor( - private readonly toastCtrl: ToastController, - private readonly patch: PatchDB, - private readonly config: ConfigService, - private readonly alertCtrl: AlertController, - private readonly loadingCtrl: LoadingController, - private readonly api: ApiService, - private readonly errToast: ErrorToastService, - ) {} - - async presentAlertResetTor() { - const isTor = this.config.isTor() - const shared = - 'Optionally wipe state to forcibly acquire new guard nodes. It is recommended to try without wiping state first.' - const alert = await this.alertCtrl.create({ - header: isTor ? 'Warning' : 'Confirm', - message: isTor - ? `You are currently connected over Tor. If you reset the Tor daemon, you will loose connectivity until it comes back online.

${shared}` - : `Reset Tor?

${shared}`, - inputs: [ - { - label: 'Wipe state', - type: 'checkbox', - value: 'wipe', - }, - ], - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Reset', - handler: (value: string[]) => { - this.resetTor(value.some(v => v === 'wipe')) - }, - cssClass: 'enter-click', - }, - ], - cssClass: isTor ? 'alert-warning-message' : '', - }) - await alert.present() - } - - async presentAlertZram(enabled: boolean) { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: enabled - ? 'Are you sure you want to disable zram? It provides significant performance benefits on low RAM devices.' - : 'Enable zram? It will only make a difference on lower RAM devices.', - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: enabled ? 'Disable' : 'Enable', - handler: () => { - this.toggleZram(enabled) - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } - - private async resetTor(wipeState: boolean) { - const loader = await this.loadingCtrl.create({ - message: 'Resetting Tor...', - }) - await loader.present() - - try { - await this.api.resetTor({ - 'wipe-state': wipeState, - reason: 'User triggered', - }) - const toast = await this.toastCtrl.create({ - header: 'Tor reset in progress', - position: 'bottom', - duration: 4000, - buttons: [ - { - side: 'start', - icon: 'close', - handler: () => { - return true - }, - }, - ], - }) - await toast.present() - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } - - private async toggleZram(enabled: boolean) { - const loader = await this.loadingCtrl.create({ - message: enabled ? 'Disabling zram...' : 'Enabling zram...', - }) - await loader.present() - - try { - await this.api.toggleZram({ enable: !enabled }) - const toast = await this.toastCtrl.create({ - header: `Zram ${enabled ? 'disabled' : 'enabled'}`, - position: 'bottom', - duration: 4000, - buttons: [ - { - side: 'start', - icon: 'close', - handler: () => { - return true - }, - }, - ], - }) - await toast.present() - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } -} diff --git a/web/projects/ui/src/app/pages/server-routes/server-routing.module.ts b/web/projects/ui/src/app/pages/server-routes/server-routing.module.ts index 5c728e668..e7eb43aad 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-routing.module.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-routing.module.ts @@ -80,13 +80,6 @@ const routes: Routes = [ loadChildren: () => import('./wifi/wifi.module').then(m => m.WifiPageModule), }, - { - path: 'experimental-features', - loadChildren: () => - import('./experimental-features/experimental-features.module').then( - m => m.ExperimentalFeaturesPageModule, - ), - }, ] @NgModule({ diff --git a/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index 38087b354..ff9cfc775 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -54,6 +54,7 @@ export class ServerShowPage { private readonly ClientStorageService: ClientStorageService, private readonly authService: AuthService, private readonly toastCtrl: ToastController, + private readonly config: ConfigService, @Inject(WINDOW) private readonly windowRef: Window, ) {} @@ -178,6 +179,73 @@ export class ServerShowPage { } } + async presentAlertResetTor() { + const isTor = this.config.isTor() + const shared = + 'Optionally wipe state to forcibly acquire new guard nodes. It is recommended to try without wiping state first.' + const alert = await this.alertCtrl.create({ + header: isTor ? 'Warning' : 'Confirm', + message: isTor + ? `You are currently connected over Tor. If you reset the Tor daemon, you will loose connectivity until it comes back online.

${shared}` + : `Reset Tor?

${shared}`, + inputs: [ + { + label: 'Wipe state', + type: 'checkbox', + value: 'wipe', + }, + ], + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Reset', + handler: (value: string[]) => { + this.resetTor(value.some(v => v === 'wipe')) + }, + cssClass: 'enter-click', + }, + ], + cssClass: isTor ? 'alert-warning-message' : '', + }) + await alert.present() + } + + private async resetTor(wipeState: boolean) { + const loader = await this.loadingCtrl.create({ + message: 'Resetting Tor...', + }) + await loader.present() + + try { + await this.embassyApi.resetTor({ + 'wipe-state': wipeState, + reason: 'User triggered', + }) + const toast = await this.toastCtrl.create({ + header: 'Tor reset in progress', + position: 'bottom', + duration: 4000, + buttons: [ + { + side: 'start', + icon: 'close', + handler: () => { + return true + }, + }, + ], + }) + await toast.present() + } catch (e: any) { + this.errToast.present(e) + } finally { + loader.dismiss() + } + } + async updateEos(): Promise { const modal = await this.modalCtrl.create({ component: OSUpdatePage, @@ -512,14 +580,11 @@ export class ServerShowPage { disabled$: of(false), }, { - title: 'Experimental Features', - description: 'Try out new and potentially unstable new features', - icon: 'flask-outline', - action: () => - this.navCtrl.navigateForward(['experimental-features'], { - relativeTo: this.route, - }), - detail: true, + title: 'Reset Tor', + description: 'May help resolve Tor connectivity issues.', + icon: 'reload-circle-outline', + action: () => this.presentAlertResetTor(), + detail: false, disabled$: of(false), }, ], diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index ea83a4f0a..9caa257a4 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -1785,11 +1785,11 @@ export module Mock { scheme: 'http', preferredExternalPort: 80, addSsl: { + addXForwardedHeaders: false, preferredExternalPort: 443, scheme: 'https', }, - secure: false, - ssl: false, + secure: null, }, suffix: '', }, @@ -1797,14 +1797,6 @@ export module Mock { id: 'abcdefg', kind: 'multi', hostnames: [ - { - kind: 'onion', - hostname: { - value: 'bitcoin-ui-address.onion', - port: 80, - sslPort: 443, - }, - }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -1816,6 +1808,14 @@ export module Mock { sslPort: 1234, }, }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-ui-address.onion', + port: 80, + sslPort: 443, + }, + }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -1857,11 +1857,11 @@ export module Mock { scheme: 'http', preferredExternalPort: 80, addSsl: { + addXForwardedHeaders: false, preferredExternalPort: 443, scheme: 'https', }, - secure: false, - ssl: false, + secure: null, }, suffix: '', }, @@ -1869,14 +1869,6 @@ export module Mock { id: 'bcdefgh', kind: 'multi', hostnames: [ - { - kind: 'onion', - hostname: { - value: 'bitcoin-rpc-address.onion', - port: 80, - sslPort: 443, - }, - }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -1888,6 +1880,14 @@ export module Mock { sslPort: 2345, }, }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-rpc-address.onion', + port: 80, + sslPort: 443, + }, + }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -1929,8 +1929,7 @@ export module Mock { scheme: 'bitcoin', preferredExternalPort: 8333, addSsl: null, - secure: true, - ssl: false, + secure: { ssl: false }, }, suffix: '', }, @@ -1938,14 +1937,6 @@ export module Mock { id: 'cdefghi', kind: 'multi', hostnames: [ - { - kind: 'onion', - hostname: { - value: 'bitcoin-p2p-address.onion', - port: 8333, - sslPort: null, - }, - }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -1957,6 +1948,14 @@ export module Mock { sslPort: null, }, }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-p2p-address.onion', + port: 8333, + sslPort: null, + }, + }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -2032,11 +2031,11 @@ export module Mock { scheme: 'http', preferredExternalPort: 80, addSsl: { + addXForwardedHeaders: false, preferredExternalPort: 443, scheme: 'https', }, - secure: true, - ssl: true, + secure: { ssl: true }, }, suffix: '', }, @@ -2044,14 +2043,6 @@ export module Mock { id: 'hijklmnop', kind: 'multi', hostnames: [ - { - kind: 'onion', - hostname: { - value: 'proxy-ui-address.onion', - port: 80, - sslPort: 443, - }, - }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -2063,6 +2054,14 @@ export module Mock { sslPort: 4567, }, }, + { + kind: 'onion', + hostname: { + value: 'proxy-ui-address.onion', + port: 80, + sslPort: 443, + }, + }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -2184,8 +2183,7 @@ export module Mock { scheme: 'grpc', preferredExternalPort: 10009, addSsl: null, - secure: true, - ssl: true, + secure: { ssl: true }, }, suffix: '', }, @@ -2193,14 +2191,6 @@ export module Mock { id: 'qrstuv', kind: 'multi', hostnames: [ - { - kind: 'onion', - hostname: { - value: 'lnd-grpc-address.onion', - port: 10009, - sslPort: null, - }, - }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -2212,6 +2202,14 @@ export module Mock { sslPort: null, }, }, + { + kind: 'onion', + hostname: { + value: 'lnd-grpc-address.onion', + port: 10009, + sslPort: null, + }, + }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -2253,8 +2251,7 @@ export module Mock { scheme: 'lndconnect', preferredExternalPort: 10009, addSsl: null, - secure: true, - ssl: true, + secure: { ssl: true }, }, suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand', }, @@ -2262,14 +2259,6 @@ export module Mock { id: 'qrstuv', kind: 'multi', hostnames: [ - { - kind: 'onion', - hostname: { - value: 'lnd-grpc-address.onion', - port: 10009, - sslPort: null, - }, - }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -2281,6 +2270,14 @@ export module Mock { sslPort: null, }, }, + { + kind: 'onion', + hostname: { + value: 'lnd-grpc-address.onion', + port: 10009, + sslPort: null, + }, + }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -2322,8 +2319,7 @@ export module Mock { scheme: null, preferredExternalPort: 9735, addSsl: null, - secure: true, - ssl: true, + secure: { ssl: true }, }, suffix: '', }, @@ -2331,14 +2327,6 @@ export module Mock { id: 'rstuvw', kind: 'multi', hostnames: [ - { - kind: 'onion', - hostname: { - value: 'lnd-p2p-address.onion', - port: 9735, - sslPort: null, - }, - }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -2350,6 +2338,14 @@ export module Mock { sslPort: null, }, }, + { + kind: 'onion', + hostname: { + value: 'lnd-p2p-address.onion', + port: 9735, + sslPort: null, + }, + }, { kind: 'ip', networkInterfaceId: 'elan0', diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 9d804caf9..c46cee61d 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -77,11 +77,6 @@ export module RR { } // net.tor.reset export type ResetTorRes = null - export type ToggleZramReq = { - enable: boolean - } // server.experimental.zram - export type ToggleZramRes = null - // sessions export type GetSessionsReq = {} // sessions.list diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index 17ce2d9d6..3f1d9881d 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -97,8 +97,6 @@ export abstract class ApiService { abstract resetTor(params: RR.ResetTorReq): Promise - abstract toggleZram(params: RR.ToggleZramReq): Promise - // marketplace URLs abstract marketplaceProxy( diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 18d47e2ce..73765b44d 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -189,10 +189,6 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'net.tor.reset', params }) } - async toggleZram(params: RR.ToggleZramReq): Promise { - return this.rpcRequest({ method: 'server.experimental.zram', params }) - } - // marketplace URLs async marketplaceProxy( diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index a2e5324a1..73cc71ef8 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -370,20 +370,6 @@ export class MockApiService extends ApiService { return null } - async toggleZram(params: RR.ToggleZramReq): Promise { - await pauseFor(2000) - const patch = [ - { - op: PatchOp.REPLACE, - path: '/server-info/zram', - value: params.enable, - }, - ] - this.mockRevision(patch) - - return null - } - // marketplace URLs async marketplaceProxy( diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 35942e92f..d9fcba0ef 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -73,7 +73,6 @@ export const mockPatchData: DataModel = { pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m', 'ca-fingerprint': 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15', 'ntp-synced': false, - zram: false, platform: 'x86_64-nonfree', }, 'package-data': { @@ -401,35 +400,28 @@ export const mockPatchData: DataModel = { disabled: false, masked: false, name: 'Web UI', - description: 'A launchable web app for Bitcoin Proxy', + description: + 'A launchable web app for you to interact with your Bitcoin node', type: 'ui', addressInfo: { username: null, - hostId: 'hijklmnop', + hostId: 'abcdefg', bindOptions: { scheme: 'http', preferredExternalPort: 80, addSsl: { + addXForwardedHeaders: false, preferredExternalPort: 443, scheme: 'https', }, - secure: true, - ssl: true, + secure: { ssl: false }, }, suffix: '', }, hostInfo: { - id: 'hijklmnop', + id: 'abcdefg', kind: 'multi', hostnames: [ - { - kind: 'onion', - hostname: { - value: 'proxy-ui-address.onion', - port: 80, - sslPort: 443, - }, - }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -438,7 +430,15 @@ export const mockPatchData: DataModel = { kind: 'local', value: 'adjective-noun.local', port: null, - sslPort: 4567, + sslPort: 1234, + }, + }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-ui-address.onion', + port: 80, + sslPort: 443, }, }, { @@ -449,7 +449,7 @@ export const mockPatchData: DataModel = { kind: 'ipv4', value: '192.168.1.5', port: null, - sslPort: 4567, + sslPort: 1234, }, }, { @@ -460,40 +460,147 @@ export const mockPatchData: DataModel = { kind: 'ipv6', value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', port: null, - sslPort: 4567, + sslPort: 1234, }, }, + ], + }, + }, + rpc: { + id: 'rpc', + hasPrimary: false, + disabled: false, + masked: false, + name: 'RPC', + description: + 'Used by dependent services and client wallets for connecting to your node', + type: 'api', + addressInfo: { + username: null, + hostId: 'bcdefgh', + bindOptions: { + scheme: 'http', + preferredExternalPort: 80, + addSsl: { + preferredExternalPort: 443, + scheme: 'https', + addXForwardedHeaders: null, + }, + secure: null, + }, + suffix: '', + }, + hostInfo: { + id: 'bcdefgh', + kind: 'multi', + hostnames: [ { kind: 'ip', - networkInterfaceId: 'wlan0', + networkInterfaceId: 'elan0', public: false, hostname: { kind: 'local', value: 'adjective-noun.local', port: null, - sslPort: 4567, + sslPort: 2345, + }, + }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-rpc-address.onion', + port: 80, + sslPort: 443, }, }, { kind: 'ip', - networkInterfaceId: 'wlan0', + networkInterfaceId: 'elan0', public: false, hostname: { kind: 'ipv4', - value: '192.168.1.7', + value: '192.168.1.5', port: null, - sslPort: 4567, + sslPort: 2345, }, }, { kind: 'ip', - networkInterfaceId: 'wlan0', + networkInterfaceId: 'elan0', public: false, hostname: { kind: 'ipv6', value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', port: null, - sslPort: 4567, + sslPort: 2345, + }, + }, + ], + }, + }, + p2p: { + id: 'p2p', + hasPrimary: true, + disabled: false, + masked: false, + name: 'P2P', + description: + 'Used for connecting to other nodes on the Bitcoin network', + type: 'p2p', + addressInfo: { + username: null, + hostId: 'cdefghi', + bindOptions: { + scheme: 'bitcoin', + preferredExternalPort: 8333, + addSsl: null, + secure: { ssl: false }, + }, + suffix: '', + }, + hostInfo: { + id: 'cdefghi', + kind: 'multi', + hostnames: [ + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: 3456, + sslPort: null, + }, + }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-p2p-address.onion', + port: 8333, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.1.5', + port: 3456, + sslPort: null, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'elan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', + port: 3456, + sslPort: null, }, }, ], @@ -660,8 +767,7 @@ export const mockPatchData: DataModel = { scheme: 'grpc', preferredExternalPort: 10009, addSsl: null, - secure: true, - ssl: true, + secure: { ssl: true }, }, suffix: '', }, @@ -669,14 +775,6 @@ export const mockPatchData: DataModel = { id: 'qrstuv', kind: 'multi', hostnames: [ - { - kind: 'onion', - hostname: { - value: 'lnd-grpc-address.onion', - port: 10009, - sslPort: null, - }, - }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -688,6 +786,14 @@ export const mockPatchData: DataModel = { sslPort: null, }, }, + { + kind: 'onion', + hostname: { + value: 'lnd-grpc-address.onion', + port: 10009, + sslPort: null, + }, + }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -729,8 +835,7 @@ export const mockPatchData: DataModel = { scheme: 'lndconnect', preferredExternalPort: 10009, addSsl: null, - secure: true, - ssl: true, + secure: { ssl: true }, }, suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand', }, @@ -738,14 +843,6 @@ export const mockPatchData: DataModel = { id: 'qrstuv', kind: 'multi', hostnames: [ - { - kind: 'onion', - hostname: { - value: 'lnd-grpc-address.onion', - port: 10009, - sslPort: null, - }, - }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -757,6 +854,14 @@ export const mockPatchData: DataModel = { sslPort: null, }, }, + { + kind: 'onion', + hostname: { + value: 'lnd-grpc-address.onion', + port: 10009, + sslPort: null, + }, + }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -798,8 +903,7 @@ export const mockPatchData: DataModel = { scheme: null, preferredExternalPort: 9735, addSsl: null, - secure: true, - ssl: true, + secure: { ssl: true }, }, suffix: '', }, @@ -807,14 +911,6 @@ export const mockPatchData: DataModel = { id: 'rstuvw', kind: 'multi', hostnames: [ - { - kind: 'onion', - hostname: { - value: 'lnd-p2p-address.onion', - port: 9735, - sslPort: null, - }, - }, { kind: 'ip', networkInterfaceId: 'elan0', @@ -826,6 +922,14 @@ export const mockPatchData: DataModel = { sslPort: null, }, }, + { + kind: 'onion', + hostname: { + value: 'lnd-p2p-address.onion', + port: 9735, + sslPort: null, + }, + }, { kind: 'ip', networkInterfaceId: 'elan0', diff --git a/web/projects/ui/src/app/services/config.service.ts b/web/projects/ui/src/app/services/config.service.ts index 50f228e95..d6ad17019 100644 --- a/web/projects/ui/src/app/services/config.service.ts +++ b/web/projects/ui/src/app/services/config.service.ts @@ -1,16 +1,16 @@ import { DOCUMENT } from '@angular/common' import { Inject, Injectable } from '@angular/core' import { WorkspaceConfig } from '@start9labs/shared' -import { - HostnameInfoIp, - HostnameInfoOnion, -} from '@start9labs/start-sdk/mjs/lib/types' +import { types } from '@start9labs/start-sdk' import { InstalledPackageDataEntry, PackageMainStatus, PackageState, } from 'src/app/services/patch-db/data-model' +type HostnameInfoIp = types.HostnameInfoIp +type HostnameInfoOnion = types.HostnameInfoOnion + const { gitHash, useMocks, @@ -79,14 +79,14 @@ export class ConfigService { if (host.kind === 'multi') { const onionHostname = host.hostnames.find( - h => h.kind === 'onion', + (h: any) => h.kind === 'onion', ) as HostnameInfoOnion if (this.isTor() && onionHostname) { url.hostname = onionHostname.hostname.value } else { const ipHostname = host.hostnames.find( - h => h.kind === 'ip', + (h: any) => h.kind === 'ip', ) as HostnameInfoIp if (!ipHostname) return '' diff --git a/web/projects/ui/src/app/services/patch-db/data-model.ts b/web/projects/ui/src/app/services/patch-db/data-model.ts index e00350528..931b3b949 100644 --- a/web/projects/ui/src/app/services/patch-db/data-model.ts +++ b/web/projects/ui/src/app/services/patch-db/data-model.ts @@ -2,7 +2,8 @@ import { ConfigSpec } from 'src/app/pkg-config/config-types' import { Url } from '@start9labs/shared' import { MarketplaceManifest } from '@start9labs/marketplace' import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info' -import { ServiceInterfaceWithHostInfo } from '@start9labs/start-sdk/mjs/lib/types' +import { types } from '@start9labs/start-sdk' +type ServiceInterfaceWithHostInfo = types.ServiceInterfaceWithHostInfo export interface DataModel { 'server-info': ServerInfo @@ -78,7 +79,6 @@ export interface ServerInfo { pubkey: string 'ca-fingerprint': string 'ntp-synced': boolean - zram: boolean platform: string }