mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
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 <me@drbonez.dev>
This commit is contained in:
100
.github/workflows/start-registry.yaml
vendored
Normal file
100
.github/workflows/start-registry.yaml
vendored
Normal file
@@ -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
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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<patch_db::value::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
#[derive(Clone, Deserialize, Serialize, TS)]
|
||||
pub struct ErrorData {
|
||||
pub details: String,
|
||||
pub debug: String,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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<C: Context>() -> ParentHandler<C> {
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct BackupMetadata {
|
||||
pub timestamp: DateTime<Utc>,
|
||||
#[serde(default)]
|
||||
pub network_keys: BTreeMap<HostId, Base64<[u8; 32]>>,
|
||||
#[serde(default)]
|
||||
pub tor_keys: BTreeMap<HostId, Base32<[u8; 64]>>, // DEPRECATED
|
||||
pub registry: Option<Url>,
|
||||
}
|
||||
|
||||
@@ -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::<Vec<PackageId>>(|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()?
|
||||
{
|
||||
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)
|
||||
.any(|(_, t)| t.active && t.task.severity == TaskSeverity::Critical)
|
||||
{
|
||||
pde.as_status_info_mut()
|
||||
.as_desired_mut()
|
||||
.ser(&DesiredStatus::Stopped)?;
|
||||
}
|
||||
})()
|
||||
.transpose()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result?
|
||||
{
|
||||
let svc = self.services.get(&id).await;
|
||||
if let Some(svc) = &*svc {
|
||||
svc.stop(procedure_id.clone(), false).await?;
|
||||
}
|
||||
}
|
||||
.result?;
|
||||
check_tasks.complete();
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Url>,
|
||||
#[ts(type = "string")]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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<RpcContinuations> + Clone + Sized {
|
||||
const UI_DIR: &'static Dir<'static>;
|
||||
fn api() -> ParentHandler<Self>;
|
||||
|
||||
@@ -376,7 +376,7 @@ impl Default for AlpnInfo {
|
||||
}
|
||||
}
|
||||
|
||||
type Mapping<A: Accept> = BTreeMap<Option<InternedString>, InOMap<DynVHostTarget<A>, Weak<()>>>;
|
||||
type Mapping<A> = BTreeMap<Option<InternedString>, InOMap<DynVHostTarget<A>, Weak<()>>>;
|
||||
|
||||
pub struct GetVHostAcmeProvider<A: Accept + 'static>(pub Watch<Mapping<A>>);
|
||||
impl<A: Accept + 'static> Clone for GetVHostAcmeProvider<A> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -55,7 +55,7 @@ pub trait FileSource: Send + Sync + Sized + 'static {
|
||||
fn to_vec(
|
||||
src: &impl FileSource,
|
||||
verify: Option<(Hash, u64)>,
|
||||
) -> BoxFuture<Result<Vec<u8>, Error>> {
|
||||
) -> BoxFuture<'_, Result<Vec<u8>, Error>> {
|
||||
async move {
|
||||
let mut vec = Vec::with_capacity(if let Some((_, size)) = &verify {
|
||||
*size
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -130,11 +130,11 @@ impl Handler<RunAction> 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<RunAction> 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<RunAction> 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<RunAction> 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 {
|
||||
<Self as Handler<super::control::Stop>>::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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Start> for ServiceActor {
|
||||
type Response = ();
|
||||
fn conflicts_with(_: &Start) -> ConflictBuilder<Self> {
|
||||
ConflictBuilder::everything().except::<RunAction>()
|
||||
}
|
||||
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<Stop> for ServiceActor {
|
||||
type Response = ();
|
||||
fn conflicts_with(_: &Stop) -> ConflictBuilder<Self> {
|
||||
ConflictBuilder::everything().except::<RunAction>()
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<MainStatus, Error> {
|
||||
) -> Result<StatusInfo, Error> {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -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<BTreeSet<P
|
||||
.keys()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub enum DependencyKind {
|
||||
Exists,
|
||||
Running,
|
||||
}
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||
#[serde(rename_all_fields = "camelCase")]
|
||||
@@ -287,8 +279,7 @@ pub struct CheckDependenciesResult {
|
||||
satisfies: BTreeSet<VersionString>,
|
||||
is_running: bool,
|
||||
tasks: BTreeMap<ReplayId, TaskEntry>,
|
||||
#[ts(as = "BTreeMap::<HealthCheckId, NamedHealthCheckResult>")]
|
||||
health_checks: OrdMap<HealthCheckId, NamedHealthCheckResult>,
|
||||
health_checks: BTreeMap<HealthCheckId, NamedHealthCheckResult>,
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<InitKind>,
|
||||
recovery_source: Option<impl GenericMountGuard>,
|
||||
) -> Result<ServiceRef, Error> {
|
||||
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<Option<ServiceRef>, Error> {
|
||||
let handle_installed = {
|
||||
let ctx = ctx.clone();
|
||||
move |s9pk: S9pk, i: Model<PackageDataEntry>| 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::<MountGuard>)
|
||||
Self::new(ctx, s9pk, Guid::new(), None, None::<MountGuard>)
|
||||
.await
|
||||
.map(Some)
|
||||
}
|
||||
@@ -319,7 +304,7 @@ impl Service {
|
||||
s9pk,
|
||||
&s9pk_path,
|
||||
&None,
|
||||
None,
|
||||
InitKind::Install,
|
||||
None::<Never>,
|
||||
None,
|
||||
)
|
||||
@@ -353,7 +338,7 @@ impl Service {
|
||||
s9pk,
|
||||
&s9pk_path,
|
||||
&None,
|
||||
Some(entry.as_status().de()?.run_state()),
|
||||
InitKind::Update,
|
||||
None::<Never>,
|
||||
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::<MountGuard>,
|
||||
)
|
||||
.await
|
||||
match Self::new(ctx.clone(), s9pk, Guid::new(), None, None::<MountGuard>).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<Url>,
|
||||
prev_state: Option<StartStop>,
|
||||
kind: InitKind,
|
||||
recovery_source: Option<impl GenericMountGuard>,
|
||||
progress: Option<InstallProgressHandles>,
|
||||
) -> Result<ServiceRef, Error> {
|
||||
@@ -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<Utc>,
|
||||
}
|
||||
|
||||
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<Notify>,
|
||||
}
|
||||
|
||||
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<Option<BoxFuture<'static, Result<(), RpcError>>>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
|
||||
@@ -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<RunningStatus>,
|
||||
// This tracks references to callbacks registered by the running service:
|
||||
pub(super) callbacks: BTreeSet<Arc<CallbackId>>,
|
||||
/// 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<StartStop>,
|
||||
/// This represents a currently running task that affects the service's shown state, such as BackingUp or Restarting.
|
||||
pub(super) transition_state: Option<TransitionState>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServiceStateKinds {
|
||||
pub transition_state: Option<TransitionKind>,
|
||||
pub running_status: Option<RunningStatus>,
|
||||
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<Self, Error> {
|
||||
pub async fn new(ctx: &RpcContext, s9pk: S9pk) -> Result<Self, Error> {
|
||||
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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ServiceActorSeed>);
|
||||
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<Either<_, _>> = 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::<StatusInfo>();
|
||||
let mut transition: Option<Transition> = 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,
|
||||
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>(())
|
||||
}
|
||||
})),
|
||||
Err(e) => {
|
||||
.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<super::persistent_container::ServiceState>,
|
||||
watch: &mut TypedDbWatch<StatusInfo>,
|
||||
seed: &'a Arc<ServiceActorSeed>,
|
||||
start_stop_task: &mut Option<
|
||||
Either<BoxFuture<'a, Result<(), Error>>, BoxFuture<'a, Result<(), Error>>>,
|
||||
>,
|
||||
transition: &mut Option<Transition<'a>>,
|
||||
) -> 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)?])
|
||||
.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>(())
|
||||
.filter(|task| task.kind == TransitionKind::Stopping);
|
||||
*transition = task.or_else(|| Some(seed.stop()));
|
||||
}
|
||||
.boxed(),
|
||||
)
|
||||
}));
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<MainStatus> 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,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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,22 +72,13 @@ impl Handler<Backup> 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)
|
||||
@@ -53,38 +88,15 @@ impl Handler<Backup> for ServiceActor {
|
||||
.await?;
|
||||
backup_guard.unmount(true).await?;
|
||||
|
||||
if temp.restore().is_start() {
|
||||
current
|
||||
.wait_for(|s| s.running_status.is_some())
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
.await
|
||||
.with_kind(ErrorKind::Unknown)?;
|
||||
.map_err(RpcError::from)
|
||||
}
|
||||
drop(temp);
|
||||
Ok::<_, Arc<Error>>(())
|
||||
});
|
||||
let cancel_handle = transition.cancellation_handle();
|
||||
let transition = transition.shared();
|
||||
let job_transition = transition.clone();
|
||||
jobs.add_job(job_transition.map(|_| ()));
|
||||
.shared();
|
||||
|
||||
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;
|
||||
}
|
||||
Ok(transition
|
||||
.map(|r| {
|
||||
r.ok_or_else(|| Error::new(eyre!("Backup canceled"), ErrorKind::Cancelled))?
|
||||
.map_err(|e| e.clone_output())
|
||||
})
|
||||
.boxed())
|
||||
self.0.backup.replace(Some(transition.clone().boxed()));
|
||||
|
||||
Ok(transition.map_err(Error::from).boxed())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
pub async fn abort(mut self) {
|
||||
self.cancel_handle.cancel_and_wait().await
|
||||
}
|
||||
fn new(
|
||||
task: impl Future<Output = ()> + 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,
|
||||
impl ServiceActorSeed {
|
||||
pub fn start(&self) -> Transition<'_> {
|
||||
Transition {
|
||||
kind: TransitionKind::Starting,
|
||||
future: self.persistent_container.start().boxed(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Drop for TransitionState {
|
||||
fn drop(&mut self) {
|
||||
self.cancel_handle.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TempDesiredRestore(pub(super) Arc<watch::Sender<ServiceState>>, StartStop);
|
||||
impl TempDesiredRestore {
|
||||
pub fn new(state: &Arc<watch::Sender<ServiceState>>) -> Self {
|
||||
Self(state.clone(), state.borrow().desired_state)
|
||||
pub fn stop(&self) -> Transition<'_> {
|
||||
Transition {
|
||||
kind: TransitionKind::Stopping,
|
||||
future: self.persistent_container.stop().boxed(),
|
||||
}
|
||||
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<Option<StartStop>>;
|
||||
// fn deref(&self) -> &Self::Target {
|
||||
// &*self.0
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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<Restart> for ServiceActor {
|
||||
type Response = BoxFuture<'static, Option<()>>;
|
||||
fn conflicts_with(_: &Restart) -> ConflictBuilder<Self> {
|
||||
ConflictBuilder::everything().except::<GetActionInput>()
|
||||
}
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
use futures::Future;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
pub async fn cancellable<T>(
|
||||
cancel_transition: &Notify,
|
||||
transition: impl Future<Output = T>,
|
||||
) -> Result<T, Error> {
|
||||
tokio::select! {
|
||||
a = transition => Ok(a),
|
||||
_ = cancel_transition.notified() => Err(Error::new(eyre!("transition was cancelled"), ErrorKind::Cancelled)),
|
||||
}
|
||||
}
|
||||
@@ -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<Self>"]
|
||||
pub struct StatusInfo {
|
||||
pub health: BTreeMap<HealthCheckId, NamedHealthCheckResult>,
|
||||
pub error: Option<ErrorData>,
|
||||
#[ts(type = "string | null")]
|
||||
pub started: Option<DateTime<Utc>>,
|
||||
pub desired: DesiredStatus,
|
||||
}
|
||||
impl StatusInfo {
|
||||
pub fn stop(&mut self) {
|
||||
self.desired = self.desired.stop();
|
||||
self.health.clear();
|
||||
}
|
||||
}
|
||||
impl Model<StatusInfo> {
|
||||
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<String>,
|
||||
},
|
||||
pub enum DesiredStatus {
|
||||
Stopped,
|
||||
Restarting,
|
||||
Stopping,
|
||||
Starting {
|
||||
#[ts(as = "BTreeMap<HealthCheckId, NamedHealthCheckResult>")]
|
||||
health: OrdMap<HealthCheckId, NamedHealthCheckResult>,
|
||||
},
|
||||
Running {
|
||||
#[ts(type = "string")]
|
||||
started: DateTime<Utc>,
|
||||
#[ts(as = "BTreeMap<HealthCheckId, NamedHealthCheckResult>")]
|
||||
health: OrdMap<HealthCheckId, NamedHealthCheckResult>,
|
||||
},
|
||||
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<HealthCheckId, NamedHealthCheckResult>> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1039,6 +1039,7 @@ pub async fn test_smtp(
|
||||
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
|
||||
|
||||
AsyncSmtpTransport::<Tokio1Executor>::relay(&server)?
|
||||
.port(port)
|
||||
.credentials(Credentials::new(login, password))
|
||||
.build()
|
||||
.send(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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::<TunnelDatabase>::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);
|
||||
|
||||
@@ -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::<IpAddr>("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,17 +612,14 @@ 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."
|
||||
);
|
||||
let san_info = prompt(
|
||||
&default_prompt,
|
||||
prompt(
|
||||
"Subject Alternative Name(s): ",
|
||||
|s| {
|
||||
s.split(",")
|
||||
.map(|s| {
|
||||
@@ -651,14 +629,17 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
|
||||
} else if is_valid_domain(s) {
|
||||
Ok(s.into())
|
||||
} else {
|
||||
Err(format!("{s} is not a valid ip address or domain"))
|
||||
Err(format!(
|
||||
"{s} is not a valid ip address or domain"
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
listen.map(|l| vec![InternedString::from_display(&l.ip())]),
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
};
|
||||
|
||||
ctx.call_remote::<TunnelContext>(
|
||||
"web.generate-certificate",
|
||||
|
||||
@@ -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<Pending> for CActor {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, T>(f: F, id: usize, write: bool) -> T
|
||||
where
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
use std::collections::BTreeMap;
|
||||
std::thread_local! {
|
||||
static LOCK_CTX: std::cell::RefCell<BTreeMap<usize, Result<(), usize>>> = std::cell::RefCell::new(BTreeMap::new());
|
||||
}
|
||||
|
||||
@@ -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_12::Version>),
|
||||
V0_4_0_alpha_13(Wrapper<v0_4_0_alpha_13::Version>),
|
||||
V0_4_0_alpha_14(Wrapper<v0_4_0_alpha_14::Version>),
|
||||
V0_4_0_alpha_15(Wrapper<v0_4_0_alpha_15::Version>), // VERSION_BUMP
|
||||
V0_4_0_alpha_15(Wrapper<v0_4_0_alpha_15::Version>),
|
||||
V0_4_0_alpha_16(Wrapper<v0_4_0_alpha_16::Version>), // 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(),
|
||||
}
|
||||
}
|
||||
|
||||
66
core/startos/src/version/v0_4_0_alpha_16.rs
Normal file
66
core/startos/src/version/v0_4_0_alpha_16.rs
Normal file
@@ -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<Self::PreUpRes, Error> {
|
||||
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<Value, Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
5
debian/startos/postinst
vendored
5
debian/startos/postinst
vendored
@@ -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
|
||||
|
||||
2
patch-db
2
patch-db
Submodule patch-db updated: 90b336d6a9...bdb5a10114
@@ -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<MainStatus>
|
||||
}): Promise<StatusInfo>
|
||||
/** indicate to the host os what runstate the service is in */
|
||||
setMainStatus(options: SetMainStatus): Promise<null>
|
||||
|
||||
|
||||
8
sdk/base/lib/osBindings/DesiredStatus.ts
Normal file
8
sdk/base/lib/osBindings/DesiredStatus.ts
Normal file
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
|
||||
12
sdk/base/lib/osBindings/StatusInfo.ts
Normal file
12
sdk/base/lib/osBindings/StatusInfo.ts
Normal file
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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<T extends any[], Then, Else> =
|
||||
|
||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<div class="placeholder">No devices</div>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -55,6 +55,8 @@ import { MappedDevice, MappedForward } from './utils'
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<div class="placeholder">No port forwards</div>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -67,6 +67,8 @@ import { SUBNETS_ADD } from './add'
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
} @empty {
|
||||
<div class="placeholder">No subnets</div>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
selector: 'service-error',
|
||||
template: `
|
||||
<header>{{ 'Service Launch Error' | i18n }}</header>
|
||||
<p class="error-message">{{ error?.message }}</p>
|
||||
<p class="error-message">{{ error?.details }}</p>
|
||||
<p>{{ error?.debug }}</p>
|
||||
<h4>
|
||||
{{ '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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
<span class="loading-dots"></span>
|
||||
}
|
||||
|
||||
@if ($any(pkg().status)?.started; as started) {
|
||||
@if (pkg().statusInfo.started; as started) {
|
||||
<service-uptime [started]="started" />
|
||||
}
|
||||
</h3>
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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<string | null>(null)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ import { StatusComponent } from './status.component'
|
||||
></td>
|
||||
<td [style.grid-area]="'2 / 2'">{{ manifest.version }}</td>
|
||||
<td class="uptime">
|
||||
@if ($any(pkg.status)?.started; as started) {
|
||||
@if (pkg.statusInfo.started; as started) {
|
||||
<span>{{ 'Uptime' | i18n }}:</span>
|
||||
<service-uptime [started]="started" />
|
||||
} @else {
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
<button
|
||||
tuiCell
|
||||
[action]="a"
|
||||
(click)="handle(pkg.mainStatus, pkg.icon, pkg.manifest, a)"
|
||||
(click)="handle(pkg.status, pkg.icon, pkg.manifest, a)"
|
||||
></button>
|
||||
}
|
||||
</section>
|
||||
@@ -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 },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -25,7 +25,7 @@ const INACTIVE: PrimaryStatus[] = [
|
||||
'updating',
|
||||
'removing',
|
||||
'restoring',
|
||||
'backingUp',
|
||||
'backing-up',
|
||||
]
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -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) {
|
||||
<service-error [pkg]="pkg" />
|
||||
} @else if (installing()) {
|
||||
<service-install-progress [pkg]="pkg" />
|
||||
@@ -45,9 +45,9 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
|
||||
}
|
||||
</service-status>
|
||||
|
||||
@if (status() !== 'backingUp') {
|
||||
@if (status() !== 'backing-up') {
|
||||
<service-health-checks [checks]="health()" />
|
||||
<service-uptime class="g-card" [started]="$any(pkg.status).started" />
|
||||
<service-uptime class="g-card" [started]="pkg.statusInfo.started" />
|
||||
<service-interfaces [pkg]="pkg" [disabled]="status() !== 'running'" />
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
<tui-icon icon="@tui.check" class="g-positive" />
|
||||
{{ 'complete' | i18n }}
|
||||
} @else {
|
||||
@if ((pkg.key | tuiMapper: toStatus | async) === 'backingUp') {
|
||||
@if ((pkg.key | tuiMapper: toStatus | async) === 'backing-up') {
|
||||
<tui-loader size="s" />
|
||||
{{ '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')
|
||||
}
|
||||
|
||||
@@ -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<PackageActionData>(ActionInputModal, {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<T.MainStatus['main']>[] = [
|
||||
const appPath = `/packageData/${id}/statusInfo/desired/main`
|
||||
const appPatch: ReplaceOperation<T.DesiredStatus['main']>[] = [
|
||||
{
|
||||
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<RR.StartPackageRes> {
|
||||
const path = `/packageData/${params.id}/status`
|
||||
const path = `/packageData/${params.id}/statusInfo`
|
||||
|
||||
await pauseFor(2000)
|
||||
|
||||
setTimeout(async () => {
|
||||
const patch2: ReplaceOperation<T.MainStatus & { main: 'running' }>[] = [
|
||||
const patch2: ReplaceOperation<T.StatusInfo>[] = [
|
||||
{
|
||||
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<T.StatusInfo>[] = [
|
||||
{
|
||||
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<RR.RestartPackageRes> {
|
||||
await pauseFor(2000)
|
||||
const path = `/packageData/${params.id}/status`
|
||||
const path = `/packageData/${params.id}/statusInfo`
|
||||
|
||||
setTimeout(async () => {
|
||||
const patch2: ReplaceOperation<T.MainStatus & { main: 'running' }>[] = [
|
||||
const patch2: ReplaceOperation<T.StatusInfo>[] = [
|
||||
{
|
||||
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<T.MainStatus & { main: 'restarting' }>[] = [
|
||||
const patch: ReplaceOperation<T.StatusInfo>[] = [
|
||||
{
|
||||
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<RR.StopPackageRes> {
|
||||
await pauseFor(2000)
|
||||
const path = `/packageData/${params.id}/status`
|
||||
const path = `/packageData/${params.id}/statusInfo`
|
||||
|
||||
setTimeout(() => {
|
||||
const patch2: ReplaceOperation<T.MainStatus & { main: 'stopped' }>[] = [
|
||||
const patch2: ReplaceOperation<T.StatusInfo>[] = [
|
||||
{
|
||||
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<T.MainStatus & { main: 'stopping' }>[] = [
|
||||
const patch: ReplaceOperation<T.StatusInfo>[] = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: path,
|
||||
value: { main: 'stopping' },
|
||||
value: {
|
||||
desired: { main: 'stopped' },
|
||||
error: null,
|
||||
health: {},
|
||||
started: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<string, PkgDependencyErrors>
|
||||
export type PkgDependencyErrors = Record<string, DependencyError | null>
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
statusInfo.desired.main === 'running' &&
|
||||
(!statusInfo.started ||
|
||||
Object.values(statusInfo.health)
|
||||
.filter(h => !!h)
|
||||
.some(h => h.result === 'starting')
|
||||
.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<PrimaryStatus, StatusRendering> = {
|
||||
@@ -122,7 +133,7 @@ export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
|
||||
color: 'dark-shade',
|
||||
showDots: false,
|
||||
},
|
||||
backingUp: {
|
||||
'backing-up': {
|
||||
display: 'Backing Up',
|
||||
color: 'primary',
|
||||
showDots: true,
|
||||
@@ -137,7 +148,7 @@ export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
|
||||
color: 'success',
|
||||
showDots: false,
|
||||
},
|
||||
taskRequired: {
|
||||
'task-required': {
|
||||
display: 'Task Required',
|
||||
color: 'warning',
|
||||
showDots: false,
|
||||
|
||||
Reference in New Issue
Block a user