From 3c274997953bc8d2b5909f73418eb8523df49c8b Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Tue, 2 Dec 2025 16:31:02 -0700 Subject: [PATCH] Refactor/status info (#3066) * refactor status info * wip fe * frontend changes and version bump * fix tests and motd * add registry workflow * better starttunnel instructions * placeholders for starttunnel tables --------- Co-authored-by: Aiden McClelland --- .github/workflows/start-registry.yaml | 100 +++++++++ START-TUNNEL.md | 14 +- build/lib/motd | 2 +- core/Cargo.lock | 2 +- core/models/src/errors.rs | 3 +- core/startos/Cargo.toml | 4 +- core/startos/src/backup/mod.rs | 15 +- core/startos/src/context/rpc.rs | 63 +++--- core/startos/src/control.rs | 52 +++-- core/startos/src/db/mod.rs | 1 - core/startos/src/db/model/package.rs | 4 +- core/startos/src/lxc/mod.rs | 8 +- core/startos/src/net/dns.rs | 1 - core/startos/src/net/static_server.rs | 5 - core/startos/src/net/vhost.rs | 2 +- core/startos/src/registry/mod.rs | 1 - .../src/s9pk/merkle_archive/source/mod.rs | 2 +- core/startos/src/s9pk/v2/compat.rs | 2 +- core/startos/src/service/action.rs | 46 ++-- core/startos/src/service/control.rs | 67 ------ core/startos/src/service/effects/action.rs | 13 +- core/startos/src/service/effects/control.rs | 79 +++++-- .../startos/src/service/effects/dependency.rs | 25 +-- core/startos/src/service/effects/health.rs | 14 +- .../src/service/effects/subcontainer/mod.rs | 8 +- core/startos/src/service/mod.rs | 152 ++++--------- .../src/service/persistent_container.rs | 36 +--- core/startos/src/service/rpc.rs | 5 - core/startos/src/service/service_actor.rs | 201 +++++++----------- core/startos/src/service/service_map.rs | 43 ++-- core/startos/src/service/start_stop.rs | 18 +- core/startos/src/service/transition/backup.rs | 128 ++++++----- core/startos/src/service/transition/mod.rs | 93 ++------ .../startos/src/service/transition/restart.rs | 83 -------- core/startos/src/service/util.rs | 14 -- core/startos/src/status/mod.rs | 131 ++++++------ core/startos/src/system.rs | 1 + core/startos/src/tunnel/auth.rs | 2 +- core/startos/src/tunnel/context.rs | 14 +- core/startos/src/tunnel/web.rs | 121 +++++------ core/startos/src/util/actor/concurrent.rs | 2 +- core/startos/src/util/io.rs | 4 +- core/startos/src/util/sync.rs | 3 +- core/startos/src/version/mod.rs | 12 +- core/startos/src/version/v0_4_0_alpha_16.rs | 66 ++++++ debian/startos/postinst | 5 +- patch-db | 2 +- sdk/base/lib/Effects.ts | 4 +- sdk/base/lib/osBindings/DesiredStatus.ts | 8 + .../{DependencyKind.ts => ErrorData.ts} | 2 +- sdk/base/lib/osBindings/MainStatus.ts | 25 --- sdk/base/lib/osBindings/PackageDataEntry.ts | 4 +- sdk/base/lib/osBindings/StatusInfo.ts | 12 ++ sdk/base/lib/osBindings/index.ts | 5 +- sdk/package/lib/StartSdk.ts | 2 +- web/package-lock.json | 4 +- web/package.json | 2 +- .../app/routes/home/routes/devices/index.ts | 3 +- .../routes/home/routes/port-forwards/index.ts | 2 + .../app/routes/home/routes/subnets/index.ts | 2 + web/projects/start-tunnel/src/styles.scss | 6 + .../services/components/error.component.ts | 6 +- .../services/components/status.component.ts | 2 +- .../services/components/task.component.ts | 3 +- .../services/components/uptime.component.ts | 11 +- .../services/dashboard/service.component.ts | 2 +- .../services/dashboard/status.component.ts | 8 +- .../services/dashboard/ui-launch.component.ts | 3 +- .../services/modals/action-input.component.ts | 3 +- .../services/routes/actions.component.ts | 12 +- .../services/routes/interface.component.ts | 4 +- .../services/routes/outlet.component.ts | 2 +- .../services/routes/service.component.ts | 16 +- .../routes/backups/progress.component.ts | 4 +- .../ui/src/app/services/action.service.ts | 6 +- .../ui/src/app/services/api/api.fixures.ts | 19 +- .../services/api/embassy-mock-api.service.ts | 55 +++-- .../ui/src/app/services/api/mock-patch.ts | 14 +- .../ui/src/app/services/dep-error.service.ts | 5 +- .../services/pkg-status-rendering.service.ts | 57 +++-- 80 files changed, 920 insertions(+), 1062 deletions(-) create mode 100644 .github/workflows/start-registry.yaml delete mode 100644 core/startos/src/service/control.rs delete mode 100644 core/startos/src/service/transition/restart.rs delete mode 100644 core/startos/src/service/util.rs create mode 100644 core/startos/src/version/v0_4_0_alpha_16.rs create mode 100644 sdk/base/lib/osBindings/DesiredStatus.ts rename sdk/base/lib/osBindings/{DependencyKind.ts => ErrorData.ts} (65%) delete mode 100644 sdk/base/lib/osBindings/MainStatus.ts create mode 100644 sdk/base/lib/osBindings/StatusInfo.ts diff --git a/.github/workflows/start-registry.yaml b/.github/workflows/start-registry.yaml new file mode 100644 index 000000000..b83df5448 --- /dev/null +++ b/.github/workflows/start-registry.yaml @@ -0,0 +1,100 @@ +name: Start-Registry + +on: + workflow_call: + workflow_dispatch: + inputs: + environment: + type: choice + description: Environment + options: + - NONE + - dev + - unstable + - dev-unstable + runner: + type: choice + description: Runner + options: + - standard + - fast + arch: + type: choice + description: Architecture + options: + - ALL + - x86_64 + - aarch64 + - riscv64 + push: + branches: + - master + - next/* + pull_request: + branches: + - master + - next/* + +env: + NODEJS_VERSION: "24.11.0" + ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}' + +jobs: + compile: + name: Build Debian Package + strategy: + fail-fast: true + matrix: + arch: >- + ${{ + fromJson('{ + "x86_64": ["x86_64"], + "aarch64": ["aarch64"], + "riscv64": ["riscv64"], + "ALL": ["x86_64", "aarch64", "riscv64"] + }')[github.event.inputs.platform || 'ALL'] + }} + runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }} + steps: + - name: Cleaning up unnecessary files + run: | + sudo apt-get remove --purge -y google-chrome-stable firefox mono-devel + sudo apt-get autoremove -y + sudo apt-get clean + + - run: | + sudo mount -t tmpfs tmpfs . + if: ${{ github.event.inputs.runner == 'fast' }} + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODEJS_VERSION }} + + - name: Set up docker QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Configure sccache + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Make + run: make registry-deb + env: + PLATFORM: ${{ matrix.arch }} + SCCACHE_GHA_ENABLED: on + SCCACHE_GHA_VERSION: 0 + + - uses: actions/upload-artifact@v4 + with: + name: start-registry_${{ matrix.arch }}.deb + path: results/start-registry-*_${{ matrix.arch }}.deb diff --git a/START-TUNNEL.md b/START-TUNNEL.md index 1620445c2..8fe02e755 100644 --- a/START-TUNNEL.md +++ b/START-TUNNEL.md @@ -26,18 +26,18 @@ Use it for private remote access to self-hosted services running on a personal s 1. Access the VPS via SSH. -1. Install StartTunnel: +1. Run the StartTunnel install script: -```sh -TMP_DIR=$(mktemp -d) && (cd $TMP_DIR && wget https://github.com/Start9Labs/start-os/releases/download/v0.4.0-alpha.15/start-tunnel-0.4.0-alpha.15-a53b15f.dev_$(uname -m).deb && apt-get install -y ./start-tunnel-0.4.0-alpha.15-a53b15f.dev_$(uname -m).deb) && rm -rf $TMP_DIR && systemctl start start-tunneld && echo "Installation Succeeded" -``` + curl -fsSL https://start9labs.github.io/start-tunnel | sh -5. [Initialize the web interface](#web-interface) (recommended) +1. [Initialize the web interface](#web-interface) (recommended) ## Updating +Simply re-run the install command: + ```sh -TMP_DIR=$(mktemp -d) && (cd $TMP_DIR && wget https://github.com/Start9Labs/start-os/releases/download/v0.4.0-alpha.15/start-tunnel-0.4.0-alpha.15-a53b15f.dev_$(uname -m).deb && apt-get install --reinstall -y ./start-tunnel-0.4.0-alpha.15-a53b15f.dev_$(uname -m).deb) && rm -rf $TMP_DIR && systemctl daemon-reload && systemctl restart start-tunneld && echo "Update Succeeded" +curl -fsSL https://start9labs.github.io/start-tunnel | sh ``` ## CLI @@ -84,7 +84,7 @@ Enable the web interface (recommended in most cases) to access your StartTunnel 3. Paste the contents of your Root CA. -4. Save the file as `ca.crt` or `ca.pem` (make sure it saves as plain text, not rich text). +4. Save the file with a `.crt` extension (e.g. `start-tunnel.crt`) (make sure it saves as plain text, not rich text). 5. Trust the Root CA on your client device(s): diff --git a/build/lib/motd b/build/lib/motd index 3b0a5f9de..69b443364 100755 --- a/build/lib/motd +++ b/build/lib/motd @@ -22,7 +22,7 @@ parse_essential_db_info() { RAM_GB="unknown" fi - RUNNING_SERVICES=$(jq -r '[.value.packageData[] | select(.status.main == "running")] | length' "$DB_DUMP" 2>/dev/null) + RUNNING_SERVICES=$(jq -r '[.value.packageData[] | select(.statusInfo.started != null)] | length' "$DB_DUMP" 2>/dev/null) TOTAL_SERVICES=$(jq -r '.value.packageData | length' "$DB_DUMP" 2>/dev/null) rm -f "$DB_DUMP" diff --git a/core/Cargo.lock b/core/Cargo.lock index 5c7daee00..1e2d0661f 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -7908,7 +7908,7 @@ dependencies = [ [[package]] name = "start-os" -version = "0.4.0-alpha.15" +version = "0.4.0-alpha.16" dependencies = [ "aes 0.7.5", "arti-client", diff --git a/core/models/src/errors.rs b/core/models/src/errors.rs index 929a9502c..3837cb3e4 100644 --- a/core/models/src/errors.rs +++ b/core/models/src/errors.rs @@ -11,6 +11,7 @@ use rpc_toolkit::yajrc::{ }; use serde::{Deserialize, Serialize}; use tokio::task::JoinHandle; +use ts_rs::TS; use crate::InvalidId; @@ -407,7 +408,7 @@ impl From for Error { } } -#[derive(Clone, Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize, TS)] pub struct ErrorData { pub details: String, pub debug: String, diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 7c16ef24a..17667641e 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -15,7 +15,7 @@ license = "MIT" name = "start-os" readme = "README.md" repository = "https://github.com/Start9Labs/start-os" -version = "0.4.0-alpha.15" # VERSION_BUMP +version = "0.4.0-alpha.16" # VERSION_BUMP [lib] name = "startos" @@ -64,7 +64,7 @@ default = ["cli", "cli-container", "registry", "startd", "tunnel"] dev = ["backtrace-on-stack-overflow"] docker = [] registry = [] -startd = [] +startd = ["procfs", "pty-process"] test = [] tunnel = [] unstable = ["backtrace-on-stack-overflow"] diff --git a/core/startos/src/backup/mod.rs b/core/startos/src/backup/mod.rs index b68a2fd9d..697c81576 100644 --- a/core/startos/src/backup/mod.rs +++ b/core/startos/src/backup/mod.rs @@ -1,15 +1,12 @@ use std::collections::BTreeMap; -use chrono::{DateTime, Utc}; -use models::{HostId, PackageId}; -use reqwest::Url; +use models::PackageId; use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async}; use serde::{Deserialize, Serialize}; use crate::context::CliContext; #[allow(unused_imports)] use crate::prelude::*; -use crate::util::serde::{Base32, Base64}; pub mod backup_bulk; pub mod os; @@ -58,13 +55,3 @@ pub fn package_backup() -> ParentHandler { .with_call_remote::(), ) } - -#[derive(Deserialize, Serialize)] -struct BackupMetadata { - pub timestamp: DateTime, - #[serde(default)] - pub network_keys: BTreeMap>, - #[serde(default)] - pub tor_keys: BTreeMap>, // DEPRECATED - pub registry: Option, -} diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index 7ffd339d9..6a90362cf 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -45,6 +45,7 @@ use crate::service::ServiceMap; use crate::service::action::update_tasks; use crate::service::effects::callbacks::ServiceCallbacks; use crate::shutdown::Shutdown; +use crate::status::DesiredStatus; use crate::util::io::delete_file; use crate::util::lshw::LshwDevice; use crate::util::sync::{SyncMutex, SyncRwLock, Watch}; @@ -416,46 +417,34 @@ impl RpcContext { } } } - for id in - self.db - .mutate::>(|db| { - for (package_id, action_input) in &action_input { - for (action_id, input) in action_input { - for (_, pde) in - db.as_public_mut().as_package_data_mut().as_entries_mut()? - { - pde.as_tasks_mut().mutate(|tasks| { - Ok(update_tasks(tasks, package_id, action_id, input, false)) - })?; - } + + self.db + .mutate(|db| { + for (package_id, action_input) in &action_input { + for (action_id, input) in action_input { + for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? { + pde.as_tasks_mut().mutate(|tasks| { + Ok(update_tasks(tasks, package_id, action_id, input, false)) + })?; } } - db.as_public() - .as_package_data() - .as_entries()? + } + for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? { + if pde + .as_tasks() + .de()? .into_iter() - .filter_map(|(id, pkg)| { - (|| { - if pkg.as_tasks().de()?.into_iter().any(|(_, t)| { - t.active && t.task.severity == TaskSeverity::Critical - }) { - Ok(Some(id)) - } else { - Ok(None) - } - })() - .transpose() - }) - .collect() - }) - .await - .result? - { - let svc = self.services.get(&id).await; - if let Some(svc) = &*svc { - svc.stop(procedure_id.clone(), false).await?; - } - } + .any(|(_, t)| t.active && t.task.severity == TaskSeverity::Critical) + { + pde.as_status_info_mut() + .as_desired_mut() + .ser(&DesiredStatus::Stopped)?; + } + } + Ok(()) + }) + .await + .result?; check_tasks.complete(); Ok(()) diff --git a/core/startos/src/control.rs b/core/startos/src/control.rs index 4690c8a39..d7ad123fa 100644 --- a/core/startos/src/control.rs +++ b/core/startos/src/control.rs @@ -1,5 +1,4 @@ use clap::Parser; -use color_eyre::eyre::eyre; use models::PackageId; use serde::{Deserialize, Serialize}; use tracing::instrument; @@ -8,7 +7,6 @@ use ts_rs::TS; use crate::Error; use crate::context::RpcContext; use crate::prelude::*; -use crate::rpc_continuations::Guid; #[derive(Deserialize, Serialize, Parser, TS)] #[serde(rename_all = "camelCase")] @@ -19,37 +17,51 @@ pub struct ControlParams { #[instrument(skip_all)] pub async fn start(ctx: RpcContext, ControlParams { id }: ControlParams) -> Result<(), Error> { - ctx.services - .get(&id) + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(&id) + .or_not_found(&id)? + .as_status_info_mut() + .as_desired_mut() + .map_mutate(|s| Ok(s.start())) + }) .await - .as_ref() - .or_not_found(lazy_format!("Manager for {id}"))? - .start(Guid::new()) - .await?; + .result?; Ok(()) } pub async fn stop(ctx: RpcContext, ControlParams { id }: ControlParams) -> Result<(), Error> { - ctx.services - .get(&id) + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(&id) + .or_not_found(&id)? + .as_status_info_mut() + .stop() + }) .await - .as_ref() - .ok_or_else(|| Error::new(eyre!("Manager not found"), crate::ErrorKind::InvalidRequest))? - .stop(Guid::new(), true) - .await?; + .result?; Ok(()) } pub async fn restart(ctx: RpcContext, ControlParams { id }: ControlParams) -> Result<(), Error> { - ctx.services - .get(&id) + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(&id) + .or_not_found(&id)? + .as_status_info_mut() + .as_desired_mut() + .map_mutate(|s| Ok(s.restart())) + }) .await - .as_ref() - .ok_or_else(|| Error::new(eyre!("Manager not found"), crate::ErrorKind::InvalidRequest))? - .restart(Guid::new(), false) - .await?; + .result?; Ok(()) } diff --git a/core/startos/src/db/mod.rs b/core/startos/src/db/mod.rs index aad28d1eb..a6e28f1fe 100644 --- a/core/startos/src/db/mod.rs +++ b/core/startos/src/db/mod.rs @@ -1,7 +1,6 @@ pub mod model; pub mod prelude; -use std::panic::UnwindSafe; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; diff --git a/core/startos/src/db/model/package.rs b/core/startos/src/db/model/package.rs index 2a2abe2b6..9cebae9bf 100644 --- a/core/startos/src/db/model/package.rs +++ b/core/startos/src/db/model/package.rs @@ -16,7 +16,7 @@ use crate::net::service_interface::ServiceInterface; use crate::prelude::*; use crate::progress::FullProgress; use crate::s9pk::manifest::Manifest; -use crate::status::MainStatus; +use crate::status::StatusInfo; use crate::util::serde::{Pem, is_partial_of}; #[derive(Debug, Default, Deserialize, Serialize, TS)] @@ -365,7 +365,7 @@ impl Default for ActionVisibility { pub struct PackageDataEntry { pub state_info: PackageState, pub s9pk: PathBuf, - pub status: MainStatus, + pub status_info: StatusInfo, #[ts(type = "string | null")] pub registry: Option, #[ts(type = "string")] diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs index 0a9a12f90..f82e8eafa 100644 --- a/core/startos/src/lxc/mod.rs +++ b/core/startos/src/lxc/mod.rs @@ -5,11 +5,11 @@ use std::sync::{Arc, Weak}; use std::time::Duration; use clap::builder::ValueParserFactory; -use futures::{AsyncWriteExt, StreamExt}; -use imbl_value::{InOMap, InternedString}; +use futures::StreamExt; +use imbl_value::InternedString; use models::{FromStrParser, InvalidId, PackageId}; use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{GenericRpcMethod, RpcRequest, RpcResponse}; +use rpc_toolkit::{RpcRequest, RpcResponse}; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; @@ -17,7 +17,7 @@ use tokio::sync::Mutex; use tokio::time::Instant; use ts_rs::TS; -use crate::context::{CliContext, RpcContext}; +use crate::context::RpcContext; use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::block_dev::BlockDev; use crate::disk::mount::filesystem::idmapped::IdMapped; diff --git a/core/startos/src/net/dns.rs b/core/startos/src/net/dns.rs index fac1249cd..684f6378e 100644 --- a/core/startos/src/net/dns.rs +++ b/core/startos/src/net/dns.rs @@ -23,7 +23,6 @@ use hickory_server::proto::rr::{Name, Record, RecordType}; use hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo}; use imbl::OrdMap; use imbl_value::InternedString; -use itertools::Itertools; use models::{GatewayId, OptionExt, PackageId}; use patch_db::json_ptr::JsonPointer; use rpc_toolkit::{ diff --git a/core/startos/src/net/static_server.rs b/core/startos/src/net/static_server.rs index de78905b8..1f65e2a0b 100644 --- a/core/startos/src/net/static_server.rs +++ b/core/startos/src/net/static_server.rs @@ -73,11 +73,6 @@ macro_rules! else_empty_dir { }}; } -const EMBEDDED_UI_ROOT: Dir<'_> = else_empty_dir!( - feature = "startd" => - include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static") -); - pub trait UiContext: Context + AsRef + Clone + Sized { const UI_DIR: &'static Dir<'static>; fn api() -> ParentHandler; diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index 7447b8b76..fb609cd19 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -376,7 +376,7 @@ impl Default for AlpnInfo { } } -type Mapping = BTreeMap, InOMap, Weak<()>>>; +type Mapping = BTreeMap, InOMap, Weak<()>>>; pub struct GetVHostAcmeProvider(pub Watch>); impl Clone for GetVHostAcmeProvider { diff --git a/core/startos/src/registry/mod.rs b/core/startos/src/registry/mod.rs index 11fe2b807..4bae32d14 100644 --- a/core/startos/src/registry/mod.rs +++ b/core/startos/src/registry/mod.rs @@ -11,7 +11,6 @@ use crate::context::CliContext; use crate::middleware::cors::Cors; use crate::middleware::signature::SignatureAuth; use crate::net::static_server::{bad_request, not_found, server_error}; -use crate::net::web_server::{Accept, WebServer}; use crate::prelude::*; use crate::registry::context::RegistryContext; use crate::registry::device_info::DeviceInfoMiddleware; diff --git a/core/startos/src/s9pk/merkle_archive/source/mod.rs b/core/startos/src/s9pk/merkle_archive/source/mod.rs index c2daed6da..e56052543 100644 --- a/core/startos/src/s9pk/merkle_archive/source/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/source/mod.rs @@ -55,7 +55,7 @@ pub trait FileSource: Send + Sync + Sized + 'static { fn to_vec( src: &impl FileSource, verify: Option<(Hash, u64)>, - ) -> BoxFuture, Error>> { + ) -> BoxFuture<'_, Result, Error>> { async move { let mut vec = Vec::with_capacity(if let Some((_, size)) = &verify { *size diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs index 4e01f8889..5d978fedd 100644 --- a/core/startos/src/s9pk/v2/compat.rs +++ b/core/startos/src/s9pk/v2/compat.rs @@ -8,7 +8,7 @@ use models::{ImageId, VolumeId}; use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt}; use tokio::process::Command; -use crate::dependencies::{DepInfo, Dependencies, MetadataSrc}; +use crate::dependencies::{DepInfo, Dependencies}; use crate::prelude::*; use crate::s9pk::manifest::{DeviceFilter, Manifest}; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; diff --git a/core/startos/src/service/action.rs b/core/startos/src/service/action.rs index ae7b17de8..2081ab939 100644 --- a/core/startos/src/service/action.rs +++ b/core/startos/src/service/action.rs @@ -130,11 +130,11 @@ impl Handler for ServiceActor { ref action_id, input, }: RunAction, - jobs: &BackgroundJobQueue, + _: &BackgroundJobQueue, ) -> Self::Response { let container = &self.0.persistent_container; let package_id = &self.0.id; - let action = self + let pde = self .0 .ctx .db @@ -143,9 +143,10 @@ impl Handler for ServiceActor { .into_public() .into_package_data() .into_idx(package_id) - .or_not_found(package_id)? - .into_actions() - .into_idx(action_id) + .or_not_found(package_id)?; + let action = pde + .as_actions() + .as_idx(action_id) .or_not_found(lazy_format!("{package_id} action {action_id}"))? .de()?; if matches!(&action.visibility, ActionVisibility::Disabled(_)) { @@ -154,7 +155,7 @@ impl Handler for ServiceActor { ErrorKind::Action, )); } - let running = container.state.borrow().running_status.as_ref().is_some(); + let running = pde.as_status_info().as_started().transpose_ref().is_some(); if match action.allowed_statuses { AllowedStatuses::OnlyRunning => !running, AllowedStatuses::OnlyStopped => running, @@ -177,44 +178,21 @@ impl Handler for ServiceActor { .await .with_kind(ErrorKind::Action)?; let package_id = package_id.clone(); - for to_stop in self - .0 + self.0 .ctx .db .mutate(|db| { - let mut to_stop = Vec::new(); - for (id, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? { + for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? { if pde.as_tasks_mut().mutate(|tasks| { Ok(update_tasks(tasks, &package_id, action_id, &input, true)) })? { - to_stop.push(id) + pde.as_status_info_mut().stop()?; } } - Ok(to_stop) + Ok(()) }) .await - .result? - { - if to_stop == package_id { - >::handle( - self, - id.clone(), - super::control::Stop { wait: false }, - jobs, - ) - .await; - } else { - self.0 - .ctx - .services - .get(&to_stop) - .await - .as_ref() - .or_not_found(&to_stop)? - .stop(id.clone(), false) - .await?; - } - } + .result?; Ok(result) } } diff --git a/core/startos/src/service/control.rs b/core/startos/src/service/control.rs deleted file mode 100644 index c2dc0b266..000000000 --- a/core/startos/src/service/control.rs +++ /dev/null @@ -1,67 +0,0 @@ -use futures::future::OptionFuture; - -use crate::prelude::*; -use crate::rpc_continuations::Guid; -use crate::service::action::RunAction; -use crate::service::start_stop::StartStop; -use crate::service::transition::TransitionKind; -use crate::service::{Service, ServiceActor}; -use crate::util::actor::background::BackgroundJobQueue; -use crate::util::actor::{ConflictBuilder, Handler}; - -pub(super) struct Start; -impl Handler for ServiceActor { - type Response = (); - fn conflicts_with(_: &Start) -> ConflictBuilder { - ConflictBuilder::everything().except::() - } - async fn handle(&mut self, _: Guid, _: Start, _: &BackgroundJobQueue) -> Self::Response { - self.0.persistent_container.state.send_modify(|x| { - x.desired_state = StartStop::Start; - }); - self.0.synchronized.notified().await - } -} -impl Service { - pub async fn start(&self, id: Guid) -> Result<(), Error> { - self.actor.send(id, Start).await - } -} - -pub(super) struct Stop { - pub wait: bool, -} -impl Handler for ServiceActor { - type Response = (); - fn conflicts_with(_: &Stop) -> ConflictBuilder { - ConflictBuilder::everything().except::() - } - async fn handle( - &mut self, - _: Guid, - Stop { wait }: Stop, - _: &BackgroundJobQueue, - ) -> Self::Response { - let mut transition_state = None; - self.0.persistent_container.state.send_modify(|x| { - x.desired_state = StartStop::Stop; - if x.transition_state.as_ref().map(|x| x.kind()) == Some(TransitionKind::Restarting) { - transition_state = std::mem::take(&mut x.transition_state); - } - }); - let notif = if wait { - Some(self.0.synchronized.notified()) - } else { - None - }; - if let Some(restart) = transition_state { - restart.abort().await; - } - OptionFuture::from(notif).await; - } -} -impl Service { - pub async fn stop(&self, id: Guid, wait: bool) -> Result<(), Error> { - self.actor.send(id, Stop { wait }).await - } -} diff --git a/core/startos/src/service/effects/action.rs b/core/startos/src/service/effects/action.rs index f4f030bd0..ba2a0185d 100644 --- a/core/startos/src/service/effects/action.rs +++ b/core/startos/src/service/effects/action.rs @@ -261,19 +261,20 @@ async fn create_task( }, None => true, }; - if active && task.severity == TaskSeverity::Critical { - context.stop(procedure_id, false).await?; - } context .seed .ctx .db .mutate(|db| { - db.as_public_mut() + let pde = db + .as_public_mut() .as_package_data_mut() .as_idx_mut(src_id) - .or_not_found(src_id)? - .as_tasks_mut() + .or_not_found(src_id)?; + if active && task.severity == TaskSeverity::Critical { + pde.as_status_info_mut().stop()?; + } + pde.as_tasks_mut() .insert(&replay_id, &TaskEntry { active, task }) }) .await diff --git a/core/startos/src/service/effects/control.rs b/core/startos/src/service/effects/control.rs index ff4ed5f27..4289482f4 100644 --- a/core/startos/src/service/effects/control.rs +++ b/core/startos/src/service/effects/control.rs @@ -1,12 +1,13 @@ use std::str::FromStr; +use chrono::Utc; use clap::builder::ValueParserFactory; use models::{FromStrParser, PackageId}; use crate::service::RebuildParams; use crate::service::effects::prelude::*; use crate::service::rpc::CallbackId; -use crate::status::MainStatus; +use crate::status::{DesiredStatus, StatusInfo}; pub async fn rebuild(context: EffectContext) -> Result<(), Error> { let seed = context.deref()?.seed.clone(); @@ -21,15 +22,44 @@ pub async fn rebuild(context: EffectContext) -> Result<(), Error> { Ok(()) } -pub async fn restart(context: EffectContext, EventId { event_id }: EventId) -> Result<(), Error> { +pub async fn restart(context: EffectContext) -> Result<(), Error> { let context = context.deref()?; - context.restart(event_id, false).await?; + let id = &context.seed.id; + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(id) + .or_not_found(id)? + .as_status_info_mut() + .as_desired_mut() + .map_mutate(|s| Ok(s.restart())) + }) + .await + .result?; Ok(()) } -pub async fn shutdown(context: EffectContext, EventId { event_id }: EventId) -> Result<(), Error> { +pub async fn shutdown(context: EffectContext) -> Result<(), Error> { let context = context.deref()?; - context.stop(event_id, false).await?; + let id = &context.seed.id; + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(id) + .or_not_found(id)? + .as_status_info_mut() + .stop() + }) + .await + .result?; Ok(()) } @@ -50,7 +80,7 @@ pub async fn get_status( package_id, callback, }: GetStatusParams, -) -> Result { +) -> Result { let context = context.deref()?; let id = package_id.unwrap_or_else(|| context.seed.id.clone()); let db = context.seed.ctx.db.peek().await; @@ -68,13 +98,13 @@ pub async fn get_status( .as_package_data() .as_idx(&id) .or_not_found(&id)? - .as_status() + .as_status_info() .de()?; Ok(status) } -#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export)] pub enum SetMainStatusStatus { @@ -109,9 +139,34 @@ pub async fn set_main_status( SetMainStatus { status }: SetMainStatus, ) -> Result<(), Error> { let context = context.deref()?; - match status { - SetMainStatusStatus::Running => context.seed.started(), - SetMainStatusStatus::Stopped => context.seed.stopped(), - } + let id = &context.seed.id; + context + .seed + .ctx + .db + .mutate(|db| { + let s = db + .as_public_mut() + .as_package_data_mut() + .as_idx_mut(id) + .or_not_found(id)? + .as_status_info_mut(); + let prev = s.as_started_mut().replace(&match status { + SetMainStatusStatus::Running => Some(Utc::now()), + SetMainStatusStatus::Stopped => None, + })?; + if prev.is_none() && status == SetMainStatusStatus::Running { + s.as_desired_mut().map_mutate(|s| { + Ok(match s { + DesiredStatus::Restarting => DesiredStatus::Running, + x => x, + }) + })?; + } + + Ok(()) + }) + .await + .result?; Ok(()) } diff --git a/core/startos/src/service/effects/dependency.rs b/core/startos/src/service/effects/dependency.rs index 38feeb90f..8177c303b 100644 --- a/core/startos/src/service/effects/dependency.rs +++ b/core/startos/src/service/effects/dependency.rs @@ -4,7 +4,6 @@ use std::str::FromStr; use clap::builder::ValueParserFactory; use exver::VersionRange; -use imbl::OrdMap; use imbl_value::InternedString; use models::{FromStrParser, HealthCheckId, PackageId, ReplayId, VersionString, VolumeId}; @@ -96,13 +95,6 @@ pub async fn get_installed_packages(context: EffectContext) -> Result, is_running: bool, tasks: BTreeMap, - #[ts(as = "BTreeMap::")] - health_checks: OrdMap, + health_checks: BTreeMap, } pub async fn check_dependencies( context: EffectContext, @@ -336,14 +327,12 @@ pub async fn check_dependencies( let installed_version = manifest.as_version().de()?.into_version(); let satisfies = manifest.as_satisfies().de()?; let installed_version = Some(installed_version.clone().into()); - let is_installed = true; - let status = package.as_status().de()?; - let is_running = if is_installed { - status.running() - } else { - false - }; - let health_checks = status.health().cloned().unwrap_or_default(); + let is_running = package + .as_status_info() + .as_started() + .transpose_ref() + .is_some(); + let health_checks = package.as_status_info().as_health().de()?; let tasks = tasks .iter() .filter(|(_, v)| v.task.package_id == package_id) diff --git a/core/startos/src/service/effects/health.rs b/core/startos/src/service/effects/health.rs index 013172a92..587a5c84f 100644 --- a/core/startos/src/service/effects/health.rs +++ b/core/startos/src/service/effects/health.rs @@ -1,7 +1,6 @@ use models::HealthCheckId; use crate::service::effects::prelude::*; -use crate::status::MainStatus; use crate::status::health_check::NamedHealthCheckResult; #[derive(Debug, Clone, Serialize, Deserialize, TS)] @@ -28,16 +27,9 @@ pub async fn set_health( .as_package_data_mut() .as_idx_mut(package_id) .or_not_found(package_id)? - .as_status_mut() - .mutate(|main| { - match main { - MainStatus::Running { health, .. } | MainStatus::Starting { health } => { - health.insert(id, result); - } - _ => (), - } - Ok(()) - }) + .as_status_info_mut() + .as_health_mut() + .insert(&id, &result) }) .await .result?; diff --git a/core/startos/src/service/effects/subcontainer/mod.rs b/core/startos/src/service/effects/subcontainer/mod.rs index 395f5fcd1..ae7b72248 100644 --- a/core/startos/src/service/effects/subcontainer/mod.rs +++ b/core/startos/src/service/effects/subcontainer/mod.rs @@ -11,14 +11,14 @@ use crate::service::effects::prelude::*; use crate::service::persistent_container::Subcontainer; use crate::util::Invoke; -#[cfg(feature = "cli-container")] +#[cfg(any(feature = "cli-container", feature = "startd"))] mod sync; -#[cfg(not(feature = "cli-container"))] +#[cfg(not(any(feature = "cli-container", feature = "startd")))] mod sync_dummy; pub use sync::*; -#[cfg(not(feature = "cli-container"))] +#[cfg(not(any(feature = "cli-container", feature = "startd")))] use sync_dummy as sync; #[derive(Debug, Deserialize, Serialize, Parser, TS)] @@ -41,7 +41,7 @@ pub async fn destroy_subcontainer_fs( .await .remove(&guid) { - #[cfg(feature = "container-runtime")] + #[cfg(feature = "startd")] if tokio::fs::metadata(overlay.overlay.path().join("proc/1")) .await .is_ok() diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index e2a93d7e4..cdca288e1 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -9,33 +9,31 @@ use std::sync::{Arc, Weak}; use std::time::Duration; use axum::extract::ws::{Utf8Bytes, WebSocket}; -use chrono::{DateTime, Utc}; use clap::Parser; use futures::future::BoxFuture; use futures::stream::FusedStream; use futures::{FutureExt, SinkExt, StreamExt, TryStreamExt}; -use helpers::NonDetachingJoinHandle; +use helpers::{AtomicFile, NonDetachingJoinHandle}; use imbl_value::{InternedString, json}; use itertools::Itertools; use models::{ActionId, HostId, ImageId, PackageId}; use nix::sys::signal::Signal; use persistent_container::{PersistentContainer, Subcontainer}; -use rpc_toolkit::{HandlerArgs, HandlerFor}; +use rpc_toolkit::HandlerArgs; +use rpc_toolkit::yajrc::RpcError; use serde::{Deserialize, Serialize}; use service_actor::ServiceActor; -use start_stop::StartStop; use termion::raw::IntoRawMode; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::process::Command; -use tokio::sync::Notify; use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; use ts_rs::TS; use url::Url; use crate::context::{CliContext, RpcContext}; use crate::db::model::package::{ - InstalledState, ManifestPreference, PackageDataEntry, PackageState, PackageStateMatchModelRef, - TaskSeverity, UpdatingState, + InstalledState, ManifestPreference, PackageState, PackageStateMatchModelRef, TaskSeverity, + UpdatingState, }; use crate::disk::mount::filesystem::ReadOnly; use crate::disk::mount::guard::{GenericMountGuard, MountGuard}; @@ -49,15 +47,15 @@ use crate::service::service_map::InstallProgressHandles; use crate::service::uninstall::cleanup; use crate::util::Never; use crate::util::actor::concurrent::ConcurrentActor; -use crate::util::io::{AsyncReadStream, TermSize, create_file, delete_file}; +use crate::util::io::{AsyncReadStream, TermSize, delete_file}; use crate::util::net::WebSocketExt; use crate::util::serde::Pem; +use crate::util::sync::SyncMutex; use crate::volume::data_dir; use crate::{CAP_1_KiB, DATA_DIR}; pub mod action; pub mod cli; -mod control; pub mod effects; pub mod persistent_container; mod rpc; @@ -66,7 +64,6 @@ pub mod service_map; pub mod start_stop; mod transition; pub mod uninstall; -mod util; pub use service_map::ServiceMap; @@ -222,24 +219,17 @@ impl Service { async fn new( ctx: RpcContext, s9pk: S9pk, - start: StartStop, procedure_id: Guid, init_kind: Option, recovery_source: Option, ) -> Result { let id = s9pk.as_manifest().id.clone(); - let persistent_container = PersistentContainer::new( - &ctx, s9pk, - start, - // desired_state.subscribe(), - // temp_desired_state.subscribe(), - ) - .await?; + let persistent_container = PersistentContainer::new(&ctx, s9pk).await?; let seed = Arc::new(ServiceActorSeed { id, persistent_container, ctx, - synchronized: Arc::new(Notify::new()), + backup: SyncMutex::new(None), }); let service: ServiceRef = Self { actor: ConcurrentActor::new(ServiceActor(seed.clone())), @@ -279,19 +269,14 @@ impl Service { ) -> Result, Error> { let handle_installed = { let ctx = ctx.clone(); - move |s9pk: S9pk, i: Model| async move { + move |s9pk: S9pk| async move { for volume_id in &s9pk.as_manifest().volumes { let path = data_dir(DATA_DIR, &s9pk.as_manifest().id, volume_id); if tokio::fs::metadata(&path).await.is_err() { tokio::fs::create_dir_all(&path).await?; } } - let start_stop = if i.as_status().de()?.running() { - StartStop::Start - } else { - StartStop::Stop - }; - Self::new(ctx, s9pk, start_stop, Guid::new(), None, None::) + Self::new(ctx, s9pk, Guid::new(), None, None::) .await .map(Some) } @@ -319,7 +304,7 @@ impl Service { s9pk, &s9pk_path, &None, - None, + InitKind::Install, None::, None, ) @@ -353,7 +338,7 @@ impl Service { s9pk, &s9pk_path, &None, - Some(entry.as_status().de()?.run_state()), + InitKind::Update, None::, None, ) @@ -388,7 +373,7 @@ impl Service { } }) .await.result?; - handle_installed(s9pk, entry).await + handle_installed(s9pk).await } PackageStateMatchModelRef::Removing(_) | PackageStateMatchModelRef::Restoring(_) => { if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| { @@ -396,11 +381,7 @@ impl Service { tracing::debug!("{e:?}") }) { let err_state = |e: Error| async move { - let state = crate::status::MainStatus::Error { - on_rebuild: StartStop::Stop, - message: e.to_string(), - debug: Some(format!("{e:?}")), - }; + let e = e.into(); ctx.db .mutate(move |db| { if let Some(pde) = @@ -413,22 +394,14 @@ impl Service { .clone(), })) })?; - pde.as_status_mut().ser(&state)?; + pde.as_status_info_mut().as_error_mut().ser(&Some(e))?; } Ok(()) }) .await .result }; - match Self::new( - ctx.clone(), - s9pk, - StartStop::Stop, - Guid::new(), - None, - None::, - ) - .await + match Self::new(ctx.clone(), s9pk, Guid::new(), None, None::).await { Ok(service) => match async { service @@ -463,7 +436,7 @@ impl Service { Ok(None) } PackageStateMatchModelRef::Installed(_) => { - handle_installed(S9pk::open(s9pk_path, Some(id)).await?, entry).await + handle_installed(S9pk::open(s9pk_path, Some(id)).await?).await } PackageStateMatchModelRef::Error(e) => Err(Error::new( eyre!("Failed to parse PackageDataEntry, found {e:?}"), @@ -478,7 +451,7 @@ impl Service { s9pk: S9pk, s9pk_path: &PathBuf, registry: &Option, - prev_state: Option, + kind: InitKind, recovery_source: Option, progress: Option, ) -> Result { @@ -489,15 +462,8 @@ impl Service { let service = Self::new( ctx.clone(), s9pk, - StartStop::Stop, procedure_id.clone(), - Some(if recovery_source.is_some() { - InitKind::Restore - } else if prev_state.is_some() { - InitKind::Update - } else { - InitKind::Install - }), + Some(kind), recovery_source, ) .await?; @@ -550,8 +516,7 @@ impl Service { } } } - let has_critical = ctx - .db + ctx.db .mutate(|db| { for (action_id, input) in &action_input { for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? { @@ -566,13 +531,15 @@ impl Service { .as_idx_mut(&manifest.id) .or_not_found(&manifest.id)?; let actions = entry.as_actions().keys()?; - let has_critical = entry.as_tasks_mut().mutate(|t| { + if entry.as_tasks_mut().mutate(|t| { t.retain(|_, v| { v.task.package_id != manifest.id || actions.contains(&v.task.action_id) }); Ok(t.iter() .any(|(_, t)| t.active && t.task.severity == TaskSeverity::Critical)) - })?; + })? { + entry.as_status_info_mut().stop()?; + } entry .as_state_info_mut() .ser(&PackageState::Installed(InstalledState { manifest }))?; @@ -581,38 +548,42 @@ impl Service { entry.as_icon_mut().ser(&icon)?; entry.as_registry_mut().ser(registry)?; - Ok(has_critical) + Ok(()) }) .await .result?; - if prev_state == Some(StartStop::Start) && !has_critical { - service.start(procedure_id).await?; - } - Ok(service) } #[instrument(skip_all)] pub async fn backup(&self, guard: impl GenericMountGuard) -> Result<(), Error> { let id = &self.seed.id; - let mut file = create_file(guard.path().join(id).with_extension("s9pk")).await?; + let mut file = AtomicFile::new(guard.path().join(id).with_extension("s9pk"), None::<&str>) + .await + .with_kind(ErrorKind::Filesystem)?; self.seed .persistent_container .s9pk .clone() - .serialize(&mut file, true) - .await?; - drop(file); - self.actor - .send( - Guid::new(), - transition::backup::Backup { - path: guard.path().join("data"), - }, - ) - .await?? + .serialize(&mut *file, true) .await?; + file.save().await.with_kind(ErrorKind::Filesystem)?; + // TODO: reverify? + self.seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(id) + .or_not_found(id)? + .as_status_info_mut() + .as_desired_mut() + .map_mutate(|s| Ok(s.backing_up())) + }) + .await + .result?; Ok(()) } @@ -661,41 +632,12 @@ impl Service { } } -#[derive(Debug, Clone)] -pub struct RunningStatus { - started: DateTime, -} - struct ServiceActorSeed { ctx: RpcContext, id: PackageId, /// Needed to interact with the container for the service persistent_container: PersistentContainer, - /// This is notified every time the background job created in ServiceActor::init responds to a change - synchronized: Arc, -} - -impl ServiceActorSeed { - /// Used to indicate that we have finished the task of starting the service - pub fn started(&self) { - self.persistent_container.state.send_modify(|state| { - state.running_status = - Some( - state - .running_status - .take() - .unwrap_or_else(|| RunningStatus { - started: Utc::now(), - }), - ); - }); - } - /// Used to indicate that we have finished the task of stopping the service - pub fn stopped(&self) { - self.persistent_container.state.send_modify(|state| { - state.running_status = None; - }); - } + backup: SyncMutex>>>, } #[derive(Deserialize, Serialize, Parser, TS)] diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index 80f764a00..89d7b9a62 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -34,9 +34,7 @@ use crate::service::effects::handler; use crate::service::rpc::{ CallbackHandle, CallbackId, CallbackParams, ExitParams, InitKind, InitParams, }; -use crate::service::start_stop::StartStop; -use crate::service::transition::{TransitionKind, TransitionState}; -use crate::service::{RunningStatus, Service, rpc}; +use crate::service::{Service, rpc}; use crate::util::Invoke; use crate::util::io::create_file; use crate::util::rpc_client::UnixRpcClient; @@ -49,41 +47,15 @@ const RPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub struct ServiceState { // indicates whether the service container runtime has been initialized yet pub(super) rt_initialized: bool, - // This contains the start time and health check information for when the service is running. Note: Will be overwritting to the db, - pub(super) running_status: Option, // This tracks references to callbacks registered by the running service: pub(super) callbacks: BTreeSet>, - /// Setting this value causes the service actor to try to bring the service to the specified state. This is done in the background job created in ServiceActor::init - pub(super) desired_state: StartStop, - /// Override the current desired state for the service during a transition (this is protected by a guard that sets this value to null on drop) - pub(super) temp_desired_state: Option, - /// This represents a currently running task that affects the service's shown state, such as BackingUp or Restarting. - pub(super) transition_state: Option, -} - -#[derive(Debug)] -pub struct ServiceStateKinds { - pub transition_state: Option, - pub running_status: Option, - pub desired_state: StartStop, } impl ServiceState { - pub fn new(desired_state: StartStop) -> Self { + pub fn new() -> Self { Self { rt_initialized: false, - running_status: Default::default(), callbacks: Default::default(), - temp_desired_state: Default::default(), - transition_state: Default::default(), - desired_state, - } - } - pub fn kinds(&self) -> ServiceStateKinds { - ServiceStateKinds { - transition_state: self.transition_state.as_ref().map(|x| x.kind()), - desired_state: self.temp_desired_state.unwrap_or(self.desired_state), - running_status: self.running_status.clone(), } } } @@ -117,7 +89,7 @@ pub struct PersistentContainer { impl PersistentContainer { #[instrument(skip_all)] - pub async fn new(ctx: &RpcContext, s9pk: S9pk, start: StartStop) -> Result { + pub async fn new(ctx: &RpcContext, s9pk: S9pk) -> Result { let lxc_container = ctx .lxc_manager .create( @@ -305,7 +277,7 @@ impl PersistentContainer { assets, images, subcontainers: Arc::new(Mutex::new(BTreeMap::new())), - state: Arc::new(watch::channel(ServiceState::new(start)).0), + state: Arc::new(watch::channel(ServiceState::new()).0), net_service, destroyed: false, }) diff --git a/core/startos/src/service/rpc.rs b/core/startos/src/service/rpc.rs index 7b8f0a598..dc74c9843 100644 --- a/core/startos/src/service/rpc.rs +++ b/core/startos/src/service/rpc.rs @@ -203,11 +203,6 @@ impl serde::Serialize for Sandbox { pub struct CallbackId(u64); impl CallbackId { pub fn register(self, container: &PersistentContainer) -> CallbackHandle { - crate::dbg!(eyre!( - "callback {} registered for {}", - self.0, - container.s9pk.as_manifest().id - )); let this = Arc::new(self); let res = Arc::downgrade(&this); container diff --git a/core/startos/src/service/service_actor.rs b/core/startos/src/service/service_actor.rs index 4dd99a86e..755f3ea42 100644 --- a/core/startos/src/service/service_actor.rs +++ b/core/startos/src/service/service_actor.rs @@ -1,17 +1,14 @@ use std::sync::Arc; use std::time::Duration; -use futures::FutureExt; -use futures::future::{BoxFuture, Either}; use imbl::vector; +use patch_db::TypedDbWatch; use super::ServiceActorSeed; -use super::start_stop::StartStop; use crate::prelude::*; use crate::service::SYNC_RETRY_COOLDOWN_SECONDS; -use crate::service::persistent_container::ServiceStateKinds; -use crate::service::transition::TransitionKind; -use crate::status::MainStatus; +use crate::service::transition::{Transition, TransitionKind}; +use crate::status::{DesiredStatus, StatusInfo}; use crate::util::actor::Actor; use crate::util::actor::background::BackgroundJobQueue; @@ -21,159 +18,123 @@ pub(super) struct ServiceActor(pub(super) Arc); impl Actor for ServiceActor { fn init(&mut self, jobs: &BackgroundJobQueue) { let seed = self.0.clone(); - let mut current = seed.persistent_container.state.subscribe(); + let mut state = seed.persistent_container.state.subscribe(); + let initialized = async move { state.wait_for(|s| s.rt_initialized).await.map(|_| ()) }; + jobs.add_job(async move { - let _ = current.wait_for(|s| s.rt_initialized).await; - let mut start_stop_task: Option> = None; + if initialized.await.is_err() { + return; + } + let mut watch = seed + .ctx + .db + .watch( + format!("/public/packageData/{}/statusInfo", seed.id) + .parse() + .unwrap(), + ) // TODO: typed pointers + .await + .typed::(); + let mut transition: Option = None; loop { - let wait = match service_actor_loop(¤t, &seed, &mut start_stop_task).await { - Ok(()) => Either::Right(current.changed().then(|res| async move { - match res { - Ok(()) => (), - Err(_) => futures::future::pending().await, - } - })), - Err(e) => { + let res = service_actor_loop(&mut watch, &seed, &mut transition).await; + let wait = async { + if let Err(e) = async { + res?; + watch.changed().await?; + Ok::<_, Error>(()) + } + .await + { tracing::error!("error synchronizing state of service: {e}"); tracing::debug!("{e:?}"); - - seed.synchronized.notify_waiters(); - tracing::error!("Retrying in {}s...", SYNC_RETRY_COOLDOWN_SECONDS); - Either::Left(tokio::time::sleep(Duration::from_secs( - SYNC_RETRY_COOLDOWN_SECONDS, - ))) + tokio::time::timeout( + Duration::from_secs(SYNC_RETRY_COOLDOWN_SECONDS), + async { + watch.changed().await.log_err(); + }, + ) + .await + .ok(); } }; tokio::pin!(wait); - let start_stop_handler = async { - match &mut start_stop_task { - Some(task) => { - let err = task.await.log_err().is_none(); // TODO: ideally this error should be sent to service logs - start_stop_task.take(); + let transition_handler = async { + match &mut transition { + Some(Transition { future, .. }) => { + let err = future.await.log_err().is_none(); // TODO: ideally this error should be sent to service logs + transition.take(); if err { tokio::time::sleep(Duration::from_secs( SYNC_RETRY_COOLDOWN_SECONDS, )) .await; + } else { + futures::future::pending().await } } _ => futures::future::pending().await, } }; - tokio::pin!(start_stop_handler); - futures::future::select(wait, start_stop_handler).await; + tokio::pin!(transition_handler); + futures::future::select(wait, transition_handler).await; } }); } } async fn service_actor_loop<'a>( - current: &tokio::sync::watch::Receiver, + watch: &mut TypedDbWatch, seed: &'a Arc, - start_stop_task: &mut Option< - Either>, BoxFuture<'a, Result<(), Error>>>, - >, + transition: &mut Option>, ) -> Result<(), Error> { let id = &seed.id; - let kinds = current.borrow().kinds(); + let status_model = watch.peek_and_mark_seen()?; + let status = status_model.de()?; - let major_changes_state = seed - .ctx - .db - .mutate(|d| { - if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) { - let previous = i.as_status().de()?; - let main_status = match &kinds { - ServiceStateKinds { - transition_state: Some(TransitionKind::Restarting), - .. - } => MainStatus::Restarting, - ServiceStateKinds { - transition_state: Some(TransitionKind::BackingUp), - .. - } => previous.backing_up(), - ServiceStateKinds { - running_status: Some(status), - desired_state: StartStop::Start, - .. - } => MainStatus::Running { - started: status.started, - health: previous.health().cloned().unwrap_or_default(), - }, - ServiceStateKinds { - running_status: None, - desired_state: StartStop::Start, - .. - } => MainStatus::Starting { - health: previous.health().cloned().unwrap_or_default(), - }, - ServiceStateKinds { - running_status: Some(_), - desired_state: StartStop::Stop, - .. - } => MainStatus::Stopping, - ServiceStateKinds { - running_status: None, - desired_state: StartStop::Stop, - .. - } => MainStatus::Stopped, - }; - i.as_status_mut().ser(&main_status)?; - return Ok(previous - .major_changes(&main_status) - .then_some((previous, main_status))); - } - Ok(None) - }) - .await - .result?; - - if let Some((previous, new_state)) = major_changes_state { - if let Some(callbacks) = seed.ctx.callbacks.get_status(id) { - callbacks - .call(vector![to_value(&previous)?, to_value(&new_state)?]) - .await?; - } + if let Some(callbacks) = seed.ctx.callbacks.get_status(id) { + callbacks + .call(vector![patch_db::ModelExt::into_value(status_model)]) + .await?; } - seed.synchronized.notify_waiters(); - match kinds { - ServiceStateKinds { - running_status: None, - desired_state: StartStop::Start, + match status { + StatusInfo { + desired: DesiredStatus::Running | DesiredStatus::Restarting, + started: None, .. } => { - let task = start_stop_task + let task = transition .take() - .filter(|task| matches!(task, Either::Right(_))); - *start_stop_task = Some( - task.unwrap_or_else(|| Either::Right(seed.persistent_container.start().boxed())), - ); + .filter(|task| task.kind == TransitionKind::Starting); + *transition = task.or_else(|| Some(seed.start())); } - ServiceStateKinds { - running_status: Some(_), - desired_state: StartStop::Stop, + StatusInfo { + desired: + DesiredStatus::Stopped | DesiredStatus::Restarting | DesiredStatus::BackingUp { .. }, + started: Some(_), .. } => { - let task = start_stop_task + let task = transition .take() - .filter(|task| matches!(task, Either::Left(_))); - *start_stop_task = Some(task.unwrap_or_else(|| { - Either::Left( - async { - seed.persistent_container.stop().await?; - seed.persistent_container - .state - .send_if_modified(|s| s.running_status.take().is_some()); - Ok::<_, Error>(()) - } - .boxed(), - ) - })); + .filter(|task| task.kind == TransitionKind::Stopping); + *transition = task.or_else(|| Some(seed.stop())); + } + StatusInfo { + desired: DesiredStatus::BackingUp { .. }, + started: None, + .. + } => { + let task = transition + .take() + .filter(|task| task.kind == TransitionKind::BackingUp); + *transition = task.or_else(|| Some(seed.backup())); + } + _ => { + *transition = None; } - _ => (), }; Ok(()) } diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index 18ed115bc..46e99bba7 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; @@ -27,11 +28,10 @@ use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle, ProgressT use crate::s9pk::S9pk; use crate::s9pk::manifest::PackageId; use crate::s9pk::merkle_archive::source::FileSource; -use crate::service::rpc::ExitParams; -use crate::service::start_stop::StartStop; +use crate::service::rpc::{ExitParams, InitKind}; use crate::service::{LoadDisposition, Service, ServiceRef}; use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment; -use crate::status::MainStatus; +use crate::status::{DesiredStatus, StatusInfo}; use crate::util::serde::{Base32, Pem}; use crate::util::sync::SyncMutex; @@ -123,17 +123,7 @@ impl ServiceMap { ctx.db .mutate(|db| { if let Some(pde) = db.as_public_mut().as_package_data_mut().as_idx_mut(id) { - pde.as_status_mut().map_mutate(|s| { - Ok(MainStatus::Error { - on_rebuild: if s.running() { - StartStop::Start - } else { - StartStop::Stop - }, - message: e.details, - debug: Some(e.debug), - }) - })?; + pde.as_status_info_mut().as_error_mut().ser(&Some(e))?; } Ok(()) }) @@ -242,7 +232,12 @@ impl ServiceMap { PackageState::Installing(installing) }, s9pk: installed_path, - status: MainStatus::Stopped, + status_info: StatusInfo { + error: None, + health: BTreeMap::new(), + started: None, + desired: DesiredStatus::Stopped, + }, registry, developer_key: Pem::new(developer_key), icon, @@ -333,15 +328,9 @@ impl ServiceMap { next_can_migrate_from.clone(), )) }; - let run_state = service - .seed - .persistent_container - .state - .borrow() - .desired_state; let cleanup = service.uninstall(uninit, false, false).await?; progress.complete(); - Some((run_state, cleanup)) + Some(cleanup) } else { None }; @@ -350,7 +339,13 @@ impl ServiceMap { s9pk, &installed_path, ®istry, - prev.as_ref().map(|(s, _)| *s), + if recovery_source.is_some() { + InitKind::Restore + } else if prev.is_some() { + InitKind::Update + } else { + InitKind::Install + }, recovery_source, Some(InstallProgressHandles { finalization_progress, @@ -360,7 +355,7 @@ impl ServiceMap { .await?; *service = Some(new_service.into()); - if let Some((_, cleanup)) = prev { + if let Some(cleanup) = prev { cleanup.await?; } diff --git a/core/startos/src/service/start_stop.rs b/core/startos/src/service/start_stop.rs index 3a83db18c..e6ffcf1d5 100644 --- a/core/startos/src/service/start_stop.rs +++ b/core/startos/src/service/start_stop.rs @@ -9,23 +9,7 @@ pub enum StartStop { } impl StartStop { - pub(crate) fn is_start(&self) -> bool { + pub fn is_start(&self) -> bool { matches!(self, StartStop::Start) } } -// impl From for StartStop { -// fn from(value: MainStatus) -> Self { -// match value { -// MainStatus::Stopped => StartStop::Stop, -// MainStatus::Restoring => StartStop::Stop, -// MainStatus::Restarting => StartStop::Start, -// MainStatus::Stopping { .. } => StartStop::Stop, -// MainStatus::Starting => StartStop::Start, -// MainStatus::Running { -// started: _, -// health: _, -// } => StartStop::Start, -// MainStatus::BackingUp { on_complete } => on_complete, -// } -// } -// } diff --git a/core/startos/src/service/transition/backup.rs b/core/startos/src/service/transition/backup.rs index f61ffa4ae..8ec9fe77a 100644 --- a/core/startos/src/service/transition/backup.rs +++ b/core/startos/src/service/transition/backup.rs @@ -1,22 +1,66 @@ use std::path::PathBuf; -use std::sync::Arc; -use futures::FutureExt; use futures::future::BoxFuture; +use futures::{FutureExt, TryFutureExt}; use models::ProcedureName; +use rpc_toolkit::yajrc::RpcError; -use super::TempDesiredRestore; use crate::disk::mount::filesystem::ReadWrite; use crate::prelude::*; use crate::rpc_continuations::Guid; -use crate::service::ServiceActor; use crate::service::action::GetActionInput; -use crate::service::transition::{TransitionKind, TransitionState}; +use crate::service::start_stop::StartStop; +use crate::service::transition::{Transition, TransitionKind}; +use crate::service::{ServiceActor, ServiceActorSeed}; +use crate::status::DesiredStatus; use crate::util::actor::background::BackgroundJobQueue; use crate::util::actor::{ConflictBuilder, Handler}; -use crate::util::future::RemoteCancellable; use crate::util::serde::NoOutput; +impl ServiceActorSeed { + pub fn backup(&self) -> Transition<'_> { + Transition { + kind: TransitionKind::BackingUp, + future: async { + let res = if let Some(fut) = self.backup.replace(None) { + fut.await.map_err(Error::from) + } else { + Err(Error::new( + eyre!("No backup to resume"), + ErrorKind::Cancelled, + )) + }; + let id = &self.id; + self.ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(id) + .or_not_found(id)? + .as_status_info_mut() + .as_desired_mut() + .map_mutate(|s| { + Ok(match s { + DesiredStatus::BackingUp { + on_complete: StartStop::Start, + } => DesiredStatus::Running, + DesiredStatus::BackingUp { + on_complete: StartStop::Stop, + } => DesiredStatus::Stopped, + x => x, + }) + }) + }) + .await + .result?; + res + } + .boxed(), + } + } +} + pub(in crate::service) struct Backup { pub path: PathBuf, } @@ -28,63 +72,31 @@ impl Handler for ServiceActor { async fn handle( &mut self, id: Guid, - backup: Backup, - jobs: &BackgroundJobQueue, + Backup { path }: Backup, + _: &BackgroundJobQueue, ) -> Self::Response { - // So Need a handle to just a single field in the state - let temp: TempDesiredRestore = TempDesiredRestore::new(&self.0.persistent_container.state); - let mut current = self.0.persistent_container.state.subscribe(); - let path = backup.path.clone(); let seed = self.0.clone(); - let transition = RemoteCancellable::new(async move { - temp.stop(); - current - .wait_for(|s| s.running_status.is_none()) - .await - .with_kind(ErrorKind::Unknown)?; + let transition = async move { + async { + let backup_guard = seed + .persistent_container + .mount_backup(path, ReadWrite) + .await?; + seed.persistent_container + .execute::(id, ProcedureName::CreateBackup, Value::Null, None) + .await?; + backup_guard.unmount(true).await?; - let backup_guard = seed - .persistent_container - .mount_backup(path, ReadWrite) - .await?; - seed.persistent_container - .execute::(id, ProcedureName::CreateBackup, Value::Null, None) - .await?; - backup_guard.unmount(true).await?; - - if temp.restore().is_start() { - current - .wait_for(|s| s.running_status.is_some()) - .await - .with_kind(ErrorKind::Unknown)?; + Ok::<_, Error>(()) } - drop(temp); - Ok::<_, Arc>(()) - }); - let cancel_handle = transition.cancellation_handle(); - let transition = transition.shared(); - let job_transition = transition.clone(); - jobs.add_job(job_transition.map(|_| ())); - - let mut old = None; - self.0.persistent_container.state.send_modify(|s| { - old = std::mem::replace( - &mut s.transition_state, - Some(TransitionState { - kind: TransitionKind::BackingUp, - cancel_handle, - }), - ) - }); - if let Some(t) = old { - t.abort().await; + .await + .map_err(RpcError::from) } - Ok(transition - .map(|r| { - r.ok_or_else(|| Error::new(eyre!("Backup canceled"), ErrorKind::Cancelled))? - .map_err(|e| e.clone_output()) - }) - .boxed()) + .shared(); + + self.0.backup.replace(Some(transition.clone().boxed())); + + Ok(transition.map_err(Error::from).boxed()) } } diff --git a/core/startos/src/service/transition/mod.rs b/core/startos/src/service/transition/mod.rs index 2c90e740c..1663d8bcd 100644 --- a/core/startos/src/service/transition/mod.rs +++ b/core/startos/src/service/transition/mod.rs @@ -1,91 +1,42 @@ -use std::sync::Arc; +use futures::FutureExt; +use futures::future::BoxFuture; -use futures::{Future, FutureExt}; -use tokio::sync::watch; - -use super::persistent_container::ServiceState; -use crate::service::start_stop::StartStop; -use crate::util::actor::background::BackgroundJobQueue; -use crate::util::future::{CancellationHandle, RemoteCancellable}; +use crate::prelude::*; +use crate::service::ServiceActorSeed; pub mod backup; -pub mod restart; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum TransitionKind { BackingUp, - Restarting, + Starting, + Stopping, } -/// Used only in the manager/mod and is used to keep track of the state of the manager during the -/// transitional states -pub struct TransitionState { - cancel_handle: CancellationHandle, - kind: TransitionKind, +pub struct Transition<'a> { + pub kind: TransitionKind, + pub future: BoxFuture<'a, Result<(), Error>>, } -impl ::std::fmt::Debug for TransitionState { +impl<'a> ::std::fmt::Debug for Transition<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("TransitionState") + f.debug_struct("Transition") .field("kind", &self.kind) .finish_non_exhaustive() } } -impl TransitionState { - pub fn kind(&self) -> TransitionKind { - self.kind +impl ServiceActorSeed { + pub fn start(&self) -> Transition<'_> { + Transition { + kind: TransitionKind::Starting, + future: self.persistent_container.start().boxed(), + } } - pub async fn abort(mut self) { - self.cancel_handle.cancel_and_wait().await - } - fn new( - task: impl Future + Send + 'static, - kind: TransitionKind, - jobs: &BackgroundJobQueue, - ) -> Self { - let task = RemoteCancellable::new(task); - let cancel_handle = task.cancellation_handle(); - jobs.add_job(task.map(|_| ())); - Self { - cancel_handle, - kind, + + pub fn stop(&self) -> Transition<'_> { + Transition { + kind: TransitionKind::Stopping, + future: self.persistent_container.stop().boxed(), } } } -impl Drop for TransitionState { - fn drop(&mut self) { - self.cancel_handle.cancel(); - } -} - -#[derive(Debug, Clone)] -pub struct TempDesiredRestore(pub(super) Arc>, StartStop); -impl TempDesiredRestore { - pub fn new(state: &Arc>) -> Self { - Self(state.clone(), state.borrow().desired_state) - } - pub fn stop(&self) { - self.0 - .send_modify(|s| s.temp_desired_state = Some(StartStop::Stop)); - } - pub fn restore(&self) -> StartStop { - let restore_state = self.1; - self.0 - .send_modify(|s| s.temp_desired_state = Some(restore_state)); - restore_state - } -} -impl Drop for TempDesiredRestore { - fn drop(&mut self) { - self.0.send_modify(|s| { - s.temp_desired_state.take(); - s.transition_state.take(); - }); - } -} -// impl Deref for TempDesiredState { -// type Target = watch::Sender>; -// fn deref(&self) -> &Self::Target { -// &*self.0 -// } -// } diff --git a/core/startos/src/service/transition/restart.rs b/core/startos/src/service/transition/restart.rs deleted file mode 100644 index a271e7f44..000000000 --- a/core/startos/src/service/transition/restart.rs +++ /dev/null @@ -1,83 +0,0 @@ -use futures::FutureExt; -use futures::future::BoxFuture; - -use super::TempDesiredRestore; -use crate::prelude::*; -use crate::rpc_continuations::Guid; -use crate::service::action::GetActionInput; -use crate::service::transition::{TransitionKind, TransitionState}; -use crate::service::{Service, ServiceActor}; -use crate::util::actor::background::BackgroundJobQueue; -use crate::util::actor::{ConflictBuilder, Handler}; -use crate::util::future::RemoteCancellable; - -pub(super) struct Restart; -impl Handler for ServiceActor { - type Response = BoxFuture<'static, Option<()>>; - fn conflicts_with(_: &Restart) -> ConflictBuilder { - ConflictBuilder::everything().except::() - } - async fn handle(&mut self, _: Guid, _: Restart, jobs: &BackgroundJobQueue) -> Self::Response { - // So Need a handle to just a single field in the state - let temp = TempDesiredRestore::new(&self.0.persistent_container.state); - let mut current = self.0.persistent_container.state.subscribe(); - let state = self.0.persistent_container.state.clone(); - let transition = RemoteCancellable::new( - async move { - temp.stop(); - current - .wait_for(|s| s.running_status.is_none()) - .await - .with_kind(ErrorKind::Unknown)?; - if temp.restore().is_start() { - current - .wait_for(|s| s.running_status.is_some()) - .await - .with_kind(ErrorKind::Unknown)?; - } - drop(temp); - state.send_modify(|s| { - s.transition_state.take(); - }); - Ok::<_, Error>(()) - } - .map(|x| { - if let Err(err) = x { - tracing::debug!("{:?}", err); - tracing::warn!("{}", err); - } - }), - ); - let cancel_handle = transition.cancellation_handle(); - let transition = transition.shared(); - let job_transition = transition.clone(); - jobs.add_job(job_transition.map(|_| ())); - - let mut old = None; - self.0.persistent_container.state.send_modify(|s| { - old = std::mem::replace( - &mut s.transition_state, - Some(TransitionState { - kind: TransitionKind::Restarting, - cancel_handle, - }), - ) - }); - if let Some(t) = old { - t.abort().await; - } - transition.boxed() - } -} -impl Service { - #[instrument(skip_all)] - pub async fn restart(&self, id: Guid, wait: bool) -> Result<(), Error> { - let fut = self.actor.send(id, Restart).await?; - if wait { - if fut.await.is_none() { - tracing::warn!("Restart has been cancelled"); - } - } - Ok(()) - } -} diff --git a/core/startos/src/service/util.rs b/core/startos/src/service/util.rs deleted file mode 100644 index 3c53c2366..000000000 --- a/core/startos/src/service/util.rs +++ /dev/null @@ -1,14 +0,0 @@ -use futures::Future; -use tokio::sync::Notify; - -use crate::prelude::*; - -pub async fn cancellable( - cancel_transition: &Notify, - transition: impl Future, -) -> Result { - tokio::select! { - a = transition => Ok(a), - _ = cancel_transition.notified() => Err(Error::new(eyre!("transition was cancelled"), ErrorKind::Cancelled)), - } -} diff --git a/core/startos/src/status/mod.rs b/core/startos/src/status/mod.rs index 02e4e787c..de533504b 100644 --- a/core/startos/src/status/mod.rs +++ b/core/startos/src/status/mod.rs @@ -1,65 +1,66 @@ use std::collections::BTreeMap; use chrono::{DateTime, Utc}; -use imbl::OrdMap; +use models::{ErrorData, HealthCheckId}; use serde::{Deserialize, Serialize}; use ts_rs::TS; -use self::health_check::HealthCheckId; use crate::prelude::*; use crate::service::start_stop::StartStop; use crate::status::health_check::NamedHealthCheckResult; pub mod health_check; -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, TS)] -#[serde(tag = "main")] +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] +#[model = "Model"] +pub struct StatusInfo { + pub health: BTreeMap, + pub error: Option, + #[ts(type = "string | null")] + pub started: Option>, + pub desired: DesiredStatus, +} +impl StatusInfo { + pub fn stop(&mut self) { + self.desired = self.desired.stop(); + self.health.clear(); + } +} +impl Model { + pub fn stop(&mut self) -> Result<(), Error> { + self.as_desired_mut().map_mutate(|s| Ok(s.stop()))?; + self.as_health_mut().ser(&Default::default())?; + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, TS)] +#[serde(tag = "main")] +#[serde(rename_all = "kebab-case")] #[serde(rename_all_fields = "camelCase")] -pub enum MainStatus { - Error { - on_rebuild: StartStop, - message: String, - debug: Option, - }, +pub enum DesiredStatus { Stopped, Restarting, - Stopping, - Starting { - #[ts(as = "BTreeMap")] - health: OrdMap, - }, - Running { - #[ts(type = "string")] - started: DateTime, - #[ts(as = "BTreeMap")] - health: OrdMap, - }, - BackingUp { - on_complete: StartStop, - }, + Running, + BackingUp { on_complete: StartStop }, } -impl MainStatus { +impl Default for DesiredStatus { + fn default() -> Self { + Self::Stopped + } +} +impl DesiredStatus { pub fn running(&self) -> bool { match self { - MainStatus::Starting { .. } - | MainStatus::Running { .. } - | MainStatus::Restarting - | MainStatus::BackingUp { + Self::Running + | Self::Restarting + | Self::BackingUp { on_complete: StartStop::Start, - } - | MainStatus::Error { - on_rebuild: StartStop::Start, - .. } => true, - MainStatus::Stopped - | MainStatus::Stopping { .. } - | MainStatus::BackingUp { + Self::Stopped + | Self::BackingUp { on_complete: StartStop::Stop, - } - | MainStatus::Error { - on_rebuild: StartStop::Stop, - .. } => false, } } @@ -71,37 +72,35 @@ impl MainStatus { } } - pub fn major_changes(&self, other: &Self) -> bool { - match (self, other) { - (MainStatus::Running { .. }, MainStatus::Running { .. }) => false, - (MainStatus::Starting { .. }, MainStatus::Starting { .. }) => false, - (MainStatus::Stopping, MainStatus::Stopping) => false, - (MainStatus::Stopped, MainStatus::Stopped) => false, - (MainStatus::Restarting, MainStatus::Restarting) => false, - (MainStatus::BackingUp { .. }, MainStatus::BackingUp { .. }) => false, - (MainStatus::Error { .. }, MainStatus::Error { .. }) => false, - _ => true, - } - } - pub fn backing_up(&self) -> Self { - MainStatus::BackingUp { - on_complete: if self.running() { - StartStop::Start - } else { - StartStop::Stop - }, + Self::BackingUp { + on_complete: self.run_state(), } } - pub fn health(&self) -> Option<&OrdMap> { + pub fn stop(&self) -> Self { match self { - MainStatus::Running { health, .. } | MainStatus::Starting { health } => Some(health), - MainStatus::BackingUp { .. } - | MainStatus::Stopped - | MainStatus::Stopping { .. } - | MainStatus::Restarting - | MainStatus::Error { .. } => None, + Self::BackingUp { .. } => Self::BackingUp { + on_complete: StartStop::Stop, + }, + _ => Self::Stopped, + } + } + + pub fn start(&self) -> Self { + match self { + Self::BackingUp { .. } => Self::BackingUp { + on_complete: StartStop::Start, + }, + Self::Stopped => Self::Running, + x => *x, + } + } + + pub fn restart(&self) -> Self { + match self { + Self::Running => Self::Restarting, + x => *x, // no-op: restart is meaningless in any other state } } } diff --git a/core/startos/src/system.rs b/core/startos/src/system.rs index a76ca8641..5b14611d8 100644 --- a/core/startos/src/system.rs +++ b/core/startos/src/system.rs @@ -1039,6 +1039,7 @@ pub async fn test_smtp( use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; AsyncSmtpTransport::::relay(&server)? + .port(port) .credentials(Credentials::new(login, password)) .build() .send( diff --git a/core/startos/src/tunnel/auth.rs b/core/startos/src/tunnel/auth.rs index 327049bb3..8e4003a6e 100644 --- a/core/startos/src/tunnel/auth.rs +++ b/core/startos/src/tunnel/auth.rs @@ -3,7 +3,7 @@ use imbl::HashMap; use imbl_value::InternedString; use itertools::Itertools; use patch_db::HasModel; -use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async}; +use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async}; use serde::{Deserialize, Serialize}; use ts_rs::TS; diff --git a/core/startos/src/tunnel/context.rs b/core/startos/src/tunnel/context.rs index c6b74fc70..12386f029 100644 --- a/core/startos/src/tunnel/context.rs +++ b/core/startos/src/tunnel/context.rs @@ -33,7 +33,7 @@ use crate::rpc_continuations::{OpenAuthedContinuations, RpcContinuations}; use crate::tunnel::TUNNEL_DEFAULT_LISTEN; use crate::tunnel::api::tunnel_api; use crate::tunnel::db::TunnelDatabase; -use crate::tunnel::wg::WIREGUARD_INTERFACE_NAME; +use crate::tunnel::wg::{WIREGUARD_INTERFACE_NAME, WgSubnetConfig}; use crate::util::Invoke; use crate::util::collections::OrdMapIterMut; use crate::util::io::read_file_to_string; @@ -100,7 +100,17 @@ impl TunnelContext { let db_path = datadir.join("tunnel.db"); let db = TypedPatchDb::::load_or_init( PatchDb::open(&db_path).await?, - || async { Ok(Default::default()) }, + || async { + let mut db = TunnelDatabase::default(); + db.wg.subnets.0.insert( + "10.59.0.1/24".parse()?, + WgSubnetConfig { + name: "Default Subnet".into(), + ..Default::default() + }, + ); + Ok(db) + }, ) .await?; let listen = config.tunnel_listen.unwrap_or(TUNNEL_DEFAULT_LISTEN); diff --git a/core/startos/src/tunnel/web.rs b/core/startos/src/tunnel/web.rs index f38f37876..78ee1818d 100644 --- a/core/startos/src/tunnel/web.rs +++ b/core/startos/src/tunnel/web.rs @@ -466,7 +466,7 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> { println!("✅ Success! ✅"); println!( - "The webserver is running. Below is your URL{} and SSL certificate.", + "The webserver is running. Below is your URL{} and Root Certificate Authority (Root CA).", if password.is_some() { ", password," } else { @@ -496,7 +496,7 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> { println!("{password}"); println!(); println!(concat!( - "If you lose or forget your password, you can reset it using the command: ", + "If you lose or forget your password, you can reset it using the following command: ", "start-tunnel auth reset-password" )); } else { @@ -516,12 +516,22 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> { .pop() .map(Pem) .or_not_found("certificate in chain")?; - println!("📝 Root SSL Certificate:"); + println!("📝 Root CA:"); print!("{cert}"); println!(concat!( - "If you haven't already, ", - "trust the certificate in your system keychain and/or browser." + "To trust your StartTunnel Root CA (above):\n", + " 1. Copy the Root CA ", + "(starting with -----BEGIN CERTIFICATE----- and ending with -----END CERTIFICATE-----).\n", + " 2. Open a text editor: \n", + " - Linux: gedit, nano, or any editor\n", + " - Mac: TextEdit\n", + " - Windows: Notepad\n", + " 3. Paste the contents of your Root CA.\n", + " 4. Save the file with a `.crt` extension ", + "(e.g. `start-tunnel.crt`) (make sure it saves as plain text, not rich text).\n", + " 5. Follow instructions to trust you StartTunnel Root CA: ", + "https://staging.docs.start9.com/user-manual/trust-ca.html#2-trust-your-servers-root-ca." )); return Ok(()); @@ -534,28 +544,14 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> { .await?, )?; - suggested_addrs.sort_by_cached_key(|a| match a { - IpAddr::V4(a) => { - if a.is_loopback() { - 3 - } else if a.is_private() { - 2 - } else { - 0 - } - } - IpAddr::V6(a) => { - if a.is_loopback() { - 5 - } else if a.is_unicast_link_local() { - 4 - } else { - 1 - } - } + suggested_addrs.retain(|ip| match ip { + IpAddr::V4(a) => !a.is_loopback() && !a.is_private(), + IpAddr::V6(a) => !a.is_loopback() && !a.is_unicast_link_local(), }); - let ip = if suggested_addrs.is_empty() { + let ip = if suggested_addrs.len() == 1 { + suggested_addrs[0] + } else if suggested_addrs.is_empty() { prompt("Listen Address: ", parse_as::("IP Address"), None).await? } else if suggested_addrs.len() > 16 { prompt( @@ -565,22 +561,7 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> { ) .await? } else { - *choose_custom_display("Listen Address:", &suggested_addrs, |a| match a { - a if a.is_loopback() => { - format!("{a} (Loopback Address: only use if planning to proxy traffic)") - } - IpAddr::V4(a) if a.is_private() => { - format!("{a} (Private Address: only available from Local Area Network)") - } - IpAddr::V6(a) if a.is_unicast_link_local() => { - format!( - "[{a}] (Private Address: only available from Local Area Network)" - ) - } - IpAddr::V6(a) => format!("[{a}]"), - a => a.to_string(), - }) - .await? + *choose("Listen Address:", &suggested_addrs).await? }; println!(concat!( @@ -608,8 +589,8 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> { impl std::fmt::Display for Choice { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Generate => write!(f, "Generate an SSL certificate"), - Self::Provide => write!(f, "Provide your own certificate and key"), + Self::Generate => write!(f, "Generate"), + Self::Provide => write!(f, "Provide"), } } } @@ -617,7 +598,7 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> { let choice = choose( concat!( "Select whether to generate an SSL certificate ", - "or provide your own certificate and key:" + "or provide your own certificate (and key):" ), &options, ) @@ -631,35 +612,35 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> { )? .filter(|a| !a.ip().is_unspecified()); - let default_prompt = if let Some(listen) = listen { - format!("Subject Alternative Name(s) [{}]: ", listen.ip()) + let san_info = if let Some(listen) = listen { + vec![InternedString::from_display(&listen.ip())] } else { - "Subject Alternative Name(s): ".to_string() + println!( + "List all IP addresses and domains for which to sign the certificate, separated by commas." + ); + prompt( + "Subject Alternative Name(s): ", + |s| { + s.split(",") + .map(|s| { + let s = s.trim(); + if let Ok(ip) = s.parse::() { + Ok(InternedString::from_display(&ip)) + } else if is_valid_domain(s) { + Ok(s.into()) + } else { + Err(format!( + "{s} is not a valid ip address or domain" + )) + } + }) + .collect() + }, + listen.map(|l| vec![InternedString::from_display(&l.ip())]), + ) + .await? }; - println!( - "List all IP addresses and domains for which to sign the certificate, separated by commas." - ); - let san_info = prompt( - &default_prompt, - |s| { - s.split(",") - .map(|s| { - let s = s.trim(); - if let Ok(ip) = s.parse::() { - Ok(InternedString::from_display(&ip)) - } else if is_valid_domain(s) { - Ok(s.into()) - } else { - Err(format!("{s} is not a valid ip address or domain")) - } - }) - .collect() - }, - listen.map(|l| vec![InternedString::from_display(&l.ip())]), - ) - .await?; - ctx.call_remote::( "web.generate-certificate", to_value(&GenerateCertParams { subject: san_info })?, diff --git a/core/startos/src/util/actor/concurrent.rs b/core/startos/src/util/actor/concurrent.rs index a929a0ab3..373f91578 100644 --- a/core/startos/src/util/actor/concurrent.rs +++ b/core/startos/src/util/actor/concurrent.rs @@ -258,7 +258,7 @@ mod test { #[derive(Clone)] struct CActor; impl Actor for CActor { - fn init(&mut self, jobs: &BackgroundJobQueue) {} + fn init(&mut self, _: &BackgroundJobQueue) {} } struct Pending; impl Handler for CActor { diff --git a/core/startos/src/util/io.rs b/core/startos/src/util/io.rs index 5378f2003..2258de5ce 100644 --- a/core/startos/src/util/io.rs +++ b/core/startos/src/util/io.rs @@ -14,7 +14,7 @@ use std::time::Duration; use bytes::{Buf, BytesMut}; use clap::builder::ValueParserFactory; use futures::future::{BoxFuture, Fuse}; -use futures::{FutureExt, Stream, StreamExt, TryStreamExt}; +use futures::{FutureExt, Stream, TryStreamExt}; use helpers::{AtomicFile, NonDetachingJoinHandle}; use inotify::{EventMask, EventStream, Inotify, WatchMask}; use models::FromStrParser; @@ -22,7 +22,7 @@ use nix::unistd::{Gid, Uid}; use serde::{Deserialize, Serialize}; use tokio::fs::{File, OpenOptions}; use tokio::io::{ - AsyncRead, AsyncReadExt, AsyncSeek, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf, SeekFrom, + AsyncRead, AsyncReadExt, AsyncSeek, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf, WriteHalf, duplex, }; use tokio::net::TcpStream; diff --git a/core/startos/src/util/sync.rs b/core/startos/src/util/sync.rs index 98b24509d..3f97e913f 100644 --- a/core/startos/src/util/sync.rs +++ b/core/startos/src/util/sync.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, VecDeque}; +use std::collections::VecDeque; use std::ops::Deref; use std::pin::Pin; use std::sync::atomic::AtomicUsize; @@ -28,6 +28,7 @@ fn annotate_lock(f: F, id: usize, write: bool) -> T where F: FnOnce() -> T, { + use std::collections::BTreeMap; std::thread_local! { static LOCK_CTX: std::cell::RefCell>> = std::cell::RefCell::new(BTreeMap::new()); } diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index 0df8ff57a..551de11b8 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -55,8 +55,9 @@ mod v0_4_0_alpha_12; mod v0_4_0_alpha_13; mod v0_4_0_alpha_14; mod v0_4_0_alpha_15; +mod v0_4_0_alpha_16; -pub type Current = v0_4_0_alpha_15::Version; // VERSION_BUMP +pub type Current = v0_4_0_alpha_16::Version; // VERSION_BUMP impl Current { #[instrument(skip(self, db))] @@ -173,7 +174,8 @@ enum Version { V0_4_0_alpha_12(Wrapper), V0_4_0_alpha_13(Wrapper), V0_4_0_alpha_14(Wrapper), - V0_4_0_alpha_15(Wrapper), // VERSION_BUMP + V0_4_0_alpha_15(Wrapper), + V0_4_0_alpha_16(Wrapper), // VERSION_BUMP Other(exver::Version), } @@ -231,7 +233,8 @@ impl Version { Self::V0_4_0_alpha_12(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_13(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_14(v) => DynVersion(Box::new(v.0)), - Self::V0_4_0_alpha_15(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP + Self::V0_4_0_alpha_15(v) => DynVersion(Box::new(v.0)), + Self::V0_4_0_alpha_16(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP Self::Other(v) => { return Err(Error::new( eyre!("unknown version {v}"), @@ -281,7 +284,8 @@ impl Version { Version::V0_4_0_alpha_12(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_13(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_14(Wrapper(x)) => x.semver(), - Version::V0_4_0_alpha_15(Wrapper(x)) => x.semver(), // VERSION_BUMP + Version::V0_4_0_alpha_15(Wrapper(x)) => x.semver(), + Version::V0_4_0_alpha_16(Wrapper(x)) => x.semver(), // VERSION_BUMP Version::Other(x) => x.clone(), } } diff --git a/core/startos/src/version/v0_4_0_alpha_16.rs b/core/startos/src/version/v0_4_0_alpha_16.rs new file mode 100644 index 000000000..a241a5f38 --- /dev/null +++ b/core/startos/src/version/v0_4_0_alpha_16.rs @@ -0,0 +1,66 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{VersionT, v0_4_0_alpha_15}; +use crate::prelude::*; +use crate::status::StatusInfo; + +lazy_static::lazy_static! { + static ref V0_4_0_alpha_16: exver::Version = exver::Version::new( + [0, 4, 0], + [PreReleaseSegment::String("alpha".into()), 16.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_4_0_alpha_15::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_4_0_alpha_16.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + #[instrument(skip_all)] + fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result { + for (_, pde) in db["public"]["packageData"] + .as_object_mut() + .into_iter() + .flat_map(|x| x.iter_mut()) + { + pde["statusInfo"] = to_value(&StatusInfo::default())?; + if match pde["status"]["main"].as_str().unwrap_or_default() { + "running" | "starting" | "restarting" => true, + "backingUp" + if pde["status"]["main"]["onComplete"] + .as_bool() + .unwrap_or(false) => + { + true + } + "error" + if pde["status"]["main"]["onRebuild"] + .as_bool() + .unwrap_or(false) => + { + true + } + _ => false, + } { + pde["statusInfo"]["desired"]["main"] = to_value(&"running")?; + } + } + + Ok(Value::Null) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/debian/startos/postinst b/debian/startos/postinst index afc71cb5f..db4a88000 100755 --- a/debian/startos/postinst +++ b/debian/startos/postinst @@ -55,8 +55,7 @@ StartOS v${VERSION} EOF # change timezone -rm -f /etc/localtime -ln -s /usr/share/zoneinfo/Etc/UTC /etc/localtime +ln -sf /usr/share/zoneinfo/Etc/UTC /etc/localtime rm /etc/resolv.conf echo "nameserver 127.0.0.1" > /etc/resolv.conf @@ -122,8 +121,6 @@ ln -sf /usr/lib/startos/scripts/wireguard-vps-proxy-setup /usr/bin/wireguard-vps echo "fs.inotify.max_user_watches=1048576" > /etc/sysctl.d/97-startos.conf -dpkg-reconfigure --frontend noninteractive locales - if ! getent group | grep '^startos:'; then groupadd startos fi diff --git a/patch-db b/patch-db index 90b336d6a..bdb5a1011 160000 --- a/patch-db +++ b/patch-db @@ -1 +1 @@ -Subproject commit 90b336d6a98d3c89798a69138786c51427a80e7d +Subproject commit bdb5a10114a085e86b52ab72e9691ec0db63c4dd diff --git a/sdk/base/lib/Effects.ts b/sdk/base/lib/Effects.ts index 24aafd310..eb44fae3e 100644 --- a/sdk/base/lib/Effects.ts +++ b/sdk/base/lib/Effects.ts @@ -13,8 +13,8 @@ import { ExportServiceInterfaceParams, ServiceInterface, CreateTaskParams, - MainStatus, MountParams, + StatusInfo, } from "./osBindings" import { PackageId, @@ -66,7 +66,7 @@ export type Effects = { getStatus(options: { packageId?: PackageId callback?: () => void - }): Promise + }): Promise /** indicate to the host os what runstate the service is in */ setMainStatus(options: SetMainStatus): Promise diff --git a/sdk/base/lib/osBindings/DesiredStatus.ts b/sdk/base/lib/osBindings/DesiredStatus.ts new file mode 100644 index 000000000..b0b755bae --- /dev/null +++ b/sdk/base/lib/osBindings/DesiredStatus.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { StartStop } from "./StartStop" + +export type DesiredStatus = + | { main: "stopped" } + | { main: "restarting" } + | { main: "running" } + | { main: "backing-up"; onComplete: StartStop } diff --git a/sdk/base/lib/osBindings/DependencyKind.ts b/sdk/base/lib/osBindings/ErrorData.ts similarity index 65% rename from sdk/base/lib/osBindings/DependencyKind.ts rename to sdk/base/lib/osBindings/ErrorData.ts index e7021ba16..3485b2f8b 100644 --- a/sdk/base/lib/osBindings/DependencyKind.ts +++ b/sdk/base/lib/osBindings/ErrorData.ts @@ -1,3 +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" +export type ErrorData = { details: string; debug: string } diff --git a/sdk/base/lib/osBindings/MainStatus.ts b/sdk/base/lib/osBindings/MainStatus.ts deleted file mode 100644 index ca5655bd8..000000000 --- a/sdk/base/lib/osBindings/MainStatus.ts +++ /dev/null @@ -1,25 +0,0 @@ -// 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 { NamedHealthCheckResult } from "./NamedHealthCheckResult" -import type { StartStop } from "./StartStop" - -export type MainStatus = - | { - main: "error" - onRebuild: StartStop - message: string - debug: string | null - } - | { main: "stopped" } - | { main: "restarting" } - | { main: "stopping" } - | { - main: "starting" - health: { [key: HealthCheckId]: NamedHealthCheckResult } - } - | { - main: "running" - started: string - health: { [key: HealthCheckId]: NamedHealthCheckResult } - } - | { main: "backingUp"; onComplete: StartStop } diff --git a/sdk/base/lib/osBindings/PackageDataEntry.ts b/sdk/base/lib/osBindings/PackageDataEntry.ts index cb65500a2..857ff9e07 100644 --- a/sdk/base/lib/osBindings/PackageDataEntry.ts +++ b/sdk/base/lib/osBindings/PackageDataEntry.ts @@ -4,17 +4,17 @@ import type { ActionMetadata } from "./ActionMetadata" import type { CurrentDependencies } from "./CurrentDependencies" import type { DataUrl } from "./DataUrl" import type { Hosts } from "./Hosts" -import type { MainStatus } from "./MainStatus" import type { PackageState } from "./PackageState" import type { ReplayId } from "./ReplayId" import type { ServiceInterface } from "./ServiceInterface" import type { ServiceInterfaceId } from "./ServiceInterfaceId" +import type { StatusInfo } from "./StatusInfo" import type { TaskEntry } from "./TaskEntry" export type PackageDataEntry = { stateInfo: PackageState s9pk: string - status: MainStatus + statusInfo: StatusInfo registry: string | null developerKey: string icon: DataUrl diff --git a/sdk/base/lib/osBindings/StatusInfo.ts b/sdk/base/lib/osBindings/StatusInfo.ts new file mode 100644 index 000000000..c4b90ee62 --- /dev/null +++ b/sdk/base/lib/osBindings/StatusInfo.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DesiredStatus } from "./DesiredStatus" +import type { ErrorData } from "./ErrorData" +import type { HealthCheckId } from "./HealthCheckId" +import type { NamedHealthCheckResult } from "./NamedHealthCheckResult" + +export type StatusInfo = { + health: { [key: HealthCheckId]: NamedHealthCheckResult } + error: ErrorData | null + started: string | null + desired: DesiredStatus +} diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts index 72f9d4745..7a97bfe81 100644 --- a/sdk/base/lib/osBindings/index.ts +++ b/sdk/base/lib/osBindings/index.ts @@ -59,11 +59,11 @@ export { CurrentDependencies } from "./CurrentDependencies" export { CurrentDependencyInfo } from "./CurrentDependencyInfo" export { DataUrl } from "./DataUrl" export { Dependencies } from "./Dependencies" -export { DependencyKind } from "./DependencyKind" export { DependencyMetadata } from "./DependencyMetadata" export { DependencyRequirement } from "./DependencyRequirement" export { DepInfo } from "./DepInfo" export { Description } from "./Description" +export { DesiredStatus } from "./DesiredStatus" export { DestroySubcontainerFsParams } from "./DestroySubcontainerFsParams" export { DeviceFilter } from "./DeviceFilter" export { DnsSettings } from "./DnsSettings" @@ -72,6 +72,7 @@ export { Duration } from "./Duration" export { EchoParams } from "./EchoParams" export { EditSignerParams } from "./EditSignerParams" export { EncryptedWire } from "./EncryptedWire" +export { ErrorData } from "./ErrorData" export { EventId } from "./EventId" export { ExportActionParams } from "./ExportActionParams" export { ExportServiceInterfaceParams } from "./ExportServiceInterfaceParams" @@ -123,7 +124,6 @@ export { LoginParams } from "./LoginParams" export { LshwDevice } from "./LshwDevice" export { LshwDisplay } from "./LshwDisplay" export { LshwProcessor } from "./LshwProcessor" -export { MainStatus } from "./MainStatus" export { Manifest } from "./Manifest" export { MaybeUtf8String } from "./MaybeUtf8String" export { MebiBytes } from "./MebiBytes" @@ -201,6 +201,7 @@ export { SignAssetParams } from "./SignAssetParams" export { SignerInfo } from "./SignerInfo" export { SmtpValue } from "./SmtpValue" export { StartStop } from "./StartStop" +export { StatusInfo } from "./StatusInfo" export { TaskCondition } from "./TaskCondition" export { TaskEntry } from "./TaskEntry" export { TaskInput } from "./TaskInput" diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts index a86ce2562..7797b1041 100644 --- a/sdk/package/lib/StartSdk.ts +++ b/sdk/package/lib/StartSdk.ts @@ -61,7 +61,7 @@ import { } from "../../base/lib/inits" import { DropGenerator } from "../../base/lib/util/Drop" -export const OSVersion = testTypeVersion("0.4.0-alpha.15") +export const OSVersion = testTypeVersion("0.4.0-alpha.16") // prettier-ignore type AnyNeverCond = diff --git a/web/package-lock.json b/web/package-lock.json index ceaadff03..ce7370579 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "startos-ui", - "version": "0.4.0-alpha.15", + "version": "0.4.0-alpha.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "startos-ui", - "version": "0.4.0-alpha.15", + "version": "0.4.0-alpha.16", "license": "MIT", "dependencies": { "@angular/animations": "^20.3.0", diff --git a/web/package.json b/web/package.json index a571a44f5..38182ba69 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "startos-ui", - "version": "0.4.0-alpha.15", + "version": "0.4.0-alpha.16", "author": "Start9 Labs, Inc", "homepage": "https://start9.com/", "license": "MIT", diff --git a/web/projects/start-tunnel/src/app/routes/home/routes/devices/index.ts b/web/projects/start-tunnel/src/app/routes/home/routes/devices/index.ts index 09063a376..88fbde5d2 100644 --- a/web/projects/start-tunnel/src/app/routes/home/routes/devices/index.ts +++ b/web/projects/start-tunnel/src/app/routes/home/routes/devices/index.ts @@ -19,7 +19,6 @@ import { PatchDB } from 'patch-db-client' import { filter, map } from 'rxjs' import { ApiService } from 'src/app/services/api/api.service' import { TunnelData } from 'src/app/services/patch-db/data-model' - import { DEVICES_ADD } from './add' import { DEVICES_CONFIG } from './config' import { MappedDevice, MappedSubnet } from './utils' @@ -84,6 +83,8 @@ import { MappedDevice, MappedSubnet } from './utils' + } @empty { +
No devices
} diff --git a/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/index.ts b/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/index.ts index e5b2ae735..b607f1876 100644 --- a/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/index.ts +++ b/web/projects/start-tunnel/src/app/routes/home/routes/port-forwards/index.ts @@ -55,6 +55,8 @@ import { MappedDevice, MappedForward } from './utils' + } @empty { +
No port forwards
} diff --git a/web/projects/start-tunnel/src/app/routes/home/routes/subnets/index.ts b/web/projects/start-tunnel/src/app/routes/home/routes/subnets/index.ts index 5e66aa3fe..78e24434d 100644 --- a/web/projects/start-tunnel/src/app/routes/home/routes/subnets/index.ts +++ b/web/projects/start-tunnel/src/app/routes/home/routes/subnets/index.ts @@ -67,6 +67,8 @@ import { SUBNETS_ADD } from './add' + } @empty { +
No subnets
} diff --git a/web/projects/start-tunnel/src/styles.scss b/web/projects/start-tunnel/src/styles.scss index 81af49f7d..4ed8d414e 100644 --- a/web/projects/start-tunnel/src/styles.scss +++ b/web/projects/start-tunnel/src/styles.scss @@ -80,6 +80,12 @@ tui-dialog[new][data-appearance~='start-9'] { } } +.placeholder { + padding: 1rem; + font: var(--tui-font-text-l); + color: var(--tui-text-tertiary); +} + qr-code { display: flex; justify-content: center; diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/error.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/error.component.ts index bb4ad0568..7fe582761 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/error.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/error.component.ts @@ -15,7 +15,7 @@ import { getManifest } from 'src/app/utils/get-package-data' selector: 'service-error', template: `
{{ 'Service Launch Error' | i18n }}
-

{{ error?.message }}

+

{{ error?.details }}

{{ error?.debug }}

{{ 'Actions' | i18n }} @@ -95,7 +95,7 @@ export class ServiceErrorComponent { overflow = false get error() { - return this.pkg.status.main === 'error' ? this.pkg.status : null + return this.pkg.statusInfo.error } rebuild() { @@ -108,7 +108,7 @@ export class ServiceErrorComponent { show() { this.dialog - .openAlert(this.error?.message as i18nKey, { label: 'Service error' }) + .openAlert(this.error?.details as i18nKey, { label: 'Service error' }) .subscribe() } } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/status.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/status.component.ts index a744244f0..d77ec9367 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/status.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/status.component.ts @@ -37,7 +37,7 @@ import { } - @if ($any(pkg().status)?.started; as started) { + @if (pkg().statusInfo.started; as started) { }

diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/task.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/task.component.ts index 216689756..64244e091 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/task.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/task.component.ts @@ -19,6 +19,7 @@ import { ServiceTasksComponent } from 'src/app/routes/portal/routes/services/com import { ActionService } from 'src/app/services/action.service' import { ApiService } from 'src/app/services/api/embassy-api.service' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' +import { getInstalledBaseStatus } from 'src/app/services/pkg-status-rendering.service' import { getManifest } from 'src/app/utils/get-package-data' @Component({ @@ -161,7 +162,7 @@ export class ServiceTaskComponent { pkgInfo: { id: this.task().packageId, title, - mainStatus: pkg.status.main, + status: getInstalledBaseStatus(pkg.statusInfo), icon: pkg.icon, }, actionInfo: { id: this.task().actionId, metadata }, diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/uptime.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/uptime.component.ts index 5cd41ec75..35a2ff618 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/uptime.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/uptime.component.ts @@ -114,11 +114,10 @@ import { distinctUntilChanged } from 'rxjs/operators' }) export class ServiceUptimeComponent { protected readonly uptime$ = timer(0, 1000).pipe( - map(() => - this.started() - ? Math.max(Date.now() - new Date(this.started()).getTime(), 0) - : 0, - ), + map(() => { + const started = this.started() + return started ? Math.max(Date.now() - new Date(started).getTime(), 0) : 0 + }), distinctUntilChanged(), map(delta => ({ seconds: Math.floor(delta / 1000) % 60, @@ -128,5 +127,5 @@ export class ServiceUptimeComponent { })), ) - readonly started = input('') + readonly started = input(null) } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/service.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/service.component.ts index 5f90890b9..6732970dd 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/service.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/service.component.ts @@ -34,7 +34,7 @@ import { StatusComponent } from './status.component' > {{ manifest.version }} - @if ($any(pkg.status)?.started; as started) { + @if (pkg.statusInfo.started; as started) { {{ 'Uptime' | i18n }}: } @else { diff --git a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts index f66d25a21..cf30de7ac 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts @@ -56,7 +56,7 @@ export class StatusComponent { const { primary, health } = this.getStatus(this.pkg) return ( !this.hasDepErrors && - primary !== 'taskRequired' && + primary !== 'task-required' && primary !== 'error' && health !== 'failure' ) @@ -80,7 +80,7 @@ export class StatusComponent { case 'updating': case 'stopping': case 'starting': - case 'backingUp': + case 'backing-up': case 'restarting': case 'removing': return true @@ -93,7 +93,7 @@ export class StatusComponent { switch (this.getStatus(this.pkg).primary) { case 'running': return 'var(--tui-status-positive)' - case 'taskRequired': + case 'task-required': return 'var(--tui-status-warning)' case 'error': return 'var(--tui-status-negative)' @@ -101,7 +101,7 @@ export class StatusComponent { case 'updating': case 'stopping': case 'starting': - case 'backingUp': + case 'backing-up': case 'restarting': case 'removing': case 'restoring': diff --git a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/ui-launch.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/ui-launch.component.ts index 13257af83..4f6211d9f 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/ui-launch.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/ui-launch.component.ts @@ -11,6 +11,7 @@ import { tuiPure } from '@taiga-ui/cdk' import { TuiDataList, TuiDropdown, TuiButton } from '@taiga-ui/core' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { InterfaceService } from '../../../components/interfaces/interface.service' +import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service' @Component({ selector: 'app-ui-launch', @@ -70,7 +71,7 @@ export class UILaunchComponent { } get isRunning(): boolean { - return this.pkg.status.main === 'running' + return getInstalledPrimaryStatus(this.pkg) === 'running' } @tuiPure diff --git a/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts index b8dbcff71..d3b4e33da 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts @@ -27,6 +27,7 @@ import { TaskInfoComponent } from 'src/app/routes/portal/modals/config-dep.compo import { ActionService } from 'src/app/services/action.service' import { ApiService } from 'src/app/services/api/embassy-api.service' import { DataModel } from 'src/app/services/patch-db/data-model' +import { BaseStatus } from 'src/app/services/pkg-status-rendering.service' import { getAllPackages, getManifest } from 'src/app/utils/get-package-data' export type PackageActionData = { @@ -34,7 +35,7 @@ export type PackageActionData = { id: string title: string icon: string - mainStatus: T.MainStatus['main'] + status: BaseStatus } actionInfo: { id: string diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts index b62be6515..06597283e 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts @@ -16,6 +16,10 @@ import { DataModel } from 'src/app/services/patch-db/data-model' import { StandardActionsService } from 'src/app/services/standard-actions.service' import { getManifest } from 'src/app/utils/get-package-data' import { ServiceActionComponent } from '../components/action.component' +import { + BaseStatus, + getInstalledBaseStatus, +} from 'src/app/services/pkg-status-rendering.service' @Component({ template: ` @@ -27,7 +31,7 @@ import { ServiceActionComponent } from '../components/action.component' } @@ -77,7 +81,7 @@ export default class ServiceActionsRoute { ? 'Other' : 'General' return { - mainStatus: pkg.status.main, + status: getInstalledBaseStatus(pkg.statusInfo), icon: pkg.icon, manifest: getManifest(pkg), actions: Object.entries(pkg.actions) @@ -131,13 +135,13 @@ export default class ServiceActionsRoute { } handle( - mainStatus: T.MainStatus['main'], + status: BaseStatus, icon: string, { id, title }: T.Manifest, action: T.ActionMetadata & { id: string }, ) { this.actions.present({ - pkgInfo: { id, title, icon, mainStatus }, + pkgInfo: { id, title, icon, status }, actionInfo: { id: action.id, metadata: action }, }) } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts index 7007172e9..eaa348667 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts @@ -22,6 +22,7 @@ import { InterfaceService, } from '../../../components/interfaces/interface.service' import { GatewayService } from 'src/app/services/gateway.service' +import { getInstalledBaseStatus } from 'src/app/services/pkg-status-rendering.service' @Component({ template: ` @@ -101,7 +102,8 @@ export default class ServiceInterfaceRoute { readonly pkg = toSignal(this.patch.watch$('packageData', this.pkgId)) readonly isRunning = computed(() => { - return this.pkg()?.status.main === 'running' + const pkg = this.pkg() + return pkg ? getInstalledBaseStatus(pkg.statusInfo) === 'running' : false }) readonly serviceInterface = computed(() => { diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/outlet.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/outlet.component.ts index f96be7297..050817123 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/outlet.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/outlet.component.ts @@ -25,7 +25,7 @@ const INACTIVE: PrimaryStatus[] = [ 'updating', 'removing', 'restoring', - 'backingUp', + 'backing-up', ] @Component({ diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/service.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/service.component.ts index 224bdfaa4..bbdbab10d 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/service.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/service.component.ts @@ -34,7 +34,7 @@ import { ServiceUptimeComponent } from '../components/uptime.component' @Component({ template: ` @if (pkg(); as pkg) { - @if (pkg.status.main === 'error') { + @if (pkg.statusInfo.error) { } @else if (installing()) { @@ -45,9 +45,9 @@ import { ServiceUptimeComponent } from '../components/uptime.component' } - @if (status() !== 'backingUp') { + @if (status() !== 'backing-up') { - + @if (errors() | async; as errors) { @@ -179,7 +179,7 @@ export class ServiceRoute { protected readonly pkg = computed(() => this.services()[this.id() || '']) protected readonly health = computed((pkg = this.pkg()) => - pkg ? toHealthCheck(pkg.status) : [], + pkg ? toHealthCheck(pkg.statusInfo) : [], ) protected readonly status = computed((pkg = this.pkg()) => @@ -202,8 +202,10 @@ export class ServiceRoute { ) } -function toHealthCheck(status: T.MainStatus): T.NamedHealthCheckResult[] { - return status.main !== 'running' || isEmptyObject(status.health) +function toHealthCheck(statusInfo: T.StatusInfo): T.NamedHealthCheckResult[] { + return statusInfo.desired.main !== 'running' || + !statusInfo.started || + isEmptyObject(statusInfo.health) ? [] - : Object.values(status.health).filter(h => !!h) + : Object.values(statusInfo.health).filter(h => !!h) } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/progress.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/progress.component.ts index ad39cb26c..8c26d5d72 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/progress.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/progress.component.ts @@ -28,7 +28,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model' {{ 'complete' | i18n }} } @else { - @if ((pkg.key | tuiMapper: toStatus | async) === 'backingUp') { + @if ((pkg.key | tuiMapper: toStatus | async) === 'backing-up') { {{ 'backing up' | i18n }} } @else { @@ -65,5 +65,5 @@ export class BackupProgressComponent { ) readonly toStatus = (pkgId: string) => - this.patch.watch$('packageData', pkgId, 'status', 'main') + this.patch.watch$('packageData', pkgId, 'statusInfo', 'desired', 'main') } diff --git a/web/projects/ui/src/app/services/action.service.ts b/web/projects/ui/src/app/services/action.service.ts index 40d7e1a42..034d39636 100644 --- a/web/projects/ui/src/app/services/action.service.ts +++ b/web/projects/ui/src/app/services/action.service.ts @@ -26,7 +26,7 @@ const allowedStatuses = { 'restoring', 'stopping', 'starting', - 'backingUp', + 'backing-up', ]), } @@ -45,9 +45,7 @@ export class ActionService { const { pkgInfo, actionInfo } = data if ( - allowedStatuses[actionInfo.metadata.allowedStatuses].has( - pkgInfo.mainStatus, - ) + allowedStatuses[actionInfo.metadata.allowedStatuses].has(pkgInfo.status) ) { if (actionInfo.metadata.hasInput) { this.formDialog.open(ActionInputModal, { 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 965950794..2020f31bc 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -1921,8 +1921,9 @@ export namespace Mock { s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk', icon: '/assets/img/service-icons/bitcoin-core.svg', lastBackup: null, - status: { - main: 'running', + statusInfo: { + error: null, + desired: { main: 'running' }, started: new Date().toISOString(), health: {}, }, @@ -2201,8 +2202,11 @@ export namespace Mock { s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk', icon: '/assets/img/service-icons/btc-rpc-proxy.png', lastBackup: null, - status: { - main: 'stopped', + statusInfo: { + desired: { main: 'stopped' }, + started: null, + health: {}, + error: null, }, actions: {}, serviceInterfaces: { @@ -2246,8 +2250,11 @@ export namespace Mock { s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk', icon: '/assets/img/service-icons/lnd.png', lastBackup: null, - status: { - main: 'stopped', + statusInfo: { + desired: { main: 'stopped' }, + error: null, + health: {}, + started: null, }, actions: { config: { 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 2fd5452a1..1799dd132 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 @@ -762,12 +762,12 @@ export class MockApiService extends ApiService { setTimeout(async () => { for (let i = 0; i < ids.length; i++) { const id = ids[i] - const appPath = `/packageData/${id}/status/main/` - const appPatch: ReplaceOperation[] = [ + const appPath = `/packageData/${id}/statusInfo/desired/main` + const appPatch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path: appPath, - value: 'backingUp', + value: 'backing-up', }, ] this.mockRevision(appPatch) @@ -1073,17 +1073,18 @@ export class MockApiService extends ApiService { } async startPackage(params: RR.StartPackageReq): Promise { - const path = `/packageData/${params.id}/status` + const path = `/packageData/${params.id}/statusInfo` await pauseFor(2000) setTimeout(async () => { - const patch2: ReplaceOperation[] = [ + const patch2: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path, value: { - main: 'running', + error: null, + desired: { main: 'running' }, started: new Date().toISOString(), health: { 'ephemeral-health-check': { @@ -1118,14 +1119,14 @@ export class MockApiService extends ApiService { this.mockRevision(patch2) }, 2000) - const originalPatch: ReplaceOperation< - T.MainStatus & { main: 'starting' } - >[] = [ + const originalPatch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path, value: { - main: 'starting', + desired: { main: 'running' }, + started: null, + error: null, health: {}, }, }, @@ -1140,15 +1141,16 @@ export class MockApiService extends ApiService { params: RR.RestartPackageReq, ): Promise { await pauseFor(2000) - const path = `/packageData/${params.id}/status` + const path = `/packageData/${params.id}/statusInfo` setTimeout(async () => { - const patch2: ReplaceOperation[] = [ + const patch2: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path, value: { - main: 'running', + desired: { main: 'running' }, + error: null, started: new Date().toISOString(), health: { 'ephemeral-health-check': { @@ -1183,12 +1185,15 @@ export class MockApiService extends ApiService { this.mockRevision(patch2) }, this.revertTime) - const patch: ReplaceOperation[] = [ + const patch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path, value: { - main: 'restarting', + desired: { main: 'restarting' }, + started: null, + error: null, + health: {}, }, }, ] @@ -1200,24 +1205,34 @@ export class MockApiService extends ApiService { async stopPackage(params: RR.StopPackageReq): Promise { await pauseFor(2000) - const path = `/packageData/${params.id}/status` + const path = `/packageData/${params.id}/statusInfo` setTimeout(() => { - const patch2: ReplaceOperation[] = [ + const patch2: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path: path, - value: { main: 'stopped' }, + value: { + desired: { main: 'stopped' }, + error: null, + health: {}, + started: null, + }, }, ] this.mockRevision(patch2) }, this.revertTime) - const patch: ReplaceOperation[] = [ + const patch: ReplaceOperation[] = [ { op: PatchOp.REPLACE, path: path, - value: { main: 'stopping' }, + value: { + desired: { main: 'stopped' }, + error: null, + health: {}, + started: new Date().toISOString(), + }, }, ] 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 25cb03402..4751a5028 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -232,8 +232,11 @@ export const mockPatchData: DataModel = { s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk', icon: '/assets/img/service-icons/bitcoin-core.svg', lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(), - status: { - main: 'stopped', + statusInfo: { + desired: { main: 'stopped' }, + error: null, + health: {}, + started: null, }, // status: { // main: 'error', @@ -518,8 +521,11 @@ export const mockPatchData: DataModel = { s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk', icon: '/assets/img/service-icons/lnd.png', lastBackup: null, - status: { - main: 'stopped', + statusInfo: { + desired: { main: 'stopped' }, + error: null, + health: {}, + started: null, }, actions: { config: { diff --git a/web/projects/ui/src/app/services/dep-error.service.ts b/web/projects/ui/src/app/services/dep-error.service.ts index 899cc8a06..d3be91e32 100644 --- a/web/projects/ui/src/app/services/dep-error.service.ts +++ b/web/projects/ui/src/app/services/dep-error.service.ts @@ -11,6 +11,7 @@ import deepEqual from 'fast-deep-equal' import { Observable } from 'rxjs' import { isInstalled } from 'src/app/utils/get-package-data' import { T } from '@start9labs/start-sdk' +import { getInstalledBaseStatus } from './pkg-status-rendering.service' export type AllDependencyErrors = Record export type PkgDependencyErrors = Record @@ -153,7 +154,7 @@ export class DepErrorService { } } - const depStatus = dep.status.main + const depStatus = getInstalledBaseStatus(dep.statusInfo) // not running if (depStatus !== 'running' && depStatus !== 'starting') { @@ -165,7 +166,7 @@ export class DepErrorService { // health check failure if (depStatus === 'running' && currentDep?.kind === 'running') { for (let id of currentDep.healthChecks) { - const check = dep.status.health[id] + const check = dep.statusInfo.health[id] if (check && check?.result !== 'success') { return { type: 'healthChecksFailed', diff --git a/web/projects/ui/src/app/services/pkg-status-rendering.service.ts b/web/projects/ui/src/app/services/pkg-status-rendering.service.ts index 9f1b7f2cf..f3bf8230f 100644 --- a/web/projects/ui/src/app/services/pkg-status-rendering.service.ts +++ b/web/projects/ui/src/app/services/pkg-status-rendering.service.ts @@ -13,7 +13,7 @@ export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus { if (pkg.stateInfo.state === 'installed') { primary = getInstalledPrimaryStatus(pkg) - health = getHealthStatus(pkg.status) + health = getHealthStatus(pkg.statusInfo) } else { primary = pkg.stateInfo.state } @@ -21,33 +21,43 @@ export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus { return { primary, health } } -export function getInstalledPrimaryStatus({ - tasks, - status, -}: T.PackageDataEntry): PrimaryStatus { +export function getInstalledBaseStatus(statusInfo: T.StatusInfo): BaseStatus { if ( - Object.values(tasks).some(t => t.active && t.task.severity === 'critical') - ) { - return 'taskRequired' - } - - if ( - Object.values(status.main === 'running' && status.health) - .filter(h => !!h) - .some(h => h.result === 'starting') + statusInfo.desired.main === 'running' && + (!statusInfo.started || + Object.values(statusInfo.health) + .filter(h => !!h) + .some(h => h.result === 'starting')) ) { return 'starting' } - return status.main + if (statusInfo.desired.main === 'stopped' && statusInfo.started) { + return 'stopping' + } + + return statusInfo.desired.main } -function getHealthStatus(status: T.MainStatus): T.HealthStatus | null { - if (status.main !== 'running' || !status.main) { +export function getInstalledPrimaryStatus({ + tasks, + statusInfo, +}: T.PackageDataEntry): PrimaryStatus { + if ( + Object.values(tasks).some(t => t.active && t.task.severity === 'critical') + ) { + return 'task-required' + } + + return getInstalledBaseStatus(statusInfo) +} + +function getHealthStatus(statusInfo: T.StatusInfo): T.HealthStatus | null { + if (statusInfo.desired.main !== 'running') { return null } - const values = Object.values(status.health).filter(h => !!h) + const values = Object.values(statusInfo.health).filter(h => !!h) if (values.some(h => h.result === 'failure')) { return 'failure' @@ -70,7 +80,7 @@ export interface StatusRendering { showDots?: boolean } -export type PrimaryStatus = +export type BaseStatus = | 'installing' | 'updating' | 'removing' @@ -80,10 +90,11 @@ export type PrimaryStatus = | 'stopping' | 'restarting' | 'stopped' - | 'backingUp' - | 'taskRequired' + | 'backing-up' | 'error' +export type PrimaryStatus = BaseStatus | 'task-required' + export type DependencyStatus = 'warning' | 'satisfied' export const PrimaryRendering: Record = { @@ -122,7 +133,7 @@ export const PrimaryRendering: Record = { color: 'dark-shade', showDots: false, }, - backingUp: { + 'backing-up': { display: 'Backing Up', color: 'primary', showDots: true, @@ -137,7 +148,7 @@ export const PrimaryRendering: Record = { color: 'success', showDots: false, }, - taskRequired: { + 'task-required': { display: 'Task Required', color: 'warning', showDots: false,