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. Access the VPS via SSH.
|
||||||
|
|
||||||
1. Install StartTunnel:
|
1. Run the StartTunnel install script:
|
||||||
|
|
||||||
```sh
|
curl -fsSL https://start9labs.github.io/start-tunnel | 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"
|
|
||||||
```
|
|
||||||
|
|
||||||
5. [Initialize the web interface](#web-interface) (recommended)
|
1. [Initialize the web interface](#web-interface) (recommended)
|
||||||
|
|
||||||
## Updating
|
## Updating
|
||||||
|
|
||||||
|
Simply re-run the install command:
|
||||||
|
|
||||||
```sh
|
```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
|
## 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.
|
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):
|
5. Trust the Root CA on your client device(s):
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ parse_essential_db_info() {
|
|||||||
RAM_GB="unknown"
|
RAM_GB="unknown"
|
||||||
fi
|
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)
|
TOTAL_SERVICES=$(jq -r '.value.packageData | length' "$DB_DUMP" 2>/dev/null)
|
||||||
|
|
||||||
rm -f "$DB_DUMP"
|
rm -f "$DB_DUMP"
|
||||||
|
|||||||
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@@ -7908,7 +7908,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "start-os"
|
name = "start-os"
|
||||||
version = "0.4.0-alpha.15"
|
version = "0.4.0-alpha.16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes 0.7.5",
|
"aes 0.7.5",
|
||||||
"arti-client",
|
"arti-client",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use rpc_toolkit::yajrc::{
|
|||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::InvalidId;
|
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 struct ErrorData {
|
||||||
pub details: String,
|
pub details: String,
|
||||||
pub debug: String,
|
pub debug: String,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ license = "MIT"
|
|||||||
name = "start-os"
|
name = "start-os"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/Start9Labs/start-os"
|
repository = "https://github.com/Start9Labs/start-os"
|
||||||
version = "0.4.0-alpha.15" # VERSION_BUMP
|
version = "0.4.0-alpha.16" # VERSION_BUMP
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "startos"
|
name = "startos"
|
||||||
@@ -64,7 +64,7 @@ default = ["cli", "cli-container", "registry", "startd", "tunnel"]
|
|||||||
dev = ["backtrace-on-stack-overflow"]
|
dev = ["backtrace-on-stack-overflow"]
|
||||||
docker = []
|
docker = []
|
||||||
registry = []
|
registry = []
|
||||||
startd = []
|
startd = ["procfs", "pty-process"]
|
||||||
test = []
|
test = []
|
||||||
tunnel = []
|
tunnel = []
|
||||||
unstable = ["backtrace-on-stack-overflow"]
|
unstable = ["backtrace-on-stack-overflow"]
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use models::PackageId;
|
||||||
use models::{HostId, PackageId};
|
|
||||||
use reqwest::Url;
|
|
||||||
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::util::serde::{Base32, Base64};
|
|
||||||
|
|
||||||
pub mod backup_bulk;
|
pub mod backup_bulk;
|
||||||
pub mod os;
|
pub mod os;
|
||||||
@@ -58,13 +55,3 @@ pub fn package_backup<C: Context>() -> ParentHandler<C> {
|
|||||||
.with_call_remote::<CliContext>(),
|
.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::action::update_tasks;
|
||||||
use crate::service::effects::callbacks::ServiceCallbacks;
|
use crate::service::effects::callbacks::ServiceCallbacks;
|
||||||
use crate::shutdown::Shutdown;
|
use crate::shutdown::Shutdown;
|
||||||
|
use crate::status::DesiredStatus;
|
||||||
use crate::util::io::delete_file;
|
use crate::util::io::delete_file;
|
||||||
use crate::util::lshw::LshwDevice;
|
use crate::util::lshw::LshwDevice;
|
||||||
use crate::util::sync::{SyncMutex, SyncRwLock, Watch};
|
use crate::util::sync::{SyncMutex, SyncRwLock, Watch};
|
||||||
@@ -416,46 +417,34 @@ impl RpcContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for id in
|
|
||||||
self.db
|
self.db
|
||||||
.mutate::<Vec<PackageId>>(|db| {
|
.mutate(|db| {
|
||||||
for (package_id, action_input) in &action_input {
|
for (package_id, action_input) in &action_input {
|
||||||
for (action_id, input) in action_input {
|
for (action_id, input) in action_input {
|
||||||
for (_, pde) in
|
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
|
||||||
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))
|
||||||
pde.as_tasks_mut().mutate(|tasks| {
|
})?;
|
||||||
Ok(update_tasks(tasks, package_id, action_id, input, false))
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
db.as_public()
|
}
|
||||||
.as_package_data()
|
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
|
||||||
.as_entries()?
|
if pde
|
||||||
|
.as_tasks()
|
||||||
|
.de()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(id, pkg)| {
|
.any(|(_, t)| t.active && t.task.severity == TaskSeverity::Critical)
|
||||||
(|| {
|
{
|
||||||
if pkg.as_tasks().de()?.into_iter().any(|(_, t)| {
|
pde.as_status_info_mut()
|
||||||
t.active && t.task.severity == TaskSeverity::Critical
|
.as_desired_mut()
|
||||||
}) {
|
.ser(&DesiredStatus::Stopped)?;
|
||||||
Ok(Some(id))
|
}
|
||||||
} else {
|
}
|
||||||
Ok(None)
|
Ok(())
|
||||||
}
|
})
|
||||||
})()
|
.await
|
||||||
.transpose()
|
.result?;
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.result?
|
|
||||||
{
|
|
||||||
let svc = self.services.get(&id).await;
|
|
||||||
if let Some(svc) = &*svc {
|
|
||||||
svc.stop(procedure_id.clone(), false).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
check_tasks.complete();
|
check_tasks.complete();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use color_eyre::eyre::eyre;
|
|
||||||
use models::PackageId;
|
use models::PackageId;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
@@ -8,7 +7,6 @@ use ts_rs::TS;
|
|||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::context::RpcContext;
|
use crate::context::RpcContext;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::rpc_continuations::Guid;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -19,37 +17,51 @@ pub struct ControlParams {
|
|||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn start(ctx: RpcContext, ControlParams { id }: ControlParams) -> Result<(), Error> {
|
pub async fn start(ctx: RpcContext, ControlParams { id }: ControlParams) -> Result<(), Error> {
|
||||||
ctx.services
|
ctx.db
|
||||||
.get(&id)
|
.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
|
.await
|
||||||
.as_ref()
|
.result?;
|
||||||
.or_not_found(lazy_format!("Manager for {id}"))?
|
|
||||||
.start(Guid::new())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn stop(ctx: RpcContext, ControlParams { id }: ControlParams) -> Result<(), Error> {
|
pub async fn stop(ctx: RpcContext, ControlParams { id }: ControlParams) -> Result<(), Error> {
|
||||||
ctx.services
|
ctx.db
|
||||||
.get(&id)
|
.mutate(|db| {
|
||||||
|
db.as_public_mut()
|
||||||
|
.as_package_data_mut()
|
||||||
|
.as_idx_mut(&id)
|
||||||
|
.or_not_found(&id)?
|
||||||
|
.as_status_info_mut()
|
||||||
|
.stop()
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.as_ref()
|
.result?;
|
||||||
.ok_or_else(|| Error::new(eyre!("Manager not found"), crate::ErrorKind::InvalidRequest))?
|
|
||||||
.stop(Guid::new(), true)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn restart(ctx: RpcContext, ControlParams { id }: ControlParams) -> Result<(), Error> {
|
pub async fn restart(ctx: RpcContext, ControlParams { id }: ControlParams) -> Result<(), Error> {
|
||||||
ctx.services
|
ctx.db
|
||||||
.get(&id)
|
.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
|
.await
|
||||||
.as_ref()
|
.result?;
|
||||||
.ok_or_else(|| Error::new(eyre!("Manager not found"), crate::ErrorKind::InvalidRequest))?
|
|
||||||
.restart(Guid::new(), false)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
|
||||||
use std::panic::UnwindSafe;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use crate::net::service_interface::ServiceInterface;
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::progress::FullProgress;
|
use crate::progress::FullProgress;
|
||||||
use crate::s9pk::manifest::Manifest;
|
use crate::s9pk::manifest::Manifest;
|
||||||
use crate::status::MainStatus;
|
use crate::status::StatusInfo;
|
||||||
use crate::util::serde::{Pem, is_partial_of};
|
use crate::util::serde::{Pem, is_partial_of};
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize, TS)]
|
#[derive(Debug, Default, Deserialize, Serialize, TS)]
|
||||||
@@ -365,7 +365,7 @@ impl Default for ActionVisibility {
|
|||||||
pub struct PackageDataEntry {
|
pub struct PackageDataEntry {
|
||||||
pub state_info: PackageState,
|
pub state_info: PackageState,
|
||||||
pub s9pk: PathBuf,
|
pub s9pk: PathBuf,
|
||||||
pub status: MainStatus,
|
pub status_info: StatusInfo,
|
||||||
#[ts(type = "string | null")]
|
#[ts(type = "string | null")]
|
||||||
pub registry: Option<Url>,
|
pub registry: Option<Url>,
|
||||||
#[ts(type = "string")]
|
#[ts(type = "string")]
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ use std::sync::{Arc, Weak};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use clap::builder::ValueParserFactory;
|
use clap::builder::ValueParserFactory;
|
||||||
use futures::{AsyncWriteExt, StreamExt};
|
use futures::StreamExt;
|
||||||
use imbl_value::{InOMap, InternedString};
|
use imbl_value::InternedString;
|
||||||
use models::{FromStrParser, InvalidId, PackageId};
|
use models::{FromStrParser, InvalidId, PackageId};
|
||||||
use rpc_toolkit::yajrc::RpcError;
|
use rpc_toolkit::yajrc::RpcError;
|
||||||
use rpc_toolkit::{GenericRpcMethod, RpcRequest, RpcResponse};
|
use rpc_toolkit::{RpcRequest, RpcResponse};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
@@ -17,7 +17,7 @@ use tokio::sync::Mutex;
|
|||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use ts_rs::TS;
|
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::bind::Bind;
|
||||||
use crate::disk::mount::filesystem::block_dev::BlockDev;
|
use crate::disk::mount::filesystem::block_dev::BlockDev;
|
||||||
use crate::disk::mount::filesystem::idmapped::IdMapped;
|
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 hickory_server::server::{Request, RequestHandler, ResponseHandler, ResponseInfo};
|
||||||
use imbl::OrdMap;
|
use imbl::OrdMap;
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
use itertools::Itertools;
|
|
||||||
use models::{GatewayId, OptionExt, PackageId};
|
use models::{GatewayId, OptionExt, PackageId};
|
||||||
use patch_db::json_ptr::JsonPointer;
|
use patch_db::json_ptr::JsonPointer;
|
||||||
use rpc_toolkit::{
|
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 {
|
pub trait UiContext: Context + AsRef<RpcContinuations> + Clone + Sized {
|
||||||
const UI_DIR: &'static Dir<'static>;
|
const UI_DIR: &'static Dir<'static>;
|
||||||
fn api() -> ParentHandler<Self>;
|
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>>);
|
pub struct GetVHostAcmeProvider<A: Accept + 'static>(pub Watch<Mapping<A>>);
|
||||||
impl<A: Accept + 'static> Clone for GetVHostAcmeProvider<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::cors::Cors;
|
||||||
use crate::middleware::signature::SignatureAuth;
|
use crate::middleware::signature::SignatureAuth;
|
||||||
use crate::net::static_server::{bad_request, not_found, server_error};
|
use crate::net::static_server::{bad_request, not_found, server_error};
|
||||||
use crate::net::web_server::{Accept, WebServer};
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::registry::context::RegistryContext;
|
use crate::registry::context::RegistryContext;
|
||||||
use crate::registry::device_info::DeviceInfoMiddleware;
|
use crate::registry::device_info::DeviceInfoMiddleware;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ pub trait FileSource: Send + Sync + Sized + 'static {
|
|||||||
fn to_vec(
|
fn to_vec(
|
||||||
src: &impl FileSource,
|
src: &impl FileSource,
|
||||||
verify: Option<(Hash, u64)>,
|
verify: Option<(Hash, u64)>,
|
||||||
) -> BoxFuture<Result<Vec<u8>, Error>> {
|
) -> BoxFuture<'_, Result<Vec<u8>, Error>> {
|
||||||
async move {
|
async move {
|
||||||
let mut vec = Vec::with_capacity(if let Some((_, size)) = &verify {
|
let mut vec = Vec::with_capacity(if let Some((_, size)) = &verify {
|
||||||
*size
|
*size
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use models::{ImageId, VolumeId};
|
|||||||
use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt};
|
use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::dependencies::{DepInfo, Dependencies, MetadataSrc};
|
use crate::dependencies::{DepInfo, Dependencies};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::s9pk::manifest::{DeviceFilter, Manifest};
|
use crate::s9pk::manifest::{DeviceFilter, Manifest};
|
||||||
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
|
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
|
||||||
|
|||||||
@@ -130,11 +130,11 @@ impl Handler<RunAction> for ServiceActor {
|
|||||||
ref action_id,
|
ref action_id,
|
||||||
input,
|
input,
|
||||||
}: RunAction,
|
}: RunAction,
|
||||||
jobs: &BackgroundJobQueue,
|
_: &BackgroundJobQueue,
|
||||||
) -> Self::Response {
|
) -> Self::Response {
|
||||||
let container = &self.0.persistent_container;
|
let container = &self.0.persistent_container;
|
||||||
let package_id = &self.0.id;
|
let package_id = &self.0.id;
|
||||||
let action = self
|
let pde = self
|
||||||
.0
|
.0
|
||||||
.ctx
|
.ctx
|
||||||
.db
|
.db
|
||||||
@@ -143,9 +143,10 @@ impl Handler<RunAction> for ServiceActor {
|
|||||||
.into_public()
|
.into_public()
|
||||||
.into_package_data()
|
.into_package_data()
|
||||||
.into_idx(package_id)
|
.into_idx(package_id)
|
||||||
.or_not_found(package_id)?
|
.or_not_found(package_id)?;
|
||||||
.into_actions()
|
let action = pde
|
||||||
.into_idx(action_id)
|
.as_actions()
|
||||||
|
.as_idx(action_id)
|
||||||
.or_not_found(lazy_format!("{package_id} action {action_id}"))?
|
.or_not_found(lazy_format!("{package_id} action {action_id}"))?
|
||||||
.de()?;
|
.de()?;
|
||||||
if matches!(&action.visibility, ActionVisibility::Disabled(_)) {
|
if matches!(&action.visibility, ActionVisibility::Disabled(_)) {
|
||||||
@@ -154,7 +155,7 @@ impl Handler<RunAction> for ServiceActor {
|
|||||||
ErrorKind::Action,
|
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 {
|
if match action.allowed_statuses {
|
||||||
AllowedStatuses::OnlyRunning => !running,
|
AllowedStatuses::OnlyRunning => !running,
|
||||||
AllowedStatuses::OnlyStopped => running,
|
AllowedStatuses::OnlyStopped => running,
|
||||||
@@ -177,44 +178,21 @@ impl Handler<RunAction> for ServiceActor {
|
|||||||
.await
|
.await
|
||||||
.with_kind(ErrorKind::Action)?;
|
.with_kind(ErrorKind::Action)?;
|
||||||
let package_id = package_id.clone();
|
let package_id = package_id.clone();
|
||||||
for to_stop in self
|
self.0
|
||||||
.0
|
|
||||||
.ctx
|
.ctx
|
||||||
.db
|
.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
let mut to_stop = Vec::new();
|
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
|
||||||
for (id, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
|
|
||||||
if pde.as_tasks_mut().mutate(|tasks| {
|
if pde.as_tasks_mut().mutate(|tasks| {
|
||||||
Ok(update_tasks(tasks, &package_id, action_id, &input, true))
|
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
|
.await
|
||||||
.result?
|
.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?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(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,
|
None => true,
|
||||||
};
|
};
|
||||||
if active && task.severity == TaskSeverity::Critical {
|
|
||||||
context.stop(procedure_id, false).await?;
|
|
||||||
}
|
|
||||||
context
|
context
|
||||||
.seed
|
.seed
|
||||||
.ctx
|
.ctx
|
||||||
.db
|
.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
db.as_public_mut()
|
let pde = db
|
||||||
|
.as_public_mut()
|
||||||
.as_package_data_mut()
|
.as_package_data_mut()
|
||||||
.as_idx_mut(src_id)
|
.as_idx_mut(src_id)
|
||||||
.or_not_found(src_id)?
|
.or_not_found(src_id)?;
|
||||||
.as_tasks_mut()
|
if active && task.severity == TaskSeverity::Critical {
|
||||||
|
pde.as_status_info_mut().stop()?;
|
||||||
|
}
|
||||||
|
pde.as_tasks_mut()
|
||||||
.insert(&replay_id, &TaskEntry { active, task })
|
.insert(&replay_id, &TaskEntry { active, task })
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
use clap::builder::ValueParserFactory;
|
use clap::builder::ValueParserFactory;
|
||||||
use models::{FromStrParser, PackageId};
|
use models::{FromStrParser, PackageId};
|
||||||
|
|
||||||
use crate::service::RebuildParams;
|
use crate::service::RebuildParams;
|
||||||
use crate::service::effects::prelude::*;
|
use crate::service::effects::prelude::*;
|
||||||
use crate::service::rpc::CallbackId;
|
use crate::service::rpc::CallbackId;
|
||||||
use crate::status::MainStatus;
|
use crate::status::{DesiredStatus, StatusInfo};
|
||||||
|
|
||||||
pub async fn rebuild(context: EffectContext) -> Result<(), Error> {
|
pub async fn rebuild(context: EffectContext) -> Result<(), Error> {
|
||||||
let seed = context.deref()?.seed.clone();
|
let seed = context.deref()?.seed.clone();
|
||||||
@@ -21,15 +22,44 @@ pub async fn rebuild(context: EffectContext) -> Result<(), Error> {
|
|||||||
Ok(())
|
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()?;
|
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(())
|
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()?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +80,7 @@ pub async fn get_status(
|
|||||||
package_id,
|
package_id,
|
||||||
callback,
|
callback,
|
||||||
}: GetStatusParams,
|
}: GetStatusParams,
|
||||||
) -> Result<MainStatus, Error> {
|
) -> Result<StatusInfo, Error> {
|
||||||
let context = context.deref()?;
|
let context = context.deref()?;
|
||||||
let id = package_id.unwrap_or_else(|| context.seed.id.clone());
|
let id = package_id.unwrap_or_else(|| context.seed.id.clone());
|
||||||
let db = context.seed.ctx.db.peek().await;
|
let db = context.seed.ctx.db.peek().await;
|
||||||
@@ -68,13 +98,13 @@ pub async fn get_status(
|
|||||||
.as_package_data()
|
.as_package_data()
|
||||||
.as_idx(&id)
|
.as_idx(&id)
|
||||||
.or_not_found(&id)?
|
.or_not_found(&id)?
|
||||||
.as_status()
|
.as_status_info()
|
||||||
.de()?;
|
.de()?;
|
||||||
|
|
||||||
Ok(status)
|
Ok(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub enum SetMainStatusStatus {
|
pub enum SetMainStatusStatus {
|
||||||
@@ -109,9 +139,34 @@ pub async fn set_main_status(
|
|||||||
SetMainStatus { status }: SetMainStatus,
|
SetMainStatus { status }: SetMainStatus,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let context = context.deref()?;
|
let context = context.deref()?;
|
||||||
match status {
|
let id = &context.seed.id;
|
||||||
SetMainStatusStatus::Running => context.seed.started(),
|
context
|
||||||
SetMainStatusStatus::Stopped => context.seed.stopped(),
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
use clap::builder::ValueParserFactory;
|
use clap::builder::ValueParserFactory;
|
||||||
use exver::VersionRange;
|
use exver::VersionRange;
|
||||||
use imbl::OrdMap;
|
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
use models::{FromStrParser, HealthCheckId, PackageId, ReplayId, VersionString, VolumeId};
|
use models::{FromStrParser, HealthCheckId, PackageId, ReplayId, VersionString, VolumeId};
|
||||||
|
|
||||||
@@ -96,13 +95,6 @@ pub async fn get_installed_packages(context: EffectContext) -> Result<BTreeSet<P
|
|||||||
.keys()
|
.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)]
|
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
|
||||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||||
#[serde(rename_all_fields = "camelCase")]
|
#[serde(rename_all_fields = "camelCase")]
|
||||||
@@ -287,8 +279,7 @@ pub struct CheckDependenciesResult {
|
|||||||
satisfies: BTreeSet<VersionString>,
|
satisfies: BTreeSet<VersionString>,
|
||||||
is_running: bool,
|
is_running: bool,
|
||||||
tasks: BTreeMap<ReplayId, TaskEntry>,
|
tasks: BTreeMap<ReplayId, TaskEntry>,
|
||||||
#[ts(as = "BTreeMap::<HealthCheckId, NamedHealthCheckResult>")]
|
health_checks: BTreeMap<HealthCheckId, NamedHealthCheckResult>,
|
||||||
health_checks: OrdMap<HealthCheckId, NamedHealthCheckResult>,
|
|
||||||
}
|
}
|
||||||
pub async fn check_dependencies(
|
pub async fn check_dependencies(
|
||||||
context: EffectContext,
|
context: EffectContext,
|
||||||
@@ -336,14 +327,12 @@ pub async fn check_dependencies(
|
|||||||
let installed_version = manifest.as_version().de()?.into_version();
|
let installed_version = manifest.as_version().de()?.into_version();
|
||||||
let satisfies = manifest.as_satisfies().de()?;
|
let satisfies = manifest.as_satisfies().de()?;
|
||||||
let installed_version = Some(installed_version.clone().into());
|
let installed_version = Some(installed_version.clone().into());
|
||||||
let is_installed = true;
|
let is_running = package
|
||||||
let status = package.as_status().de()?;
|
.as_status_info()
|
||||||
let is_running = if is_installed {
|
.as_started()
|
||||||
status.running()
|
.transpose_ref()
|
||||||
} else {
|
.is_some();
|
||||||
false
|
let health_checks = package.as_status_info().as_health().de()?;
|
||||||
};
|
|
||||||
let health_checks = status.health().cloned().unwrap_or_default();
|
|
||||||
let tasks = tasks
|
let tasks = tasks
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, v)| v.task.package_id == package_id)
|
.filter(|(_, v)| v.task.package_id == package_id)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use models::HealthCheckId;
|
use models::HealthCheckId;
|
||||||
|
|
||||||
use crate::service::effects::prelude::*;
|
use crate::service::effects::prelude::*;
|
||||||
use crate::status::MainStatus;
|
|
||||||
use crate::status::health_check::NamedHealthCheckResult;
|
use crate::status::health_check::NamedHealthCheckResult;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||||
@@ -28,16 +27,9 @@ pub async fn set_health(
|
|||||||
.as_package_data_mut()
|
.as_package_data_mut()
|
||||||
.as_idx_mut(package_id)
|
.as_idx_mut(package_id)
|
||||||
.or_not_found(package_id)?
|
.or_not_found(package_id)?
|
||||||
.as_status_mut()
|
.as_status_info_mut()
|
||||||
.mutate(|main| {
|
.as_health_mut()
|
||||||
match main {
|
.insert(&id, &result)
|
||||||
MainStatus::Running { health, .. } | MainStatus::Starting { health } => {
|
|
||||||
health.insert(id, result);
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ use crate::service::effects::prelude::*;
|
|||||||
use crate::service::persistent_container::Subcontainer;
|
use crate::service::persistent_container::Subcontainer;
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
|
|
||||||
#[cfg(feature = "cli-container")]
|
#[cfg(any(feature = "cli-container", feature = "startd"))]
|
||||||
mod sync;
|
mod sync;
|
||||||
|
|
||||||
#[cfg(not(feature = "cli-container"))]
|
#[cfg(not(any(feature = "cli-container", feature = "startd")))]
|
||||||
mod sync_dummy;
|
mod sync_dummy;
|
||||||
|
|
||||||
pub use sync::*;
|
pub use sync::*;
|
||||||
#[cfg(not(feature = "cli-container"))]
|
#[cfg(not(any(feature = "cli-container", feature = "startd")))]
|
||||||
use sync_dummy as sync;
|
use sync_dummy as sync;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||||
@@ -41,7 +41,7 @@ pub async fn destroy_subcontainer_fs(
|
|||||||
.await
|
.await
|
||||||
.remove(&guid)
|
.remove(&guid)
|
||||||
{
|
{
|
||||||
#[cfg(feature = "container-runtime")]
|
#[cfg(feature = "startd")]
|
||||||
if tokio::fs::metadata(overlay.overlay.path().join("proc/1"))
|
if tokio::fs::metadata(overlay.overlay.path().join("proc/1"))
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
|
|||||||
@@ -9,33 +9,31 @@ use std::sync::{Arc, Weak};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use axum::extract::ws::{Utf8Bytes, WebSocket};
|
use axum::extract::ws::{Utf8Bytes, WebSocket};
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use futures::stream::FusedStream;
|
use futures::stream::FusedStream;
|
||||||
use futures::{FutureExt, SinkExt, StreamExt, TryStreamExt};
|
use futures::{FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||||
use helpers::NonDetachingJoinHandle;
|
use helpers::{AtomicFile, NonDetachingJoinHandle};
|
||||||
use imbl_value::{InternedString, json};
|
use imbl_value::{InternedString, json};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use models::{ActionId, HostId, ImageId, PackageId};
|
use models::{ActionId, HostId, ImageId, PackageId};
|
||||||
use nix::sys::signal::Signal;
|
use nix::sys::signal::Signal;
|
||||||
use persistent_container::{PersistentContainer, Subcontainer};
|
use persistent_container::{PersistentContainer, Subcontainer};
|
||||||
use rpc_toolkit::{HandlerArgs, HandlerFor};
|
use rpc_toolkit::HandlerArgs;
|
||||||
|
use rpc_toolkit::yajrc::RpcError;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use service_actor::ServiceActor;
|
use service_actor::ServiceActor;
|
||||||
use start_stop::StartStop;
|
|
||||||
use termion::raw::IntoRawMode;
|
use termion::raw::IntoRawMode;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tokio::sync::Notify;
|
|
||||||
use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
|
use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::context::{CliContext, RpcContext};
|
use crate::context::{CliContext, RpcContext};
|
||||||
use crate::db::model::package::{
|
use crate::db::model::package::{
|
||||||
InstalledState, ManifestPreference, PackageDataEntry, PackageState, PackageStateMatchModelRef,
|
InstalledState, ManifestPreference, PackageState, PackageStateMatchModelRef, TaskSeverity,
|
||||||
TaskSeverity, UpdatingState,
|
UpdatingState,
|
||||||
};
|
};
|
||||||
use crate::disk::mount::filesystem::ReadOnly;
|
use crate::disk::mount::filesystem::ReadOnly;
|
||||||
use crate::disk::mount::guard::{GenericMountGuard, MountGuard};
|
use crate::disk::mount::guard::{GenericMountGuard, MountGuard};
|
||||||
@@ -49,15 +47,15 @@ use crate::service::service_map::InstallProgressHandles;
|
|||||||
use crate::service::uninstall::cleanup;
|
use crate::service::uninstall::cleanup;
|
||||||
use crate::util::Never;
|
use crate::util::Never;
|
||||||
use crate::util::actor::concurrent::ConcurrentActor;
|
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::net::WebSocketExt;
|
||||||
use crate::util::serde::Pem;
|
use crate::util::serde::Pem;
|
||||||
|
use crate::util::sync::SyncMutex;
|
||||||
use crate::volume::data_dir;
|
use crate::volume::data_dir;
|
||||||
use crate::{CAP_1_KiB, DATA_DIR};
|
use crate::{CAP_1_KiB, DATA_DIR};
|
||||||
|
|
||||||
pub mod action;
|
pub mod action;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
mod control;
|
|
||||||
pub mod effects;
|
pub mod effects;
|
||||||
pub mod persistent_container;
|
pub mod persistent_container;
|
||||||
mod rpc;
|
mod rpc;
|
||||||
@@ -66,7 +64,6 @@ pub mod service_map;
|
|||||||
pub mod start_stop;
|
pub mod start_stop;
|
||||||
mod transition;
|
mod transition;
|
||||||
pub mod uninstall;
|
pub mod uninstall;
|
||||||
mod util;
|
|
||||||
|
|
||||||
pub use service_map::ServiceMap;
|
pub use service_map::ServiceMap;
|
||||||
|
|
||||||
@@ -222,24 +219,17 @@ impl Service {
|
|||||||
async fn new(
|
async fn new(
|
||||||
ctx: RpcContext,
|
ctx: RpcContext,
|
||||||
s9pk: S9pk,
|
s9pk: S9pk,
|
||||||
start: StartStop,
|
|
||||||
procedure_id: Guid,
|
procedure_id: Guid,
|
||||||
init_kind: Option<InitKind>,
|
init_kind: Option<InitKind>,
|
||||||
recovery_source: Option<impl GenericMountGuard>,
|
recovery_source: Option<impl GenericMountGuard>,
|
||||||
) -> Result<ServiceRef, Error> {
|
) -> Result<ServiceRef, Error> {
|
||||||
let id = s9pk.as_manifest().id.clone();
|
let id = s9pk.as_manifest().id.clone();
|
||||||
let persistent_container = PersistentContainer::new(
|
let persistent_container = PersistentContainer::new(&ctx, s9pk).await?;
|
||||||
&ctx, s9pk,
|
|
||||||
start,
|
|
||||||
// desired_state.subscribe(),
|
|
||||||
// temp_desired_state.subscribe(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let seed = Arc::new(ServiceActorSeed {
|
let seed = Arc::new(ServiceActorSeed {
|
||||||
id,
|
id,
|
||||||
persistent_container,
|
persistent_container,
|
||||||
ctx,
|
ctx,
|
||||||
synchronized: Arc::new(Notify::new()),
|
backup: SyncMutex::new(None),
|
||||||
});
|
});
|
||||||
let service: ServiceRef = Self {
|
let service: ServiceRef = Self {
|
||||||
actor: ConcurrentActor::new(ServiceActor(seed.clone())),
|
actor: ConcurrentActor::new(ServiceActor(seed.clone())),
|
||||||
@@ -279,19 +269,14 @@ impl Service {
|
|||||||
) -> Result<Option<ServiceRef>, Error> {
|
) -> Result<Option<ServiceRef>, Error> {
|
||||||
let handle_installed = {
|
let handle_installed = {
|
||||||
let ctx = ctx.clone();
|
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 {
|
for volume_id in &s9pk.as_manifest().volumes {
|
||||||
let path = data_dir(DATA_DIR, &s9pk.as_manifest().id, volume_id);
|
let path = data_dir(DATA_DIR, &s9pk.as_manifest().id, volume_id);
|
||||||
if tokio::fs::metadata(&path).await.is_err() {
|
if tokio::fs::metadata(&path).await.is_err() {
|
||||||
tokio::fs::create_dir_all(&path).await?;
|
tokio::fs::create_dir_all(&path).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let start_stop = if i.as_status().de()?.running() {
|
Self::new(ctx, s9pk, Guid::new(), None, None::<MountGuard>)
|
||||||
StartStop::Start
|
|
||||||
} else {
|
|
||||||
StartStop::Stop
|
|
||||||
};
|
|
||||||
Self::new(ctx, s9pk, start_stop, Guid::new(), None, None::<MountGuard>)
|
|
||||||
.await
|
.await
|
||||||
.map(Some)
|
.map(Some)
|
||||||
}
|
}
|
||||||
@@ -319,7 +304,7 @@ impl Service {
|
|||||||
s9pk,
|
s9pk,
|
||||||
&s9pk_path,
|
&s9pk_path,
|
||||||
&None,
|
&None,
|
||||||
None,
|
InitKind::Install,
|
||||||
None::<Never>,
|
None::<Never>,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -353,7 +338,7 @@ impl Service {
|
|||||||
s9pk,
|
s9pk,
|
||||||
&s9pk_path,
|
&s9pk_path,
|
||||||
&None,
|
&None,
|
||||||
Some(entry.as_status().de()?.run_state()),
|
InitKind::Update,
|
||||||
None::<Never>,
|
None::<Never>,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -388,7 +373,7 @@ impl Service {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await.result?;
|
.await.result?;
|
||||||
handle_installed(s9pk, entry).await
|
handle_installed(s9pk).await
|
||||||
}
|
}
|
||||||
PackageStateMatchModelRef::Removing(_) | PackageStateMatchModelRef::Restoring(_) => {
|
PackageStateMatchModelRef::Removing(_) | PackageStateMatchModelRef::Restoring(_) => {
|
||||||
if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| {
|
if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| {
|
||||||
@@ -396,11 +381,7 @@ impl Service {
|
|||||||
tracing::debug!("{e:?}")
|
tracing::debug!("{e:?}")
|
||||||
}) {
|
}) {
|
||||||
let err_state = |e: Error| async move {
|
let err_state = |e: Error| async move {
|
||||||
let state = crate::status::MainStatus::Error {
|
let e = e.into();
|
||||||
on_rebuild: StartStop::Stop,
|
|
||||||
message: e.to_string(),
|
|
||||||
debug: Some(format!("{e:?}")),
|
|
||||||
};
|
|
||||||
ctx.db
|
ctx.db
|
||||||
.mutate(move |db| {
|
.mutate(move |db| {
|
||||||
if let Some(pde) =
|
if let Some(pde) =
|
||||||
@@ -413,22 +394,14 @@ impl Service {
|
|||||||
.clone(),
|
.clone(),
|
||||||
}))
|
}))
|
||||||
})?;
|
})?;
|
||||||
pde.as_status_mut().ser(&state)?;
|
pde.as_status_info_mut().as_error_mut().ser(&Some(e))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result
|
.result
|
||||||
};
|
};
|
||||||
match Self::new(
|
match Self::new(ctx.clone(), s9pk, Guid::new(), None, None::<MountGuard>).await
|
||||||
ctx.clone(),
|
|
||||||
s9pk,
|
|
||||||
StartStop::Stop,
|
|
||||||
Guid::new(),
|
|
||||||
None,
|
|
||||||
None::<MountGuard>,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
{
|
||||||
Ok(service) => match async {
|
Ok(service) => match async {
|
||||||
service
|
service
|
||||||
@@ -463,7 +436,7 @@ impl Service {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
PackageStateMatchModelRef::Installed(_) => {
|
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(
|
PackageStateMatchModelRef::Error(e) => Err(Error::new(
|
||||||
eyre!("Failed to parse PackageDataEntry, found {e:?}"),
|
eyre!("Failed to parse PackageDataEntry, found {e:?}"),
|
||||||
@@ -478,7 +451,7 @@ impl Service {
|
|||||||
s9pk: S9pk,
|
s9pk: S9pk,
|
||||||
s9pk_path: &PathBuf,
|
s9pk_path: &PathBuf,
|
||||||
registry: &Option<Url>,
|
registry: &Option<Url>,
|
||||||
prev_state: Option<StartStop>,
|
kind: InitKind,
|
||||||
recovery_source: Option<impl GenericMountGuard>,
|
recovery_source: Option<impl GenericMountGuard>,
|
||||||
progress: Option<InstallProgressHandles>,
|
progress: Option<InstallProgressHandles>,
|
||||||
) -> Result<ServiceRef, Error> {
|
) -> Result<ServiceRef, Error> {
|
||||||
@@ -489,15 +462,8 @@ impl Service {
|
|||||||
let service = Self::new(
|
let service = Self::new(
|
||||||
ctx.clone(),
|
ctx.clone(),
|
||||||
s9pk,
|
s9pk,
|
||||||
StartStop::Stop,
|
|
||||||
procedure_id.clone(),
|
procedure_id.clone(),
|
||||||
Some(if recovery_source.is_some() {
|
Some(kind),
|
||||||
InitKind::Restore
|
|
||||||
} else if prev_state.is_some() {
|
|
||||||
InitKind::Update
|
|
||||||
} else {
|
|
||||||
InitKind::Install
|
|
||||||
}),
|
|
||||||
recovery_source,
|
recovery_source,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -550,8 +516,7 @@ impl Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let has_critical = ctx
|
ctx.db
|
||||||
.db
|
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
for (action_id, 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()? {
|
||||||
@@ -566,13 +531,15 @@ impl Service {
|
|||||||
.as_idx_mut(&manifest.id)
|
.as_idx_mut(&manifest.id)
|
||||||
.or_not_found(&manifest.id)?;
|
.or_not_found(&manifest.id)?;
|
||||||
let actions = entry.as_actions().keys()?;
|
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| {
|
t.retain(|_, v| {
|
||||||
v.task.package_id != manifest.id || actions.contains(&v.task.action_id)
|
v.task.package_id != manifest.id || actions.contains(&v.task.action_id)
|
||||||
});
|
});
|
||||||
Ok(t.iter()
|
Ok(t.iter()
|
||||||
.any(|(_, t)| t.active && t.task.severity == TaskSeverity::Critical))
|
.any(|(_, t)| t.active && t.task.severity == TaskSeverity::Critical))
|
||||||
})?;
|
})? {
|
||||||
|
entry.as_status_info_mut().stop()?;
|
||||||
|
}
|
||||||
entry
|
entry
|
||||||
.as_state_info_mut()
|
.as_state_info_mut()
|
||||||
.ser(&PackageState::Installed(InstalledState { manifest }))?;
|
.ser(&PackageState::Installed(InstalledState { manifest }))?;
|
||||||
@@ -581,38 +548,42 @@ impl Service {
|
|||||||
entry.as_icon_mut().ser(&icon)?;
|
entry.as_icon_mut().ser(&icon)?;
|
||||||
entry.as_registry_mut().ser(registry)?;
|
entry.as_registry_mut().ser(registry)?;
|
||||||
|
|
||||||
Ok(has_critical)
|
Ok(())
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.result?;
|
.result?;
|
||||||
|
|
||||||
if prev_state == Some(StartStop::Start) && !has_critical {
|
|
||||||
service.start(procedure_id).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(service)
|
Ok(service)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn backup(&self, guard: impl GenericMountGuard) -> Result<(), Error> {
|
pub async fn backup(&self, guard: impl GenericMountGuard) -> Result<(), Error> {
|
||||||
let id = &self.seed.id;
|
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
|
self.seed
|
||||||
.persistent_container
|
.persistent_container
|
||||||
.s9pk
|
.s9pk
|
||||||
.clone()
|
.clone()
|
||||||
.serialize(&mut file, true)
|
.serialize(&mut *file, true)
|
||||||
.await?;
|
|
||||||
drop(file);
|
|
||||||
self.actor
|
|
||||||
.send(
|
|
||||||
Guid::new(),
|
|
||||||
transition::backup::Backup {
|
|
||||||
path: guard.path().join("data"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await??
|
|
||||||
.await?;
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,41 +632,12 @@ impl Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct RunningStatus {
|
|
||||||
started: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ServiceActorSeed {
|
struct ServiceActorSeed {
|
||||||
ctx: RpcContext,
|
ctx: RpcContext,
|
||||||
id: PackageId,
|
id: PackageId,
|
||||||
/// Needed to interact with the container for the service
|
/// Needed to interact with the container for the service
|
||||||
persistent_container: PersistentContainer,
|
persistent_container: PersistentContainer,
|
||||||
/// This is notified every time the background job created in ServiceActor::init responds to a change
|
backup: SyncMutex<Option<BoxFuture<'static, Result<(), RpcError>>>>,
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ use crate::service::effects::handler;
|
|||||||
use crate::service::rpc::{
|
use crate::service::rpc::{
|
||||||
CallbackHandle, CallbackId, CallbackParams, ExitParams, InitKind, InitParams,
|
CallbackHandle, CallbackId, CallbackParams, ExitParams, InitKind, InitParams,
|
||||||
};
|
};
|
||||||
use crate::service::start_stop::StartStop;
|
use crate::service::{Service, rpc};
|
||||||
use crate::service::transition::{TransitionKind, TransitionState};
|
|
||||||
use crate::service::{RunningStatus, Service, rpc};
|
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
use crate::util::io::create_file;
|
use crate::util::io::create_file;
|
||||||
use crate::util::rpc_client::UnixRpcClient;
|
use crate::util::rpc_client::UnixRpcClient;
|
||||||
@@ -49,41 +47,15 @@ const RPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
|||||||
pub struct ServiceState {
|
pub struct ServiceState {
|
||||||
// indicates whether the service container runtime has been initialized yet
|
// indicates whether the service container runtime has been initialized yet
|
||||||
pub(super) rt_initialized: bool,
|
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:
|
// This tracks references to callbacks registered by the running service:
|
||||||
pub(super) callbacks: BTreeSet<Arc<CallbackId>>,
|
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 {
|
impl ServiceState {
|
||||||
pub fn new(desired_state: StartStop) -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
rt_initialized: false,
|
rt_initialized: false,
|
||||||
running_status: Default::default(),
|
|
||||||
callbacks: 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 {
|
impl PersistentContainer {
|
||||||
#[instrument(skip_all)]
|
#[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
|
let lxc_container = ctx
|
||||||
.lxc_manager
|
.lxc_manager
|
||||||
.create(
|
.create(
|
||||||
@@ -305,7 +277,7 @@ impl PersistentContainer {
|
|||||||
assets,
|
assets,
|
||||||
images,
|
images,
|
||||||
subcontainers: Arc::new(Mutex::new(BTreeMap::new())),
|
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,
|
net_service,
|
||||||
destroyed: false,
|
destroyed: false,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -203,11 +203,6 @@ impl serde::Serialize for Sandbox {
|
|||||||
pub struct CallbackId(u64);
|
pub struct CallbackId(u64);
|
||||||
impl CallbackId {
|
impl CallbackId {
|
||||||
pub fn register(self, container: &PersistentContainer) -> CallbackHandle {
|
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 this = Arc::new(self);
|
||||||
let res = Arc::downgrade(&this);
|
let res = Arc::downgrade(&this);
|
||||||
container
|
container
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use futures::FutureExt;
|
|
||||||
use futures::future::{BoxFuture, Either};
|
|
||||||
use imbl::vector;
|
use imbl::vector;
|
||||||
|
use patch_db::TypedDbWatch;
|
||||||
|
|
||||||
use super::ServiceActorSeed;
|
use super::ServiceActorSeed;
|
||||||
use super::start_stop::StartStop;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::service::SYNC_RETRY_COOLDOWN_SECONDS;
|
use crate::service::SYNC_RETRY_COOLDOWN_SECONDS;
|
||||||
use crate::service::persistent_container::ServiceStateKinds;
|
use crate::service::transition::{Transition, TransitionKind};
|
||||||
use crate::service::transition::TransitionKind;
|
use crate::status::{DesiredStatus, StatusInfo};
|
||||||
use crate::status::MainStatus;
|
|
||||||
use crate::util::actor::Actor;
|
use crate::util::actor::Actor;
|
||||||
use crate::util::actor::background::BackgroundJobQueue;
|
use crate::util::actor::background::BackgroundJobQueue;
|
||||||
|
|
||||||
@@ -21,159 +18,123 @@ pub(super) struct ServiceActor(pub(super) Arc<ServiceActorSeed>);
|
|||||||
impl Actor for ServiceActor {
|
impl Actor for ServiceActor {
|
||||||
fn init(&mut self, jobs: &BackgroundJobQueue) {
|
fn init(&mut self, jobs: &BackgroundJobQueue) {
|
||||||
let seed = self.0.clone();
|
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 {
|
jobs.add_job(async move {
|
||||||
let _ = current.wait_for(|s| s.rt_initialized).await;
|
if initialized.await.is_err() {
|
||||||
let mut start_stop_task: Option<Either<_, _>> = None;
|
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 {
|
loop {
|
||||||
let wait = match service_actor_loop(¤t, &seed, &mut start_stop_task).await {
|
let res = service_actor_loop(&mut watch, &seed, &mut transition).await;
|
||||||
Ok(()) => Either::Right(current.changed().then(|res| async move {
|
let wait = async {
|
||||||
match res {
|
if let Err(e) = async {
|
||||||
Ok(()) => (),
|
res?;
|
||||||
Err(_) => futures::future::pending().await,
|
watch.changed().await?;
|
||||||
}
|
Ok::<_, Error>(())
|
||||||
})),
|
}
|
||||||
Err(e) => {
|
.await
|
||||||
|
{
|
||||||
tracing::error!("error synchronizing state of service: {e}");
|
tracing::error!("error synchronizing state of service: {e}");
|
||||||
tracing::debug!("{e:?}");
|
tracing::debug!("{e:?}");
|
||||||
|
|
||||||
seed.synchronized.notify_waiters();
|
|
||||||
|
|
||||||
tracing::error!("Retrying in {}s...", SYNC_RETRY_COOLDOWN_SECONDS);
|
tracing::error!("Retrying in {}s...", SYNC_RETRY_COOLDOWN_SECONDS);
|
||||||
Either::Left(tokio::time::sleep(Duration::from_secs(
|
tokio::time::timeout(
|
||||||
SYNC_RETRY_COOLDOWN_SECONDS,
|
Duration::from_secs(SYNC_RETRY_COOLDOWN_SECONDS),
|
||||||
)))
|
async {
|
||||||
|
watch.changed().await.log_err();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tokio::pin!(wait);
|
tokio::pin!(wait);
|
||||||
let start_stop_handler = async {
|
let transition_handler = async {
|
||||||
match &mut start_stop_task {
|
match &mut transition {
|
||||||
Some(task) => {
|
Some(Transition { future, .. }) => {
|
||||||
let err = task.await.log_err().is_none(); // TODO: ideally this error should be sent to service logs
|
let err = future.await.log_err().is_none(); // TODO: ideally this error should be sent to service logs
|
||||||
start_stop_task.take();
|
transition.take();
|
||||||
if err {
|
if err {
|
||||||
tokio::time::sleep(Duration::from_secs(
|
tokio::time::sleep(Duration::from_secs(
|
||||||
SYNC_RETRY_COOLDOWN_SECONDS,
|
SYNC_RETRY_COOLDOWN_SECONDS,
|
||||||
))
|
))
|
||||||
.await;
|
.await;
|
||||||
|
} else {
|
||||||
|
futures::future::pending().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => futures::future::pending().await,
|
_ => futures::future::pending().await,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tokio::pin!(start_stop_handler);
|
tokio::pin!(transition_handler);
|
||||||
futures::future::select(wait, start_stop_handler).await;
|
futures::future::select(wait, transition_handler).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn service_actor_loop<'a>(
|
async fn service_actor_loop<'a>(
|
||||||
current: &tokio::sync::watch::Receiver<super::persistent_container::ServiceState>,
|
watch: &mut TypedDbWatch<StatusInfo>,
|
||||||
seed: &'a Arc<ServiceActorSeed>,
|
seed: &'a Arc<ServiceActorSeed>,
|
||||||
start_stop_task: &mut Option<
|
transition: &mut Option<Transition<'a>>,
|
||||||
Either<BoxFuture<'a, Result<(), Error>>, BoxFuture<'a, Result<(), Error>>>,
|
|
||||||
>,
|
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let id = &seed.id;
|
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
|
if let Some(callbacks) = seed.ctx.callbacks.get_status(id) {
|
||||||
.ctx
|
callbacks
|
||||||
.db
|
.call(vector![patch_db::ModelExt::into_value(status_model)])
|
||||||
.mutate(|d| {
|
.await?;
|
||||||
if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) {
|
|
||||||
let previous = i.as_status().de()?;
|
|
||||||
let main_status = match &kinds {
|
|
||||||
ServiceStateKinds {
|
|
||||||
transition_state: Some(TransitionKind::Restarting),
|
|
||||||
..
|
|
||||||
} => MainStatus::Restarting,
|
|
||||||
ServiceStateKinds {
|
|
||||||
transition_state: Some(TransitionKind::BackingUp),
|
|
||||||
..
|
|
||||||
} => previous.backing_up(),
|
|
||||||
ServiceStateKinds {
|
|
||||||
running_status: Some(status),
|
|
||||||
desired_state: StartStop::Start,
|
|
||||||
..
|
|
||||||
} => MainStatus::Running {
|
|
||||||
started: status.started,
|
|
||||||
health: previous.health().cloned().unwrap_or_default(),
|
|
||||||
},
|
|
||||||
ServiceStateKinds {
|
|
||||||
running_status: None,
|
|
||||||
desired_state: StartStop::Start,
|
|
||||||
..
|
|
||||||
} => MainStatus::Starting {
|
|
||||||
health: previous.health().cloned().unwrap_or_default(),
|
|
||||||
},
|
|
||||||
ServiceStateKinds {
|
|
||||||
running_status: Some(_),
|
|
||||||
desired_state: StartStop::Stop,
|
|
||||||
..
|
|
||||||
} => MainStatus::Stopping,
|
|
||||||
ServiceStateKinds {
|
|
||||||
running_status: None,
|
|
||||||
desired_state: StartStop::Stop,
|
|
||||||
..
|
|
||||||
} => MainStatus::Stopped,
|
|
||||||
};
|
|
||||||
i.as_status_mut().ser(&main_status)?;
|
|
||||||
return Ok(previous
|
|
||||||
.major_changes(&main_status)
|
|
||||||
.then_some((previous, main_status)));
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.result?;
|
|
||||||
|
|
||||||
if let Some((previous, new_state)) = major_changes_state {
|
|
||||||
if let Some(callbacks) = seed.ctx.callbacks.get_status(id) {
|
|
||||||
callbacks
|
|
||||||
.call(vector![to_value(&previous)?, to_value(&new_state)?])
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
seed.synchronized.notify_waiters();
|
|
||||||
|
|
||||||
match kinds {
|
match status {
|
||||||
ServiceStateKinds {
|
StatusInfo {
|
||||||
running_status: None,
|
desired: DesiredStatus::Running | DesiredStatus::Restarting,
|
||||||
desired_state: StartStop::Start,
|
started: None,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let task = start_stop_task
|
let task = transition
|
||||||
.take()
|
.take()
|
||||||
.filter(|task| matches!(task, Either::Right(_)));
|
.filter(|task| task.kind == TransitionKind::Starting);
|
||||||
*start_stop_task = Some(
|
*transition = task.or_else(|| Some(seed.start()));
|
||||||
task.unwrap_or_else(|| Either::Right(seed.persistent_container.start().boxed())),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
ServiceStateKinds {
|
StatusInfo {
|
||||||
running_status: Some(_),
|
desired:
|
||||||
desired_state: StartStop::Stop,
|
DesiredStatus::Stopped | DesiredStatus::Restarting | DesiredStatus::BackingUp { .. },
|
||||||
|
started: Some(_),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let task = start_stop_task
|
let task = transition
|
||||||
.take()
|
.take()
|
||||||
.filter(|task| matches!(task, Either::Left(_)));
|
.filter(|task| task.kind == TransitionKind::Stopping);
|
||||||
*start_stop_task = Some(task.unwrap_or_else(|| {
|
*transition = task.or_else(|| Some(seed.stop()));
|
||||||
Either::Left(
|
}
|
||||||
async {
|
StatusInfo {
|
||||||
seed.persistent_container.stop().await?;
|
desired: DesiredStatus::BackingUp { .. },
|
||||||
seed.persistent_container
|
started: None,
|
||||||
.state
|
..
|
||||||
.send_if_modified(|s| s.running_status.take().is_some());
|
} => {
|
||||||
Ok::<_, Error>(())
|
let task = transition
|
||||||
}
|
.take()
|
||||||
.boxed(),
|
.filter(|task| task.kind == TransitionKind::BackingUp);
|
||||||
)
|
*transition = task.or_else(|| Some(seed.backup()));
|
||||||
}));
|
}
|
||||||
|
_ => {
|
||||||
|
*transition = None;
|
||||||
}
|
}
|
||||||
_ => (),
|
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -27,11 +28,10 @@ use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle, ProgressT
|
|||||||
use crate::s9pk::S9pk;
|
use crate::s9pk::S9pk;
|
||||||
use crate::s9pk::manifest::PackageId;
|
use crate::s9pk::manifest::PackageId;
|
||||||
use crate::s9pk::merkle_archive::source::FileSource;
|
use crate::s9pk::merkle_archive::source::FileSource;
|
||||||
use crate::service::rpc::ExitParams;
|
use crate::service::rpc::{ExitParams, InitKind};
|
||||||
use crate::service::start_stop::StartStop;
|
|
||||||
use crate::service::{LoadDisposition, Service, ServiceRef};
|
use crate::service::{LoadDisposition, Service, ServiceRef};
|
||||||
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
|
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::serde::{Base32, Pem};
|
||||||
use crate::util::sync::SyncMutex;
|
use crate::util::sync::SyncMutex;
|
||||||
|
|
||||||
@@ -123,17 +123,7 @@ impl ServiceMap {
|
|||||||
ctx.db
|
ctx.db
|
||||||
.mutate(|db| {
|
.mutate(|db| {
|
||||||
if let Some(pde) = db.as_public_mut().as_package_data_mut().as_idx_mut(id) {
|
if let Some(pde) = db.as_public_mut().as_package_data_mut().as_idx_mut(id) {
|
||||||
pde.as_status_mut().map_mutate(|s| {
|
pde.as_status_info_mut().as_error_mut().ser(&Some(e))?;
|
||||||
Ok(MainStatus::Error {
|
|
||||||
on_rebuild: if s.running() {
|
|
||||||
StartStop::Start
|
|
||||||
} else {
|
|
||||||
StartStop::Stop
|
|
||||||
},
|
|
||||||
message: e.details,
|
|
||||||
debug: Some(e.debug),
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
@@ -242,7 +232,12 @@ impl ServiceMap {
|
|||||||
PackageState::Installing(installing)
|
PackageState::Installing(installing)
|
||||||
},
|
},
|
||||||
s9pk: installed_path,
|
s9pk: installed_path,
|
||||||
status: MainStatus::Stopped,
|
status_info: StatusInfo {
|
||||||
|
error: None,
|
||||||
|
health: BTreeMap::new(),
|
||||||
|
started: None,
|
||||||
|
desired: DesiredStatus::Stopped,
|
||||||
|
},
|
||||||
registry,
|
registry,
|
||||||
developer_key: Pem::new(developer_key),
|
developer_key: Pem::new(developer_key),
|
||||||
icon,
|
icon,
|
||||||
@@ -333,15 +328,9 @@ impl ServiceMap {
|
|||||||
next_can_migrate_from.clone(),
|
next_can_migrate_from.clone(),
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
let run_state = service
|
|
||||||
.seed
|
|
||||||
.persistent_container
|
|
||||||
.state
|
|
||||||
.borrow()
|
|
||||||
.desired_state;
|
|
||||||
let cleanup = service.uninstall(uninit, false, false).await?;
|
let cleanup = service.uninstall(uninit, false, false).await?;
|
||||||
progress.complete();
|
progress.complete();
|
||||||
Some((run_state, cleanup))
|
Some(cleanup)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -350,7 +339,13 @@ impl ServiceMap {
|
|||||||
s9pk,
|
s9pk,
|
||||||
&installed_path,
|
&installed_path,
|
||||||
®istry,
|
®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,
|
recovery_source,
|
||||||
Some(InstallProgressHandles {
|
Some(InstallProgressHandles {
|
||||||
finalization_progress,
|
finalization_progress,
|
||||||
@@ -360,7 +355,7 @@ impl ServiceMap {
|
|||||||
.await?;
|
.await?;
|
||||||
*service = Some(new_service.into());
|
*service = Some(new_service.into());
|
||||||
|
|
||||||
if let Some((_, cleanup)) = prev {
|
if let Some(cleanup) = prev {
|
||||||
cleanup.await?;
|
cleanup.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,23 +9,7 @@ pub enum StartStop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StartStop {
|
impl StartStop {
|
||||||
pub(crate) fn is_start(&self) -> bool {
|
pub fn is_start(&self) -> bool {
|
||||||
matches!(self, StartStop::Start)
|
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::path::PathBuf;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use futures::FutureExt;
|
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
|
use futures::{FutureExt, TryFutureExt};
|
||||||
use models::ProcedureName;
|
use models::ProcedureName;
|
||||||
|
use rpc_toolkit::yajrc::RpcError;
|
||||||
|
|
||||||
use super::TempDesiredRestore;
|
|
||||||
use crate::disk::mount::filesystem::ReadWrite;
|
use crate::disk::mount::filesystem::ReadWrite;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::rpc_continuations::Guid;
|
use crate::rpc_continuations::Guid;
|
||||||
use crate::service::ServiceActor;
|
|
||||||
use crate::service::action::GetActionInput;
|
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::background::BackgroundJobQueue;
|
||||||
use crate::util::actor::{ConflictBuilder, Handler};
|
use crate::util::actor::{ConflictBuilder, Handler};
|
||||||
use crate::util::future::RemoteCancellable;
|
|
||||||
use crate::util::serde::NoOutput;
|
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(in crate::service) struct Backup {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
}
|
}
|
||||||
@@ -28,63 +72,31 @@ impl Handler<Backup> for ServiceActor {
|
|||||||
async fn handle(
|
async fn handle(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: Guid,
|
id: Guid,
|
||||||
backup: Backup,
|
Backup { path }: Backup,
|
||||||
jobs: &BackgroundJobQueue,
|
_: &BackgroundJobQueue,
|
||||||
) -> Self::Response {
|
) -> 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 seed = self.0.clone();
|
||||||
|
|
||||||
let transition = RemoteCancellable::new(async move {
|
let transition = async move {
|
||||||
temp.stop();
|
async {
|
||||||
current
|
let backup_guard = seed
|
||||||
.wait_for(|s| s.running_status.is_none())
|
.persistent_container
|
||||||
.await
|
.mount_backup(path, ReadWrite)
|
||||||
.with_kind(ErrorKind::Unknown)?;
|
.await?;
|
||||||
|
seed.persistent_container
|
||||||
|
.execute::<NoOutput>(id, ProcedureName::CreateBackup, Value::Null, None)
|
||||||
|
.await?;
|
||||||
|
backup_guard.unmount(true).await?;
|
||||||
|
|
||||||
let backup_guard = seed
|
Ok::<_, Error>(())
|
||||||
.persistent_container
|
|
||||||
.mount_backup(path, ReadWrite)
|
|
||||||
.await?;
|
|
||||||
seed.persistent_container
|
|
||||||
.execute::<NoOutput>(id, ProcedureName::CreateBackup, Value::Null, None)
|
|
||||||
.await?;
|
|
||||||
backup_guard.unmount(true).await?;
|
|
||||||
|
|
||||||
if temp.restore().is_start() {
|
|
||||||
current
|
|
||||||
.wait_for(|s| s.running_status.is_some())
|
|
||||||
.await
|
|
||||||
.with_kind(ErrorKind::Unknown)?;
|
|
||||||
}
|
}
|
||||||
drop(temp);
|
.await
|
||||||
Ok::<_, Arc<Error>>(())
|
.map_err(RpcError::from)
|
||||||
});
|
|
||||||
let cancel_handle = transition.cancellation_handle();
|
|
||||||
let transition = transition.shared();
|
|
||||||
let job_transition = transition.clone();
|
|
||||||
jobs.add_job(job_transition.map(|_| ()));
|
|
||||||
|
|
||||||
let mut old = None;
|
|
||||||
self.0.persistent_container.state.send_modify(|s| {
|
|
||||||
old = std::mem::replace(
|
|
||||||
&mut s.transition_state,
|
|
||||||
Some(TransitionState {
|
|
||||||
kind: TransitionKind::BackingUp,
|
|
||||||
cancel_handle,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
if let Some(t) = old {
|
|
||||||
t.abort().await;
|
|
||||||
}
|
}
|
||||||
Ok(transition
|
.shared();
|
||||||
.map(|r| {
|
|
||||||
r.ok_or_else(|| Error::new(eyre!("Backup canceled"), ErrorKind::Cancelled))?
|
self.0.backup.replace(Some(transition.clone().boxed()));
|
||||||
.map_err(|e| e.clone_output())
|
|
||||||
})
|
Ok(transition.map_err(Error::from).boxed())
|
||||||
.boxed())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,91 +1,42 @@
|
|||||||
use std::sync::Arc;
|
use futures::FutureExt;
|
||||||
|
use futures::future::BoxFuture;
|
||||||
|
|
||||||
use futures::{Future, FutureExt};
|
use crate::prelude::*;
|
||||||
use tokio::sync::watch;
|
use crate::service::ServiceActorSeed;
|
||||||
|
|
||||||
use super::persistent_container::ServiceState;
|
|
||||||
use crate::service::start_stop::StartStop;
|
|
||||||
use crate::util::actor::background::BackgroundJobQueue;
|
|
||||||
use crate::util::future::{CancellationHandle, RemoteCancellable};
|
|
||||||
|
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
pub mod restart;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub enum TransitionKind {
|
pub enum TransitionKind {
|
||||||
BackingUp,
|
BackingUp,
|
||||||
Restarting,
|
Starting,
|
||||||
|
Stopping,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used only in the manager/mod and is used to keep track of the state of the manager during the
|
pub struct Transition<'a> {
|
||||||
/// transitional states
|
pub kind: TransitionKind,
|
||||||
pub struct TransitionState {
|
pub future: BoxFuture<'a, Result<(), Error>>,
|
||||||
cancel_handle: CancellationHandle,
|
|
||||||
kind: TransitionKind,
|
|
||||||
}
|
}
|
||||||
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 {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("TransitionState")
|
f.debug_struct("Transition")
|
||||||
.field("kind", &self.kind)
|
.field("kind", &self.kind)
|
||||||
.finish_non_exhaustive()
|
.finish_non_exhaustive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TransitionState {
|
impl ServiceActorSeed {
|
||||||
pub fn kind(&self) -> TransitionKind {
|
pub fn start(&self) -> Transition<'_> {
|
||||||
self.kind
|
Transition {
|
||||||
|
kind: TransitionKind::Starting,
|
||||||
|
future: self.persistent_container.start().boxed(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub async fn abort(mut self) {
|
|
||||||
self.cancel_handle.cancel_and_wait().await
|
pub fn stop(&self) -> Transition<'_> {
|
||||||
}
|
Transition {
|
||||||
fn new(
|
kind: TransitionKind::Stopping,
|
||||||
task: impl Future<Output = ()> + Send + 'static,
|
future: self.persistent_container.stop().boxed(),
|
||||||
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 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) {
|
|
||||||
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 std::collections::BTreeMap;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use imbl::OrdMap;
|
use models::{ErrorData, HealthCheckId};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use self::health_check::HealthCheckId;
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::service::start_stop::StartStop;
|
use crate::service::start_stop::StartStop;
|
||||||
use crate::status::health_check::NamedHealthCheckResult;
|
use crate::status::health_check::NamedHealthCheckResult;
|
||||||
|
|
||||||
pub mod health_check;
|
pub mod health_check;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, TS)]
|
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||||
#[serde(tag = "main")]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[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")]
|
#[serde(rename_all_fields = "camelCase")]
|
||||||
pub enum MainStatus {
|
pub enum DesiredStatus {
|
||||||
Error {
|
|
||||||
on_rebuild: StartStop,
|
|
||||||
message: String,
|
|
||||||
debug: Option<String>,
|
|
||||||
},
|
|
||||||
Stopped,
|
Stopped,
|
||||||
Restarting,
|
Restarting,
|
||||||
Stopping,
|
Running,
|
||||||
Starting {
|
BackingUp { on_complete: StartStop },
|
||||||
#[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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
impl MainStatus {
|
impl Default for DesiredStatus {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Stopped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DesiredStatus {
|
||||||
pub fn running(&self) -> bool {
|
pub fn running(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
MainStatus::Starting { .. }
|
Self::Running
|
||||||
| MainStatus::Running { .. }
|
| Self::Restarting
|
||||||
| MainStatus::Restarting
|
| Self::BackingUp {
|
||||||
| MainStatus::BackingUp {
|
|
||||||
on_complete: StartStop::Start,
|
on_complete: StartStop::Start,
|
||||||
}
|
|
||||||
| MainStatus::Error {
|
|
||||||
on_rebuild: StartStop::Start,
|
|
||||||
..
|
|
||||||
} => true,
|
} => true,
|
||||||
MainStatus::Stopped
|
Self::Stopped
|
||||||
| MainStatus::Stopping { .. }
|
| Self::BackingUp {
|
||||||
| MainStatus::BackingUp {
|
|
||||||
on_complete: StartStop::Stop,
|
on_complete: StartStop::Stop,
|
||||||
}
|
|
||||||
| MainStatus::Error {
|
|
||||||
on_rebuild: StartStop::Stop,
|
|
||||||
..
|
|
||||||
} => false,
|
} => 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 {
|
pub fn backing_up(&self) -> Self {
|
||||||
MainStatus::BackingUp {
|
Self::BackingUp {
|
||||||
on_complete: if self.running() {
|
on_complete: self.run_state(),
|
||||||
StartStop::Start
|
|
||||||
} else {
|
|
||||||
StartStop::Stop
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn health(&self) -> Option<&OrdMap<HealthCheckId, NamedHealthCheckResult>> {
|
pub fn stop(&self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
MainStatus::Running { health, .. } | MainStatus::Starting { health } => Some(health),
|
Self::BackingUp { .. } => Self::BackingUp {
|
||||||
MainStatus::BackingUp { .. }
|
on_complete: StartStop::Stop,
|
||||||
| MainStatus::Stopped
|
},
|
||||||
| MainStatus::Stopping { .. }
|
_ => Self::Stopped,
|
||||||
| MainStatus::Restarting
|
}
|
||||||
| MainStatus::Error { .. } => None,
|
}
|
||||||
|
|
||||||
|
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};
|
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
|
||||||
|
|
||||||
AsyncSmtpTransport::<Tokio1Executor>::relay(&server)?
|
AsyncSmtpTransport::<Tokio1Executor>::relay(&server)?
|
||||||
|
.port(port)
|
||||||
.credentials(Credentials::new(login, password))
|
.credentials(Credentials::new(login, password))
|
||||||
.build()
|
.build()
|
||||||
.send(
|
.send(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use imbl::HashMap;
|
|||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use patch_db::HasModel;
|
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 serde::{Deserialize, Serialize};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ use crate::rpc_continuations::{OpenAuthedContinuations, RpcContinuations};
|
|||||||
use crate::tunnel::TUNNEL_DEFAULT_LISTEN;
|
use crate::tunnel::TUNNEL_DEFAULT_LISTEN;
|
||||||
use crate::tunnel::api::tunnel_api;
|
use crate::tunnel::api::tunnel_api;
|
||||||
use crate::tunnel::db::TunnelDatabase;
|
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::Invoke;
|
||||||
use crate::util::collections::OrdMapIterMut;
|
use crate::util::collections::OrdMapIterMut;
|
||||||
use crate::util::io::read_file_to_string;
|
use crate::util::io::read_file_to_string;
|
||||||
@@ -100,7 +100,17 @@ impl TunnelContext {
|
|||||||
let db_path = datadir.join("tunnel.db");
|
let db_path = datadir.join("tunnel.db");
|
||||||
let db = TypedPatchDb::<TunnelDatabase>::load_or_init(
|
let db = TypedPatchDb::<TunnelDatabase>::load_or_init(
|
||||||
PatchDb::open(&db_path).await?,
|
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?;
|
.await?;
|
||||||
let listen = config.tunnel_listen.unwrap_or(TUNNEL_DEFAULT_LISTEN);
|
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!("✅ Success! ✅");
|
||||||
println!(
|
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() {
|
if password.is_some() {
|
||||||
", password,"
|
", password,"
|
||||||
} else {
|
} else {
|
||||||
@@ -496,7 +496,7 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
|
|||||||
println!("{password}");
|
println!("{password}");
|
||||||
println!();
|
println!();
|
||||||
println!(concat!(
|
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"
|
"start-tunnel auth reset-password"
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
@@ -516,12 +516,22 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
|
|||||||
.pop()
|
.pop()
|
||||||
.map(Pem)
|
.map(Pem)
|
||||||
.or_not_found("certificate in chain")?;
|
.or_not_found("certificate in chain")?;
|
||||||
println!("📝 Root SSL Certificate:");
|
println!("📝 Root CA:");
|
||||||
print!("{cert}");
|
print!("{cert}");
|
||||||
|
|
||||||
println!(concat!(
|
println!(concat!(
|
||||||
"If you haven't already, ",
|
"To trust your StartTunnel Root CA (above):\n",
|
||||||
"trust the certificate in your system keychain and/or browser."
|
" 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(());
|
return Ok(());
|
||||||
@@ -534,28 +544,14 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
|
|||||||
.await?,
|
.await?,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
suggested_addrs.sort_by_cached_key(|a| match a {
|
suggested_addrs.retain(|ip| match ip {
|
||||||
IpAddr::V4(a) => {
|
IpAddr::V4(a) => !a.is_loopback() && !a.is_private(),
|
||||||
if a.is_loopback() {
|
IpAddr::V6(a) => !a.is_loopback() && !a.is_unicast_link_local(),
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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?
|
prompt("Listen Address: ", parse_as::<IpAddr>("IP Address"), None).await?
|
||||||
} else if suggested_addrs.len() > 16 {
|
} else if suggested_addrs.len() > 16 {
|
||||||
prompt(
|
prompt(
|
||||||
@@ -565,22 +561,7 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
} else {
|
} else {
|
||||||
*choose_custom_display("Listen Address:", &suggested_addrs, |a| match a {
|
*choose("Listen Address:", &suggested_addrs).await?
|
||||||
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?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(concat!(
|
println!(concat!(
|
||||||
@@ -608,8 +589,8 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
|
|||||||
impl std::fmt::Display for Choice {
|
impl std::fmt::Display for Choice {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Generate => write!(f, "Generate an SSL certificate"),
|
Self::Generate => write!(f, "Generate"),
|
||||||
Self::Provide => write!(f, "Provide your own certificate and key"),
|
Self::Provide => write!(f, "Provide"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -617,7 +598,7 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
|
|||||||
let choice = choose(
|
let choice = choose(
|
||||||
concat!(
|
concat!(
|
||||||
"Select whether to generate an SSL certificate ",
|
"Select whether to generate an SSL certificate ",
|
||||||
"or provide your own certificate and key:"
|
"or provide your own certificate (and key):"
|
||||||
),
|
),
|
||||||
&options,
|
&options,
|
||||||
)
|
)
|
||||||
@@ -631,35 +612,35 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
|
|||||||
)?
|
)?
|
||||||
.filter(|a| !a.ip().is_unspecified());
|
.filter(|a| !a.ip().is_unspecified());
|
||||||
|
|
||||||
let default_prompt = if let Some(listen) = listen {
|
let san_info = if let Some(listen) = listen {
|
||||||
format!("Subject Alternative Name(s) [{}]: ", listen.ip())
|
vec![InternedString::from_display(&listen.ip())]
|
||||||
} else {
|
} else {
|
||||||
"Subject Alternative Name(s): ".to_string()
|
println!(
|
||||||
|
"List all IP addresses and domains for which to sign the certificate, separated by commas."
|
||||||
|
);
|
||||||
|
prompt(
|
||||||
|
"Subject Alternative Name(s): ",
|
||||||
|
|s| {
|
||||||
|
s.split(",")
|
||||||
|
.map(|s| {
|
||||||
|
let s = s.trim();
|
||||||
|
if let Ok(ip) = s.parse::<IpAddr>() {
|
||||||
|
Ok(InternedString::from_display(&ip))
|
||||||
|
} else if is_valid_domain(s) {
|
||||||
|
Ok(s.into())
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"{s} is not a valid ip address or domain"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
listen.map(|l| vec![InternedString::from_display(&l.ip())]),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(
|
|
||||||
"List all IP addresses and domains for which to sign the certificate, separated by commas."
|
|
||||||
);
|
|
||||||
let san_info = prompt(
|
|
||||||
&default_prompt,
|
|
||||||
|s| {
|
|
||||||
s.split(",")
|
|
||||||
.map(|s| {
|
|
||||||
let s = s.trim();
|
|
||||||
if let Ok(ip) = s.parse::<IpAddr>() {
|
|
||||||
Ok(InternedString::from_display(&ip))
|
|
||||||
} else if is_valid_domain(s) {
|
|
||||||
Ok(s.into())
|
|
||||||
} else {
|
|
||||||
Err(format!("{s} is not a valid ip address or domain"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
},
|
|
||||||
listen.map(|l| vec![InternedString::from_display(&l.ip())]),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
ctx.call_remote::<TunnelContext>(
|
ctx.call_remote::<TunnelContext>(
|
||||||
"web.generate-certificate",
|
"web.generate-certificate",
|
||||||
to_value(&GenerateCertParams { subject: san_info })?,
|
to_value(&GenerateCertParams { subject: san_info })?,
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ mod test {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct CActor;
|
struct CActor;
|
||||||
impl Actor for CActor {
|
impl Actor for CActor {
|
||||||
fn init(&mut self, jobs: &BackgroundJobQueue) {}
|
fn init(&mut self, _: &BackgroundJobQueue) {}
|
||||||
}
|
}
|
||||||
struct Pending;
|
struct Pending;
|
||||||
impl Handler<Pending> for CActor {
|
impl Handler<Pending> for CActor {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use std::time::Duration;
|
|||||||
use bytes::{Buf, BytesMut};
|
use bytes::{Buf, BytesMut};
|
||||||
use clap::builder::ValueParserFactory;
|
use clap::builder::ValueParserFactory;
|
||||||
use futures::future::{BoxFuture, Fuse};
|
use futures::future::{BoxFuture, Fuse};
|
||||||
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
|
use futures::{FutureExt, Stream, TryStreamExt};
|
||||||
use helpers::{AtomicFile, NonDetachingJoinHandle};
|
use helpers::{AtomicFile, NonDetachingJoinHandle};
|
||||||
use inotify::{EventMask, EventStream, Inotify, WatchMask};
|
use inotify::{EventMask, EventStream, Inotify, WatchMask};
|
||||||
use models::FromStrParser;
|
use models::FromStrParser;
|
||||||
@@ -22,7 +22,7 @@ use nix::unistd::{Gid, Uid};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::fs::{File, OpenOptions};
|
use tokio::fs::{File, OpenOptions};
|
||||||
use tokio::io::{
|
use tokio::io::{
|
||||||
AsyncRead, AsyncReadExt, AsyncSeek, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf, SeekFrom,
|
AsyncRead, AsyncReadExt, AsyncSeek, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf,
|
||||||
WriteHalf, duplex,
|
WriteHalf, duplex,
|
||||||
};
|
};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::{BTreeMap, VecDeque};
|
use std::collections::VecDeque;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::atomic::AtomicUsize;
|
use std::sync::atomic::AtomicUsize;
|
||||||
@@ -28,6 +28,7 @@ fn annotate_lock<F, T>(f: F, id: usize, write: bool) -> T
|
|||||||
where
|
where
|
||||||
F: FnOnce() -> T,
|
F: FnOnce() -> T,
|
||||||
{
|
{
|
||||||
|
use std::collections::BTreeMap;
|
||||||
std::thread_local! {
|
std::thread_local! {
|
||||||
static LOCK_CTX: std::cell::RefCell<BTreeMap<usize, Result<(), usize>>> = std::cell::RefCell::new(BTreeMap::new());
|
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_13;
|
||||||
mod v0_4_0_alpha_14;
|
mod v0_4_0_alpha_14;
|
||||||
mod v0_4_0_alpha_15;
|
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 {
|
impl Current {
|
||||||
#[instrument(skip(self, db))]
|
#[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_12(Wrapper<v0_4_0_alpha_12::Version>),
|
||||||
V0_4_0_alpha_13(Wrapper<v0_4_0_alpha_13::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_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),
|
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_12(v) => DynVersion(Box::new(v.0)),
|
||||||
Self::V0_4_0_alpha_13(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_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) => {
|
Self::Other(v) => {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("unknown version {v}"),
|
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_12(Wrapper(x)) => x.semver(),
|
||||||
Version::V0_4_0_alpha_13(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_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(),
|
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
|
EOF
|
||||||
|
|
||||||
# change timezone
|
# change timezone
|
||||||
rm -f /etc/localtime
|
ln -sf /usr/share/zoneinfo/Etc/UTC /etc/localtime
|
||||||
ln -s /usr/share/zoneinfo/Etc/UTC /etc/localtime
|
|
||||||
|
|
||||||
rm /etc/resolv.conf
|
rm /etc/resolv.conf
|
||||||
echo "nameserver 127.0.0.1" > /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
|
echo "fs.inotify.max_user_watches=1048576" > /etc/sysctl.d/97-startos.conf
|
||||||
|
|
||||||
dpkg-reconfigure --frontend noninteractive locales
|
|
||||||
|
|
||||||
if ! getent group | grep '^startos:'; then
|
if ! getent group | grep '^startos:'; then
|
||||||
groupadd startos
|
groupadd startos
|
||||||
fi
|
fi
|
||||||
|
|||||||
2
patch-db
2
patch-db
Submodule patch-db updated: 90b336d6a9...bdb5a10114
@@ -13,8 +13,8 @@ import {
|
|||||||
ExportServiceInterfaceParams,
|
ExportServiceInterfaceParams,
|
||||||
ServiceInterface,
|
ServiceInterface,
|
||||||
CreateTaskParams,
|
CreateTaskParams,
|
||||||
MainStatus,
|
|
||||||
MountParams,
|
MountParams,
|
||||||
|
StatusInfo,
|
||||||
} from "./osBindings"
|
} from "./osBindings"
|
||||||
import {
|
import {
|
||||||
PackageId,
|
PackageId,
|
||||||
@@ -66,7 +66,7 @@ export type Effects = {
|
|||||||
getStatus(options: {
|
getStatus(options: {
|
||||||
packageId?: PackageId
|
packageId?: PackageId
|
||||||
callback?: () => void
|
callback?: () => void
|
||||||
}): Promise<MainStatus>
|
}): Promise<StatusInfo>
|
||||||
/** indicate to the host os what runstate the service is in */
|
/** indicate to the host os what runstate the service is in */
|
||||||
setMainStatus(options: SetMainStatus): Promise<null>
|
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.
|
// 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 { CurrentDependencies } from "./CurrentDependencies"
|
||||||
import type { DataUrl } from "./DataUrl"
|
import type { DataUrl } from "./DataUrl"
|
||||||
import type { Hosts } from "./Hosts"
|
import type { Hosts } from "./Hosts"
|
||||||
import type { MainStatus } from "./MainStatus"
|
|
||||||
import type { PackageState } from "./PackageState"
|
import type { PackageState } from "./PackageState"
|
||||||
import type { ReplayId } from "./ReplayId"
|
import type { ReplayId } from "./ReplayId"
|
||||||
import type { ServiceInterface } from "./ServiceInterface"
|
import type { ServiceInterface } from "./ServiceInterface"
|
||||||
import type { ServiceInterfaceId } from "./ServiceInterfaceId"
|
import type { ServiceInterfaceId } from "./ServiceInterfaceId"
|
||||||
|
import type { StatusInfo } from "./StatusInfo"
|
||||||
import type { TaskEntry } from "./TaskEntry"
|
import type { TaskEntry } from "./TaskEntry"
|
||||||
|
|
||||||
export type PackageDataEntry = {
|
export type PackageDataEntry = {
|
||||||
stateInfo: PackageState
|
stateInfo: PackageState
|
||||||
s9pk: string
|
s9pk: string
|
||||||
status: MainStatus
|
statusInfo: StatusInfo
|
||||||
registry: string | null
|
registry: string | null
|
||||||
developerKey: string
|
developerKey: string
|
||||||
icon: DataUrl
|
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 { CurrentDependencyInfo } from "./CurrentDependencyInfo"
|
||||||
export { DataUrl } from "./DataUrl"
|
export { DataUrl } from "./DataUrl"
|
||||||
export { Dependencies } from "./Dependencies"
|
export { Dependencies } from "./Dependencies"
|
||||||
export { DependencyKind } from "./DependencyKind"
|
|
||||||
export { DependencyMetadata } from "./DependencyMetadata"
|
export { DependencyMetadata } from "./DependencyMetadata"
|
||||||
export { DependencyRequirement } from "./DependencyRequirement"
|
export { DependencyRequirement } from "./DependencyRequirement"
|
||||||
export { DepInfo } from "./DepInfo"
|
export { DepInfo } from "./DepInfo"
|
||||||
export { Description } from "./Description"
|
export { Description } from "./Description"
|
||||||
|
export { DesiredStatus } from "./DesiredStatus"
|
||||||
export { DestroySubcontainerFsParams } from "./DestroySubcontainerFsParams"
|
export { DestroySubcontainerFsParams } from "./DestroySubcontainerFsParams"
|
||||||
export { DeviceFilter } from "./DeviceFilter"
|
export { DeviceFilter } from "./DeviceFilter"
|
||||||
export { DnsSettings } from "./DnsSettings"
|
export { DnsSettings } from "./DnsSettings"
|
||||||
@@ -72,6 +72,7 @@ export { Duration } from "./Duration"
|
|||||||
export { EchoParams } from "./EchoParams"
|
export { EchoParams } from "./EchoParams"
|
||||||
export { EditSignerParams } from "./EditSignerParams"
|
export { EditSignerParams } from "./EditSignerParams"
|
||||||
export { EncryptedWire } from "./EncryptedWire"
|
export { EncryptedWire } from "./EncryptedWire"
|
||||||
|
export { ErrorData } from "./ErrorData"
|
||||||
export { EventId } from "./EventId"
|
export { EventId } from "./EventId"
|
||||||
export { ExportActionParams } from "./ExportActionParams"
|
export { ExportActionParams } from "./ExportActionParams"
|
||||||
export { ExportServiceInterfaceParams } from "./ExportServiceInterfaceParams"
|
export { ExportServiceInterfaceParams } from "./ExportServiceInterfaceParams"
|
||||||
@@ -123,7 +124,6 @@ export { LoginParams } from "./LoginParams"
|
|||||||
export { LshwDevice } from "./LshwDevice"
|
export { LshwDevice } from "./LshwDevice"
|
||||||
export { LshwDisplay } from "./LshwDisplay"
|
export { LshwDisplay } from "./LshwDisplay"
|
||||||
export { LshwProcessor } from "./LshwProcessor"
|
export { LshwProcessor } from "./LshwProcessor"
|
||||||
export { MainStatus } from "./MainStatus"
|
|
||||||
export { Manifest } from "./Manifest"
|
export { Manifest } from "./Manifest"
|
||||||
export { MaybeUtf8String } from "./MaybeUtf8String"
|
export { MaybeUtf8String } from "./MaybeUtf8String"
|
||||||
export { MebiBytes } from "./MebiBytes"
|
export { MebiBytes } from "./MebiBytes"
|
||||||
@@ -201,6 +201,7 @@ export { SignAssetParams } from "./SignAssetParams"
|
|||||||
export { SignerInfo } from "./SignerInfo"
|
export { SignerInfo } from "./SignerInfo"
|
||||||
export { SmtpValue } from "./SmtpValue"
|
export { SmtpValue } from "./SmtpValue"
|
||||||
export { StartStop } from "./StartStop"
|
export { StartStop } from "./StartStop"
|
||||||
|
export { StatusInfo } from "./StatusInfo"
|
||||||
export { TaskCondition } from "./TaskCondition"
|
export { TaskCondition } from "./TaskCondition"
|
||||||
export { TaskEntry } from "./TaskEntry"
|
export { TaskEntry } from "./TaskEntry"
|
||||||
export { TaskInput } from "./TaskInput"
|
export { TaskInput } from "./TaskInput"
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ import {
|
|||||||
} from "../../base/lib/inits"
|
} from "../../base/lib/inits"
|
||||||
import { DropGenerator } from "../../base/lib/util/Drop"
|
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
|
// prettier-ignore
|
||||||
type AnyNeverCond<T extends any[], Then, Else> =
|
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",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.15",
|
"version": "0.4.0-alpha.16",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.15",
|
"version": "0.4.0-alpha.16",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^20.3.0",
|
"@angular/animations": "^20.3.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.15",
|
"version": "0.4.0-alpha.16",
|
||||||
"author": "Start9 Labs, Inc",
|
"author": "Start9 Labs, Inc",
|
||||||
"homepage": "https://start9.com/",
|
"homepage": "https://start9.com/",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { PatchDB } from 'patch-db-client'
|
|||||||
import { filter, map } from 'rxjs'
|
import { filter, map } from 'rxjs'
|
||||||
import { ApiService } from 'src/app/services/api/api.service'
|
import { ApiService } from 'src/app/services/api/api.service'
|
||||||
import { TunnelData } from 'src/app/services/patch-db/data-model'
|
import { TunnelData } from 'src/app/services/patch-db/data-model'
|
||||||
|
|
||||||
import { DEVICES_ADD } from './add'
|
import { DEVICES_ADD } from './add'
|
||||||
import { DEVICES_CONFIG } from './config'
|
import { DEVICES_CONFIG } from './config'
|
||||||
import { MappedDevice, MappedSubnet } from './utils'
|
import { MappedDevice, MappedSubnet } from './utils'
|
||||||
@@ -84,6 +83,8 @@ import { MappedDevice, MappedSubnet } from './utils'
|
|||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<div class="placeholder">No devices</div>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ import { MappedDevice, MappedForward } from './utils'
|
|||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<div class="placeholder">No port forwards</div>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ import { SUBNETS_ADD } from './add'
|
|||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<div class="placeholder">No subnets</div>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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 {
|
qr-code {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
|||||||
selector: 'service-error',
|
selector: 'service-error',
|
||||||
template: `
|
template: `
|
||||||
<header>{{ 'Service Launch Error' | i18n }}</header>
|
<header>{{ 'Service Launch Error' | i18n }}</header>
|
||||||
<p class="error-message">{{ error?.message }}</p>
|
<p class="error-message">{{ error?.details }}</p>
|
||||||
<p>{{ error?.debug }}</p>
|
<p>{{ error?.debug }}</p>
|
||||||
<h4>
|
<h4>
|
||||||
{{ 'Actions' | i18n }}
|
{{ 'Actions' | i18n }}
|
||||||
@@ -95,7 +95,7 @@ export class ServiceErrorComponent {
|
|||||||
overflow = false
|
overflow = false
|
||||||
|
|
||||||
get error() {
|
get error() {
|
||||||
return this.pkg.status.main === 'error' ? this.pkg.status : null
|
return this.pkg.statusInfo.error
|
||||||
}
|
}
|
||||||
|
|
||||||
rebuild() {
|
rebuild() {
|
||||||
@@ -108,7 +108,7 @@ export class ServiceErrorComponent {
|
|||||||
|
|
||||||
show() {
|
show() {
|
||||||
this.dialog
|
this.dialog
|
||||||
.openAlert(this.error?.message as i18nKey, { label: 'Service error' })
|
.openAlert(this.error?.details as i18nKey, { label: 'Service error' })
|
||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
<span class="loading-dots"></span>
|
<span class="loading-dots"></span>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if ($any(pkg().status)?.started; as started) {
|
@if (pkg().statusInfo.started; as started) {
|
||||||
<service-uptime [started]="started" />
|
<service-uptime [started]="started" />
|
||||||
}
|
}
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { ServiceTasksComponent } from 'src/app/routes/portal/routes/services/com
|
|||||||
import { ActionService } from 'src/app/services/action.service'
|
import { ActionService } from 'src/app/services/action.service'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
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'
|
import { getManifest } from 'src/app/utils/get-package-data'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -161,7 +162,7 @@ export class ServiceTaskComponent {
|
|||||||
pkgInfo: {
|
pkgInfo: {
|
||||||
id: this.task().packageId,
|
id: this.task().packageId,
|
||||||
title,
|
title,
|
||||||
mainStatus: pkg.status.main,
|
status: getInstalledBaseStatus(pkg.statusInfo),
|
||||||
icon: pkg.icon,
|
icon: pkg.icon,
|
||||||
},
|
},
|
||||||
actionInfo: { id: this.task().actionId, metadata },
|
actionInfo: { id: this.task().actionId, metadata },
|
||||||
|
|||||||
@@ -114,11 +114,10 @@ import { distinctUntilChanged } from 'rxjs/operators'
|
|||||||
})
|
})
|
||||||
export class ServiceUptimeComponent {
|
export class ServiceUptimeComponent {
|
||||||
protected readonly uptime$ = timer(0, 1000).pipe(
|
protected readonly uptime$ = timer(0, 1000).pipe(
|
||||||
map(() =>
|
map(() => {
|
||||||
this.started()
|
const started = this.started()
|
||||||
? Math.max(Date.now() - new Date(this.started()).getTime(), 0)
|
return started ? Math.max(Date.now() - new Date(started).getTime(), 0) : 0
|
||||||
: 0,
|
}),
|
||||||
),
|
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
map(delta => ({
|
map(delta => ({
|
||||||
seconds: Math.floor(delta / 1000) % 60,
|
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>
|
||||||
<td [style.grid-area]="'2 / 2'">{{ manifest.version }}</td>
|
<td [style.grid-area]="'2 / 2'">{{ manifest.version }}</td>
|
||||||
<td class="uptime">
|
<td class="uptime">
|
||||||
@if ($any(pkg.status)?.started; as started) {
|
@if (pkg.statusInfo.started; as started) {
|
||||||
<span>{{ 'Uptime' | i18n }}:</span>
|
<span>{{ 'Uptime' | i18n }}:</span>
|
||||||
<service-uptime [started]="started" />
|
<service-uptime [started]="started" />
|
||||||
} @else {
|
} @else {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class StatusComponent {
|
|||||||
const { primary, health } = this.getStatus(this.pkg)
|
const { primary, health } = this.getStatus(this.pkg)
|
||||||
return (
|
return (
|
||||||
!this.hasDepErrors &&
|
!this.hasDepErrors &&
|
||||||
primary !== 'taskRequired' &&
|
primary !== 'task-required' &&
|
||||||
primary !== 'error' &&
|
primary !== 'error' &&
|
||||||
health !== 'failure'
|
health !== 'failure'
|
||||||
)
|
)
|
||||||
@@ -80,7 +80,7 @@ export class StatusComponent {
|
|||||||
case 'updating':
|
case 'updating':
|
||||||
case 'stopping':
|
case 'stopping':
|
||||||
case 'starting':
|
case 'starting':
|
||||||
case 'backingUp':
|
case 'backing-up':
|
||||||
case 'restarting':
|
case 'restarting':
|
||||||
case 'removing':
|
case 'removing':
|
||||||
return true
|
return true
|
||||||
@@ -93,7 +93,7 @@ export class StatusComponent {
|
|||||||
switch (this.getStatus(this.pkg).primary) {
|
switch (this.getStatus(this.pkg).primary) {
|
||||||
case 'running':
|
case 'running':
|
||||||
return 'var(--tui-status-positive)'
|
return 'var(--tui-status-positive)'
|
||||||
case 'taskRequired':
|
case 'task-required':
|
||||||
return 'var(--tui-status-warning)'
|
return 'var(--tui-status-warning)'
|
||||||
case 'error':
|
case 'error':
|
||||||
return 'var(--tui-status-negative)'
|
return 'var(--tui-status-negative)'
|
||||||
@@ -101,7 +101,7 @@ export class StatusComponent {
|
|||||||
case 'updating':
|
case 'updating':
|
||||||
case 'stopping':
|
case 'stopping':
|
||||||
case 'starting':
|
case 'starting':
|
||||||
case 'backingUp':
|
case 'backing-up':
|
||||||
case 'restarting':
|
case 'restarting':
|
||||||
case 'removing':
|
case 'removing':
|
||||||
case 'restoring':
|
case 'restoring':
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { tuiPure } from '@taiga-ui/cdk'
|
|||||||
import { TuiDataList, TuiDropdown, TuiButton } from '@taiga-ui/core'
|
import { TuiDataList, TuiDropdown, TuiButton } from '@taiga-ui/core'
|
||||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||||
import { InterfaceService } from '../../../components/interfaces/interface.service'
|
import { InterfaceService } from '../../../components/interfaces/interface.service'
|
||||||
|
import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-ui-launch',
|
selector: 'app-ui-launch',
|
||||||
@@ -70,7 +71,7 @@ export class UILaunchComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isRunning(): boolean {
|
get isRunning(): boolean {
|
||||||
return this.pkg.status.main === 'running'
|
return getInstalledPrimaryStatus(this.pkg) === 'running'
|
||||||
}
|
}
|
||||||
|
|
||||||
@tuiPure
|
@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 { ActionService } from 'src/app/services/action.service'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
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'
|
import { getAllPackages, getManifest } from 'src/app/utils/get-package-data'
|
||||||
|
|
||||||
export type PackageActionData = {
|
export type PackageActionData = {
|
||||||
@@ -34,7 +35,7 @@ export type PackageActionData = {
|
|||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
icon: string
|
icon: string
|
||||||
mainStatus: T.MainStatus['main']
|
status: BaseStatus
|
||||||
}
|
}
|
||||||
actionInfo: {
|
actionInfo: {
|
||||||
id: string
|
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 { StandardActionsService } from 'src/app/services/standard-actions.service'
|
||||||
import { getManifest } from 'src/app/utils/get-package-data'
|
import { getManifest } from 'src/app/utils/get-package-data'
|
||||||
import { ServiceActionComponent } from '../components/action.component'
|
import { ServiceActionComponent } from '../components/action.component'
|
||||||
|
import {
|
||||||
|
BaseStatus,
|
||||||
|
getInstalledBaseStatus,
|
||||||
|
} from 'src/app/services/pkg-status-rendering.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@@ -27,7 +31,7 @@ import { ServiceActionComponent } from '../components/action.component'
|
|||||||
<button
|
<button
|
||||||
tuiCell
|
tuiCell
|
||||||
[action]="a"
|
[action]="a"
|
||||||
(click)="handle(pkg.mainStatus, pkg.icon, pkg.manifest, a)"
|
(click)="handle(pkg.status, pkg.icon, pkg.manifest, a)"
|
||||||
></button>
|
></button>
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
@@ -77,7 +81,7 @@ export default class ServiceActionsRoute {
|
|||||||
? 'Other'
|
? 'Other'
|
||||||
: 'General'
|
: 'General'
|
||||||
return {
|
return {
|
||||||
mainStatus: pkg.status.main,
|
status: getInstalledBaseStatus(pkg.statusInfo),
|
||||||
icon: pkg.icon,
|
icon: pkg.icon,
|
||||||
manifest: getManifest(pkg),
|
manifest: getManifest(pkg),
|
||||||
actions: Object.entries(pkg.actions)
|
actions: Object.entries(pkg.actions)
|
||||||
@@ -131,13 +135,13 @@ export default class ServiceActionsRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handle(
|
handle(
|
||||||
mainStatus: T.MainStatus['main'],
|
status: BaseStatus,
|
||||||
icon: string,
|
icon: string,
|
||||||
{ id, title }: T.Manifest,
|
{ id, title }: T.Manifest,
|
||||||
action: T.ActionMetadata & { id: string },
|
action: T.ActionMetadata & { id: string },
|
||||||
) {
|
) {
|
||||||
this.actions.present({
|
this.actions.present({
|
||||||
pkgInfo: { id, title, icon, mainStatus },
|
pkgInfo: { id, title, icon, status },
|
||||||
actionInfo: { id: action.id, metadata: action },
|
actionInfo: { id: action.id, metadata: action },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
InterfaceService,
|
InterfaceService,
|
||||||
} from '../../../components/interfaces/interface.service'
|
} from '../../../components/interfaces/interface.service'
|
||||||
import { GatewayService } from 'src/app/services/gateway.service'
|
import { GatewayService } from 'src/app/services/gateway.service'
|
||||||
|
import { getInstalledBaseStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@@ -101,7 +102,8 @@ export default class ServiceInterfaceRoute {
|
|||||||
readonly pkg = toSignal(this.patch.watch$('packageData', this.pkgId))
|
readonly pkg = toSignal(this.patch.watch$('packageData', this.pkgId))
|
||||||
|
|
||||||
readonly isRunning = computed(() => {
|
readonly isRunning = computed(() => {
|
||||||
return this.pkg()?.status.main === 'running'
|
const pkg = this.pkg()
|
||||||
|
return pkg ? getInstalledBaseStatus(pkg.statusInfo) === 'running' : false
|
||||||
})
|
})
|
||||||
|
|
||||||
readonly serviceInterface = computed(() => {
|
readonly serviceInterface = computed(() => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const INACTIVE: PrimaryStatus[] = [
|
|||||||
'updating',
|
'updating',
|
||||||
'removing',
|
'removing',
|
||||||
'restoring',
|
'restoring',
|
||||||
'backingUp',
|
'backing-up',
|
||||||
]
|
]
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
|
|||||||
@Component({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@if (pkg(); as pkg) {
|
@if (pkg(); as pkg) {
|
||||||
@if (pkg.status.main === 'error') {
|
@if (pkg.statusInfo.error) {
|
||||||
<service-error [pkg]="pkg" />
|
<service-error [pkg]="pkg" />
|
||||||
} @else if (installing()) {
|
} @else if (installing()) {
|
||||||
<service-install-progress [pkg]="pkg" />
|
<service-install-progress [pkg]="pkg" />
|
||||||
@@ -45,9 +45,9 @@ import { ServiceUptimeComponent } from '../components/uptime.component'
|
|||||||
}
|
}
|
||||||
</service-status>
|
</service-status>
|
||||||
|
|
||||||
@if (status() !== 'backingUp') {
|
@if (status() !== 'backing-up') {
|
||||||
<service-health-checks [checks]="health()" />
|
<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'" />
|
<service-interfaces [pkg]="pkg" [disabled]="status() !== 'running'" />
|
||||||
|
|
||||||
@if (errors() | async; as errors) {
|
@if (errors() | async; as errors) {
|
||||||
@@ -179,7 +179,7 @@ export class ServiceRoute {
|
|||||||
protected readonly pkg = computed(() => this.services()[this.id() || ''])
|
protected readonly pkg = computed(() => this.services()[this.id() || ''])
|
||||||
|
|
||||||
protected readonly health = computed((pkg = this.pkg()) =>
|
protected readonly health = computed((pkg = this.pkg()) =>
|
||||||
pkg ? toHealthCheck(pkg.status) : [],
|
pkg ? toHealthCheck(pkg.statusInfo) : [],
|
||||||
)
|
)
|
||||||
|
|
||||||
protected readonly status = computed((pkg = this.pkg()) =>
|
protected readonly status = computed((pkg = this.pkg()) =>
|
||||||
@@ -202,8 +202,10 @@ export class ServiceRoute {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toHealthCheck(status: T.MainStatus): T.NamedHealthCheckResult[] {
|
function toHealthCheck(statusInfo: T.StatusInfo): T.NamedHealthCheckResult[] {
|
||||||
return status.main !== 'running' || isEmptyObject(status.health)
|
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" />
|
<tui-icon icon="@tui.check" class="g-positive" />
|
||||||
{{ 'complete' | i18n }}
|
{{ 'complete' | i18n }}
|
||||||
} @else {
|
} @else {
|
||||||
@if ((pkg.key | tuiMapper: toStatus | async) === 'backingUp') {
|
@if ((pkg.key | tuiMapper: toStatus | async) === 'backing-up') {
|
||||||
<tui-loader size="s" />
|
<tui-loader size="s" />
|
||||||
{{ 'backing up' | i18n }}
|
{{ 'backing up' | i18n }}
|
||||||
} @else {
|
} @else {
|
||||||
@@ -65,5 +65,5 @@ export class BackupProgressComponent {
|
|||||||
)
|
)
|
||||||
|
|
||||||
readonly toStatus = (pkgId: string) =>
|
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',
|
'restoring',
|
||||||
'stopping',
|
'stopping',
|
||||||
'starting',
|
'starting',
|
||||||
'backingUp',
|
'backing-up',
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,9 +45,7 @@ export class ActionService {
|
|||||||
const { pkgInfo, actionInfo } = data
|
const { pkgInfo, actionInfo } = data
|
||||||
|
|
||||||
if (
|
if (
|
||||||
allowedStatuses[actionInfo.metadata.allowedStatuses].has(
|
allowedStatuses[actionInfo.metadata.allowedStatuses].has(pkgInfo.status)
|
||||||
pkgInfo.mainStatus,
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
if (actionInfo.metadata.hasInput) {
|
if (actionInfo.metadata.hasInput) {
|
||||||
this.formDialog.open<PackageActionData>(ActionInputModal, {
|
this.formDialog.open<PackageActionData>(ActionInputModal, {
|
||||||
|
|||||||
@@ -1921,8 +1921,9 @@ export namespace Mock {
|
|||||||
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
|
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
|
||||||
icon: '/assets/img/service-icons/bitcoin-core.svg',
|
icon: '/assets/img/service-icons/bitcoin-core.svg',
|
||||||
lastBackup: null,
|
lastBackup: null,
|
||||||
status: {
|
statusInfo: {
|
||||||
main: 'running',
|
error: null,
|
||||||
|
desired: { main: 'running' },
|
||||||
started: new Date().toISOString(),
|
started: new Date().toISOString(),
|
||||||
health: {},
|
health: {},
|
||||||
},
|
},
|
||||||
@@ -2201,8 +2202,11 @@ export namespace Mock {
|
|||||||
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
|
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
|
||||||
icon: '/assets/img/service-icons/btc-rpc-proxy.png',
|
icon: '/assets/img/service-icons/btc-rpc-proxy.png',
|
||||||
lastBackup: null,
|
lastBackup: null,
|
||||||
status: {
|
statusInfo: {
|
||||||
main: 'stopped',
|
desired: { main: 'stopped' },
|
||||||
|
started: null,
|
||||||
|
health: {},
|
||||||
|
error: null,
|
||||||
},
|
},
|
||||||
actions: {},
|
actions: {},
|
||||||
serviceInterfaces: {
|
serviceInterfaces: {
|
||||||
@@ -2246,8 +2250,11 @@ export namespace Mock {
|
|||||||
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
|
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
|
||||||
icon: '/assets/img/service-icons/lnd.png',
|
icon: '/assets/img/service-icons/lnd.png',
|
||||||
lastBackup: null,
|
lastBackup: null,
|
||||||
status: {
|
statusInfo: {
|
||||||
main: 'stopped',
|
desired: { main: 'stopped' },
|
||||||
|
error: null,
|
||||||
|
health: {},
|
||||||
|
started: null,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@@ -762,12 +762,12 @@ export class MockApiService extends ApiService {
|
|||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
for (let i = 0; i < ids.length; i++) {
|
for (let i = 0; i < ids.length; i++) {
|
||||||
const id = ids[i]
|
const id = ids[i]
|
||||||
const appPath = `/packageData/${id}/status/main/`
|
const appPath = `/packageData/${id}/statusInfo/desired/main`
|
||||||
const appPatch: ReplaceOperation<T.MainStatus['main']>[] = [
|
const appPatch: ReplaceOperation<T.DesiredStatus['main']>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: appPath,
|
path: appPath,
|
||||||
value: 'backingUp',
|
value: 'backing-up',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.mockRevision(appPatch)
|
this.mockRevision(appPatch)
|
||||||
@@ -1073,17 +1073,18 @@ export class MockApiService extends ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
|
async startPackage(params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
|
||||||
const path = `/packageData/${params.id}/status`
|
const path = `/packageData/${params.id}/statusInfo`
|
||||||
|
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const patch2: ReplaceOperation<T.MainStatus & { main: 'running' }>[] = [
|
const patch2: ReplaceOperation<T.StatusInfo>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path,
|
path,
|
||||||
value: {
|
value: {
|
||||||
main: 'running',
|
error: null,
|
||||||
|
desired: { main: 'running' },
|
||||||
started: new Date().toISOString(),
|
started: new Date().toISOString(),
|
||||||
health: {
|
health: {
|
||||||
'ephemeral-health-check': {
|
'ephemeral-health-check': {
|
||||||
@@ -1118,14 +1119,14 @@ export class MockApiService extends ApiService {
|
|||||||
this.mockRevision(patch2)
|
this.mockRevision(patch2)
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
|
||||||
const originalPatch: ReplaceOperation<
|
const originalPatch: ReplaceOperation<T.StatusInfo>[] = [
|
||||||
T.MainStatus & { main: 'starting' }
|
|
||||||
>[] = [
|
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path,
|
path,
|
||||||
value: {
|
value: {
|
||||||
main: 'starting',
|
desired: { main: 'running' },
|
||||||
|
started: null,
|
||||||
|
error: null,
|
||||||
health: {},
|
health: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1140,15 +1141,16 @@ export class MockApiService extends ApiService {
|
|||||||
params: RR.RestartPackageReq,
|
params: RR.RestartPackageReq,
|
||||||
): Promise<RR.RestartPackageRes> {
|
): Promise<RR.RestartPackageRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
const path = `/packageData/${params.id}/status`
|
const path = `/packageData/${params.id}/statusInfo`
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const patch2: ReplaceOperation<T.MainStatus & { main: 'running' }>[] = [
|
const patch2: ReplaceOperation<T.StatusInfo>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path,
|
path,
|
||||||
value: {
|
value: {
|
||||||
main: 'running',
|
desired: { main: 'running' },
|
||||||
|
error: null,
|
||||||
started: new Date().toISOString(),
|
started: new Date().toISOString(),
|
||||||
health: {
|
health: {
|
||||||
'ephemeral-health-check': {
|
'ephemeral-health-check': {
|
||||||
@@ -1183,12 +1185,15 @@ export class MockApiService extends ApiService {
|
|||||||
this.mockRevision(patch2)
|
this.mockRevision(patch2)
|
||||||
}, this.revertTime)
|
}, this.revertTime)
|
||||||
|
|
||||||
const patch: ReplaceOperation<T.MainStatus & { main: 'restarting' }>[] = [
|
const patch: ReplaceOperation<T.StatusInfo>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path,
|
path,
|
||||||
value: {
|
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> {
|
async stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
const path = `/packageData/${params.id}/status`
|
const path = `/packageData/${params.id}/statusInfo`
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const patch2: ReplaceOperation<T.MainStatus & { main: 'stopped' }>[] = [
|
const patch2: ReplaceOperation<T.StatusInfo>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: path,
|
path: path,
|
||||||
value: { main: 'stopped' },
|
value: {
|
||||||
|
desired: { main: 'stopped' },
|
||||||
|
error: null,
|
||||||
|
health: {},
|
||||||
|
started: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.mockRevision(patch2)
|
this.mockRevision(patch2)
|
||||||
}, this.revertTime)
|
}, this.revertTime)
|
||||||
|
|
||||||
const patch: ReplaceOperation<T.MainStatus & { main: 'stopping' }>[] = [
|
const patch: ReplaceOperation<T.StatusInfo>[] = [
|
||||||
{
|
{
|
||||||
op: PatchOp.REPLACE,
|
op: PatchOp.REPLACE,
|
||||||
path: path,
|
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',
|
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
|
||||||
icon: '/assets/img/service-icons/bitcoin-core.svg',
|
icon: '/assets/img/service-icons/bitcoin-core.svg',
|
||||||
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
|
lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(),
|
||||||
status: {
|
statusInfo: {
|
||||||
main: 'stopped',
|
desired: { main: 'stopped' },
|
||||||
|
error: null,
|
||||||
|
health: {},
|
||||||
|
started: null,
|
||||||
},
|
},
|
||||||
// status: {
|
// status: {
|
||||||
// main: 'error',
|
// main: 'error',
|
||||||
@@ -518,8 +521,11 @@ export const mockPatchData: DataModel = {
|
|||||||
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
|
s9pk: '/media/startos/data/package-data/archive/installed/asdfasdf.s9pk',
|
||||||
icon: '/assets/img/service-icons/lnd.png',
|
icon: '/assets/img/service-icons/lnd.png',
|
||||||
lastBackup: null,
|
lastBackup: null,
|
||||||
status: {
|
statusInfo: {
|
||||||
main: 'stopped',
|
desired: { main: 'stopped' },
|
||||||
|
error: null,
|
||||||
|
health: {},
|
||||||
|
started: null,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import deepEqual from 'fast-deep-equal'
|
|||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { isInstalled } from 'src/app/utils/get-package-data'
|
import { isInstalled } from 'src/app/utils/get-package-data'
|
||||||
import { T } from '@start9labs/start-sdk'
|
import { T } from '@start9labs/start-sdk'
|
||||||
|
import { getInstalledBaseStatus } from './pkg-status-rendering.service'
|
||||||
|
|
||||||
export type AllDependencyErrors = Record<string, PkgDependencyErrors>
|
export type AllDependencyErrors = Record<string, PkgDependencyErrors>
|
||||||
export type PkgDependencyErrors = Record<string, DependencyError | null>
|
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
|
// not running
|
||||||
if (depStatus !== 'running' && depStatus !== 'starting') {
|
if (depStatus !== 'running' && depStatus !== 'starting') {
|
||||||
@@ -165,7 +166,7 @@ export class DepErrorService {
|
|||||||
// health check failure
|
// health check failure
|
||||||
if (depStatus === 'running' && currentDep?.kind === 'running') {
|
if (depStatus === 'running' && currentDep?.kind === 'running') {
|
||||||
for (let id of currentDep.healthChecks) {
|
for (let id of currentDep.healthChecks) {
|
||||||
const check = dep.status.health[id]
|
const check = dep.statusInfo.health[id]
|
||||||
if (check && check?.result !== 'success') {
|
if (check && check?.result !== 'success') {
|
||||||
return {
|
return {
|
||||||
type: 'healthChecksFailed',
|
type: 'healthChecksFailed',
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
|
|||||||
|
|
||||||
if (pkg.stateInfo.state === 'installed') {
|
if (pkg.stateInfo.state === 'installed') {
|
||||||
primary = getInstalledPrimaryStatus(pkg)
|
primary = getInstalledPrimaryStatus(pkg)
|
||||||
health = getHealthStatus(pkg.status)
|
health = getHealthStatus(pkg.statusInfo)
|
||||||
} else {
|
} else {
|
||||||
primary = pkg.stateInfo.state
|
primary = pkg.stateInfo.state
|
||||||
}
|
}
|
||||||
@@ -21,33 +21,43 @@ export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
|
|||||||
return { primary, health }
|
return { primary, health }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInstalledPrimaryStatus({
|
export function getInstalledBaseStatus(statusInfo: T.StatusInfo): BaseStatus {
|
||||||
tasks,
|
|
||||||
status,
|
|
||||||
}: T.PackageDataEntry): PrimaryStatus {
|
|
||||||
if (
|
if (
|
||||||
Object.values(tasks).some(t => t.active && t.task.severity === 'critical')
|
statusInfo.desired.main === 'running' &&
|
||||||
) {
|
(!statusInfo.started ||
|
||||||
return 'taskRequired'
|
Object.values(statusInfo.health)
|
||||||
}
|
.filter(h => !!h)
|
||||||
|
.some(h => h.result === 'starting'))
|
||||||
if (
|
|
||||||
Object.values(status.main === 'running' && status.health)
|
|
||||||
.filter(h => !!h)
|
|
||||||
.some(h => h.result === 'starting')
|
|
||||||
) {
|
) {
|
||||||
return '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 {
|
export function getInstalledPrimaryStatus({
|
||||||
if (status.main !== 'running' || !status.main) {
|
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
|
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')) {
|
if (values.some(h => h.result === 'failure')) {
|
||||||
return 'failure'
|
return 'failure'
|
||||||
@@ -70,7 +80,7 @@ export interface StatusRendering {
|
|||||||
showDots?: boolean
|
showDots?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PrimaryStatus =
|
export type BaseStatus =
|
||||||
| 'installing'
|
| 'installing'
|
||||||
| 'updating'
|
| 'updating'
|
||||||
| 'removing'
|
| 'removing'
|
||||||
@@ -80,10 +90,11 @@ export type PrimaryStatus =
|
|||||||
| 'stopping'
|
| 'stopping'
|
||||||
| 'restarting'
|
| 'restarting'
|
||||||
| 'stopped'
|
| 'stopped'
|
||||||
| 'backingUp'
|
| 'backing-up'
|
||||||
| 'taskRequired'
|
|
||||||
| 'error'
|
| 'error'
|
||||||
|
|
||||||
|
export type PrimaryStatus = BaseStatus | 'task-required'
|
||||||
|
|
||||||
export type DependencyStatus = 'warning' | 'satisfied'
|
export type DependencyStatus = 'warning' | 'satisfied'
|
||||||
|
|
||||||
export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
|
export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
|
||||||
@@ -122,7 +133,7 @@ export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
|
|||||||
color: 'dark-shade',
|
color: 'dark-shade',
|
||||||
showDots: false,
|
showDots: false,
|
||||||
},
|
},
|
||||||
backingUp: {
|
'backing-up': {
|
||||||
display: 'Backing Up',
|
display: 'Backing Up',
|
||||||
color: 'primary',
|
color: 'primary',
|
||||||
showDots: true,
|
showDots: true,
|
||||||
@@ -137,7 +148,7 @@ export const PrimaryRendering: Record<PrimaryStatus, StatusRendering> = {
|
|||||||
color: 'success',
|
color: 'success',
|
||||||
showDots: false,
|
showDots: false,
|
||||||
},
|
},
|
||||||
taskRequired: {
|
'task-required': {
|
||||||
display: 'Task Required',
|
display: 'Task Required',
|
||||||
color: 'warning',
|
color: 'warning',
|
||||||
showDots: false,
|
showDots: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user