diff --git a/Makefile b/Makefile index bdddf2edb..cdf40c7e2 100644 --- a/Makefile +++ b/Makefile @@ -179,7 +179,7 @@ wormhole-squashfs: results/$(BASENAME).squashfs $(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}')) @echo "Paste the following command into the shell of your StartOS server:" @echo - @wormhole send results/$(BASENAME).squashfs 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo sh -c '"'"'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE) && cd /media/startos/images && wormhole receive --accept-file %s && mv $(BASENAME).squashfs $(SQFS_SUM).rootfs && ln -rsf ./$(SQFS_SUM).rootfs ../config/current.rootfs && sync && reboot'"'"'\n", $$3 }' + @wormhole send results/$(BASENAME).squashfs 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo sh -c '"'"'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE) && /usr/lib/startos/scripts/prune-boot && cd /media/startos/images && wormhole receive --accept-file %s && CHECKSUM=$(SQFS_SUM) /usr/lib/startos/scripts/use-img ./$(BASENAME).squashfs'"'"'\n", $$3 }' update: $(ALL_TARGETS) @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi @@ -205,9 +205,9 @@ update-squashfs: results/$(BASENAME).squashfs $(eval SQFS_SUM := $(shell b3sum results/$(BASENAME).squashfs)) $(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}')) $(call ssh,'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE)') - $(call cp,results/$(BASENAME).squashfs,/media/startos/images/$(SQFS_SUM).rootfs) - $(call ssh,'sudo ln -rsf /media/startos/images/$(SQFS_SUM).rootfs /media/startos/config/current.rootfs') - $(call ssh,'sudo reboot') + $(call ssh,'/usr/lib/startos/scripts/prune-boot') + $(call cp,results/$(BASENAME).squashfs,/media/startos/images/next.rootfs) + $(call ssh,'sudo CHECKSUM=$(SQFS_SUM) /usr/lib/startos/scripts/use-img /media/startos/images/next.rootfs') emulate-reflash: $(ALL_TARGETS) @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi diff --git a/build/lib/scripts/check-monitor b/build/lib/scripts/check-monitor deleted file mode 100755 index 3ff787180..000000000 --- a/build/lib/scripts/check-monitor +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - - -if cat /sys/class/drm/*/status | grep -qw connected; then - exit 0 -else - exit 1 -fi diff --git a/build/lib/scripts/enable-kiosk b/build/lib/scripts/enable-kiosk index 548542d23..bf2e52018 100755 --- a/build/lib/scripts/enable-kiosk +++ b/build/lib/scripts/enable-kiosk @@ -91,15 +91,6 @@ cat > /home/kiosk/kiosk.sh << 'EOF' while ! curl "http://localhost" > /dev/null; do sleep 1 done -while ! /usr/lib/startos/scripts/check-monitor; do - sleep 15 -done -( - while /usr/lib/startos/scripts/check-monitor; do - sleep 15 - done - killall firefox-esr -) & matchbox-window-manager -use_titlebar no & cp -r /home/kiosk/fx-profile /home/kiosk/fx-profile-tmp firefox-esr http://localhost --profile /home/kiosk/fx-profile-tmp diff --git a/build/lib/scripts/prune-boot b/build/lib/scripts/prune-boot new file mode 100755 index 000000000..24d002ec7 --- /dev/null +++ b/build/lib/scripts/prune-boot @@ -0,0 +1,35 @@ +#!/bin/bash + +set -e + +if [ "$UID" -ne 0 ]; then + >&2 echo 'Must be run as root' + exit 1 +fi + +# Get the current kernel version +current_kernel=$(uname -r) + +echo "Current kernel: $current_kernel" +echo "Searching for old kernel files in /boot..." + +# Extract base kernel version (without possible suffixes) +current_base=$(echo "$current_kernel" | sed 's/-.*//') + +cd /boot || { echo "/boot directory not found!"; exit 1; } + +for file in vmlinuz-* initrd.img-* System.map-* config-*; do + # Extract version from filename + version=$(echo "$file" | sed -E 's/^[^0-9]*([0-9][^ ]*).*/\1/') + # Skip if file matches current kernel version + if [[ "$file" == *"$current_kernel"* ]]; then + continue + fi + # Compare versions, delete if less than current + if dpkg --compare-versions "$version" lt "$current_kernel"; then + echo "Deleting $file (version $version is older than $current_kernel)" + sudo rm -f "$file" + fi +done + +echo "Old kernel files deleted." diff --git a/build/lib/scripts/use-img b/build/lib/scripts/use-img new file mode 100755 index 000000000..bbe530302 --- /dev/null +++ b/build/lib/scripts/use-img @@ -0,0 +1,61 @@ +#!/bin/bash + +set -e + +if [ "$UID" -ne 0 ]; then + >&2 echo 'Must be run as root' + exit 1 +fi + +if [ -z "$1" ]; then + >&2 echo "usage: $0 " + exit 1 +fi + +VERSION=$(unsquashfs -cat $1 /usr/lib/startos/VERSION.txt) +GIT_HASH=$(unsquashfs -cat $1 /usr/lib/startos/GIT_HASH.txt) +B3SUM=$(b3sum $1 | head -c 32) + +if [ -n "$CHECKSUM" ] && [ "$CHECKSUM" != "$B3SUM" ]; then + >&2 echo "CHECKSUM MISMATCH" + exit 2 +fi + +mv $1 /media/startos/images/${B3SUM}.rootfs +ln -rsf /media/startos/images/${B3SUM}.rootfs /media/startos/config/current.rootfs + +unsquashfs -n -f -d / /media/startos/images/${B3SUM}.rootfs boot + +umount -R /media/startos/next 2> /dev/null || true +umount -R /media/startos/lower 2> /dev/null || true +umount -R /media/startos/upper 2> /dev/null || true + +rm -rf /media/startos/lower /media/startos/upper /media/startos/next +mkdir /media/startos/upper +mount -t tmpfs tmpfs /media/startos/upper +mkdir -p /media/startos/lower /media/startos/upper/data /media/startos/upper/work /media/startos/next +mount /media/startos/images/${B3SUM}.rootfs /media/startos/lower +mount -t overlay \ + -olowerdir=/media/startos/lower,upperdir=/media/startos/upper/data,workdir=/media/startos/upper/work \ + overlay /media/startos/next +mkdir -p /media/startos/next/media/startos/root +mount --bind /media/startos/root /media/startos/next/media/startos/root +mkdir -p /media/startos/next/dev +mkdir -p /media/startos/next/sys +mkdir -p /media/startos/next/proc +mkdir -p /media/startos/next/boot +mount --bind /dev /media/startos/next/dev +mount --bind /sys /media/startos/next/sys +mount --bind /proc /media/startos/next/proc +mount --bind /boot /media/startos/next/boot + +chroot /media/startos/next update-grub2 + +umount -R /media/startos/next +umount -R /media/startos/upper +umount -R /media/startos/lower +rm -rf /media/startos/lower /media/startos/upper /media/startos/next + +sync + +reboot \ No newline at end of file diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json index 2d79a9c6d..6cc8af474 100644 --- a/container-runtime/package-lock.json +++ b/container-runtime/package-lock.json @@ -38,7 +38,7 @@ }, "../sdk/dist": { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.27", + "version": "0.4.0-beta.30", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts index cb00697d4..e1270e54a 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -20,7 +20,9 @@ export class MainLoop { private subcontainerRc?: SubContainerRc get mainSubContainerHandle() { this.subcontainerRc = - this.subcontainerRc ?? this.mainEvent?.daemon?.subcontainerRc() + this.subcontainerRc ?? + this.mainEvent?.daemon?.subcontainerRc() ?? + undefined return this.subcontainerRc } private healthLoops?: { diff --git a/core/Cargo.lock b/core/Cargo.lock index 9c9ad3071..812a0196c 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -5975,7 +5975,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "start-os" -version = "0.4.0-alpha.6" +version = "0.4.0-alpha.7" dependencies = [ "aes 0.7.5", "async-acme", diff --git a/core/models/src/js_engine_types.rs b/core/models/src/js_engine_types.rs deleted file mode 100644 index 3722d8c76..000000000 --- a/core/models/src/js_engine_types.rs +++ /dev/null @@ -1,164 +0,0 @@ -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; -use std::time::Duration; - -use color_eyre::eyre::bail; -use container_init::{Input, Output, ProcessId, RpcId}; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; -use tokio::sync::Mutex; - -/// Used by the js-executor, it is the ability to just create a command in an already running exec -pub type ExecCommand = Arc< - dyn Fn( - String, - Vec, - UnboundedSender, - Option, - ) -> Pin> + 'static>> - + Send - + Sync - + 'static, ->; - -/// Used by the js-executor, it is the ability to just create a command in an already running exec -pub type SendKillSignal = Arc< - dyn Fn(RpcId, u32) -> Pin> + 'static>> - + Send - + Sync - + 'static, ->; - -pub trait CommandInserter { - fn insert_command( - &self, - command: String, - args: Vec, - sender: UnboundedSender, - timeout: Option, - ) -> Pin>>>; - - fn send_signal(&self, id: RpcId, command: u32) -> Pin>>; -} - -pub type ArcCommandInserter = Arc>>>; - -pub struct ExecutingCommand { - rpc_id: RpcId, - /// Will exist until killed - command_inserter: Arc>>, - owned_futures: Arc>>>>>, -} - -impl ExecutingCommand { - pub async fn new( - command_inserter: ArcCommandInserter, - command: String, - args: Vec, - timeout: Option, - ) -> Result { - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel::(); - let rpc_id = { - let locked_command_inserter = command_inserter.lock().await; - let locked_command_inserter = match &*locked_command_inserter { - Some(a) => a, - None => bail!("Expecting containers.main in the package manifest".to_string()), - }; - match locked_command_inserter - .insert_command(command, args, sender, timeout) - .await - { - Some(a) => a, - None => bail!("Couldn't get command started ".to_string()), - } - }; - let executing_commands = ExecutingCommand { - rpc_id, - command_inserter: Arc::new(Mutex::new(Some(command_inserter.clone()))), - owned_futures: Default::default(), - }; - // let waiting = self.wait() - Ok(executing_commands) - } - - async fn wait( - rpc_id: RpcId, - mut outputs: UnboundedReceiver, - ) -> Result, String)> { - let (process_id_send, process_id_recv) = tokio::sync::oneshot::channel::(); - let mut answer = String::new(); - let mut command_error = String::new(); - let mut status: Option = None; - let mut process_id_send = Some(process_id_send); - while let Some(output) = outputs.recv().await { - match output { - Output::ProcessId(process_id) => { - if let Some(process_id_send) = process_id_send.take() { - if let Err(err) = process_id_send.send(process_id) { - tracing::error!( - "Could not get a process id {process_id:?} sent for {rpc_id:?}" - ); - tracing::debug!("{err:?}"); - } - } - } - Output::Line(value) => { - answer.push_str(&value); - answer.push('\n'); - } - Output::Error(error) => { - command_error.push_str(&error); - command_error.push('\n'); - } - Output::Done(error_code) => { - status = error_code; - break; - } - } - } - if !command_error.is_empty() { - return Err((status, command_error)); - } - - Ok(answer) - } - - async fn send_signal(&self, signal: u32) { - let locked = self.command_inserter.lock().await; - let inner = match &*locked { - Some(a) => a, - None => return, - }; - let locked = inner.lock().await; - let command_inserter = match &*locked { - Some(a) => a, - None => return, - }; - command_inserter.send_signal(self.rpc_id, signal); - } - /// Should only be called when output::done - async fn killed(&self) { - *self.owned_futures.lock().await = Default::default(); - *self.command_inserter.lock().await = Default::default(); - } - pub fn rpc_id(&self) -> RpcId { - self.rpc_id - } -} - -impl Drop for ExecutingCommand { - fn drop(&mut self) { - let command_inserter = self.command_inserter.clone(); - let rpc_id = self.rpc_id.clone(); - tokio::spawn(async move { - let command_inserter_lock = command_inserter.lock().await; - let command_inserter = match &*command_inserter_lock { - Some(a) => a, - None => { - return; - } - }; - command_inserter.send_kill_command(rpc_id, 9).await; - }); - } -} diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 26aed2d8b..ba340c3b4 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -14,7 +14,7 @@ keywords = [ name = "start-os" readme = "README.md" repository = "https://github.com/Start9Labs/start-os" -version = "0.4.0-alpha.6" # VERSION_BUMP +version = "0.4.0-alpha.7" # VERSION_BUMP license = "MIT" [lib] diff --git a/core/startos/src/action.rs b/core/startos/src/action.rs index 18765a776..4079ca7ad 100644 --- a/core/startos/src/action.rs +++ b/core/startos/src/action.rs @@ -252,14 +252,18 @@ impl fmt::Display for ActionResultV1 { } } -pub fn display_action_result(params: WithIoFormat, result: Option) { +pub fn display_action_result( + params: WithIoFormat, + result: Option, +) -> Result<(), Error> { let Some(result) = result else { - return; + return Ok(()); }; if let Some(format) = params.format { return display_serializable(format, result); } - println!("{result}") + println!("{result}"); + Ok(()) } #[derive(Deserialize, Serialize, TS)] diff --git a/core/startos/src/auth.rs b/core/startos/src/auth.rs index ac8bf721f..1249bb984 100644 --- a/core/startos/src/auth.rs +++ b/core/startos/src/auth.rs @@ -328,9 +328,7 @@ pub fn session() -> ParentHandler { from_fn_async(list) .with_metadata("get_session", Value::Bool(true)) .with_display_serializable() - .with_custom_display_fn(|handle, result| { - Ok(display_sessions(handle.params, result)) - }) + .with_custom_display_fn(|handle, result| display_sessions(handle.params, result)) .with_about("Display all server sessions") .with_call_remote::(), ) @@ -343,7 +341,7 @@ pub fn session() -> ParentHandler { ) } -fn display_sessions(params: WithIoFormat, arg: SessionList) { +fn display_sessions(params: WithIoFormat, arg: SessionList) -> Result<(), Error> { use prettytable::*; if let Some(format) = params.format { @@ -371,7 +369,8 @@ fn display_sessions(params: WithIoFormat, arg: SessionList) { } table.add_row(row); } - table.print_tty(false).unwrap(); + table.print_tty(false)?; + Ok(()) } #[derive(Deserialize, Serialize, Parser, TS)] diff --git a/core/startos/src/backup/restore.rs b/core/startos/src/backup/restore.rs index 264f00848..1885e198e 100644 --- a/core/startos/src/backup/restore.rs +++ b/core/startos/src/backup/restore.rs @@ -20,6 +20,7 @@ use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::init::init; use crate::prelude::*; +use crate::progress::ProgressUnits; use crate::s9pk::S9pk; use crate::service::service_map::DownloadInstallFuture; use crate::setup::SetupExecuteProgress; @@ -136,6 +137,7 @@ pub async fn recover_full_embassy( .collect(); let tasks = restore_packages(&rpc_ctx, backup_guard, ids).await?; restore_phase.set_total(tasks.len() as u64); + restore_phase.set_units(Some(ProgressUnits::Steps)); let restore_phase = Arc::new(Mutex::new(restore_phase)); stream::iter(tasks) .for_each_concurrent(5, |(id, res)| { diff --git a/core/startos/src/backup/target/mod.rs b/core/startos/src/backup/target/mod.rs index 536cc336f..71df2925f 100644 --- a/core/startos/src/backup/target/mod.rs +++ b/core/startos/src/backup/target/mod.rs @@ -157,7 +157,7 @@ pub fn target() -> ParentHandler { from_fn_async(info) .with_display_serializable() .with_custom_display_fn::(|params, info| { - Ok(display_backup_info(params.params, info)) + display_backup_info(params.params, info) }) .with_about("Display package backup information") .with_call_remote::(), @@ -227,7 +227,7 @@ pub struct PackageBackupInfo { pub timestamp: DateTime, } -fn display_backup_info(params: WithIoFormat, info: BackupInfo) { +fn display_backup_info(params: WithIoFormat, info: BackupInfo) -> Result<(), Error> { use prettytable::*; if let Some(format) = params.format { @@ -260,7 +260,8 @@ fn display_backup_info(params: WithIoFormat, info: BackupInfo) { ]; table.add_row(row); } - table.print_tty(false).unwrap(); + table.print_tty(false)?; + Ok(()) } #[derive(Deserialize, Serialize, Parser, TS)] diff --git a/core/startos/src/disk/mod.rs b/core/startos/src/disk/mod.rs index d1fbe282f..d064c5cd2 100644 --- a/core/startos/src/disk/mod.rs +++ b/core/startos/src/disk/mod.rs @@ -48,9 +48,7 @@ pub fn disk() -> ParentHandler { "list", from_fn_async(list) .with_display_serializable() - .with_custom_display_fn(|handle, result| { - Ok(display_disk_info(handle.params, result)) - }) + .with_custom_display_fn(|handle, result| display_disk_info(handle.params, result)) .with_about("List disk info") .with_call_remote::(), ) @@ -65,7 +63,7 @@ pub fn disk() -> ParentHandler { ) } -fn display_disk_info(params: WithIoFormat, args: Vec) { +fn display_disk_info(params: WithIoFormat, args: Vec) -> Result<(), Error> { use prettytable::*; if let Some(format) = params.format { @@ -124,7 +122,8 @@ fn display_disk_info(params: WithIoFormat, args: Vec) { table.add_row(row); } } - table.print_tty(false).unwrap(); + table.print_tty(false)?; + Ok(()) } // #[command(display(display_disk_info))] diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index d03a0c72a..5261d2646 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -32,7 +32,7 @@ use crate::net::utils::find_wifi_iface; use crate::net::web_server::{UpgradableListener, WebServerAcceptorSetter}; use crate::prelude::*; use crate::progress::{ - FullProgress, FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar, + FullProgress, FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar, ProgressUnits, }; use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::s9pk::v2::pack::{CONTAINER_DATADIR, CONTAINER_TOOL}; @@ -259,6 +259,7 @@ pub async fn run_script>(path: P, mut progress: PhaseProgressTrac if let Err(e) = async { let script = tokio::fs::read_to_string(script).await?; progress.set_total(script.as_bytes().iter().filter(|b| **b == b'\n').count() as u64); + progress.set_units(Some(ProgressUnits::Bytes)); let mut reader = IOHook::new(Cursor::new(script.as_bytes())); reader.post_read(|buf| progress += buf.iter().filter(|b| **b == b'\n').count() as u64); Command::new("/bin/bash") diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index b9ba7863f..86bba8264 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -89,7 +89,7 @@ use crate::context::{ use crate::disk::fsck::RequiresReboot; use crate::registry::context::{RegistryContext, RegistryUrlParams}; use crate::system::kiosk; -use crate::util::serde::{HandlerExtSerde, WithIoFormat}; +use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; #[derive(Deserialize, Serialize, Parser, TS)] #[serde(rename_all = "camelCase")] @@ -201,15 +201,6 @@ pub fn main_api() -> ParentHandler { if &*PLATFORM != "raspberrypi" { api = api.subcommand("kiosk", kiosk::()); } - #[cfg(feature = "dev")] - { - api = api.subcommand( - "lxc", - lxc::dev::lxc::().with_about( - "Commands related to lxc containers i.e. create, list, remove, connect", - ), - ); - } api } @@ -220,7 +211,7 @@ pub fn server() -> ParentHandler { from_fn_async(system::time) .with_display_serializable() .with_custom_display_fn(|handle, result| { - Ok(system::display_time(handle.params, result)) + system::display_time(handle.params, result) }) .with_about("Display current time and server uptime") .with_call_remote::() @@ -416,6 +407,46 @@ pub fn package() -> ParentHandler { .with_about("Rebuild service container") .with_call_remote::(), ) + .subcommand( + "stats", + from_fn_async(lxc::stats) + .with_display_serializable() + .with_custom_display_fn(|args, res| { + if let Some(format) = args.params.format { + return display_serializable(format, res); + } + + use prettytable::*; + let mut table = table!([ + "Name", + "Container ID", + "Memory Usage", + "Memory Limit", + "Memory %" + ]); + for (id, stats) in res { + if let Some(stats) = stats { + table.add_row(row![ + &*id, + &*stats.container_id, + stats.memory_usage, + stats.memory_limit, + format!( + "{:.2}", + stats.memory_usage.0 as f64 / stats.memory_limit.0 as f64 + * 100.0 + ) + ]); + } else { + table.add_row(row![&*id, "N/A", "0 MiB", "0 MiB", "0"]); + } + } + table.print_tty(false)?; + Ok(()) + }) + .with_about("List information related to the lxc containers i.e. CPU, Memory, Disk") + .with_call_remote::(), + ) .subcommand("logs", logs::package_logs()) .subcommand( "logs", diff --git a/core/startos/src/lxc/dev.rs b/core/startos/src/lxc/dev.rs deleted file mode 100644 index a918672da..000000000 --- a/core/startos/src/lxc/dev.rs +++ /dev/null @@ -1,174 +0,0 @@ -use std::ops::Deref; - -use clap::Parser; -use rpc_toolkit::{ - from_fn_async, CallRemoteHandler, Context, Empty, HandlerArgs, HandlerExt, HandlerFor, - ParentHandler, -}; -use serde::{Deserialize, Serialize}; -use ts_rs::TS; - -use crate::context::{CliContext, RpcContext}; -use crate::lxc::{ContainerId, LxcConfig}; -use crate::prelude::*; -use crate::rpc_continuations::Guid; -use crate::service::ServiceStats; - -pub fn lxc() -> ParentHandler { - ParentHandler::new() - .subcommand( - "create", - from_fn_async(create) - .with_about("Create lxc container") - .with_call_remote::(), - ) - .subcommand( - "list", - from_fn_async(list) - .with_custom_display_fn(|_, res| { - use prettytable::*; - let mut table = table!([bc => "GUID"]); - for guid in res { - table.add_row(row![&*guid]); - } - table.printstd(); - Ok(()) - }) - .with_about("List lxc containers") - .with_call_remote::(), - ) - .subcommand( - "stats", - from_fn_async(stats) - .with_custom_display_fn(|_, res| { - use prettytable::*; - let mut table = table!([ - "Container ID", - "Name", - "Memory Usage", - "Memory Limit", - "Memory %" - ]); - for ServiceStats { - container_id, - package_id, - memory_usage, - memory_limit, - } in res - { - table.add_row(row![ - &*container_id, - &*package_id, - memory_usage, - memory_limit, - format!( - "{:.2}", - memory_usage.0 as f64 / memory_limit.0 as f64 * 100.0 - ) - ]); - } - table.printstd(); - Ok(()) - }) - .with_about("List information related to the lxc containers i.e. CPU, Memory, Disk") - .with_call_remote::(), - ) - .subcommand( - "remove", - from_fn_async(remove) - .no_display() - .with_about("Remove lxc container") - .with_call_remote::(), - ) - .subcommand("connect", from_fn_async(connect_rpc).no_cli()) - .subcommand( - "connect", - from_fn_async(connect_rpc_cli) - .no_display() - .with_about("Connect to a lxc container"), - ) -} - -pub async fn create(ctx: RpcContext) -> Result { - let container = ctx.lxc_manager.create(None, LxcConfig::default()).await?; - let guid = container.guid.deref().clone(); - ctx.dev.lxc.lock().await.insert(guid.clone(), container); - Ok(guid) -} - -pub async fn list(ctx: RpcContext) -> Result, Error> { - Ok(ctx.dev.lxc.lock().await.keys().cloned().collect()) -} - -pub async fn stats(ctx: RpcContext) -> Result, Error> { - let ids = ctx.db.peek().await.as_public().as_package_data().keys()?; - let guids: Vec<_> = ctx.dev.lxc.lock().await.keys().cloned().collect(); - - let mut stats = Vec::with_capacity(guids.len()); - for id in ids { - let service: tokio::sync::OwnedRwLockReadGuard> = - ctx.services.get(&id).await; - - let service_ref = service.as_ref().or_not_found(&id)?; - - stats.push(service_ref.stats().await?); - } - Ok(stats) -} - -#[derive(Deserialize, Serialize, Parser, TS)] -pub struct RemoveParams { - #[ts(type = "string")] - pub guid: ContainerId, -} - -pub async fn remove(ctx: RpcContext, RemoveParams { guid }: RemoveParams) -> Result<(), Error> { - if let Some(container) = ctx.dev.lxc.lock().await.remove(&guid) { - container.exit().await?; - } - Ok(()) -} - -#[derive(Deserialize, Serialize, Parser, TS)] -pub struct ConnectParams { - #[ts(type = "string")] - pub guid: ContainerId, -} - -pub async fn connect_rpc( - ctx: RpcContext, - ConnectParams { guid }: ConnectParams, -) -> Result { - super::connect( - &ctx, - ctx.dev.lxc.lock().await.get(&guid).ok_or_else(|| { - Error::new(eyre!("No container with guid: {guid}"), ErrorKind::NotFound) - })?, - ) - .await -} - -pub async fn connect_rpc_cli( - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgs, -) -> Result<(), Error> { - let ctx = context.clone(); - let guid = CallRemoteHandler::::new(from_fn_async(connect_rpc)) - .handle_async(HandlerArgs { - context, - parent_method, - method, - params: rpc_toolkit::util::Flat(params, Empty {}), - inherited_params, - raw_params, - }) - .await?; - - super::connect_cli(&ctx, guid).await -} diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs index 0648fc814..29a14028a 100644 --- a/core/startos/src/lxc/mod.rs +++ b/core/startos/src/lxc/mod.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use std::net::Ipv4Addr; use std::path::Path; use std::sync::{Arc, Weak}; @@ -7,7 +7,7 @@ use std::time::Duration; use clap::builder::ValueParserFactory; use futures::{AsyncWriteExt, StreamExt}; use imbl_value::{InOMap, InternedString}; -use models::{FromStrParser, InvalidId}; +use models::{FromStrParser, InvalidId, PackageId}; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::{GenericRpcMethod, RpcRequest, RpcResponse}; use rustyline_async::{ReadlineEvent, SharedWriter}; @@ -28,13 +28,11 @@ use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; use crate::disk::mount::util::unmount; use crate::prelude::*; use crate::rpc_continuations::{Guid, RpcContinuation}; +use crate::service::ServiceStats; use crate::util::io::open_file; use crate::util::rpc_client::UnixRpcClient; use crate::util::{new_guid, Invoke}; -// #[cfg(feature = "dev")] -pub mod dev; - const LXC_CONTAINER_DIR: &str = "/var/lib/lxc"; const RPC_DIR: &str = "media/startos/rpc"; // must not be absolute path pub const CONTAINER_RPC_SERVER_SOCKET: &str = "service.sock"; // must not be absolute path @@ -564,3 +562,21 @@ pub async fn connect_cli(ctx: &CliContext, guid: Guid) -> Result<(), Error> { Ok(()) } + +pub async fn stats(ctx: RpcContext) -> Result>, Error> { + let ids = ctx.db.peek().await.as_public().as_package_data().keys()?; + + let mut stats = BTreeMap::new(); + for id in ids { + let service: tokio::sync::OwnedRwLockReadGuard> = + ctx.services.get(&id).await; + + let Some(service_ref) = service.as_ref() else { + stats.insert(id, None); + continue; + }; + + stats.insert(id, Some(service_ref.stats().await?)); + } + Ok(stats) +} diff --git a/core/startos/src/net/host/binding.rs b/core/startos/src/net/host/binding.rs index 2d27a38b2..f82c1a1cf 100644 --- a/core/startos/src/net/host/binding.rs +++ b/core/startos/src/net/host/binding.rs @@ -159,7 +159,7 @@ pub fn binding( use prettytable::*; if let Some(format) = params.format { - return Ok(display_serializable(format, res)); + return display_serializable(format, res); } let mut table = Table::new(); @@ -182,7 +182,7 @@ pub fn binding( ]); } - table.print_tty(false).unwrap(); + table.print_tty(false)?; Ok(()) }) diff --git a/core/startos/src/net/network_interface.rs b/core/startos/src/net/network_interface.rs index c0a138d22..80c860c0a 100644 --- a/core/startos/src/net/network_interface.rs +++ b/core/startos/src/net/network_interface.rs @@ -47,7 +47,7 @@ pub fn network_interface_api() -> ParentHandler { use prettytable::*; if let Some(format) = params.format { - return Ok(display_serializable(format, res)); + return display_serializable(format, res); } let mut table = Table::new(); @@ -78,7 +78,7 @@ pub fn network_interface_api() -> ParentHandler { ]); } - table.print_tty(false).unwrap(); + table.print_tty(false)?; Ok(()) }) diff --git a/core/startos/src/net/tor.rs b/core/startos/src/net/tor.rs index 4becf62f0..6977d148b 100644 --- a/core/startos/src/net/tor.rs +++ b/core/startos/src/net/tor.rs @@ -90,9 +90,7 @@ pub fn tor() -> ParentHandler { "list-services", from_fn_async(list_services) .with_display_serializable() - .with_custom_display_fn(|handle, result| { - Ok(display_services(handle.params, result)) - }) + .with_custom_display_fn(|handle, result| display_services(handle.params, result)) .with_about("Display Tor V3 Onion Addresses") .with_call_remote::(), ) @@ -210,7 +208,10 @@ pub async fn reset( .await } -pub fn display_services(params: WithIoFormat, services: Vec) { +pub fn display_services( + params: WithIoFormat, + services: Vec, +) -> Result<(), Error> { use prettytable::*; if let Some(format) = params.format { @@ -222,7 +223,8 @@ pub fn display_services(params: WithIoFormat, services: Vec Result, Error> { diff --git a/core/startos/src/net/wifi.rs b/core/startos/src/net/wifi.rs index fa494194b..73fa69db7 100644 --- a/core/startos/src/net/wifi.rs +++ b/core/startos/src/net/wifi.rs @@ -70,9 +70,7 @@ pub fn wifi() -> ParentHandler { "get", from_fn_async(get) .with_display_serializable() - .with_custom_display_fn(|handle, result| { - Ok(display_wifi_info(handle.params, result)) - }) + .with_custom_display_fn(|handle, result| display_wifi_info(handle.params, result)) .with_about("List wifi info") .with_call_remote::(), ) @@ -134,7 +132,7 @@ pub fn available() -> ParentHandler { "get", from_fn_async(get_available) .with_display_serializable() - .with_custom_display_fn(|handle, result| Ok(display_wifi_list(handle.params, result))) + .with_custom_display_fn(|handle, result| display_wifi_list(handle.params, result)) .with_about("List available wifi networks") .with_call_remote::(), ) @@ -363,7 +361,7 @@ pub struct WifiListOut { security: Vec, } pub type WifiList = HashMap; -fn display_wifi_info(params: WithIoFormat, info: WifiListInfo) { +fn display_wifi_info(params: WithIoFormat, info: WifiListInfo) -> Result<(), Error> { use prettytable::*; if let Some(format) = params.format { @@ -424,10 +422,11 @@ fn display_wifi_info(params: WithIoFormat, info: WifiListInfo) { ]); } - table_global.print_tty(false).unwrap(); + table_global.print_tty(false)?; + Ok(()) } -fn display_wifi_list(params: WithIoFormat, info: Vec) { +fn display_wifi_list(params: WithIoFormat, info: Vec) -> Result<(), Error> { use prettytable::*; if let Some(format) = params.format { @@ -448,7 +447,8 @@ fn display_wifi_list(params: WithIoFormat, info: Vec) { ]); } - table_global.print_tty(false).unwrap(); + table_global.print_tty(false)?; + Ok(()) } // #[command(display(display_wifi_info))] diff --git a/core/startos/src/progress.rs b/core/startos/src/progress.rs index 38497d6cf..df3810d6c 100644 --- a/core/startos/src/progress.rs +++ b/core/startos/src/progress.rs @@ -24,6 +24,13 @@ lazy_static::lazy_static! { static ref BYTES: ProgressStyle = ProgressStyle::with_template("{spinner} {wide_msg} [{bytes}/?] [{binary_bytes_per_sec} {elapsed}]").unwrap(); } +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, TS)] +#[serde(rename_all = "kebab-case")] +pub enum ProgressUnits { + Bytes, + Steps, +} + #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, TS)] #[serde(untagged)] pub enum Progress { @@ -34,13 +41,14 @@ pub enum Progress { done: u64, #[ts(type = "number | null")] total: Option, + units: Option, }, } impl Progress { pub fn new() -> Self { Progress::NotStarted(()) } - pub fn update_bar(self, bar: &ProgressBar, bytes: bool) { + pub fn update_bar(self, bar: &ProgressBar) { match self { Self::NotStarted(()) => { bar.set_style(SPINNER.clone()); @@ -52,8 +60,12 @@ impl Progress { Self::Complete(true) => { bar.finish(); } - Self::Progress { done, total: None } => { - if bytes { + Self::Progress { + done, + total: None, + units, + } => { + if units == Some(ProgressUnits::Bytes) { bar.set_style(BYTES.clone()); } else { bar.set_style(STEPS.clone()); @@ -64,8 +76,9 @@ impl Progress { Self::Progress { done, total: Some(total), + units, } => { - if bytes { + if units == Some(ProgressUnits::Bytes) { bar.set_style(PERCENTAGE_BYTES.clone()); } else { bar.set_style(PERCENTAGE.clone()); @@ -84,14 +97,22 @@ impl Progress { } pub fn set_done(&mut self, done: u64) { *self = match *self { - Self::Complete(false) | Self::NotStarted(()) => Self::Progress { done, total: None }, - Self::Progress { mut done, total } => { + Self::Complete(false) | Self::NotStarted(()) => Self::Progress { + done, + total: None, + units: None, + }, + Self::Progress { + mut done, + total, + units, + } => { if let Some(total) = total { if done > total { done = total; } } - Self::Progress { done, total } + Self::Progress { done, total, units } } Self::Complete(true) => Self::Complete(true), }; @@ -101,10 +122,12 @@ impl Progress { Self::Complete(false) | Self::NotStarted(()) => Self::Progress { done: 0, total: Some(total), + units: None, }, - Self::Progress { done, .. } => Self::Progress { + Self::Progress { done, units, .. } => Self::Progress { done, total: Some(total), + units, }, Self::Complete(true) => Self::Complete(true), } @@ -113,17 +136,30 @@ impl Progress { if let Self::Progress { done, total: Some(old), + units, } = *self { *self = Self::Progress { done, total: Some(old + total), + units, }; } else { self.set_total(total) } } - pub fn complete(&mut self) { + pub fn set_units(&mut self, units: Option) { + *self = match *self { + Self::Complete(false) | Self::NotStarted(()) => Self::Progress { + done: 0, + total: None, + units, + }, + Self::Progress { done, total, .. } => Self::Progress { done, total, units }, + Self::Complete(true) => Self::Complete(true), + }; + } + pub fn set_complete(&mut self) { *self = Self::Complete(true); } pub fn is_complete(&self) -> bool { @@ -137,15 +173,16 @@ impl std::ops::Add for Progress { Self::Complete(false) | Self::NotStarted(()) => Self::Progress { done: rhs, total: None, + units: None, }, - Self::Progress { done, total } => { + Self::Progress { done, total, units } => { let mut done = done + rhs; if let Some(total) = total { if done > total { done = total; } } - Self::Progress { done, total } + Self::Progress { done, total, units } } Self::Complete(true) => Self::Complete(true), } @@ -337,7 +374,7 @@ impl FullProgressTracker { } } pub fn complete(&self) { - self.overall.send_modify(|o| o.complete()); + self.overall.send_modify(|o| o.set_complete()); } } @@ -355,6 +392,7 @@ impl PhaseProgressTrackerHandle { Progress::Progress { done, total: Some(total), + .. } => ((done as f64 / total as f64) * overall_contribution as f64) as u64, _ => 0, }; @@ -380,8 +418,11 @@ impl PhaseProgressTrackerHandle { self.progress.send_modify(|p| p.add_total(total)); self.update_overall(); } + pub fn set_units(&mut self, units: Option) { + self.progress.send_modify(|p| p.set_units(units)); + } pub fn complete(&mut self) { - self.progress.send_modify(|p| p.complete()); + self.progress.send_modify(|p| p.set_complete()); self.update_overall(); } pub fn writer(self, writer: W) -> ProgressTrackerWriter { @@ -501,7 +542,7 @@ impl PhasedProgressBar { ); } } - progress.overall.update_bar(&self.overall, false); + progress.overall.update_bar(&self.overall); for (name, bar) in self.phases.iter() { if let Some(progress) = progress.phases.iter().find_map(|p| { if &p.name == name { @@ -510,7 +551,7 @@ impl PhasedProgressBar { None } }) { - progress.update_bar(bar, true); + progress.update_bar(bar); } } } diff --git a/core/startos/src/registry/admin.rs b/core/startos/src/registry/admin.rs index 02a852dd1..7619cdebc 100644 --- a/core/startos/src/registry/admin.rs +++ b/core/startos/src/registry/admin.rs @@ -47,7 +47,7 @@ pub fn admin_api() -> ParentHandler { from_fn_async(list_admins) .with_metadata("admin", Value::Bool(true)) .with_display_serializable() - .with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result))) + .with_custom_display_fn(|handle, result| display_signers(handle.params, result)) .with_about("List admin signers") .with_call_remote::(), ) @@ -60,7 +60,7 @@ fn signers_api() -> ParentHandler { from_fn_async(list_signers) .with_metadata("admin", Value::Bool(true)) .with_display_serializable() - .with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result))) + .with_custom_display_fn(|handle, result| display_signers(handle.params, result)) .with_about("List signers") .with_call_remote::(), ) @@ -133,7 +133,10 @@ pub async fn list_signers(ctx: RegistryContext) -> Result(params: WithIoFormat, signers: BTreeMap) { +pub fn display_signers( + params: WithIoFormat, + signers: BTreeMap, +) -> Result<(), Error> { use prettytable::*; if let Some(format) = params.format { @@ -155,7 +158,8 @@ pub fn display_signers(params: WithIoFormat, signers: BTreeMap Result { diff --git a/core/startos/src/registry/os/asset/add.rs b/core/startos/src/registry/os/asset/add.rs index 89a1ec9b1..ffea754e3 100644 --- a/core/startos/src/registry/os/asset/add.rs +++ b/core/startos/src/registry/os/asset/add.rs @@ -14,7 +14,7 @@ use url::Url; use crate::context::CliContext; use crate::prelude::*; -use crate::progress::FullProgressTracker; +use crate::progress::{FullProgressTracker, ProgressUnits}; use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; @@ -246,6 +246,7 @@ pub async fn cli_add_asset( if let Some(size) = src.size().await { verify_phase.set_total(size); } + verify_phase.set_units(Some(ProgressUnits::Bytes)); let mut writer = verify_phase.writer(VerifyingWriter::new( tokio::io::sink(), Some((blake3::Hash::from_bytes(*commitment.hash), commitment.size)), diff --git a/core/startos/src/registry/os/asset/get.rs b/core/startos/src/registry/os/asset/get.rs index a3da7047c..cf9baca37 100644 --- a/core/startos/src/registry/os/asset/get.rs +++ b/core/startos/src/registry/os/asset/get.rs @@ -13,7 +13,7 @@ use ts_rs::TS; use crate::context::CliContext; use crate::prelude::*; -use crate::progress::FullProgressTracker; +use crate::progress::{FullProgressTracker, ProgressUnits}; use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; @@ -167,6 +167,7 @@ async fn cli_get_os_asset( let mut download_phase = progress.add_phase(InternedString::intern("Downloading File"), Some(100)); download_phase.set_total(res.commitment.size); + download_phase.set_units(Some(ProgressUnits::Bytes)); let reverify_phase = if reverify { Some(progress.add_phase(InternedString::intern("Reverifying File"), Some(10))) } else { diff --git a/core/startos/src/registry/os/version/mod.rs b/core/startos/src/registry/os/version/mod.rs index 6c9a58328..263ce5073 100644 --- a/core/startos/src/registry/os/version/mod.rs +++ b/core/startos/src/registry/os/version/mod.rs @@ -49,7 +49,7 @@ pub fn version_api() -> ParentHandler { .with_metadata("get_device_info", Value::Bool(true)) .with_display_serializable() .with_custom_display_fn(|handle, result| { - Ok(display_version_info(handle.params, result)) + display_version_info(handle.params, result) }) .with_about("Get OS versions and related version info") .with_call_remote::(), @@ -197,7 +197,10 @@ pub async fn get_version( .collect() } -pub fn display_version_info(params: WithIoFormat, info: BTreeMap) { +pub fn display_version_info( + params: WithIoFormat, + info: BTreeMap, +) -> Result<(), Error> { use prettytable::*; if let Some(format) = params.format { @@ -223,5 +226,6 @@ pub fn display_version_info(params: WithIoFormat, info: BTreeMap() -> ParentHandler { "list", from_fn_async(list_version_signers) .with_display_serializable() - .with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result))) + .with_custom_display_fn(|handle, result| display_signers(handle.params, result)) .with_about("List version signers and related signer info") .with_call_remote::(), ) diff --git a/core/startos/src/registry/package/add.rs b/core/startos/src/registry/package/add.rs index 0ea3ae980..e3e1a0bb6 100644 --- a/core/startos/src/registry/package/add.rs +++ b/core/startos/src/registry/package/add.rs @@ -12,7 +12,7 @@ use url::Url; use crate::context::CliContext; use crate::prelude::*; -use crate::progress::{FullProgressTracker, ProgressTrackerWriter}; +use crate::progress::{FullProgressTracker, ProgressTrackerWriter, ProgressUnits}; use crate::registry::context::RegistryContext; use crate::registry::package::index::PackageVersionInfo; use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; @@ -135,6 +135,7 @@ pub async fn cli_add_package( if let Some(len) = len { verify_phase.set_total(len); } + verify_phase.set_units(Some(ProgressUnits::Bytes)); let mut verify_writer = ProgressTrackerWriter::new(tokio::io::sink(), verify_phase); src.serialize(&mut TrackingIO::new(0, &mut verify_writer), true) .await?; diff --git a/core/startos/src/registry/package/category.rs b/core/startos/src/registry/package/category.rs index 7c3aafad8..57ffb27a9 100644 --- a/core/startos/src/registry/package/category.rs +++ b/core/startos/src/registry/package/category.rs @@ -52,7 +52,7 @@ pub fn category_api() -> ParentHandler { from_fn_async(list_categories) .with_display_serializable() .with_custom_display_fn(|params, categories| { - Ok(display_categories(params.params, categories)) + display_categories(params.params, categories) }) .with_call_remote::(), ) @@ -182,7 +182,7 @@ pub async fn list_categories( pub fn display_categories( params: WithIoFormat, categories: BTreeMap, -) { +) -> Result<(), Error> { use prettytable::*; if let Some(format) = params.format { @@ -197,5 +197,6 @@ pub fn display_categories( for (id, info) in categories { table.add_row(row![&*id, &info.name]); } - table.print_tty(false).unwrap(); + table.print_tty(false)?; + Ok(()) } diff --git a/core/startos/src/registry/package/signer.rs b/core/startos/src/registry/package/signer.rs index 7760bad3f..393c1c48e 100644 --- a/core/startos/src/registry/package/signer.rs +++ b/core/startos/src/registry/package/signer.rs @@ -36,7 +36,7 @@ pub fn signer_api() -> ParentHandler { "list", from_fn_async(list_package_signers) .with_display_serializable() - .with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result))) + .with_custom_display_fn(|handle, result| display_signers(handle.params, result)) .with_about("List package signers and related signer info") .with_call_remote::(), ) diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs index bc4b51663..35cfabc01 100644 --- a/core/startos/src/s9pk/v2/compat.rs +++ b/core/startos/src/s9pk/v2/compat.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use std::sync::Arc; use exver::{ExtendedVersion, VersionRange}; -use models::{Id, ImageId, VolumeId}; +use models::{ImageId, VolumeId}; use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt}; use tokio::process::Command; @@ -259,6 +259,7 @@ impl TryFrom for Manifest { }, git_hash: value.git_hash, os_version: value.eos_version, + sdk_version: None, }) } } diff --git a/core/startos/src/s9pk/v2/manifest.rs b/core/startos/src/s9pk/v2/manifest.rs index 609204e43..fb1985526 100644 --- a/core/startos/src/s9pk/v2/manifest.rs +++ b/core/startos/src/s9pk/v2/manifest.rs @@ -66,6 +66,8 @@ pub struct Manifest { #[serde(default = "current_version")] #[ts(type = "string")] pub os_version: Version, + #[ts(type = "string | null")] + pub sdk_version: Option, } impl Manifest { pub fn validate_for<'a, T: Clone>( diff --git a/core/startos/src/service/effects/action.rs b/core/startos/src/service/effects/action.rs index 4c5c554e8..6f1bef852 100644 --- a/core/startos/src/service/effects/action.rs +++ b/core/startos/src/service/effects/action.rs @@ -31,7 +31,7 @@ pub fn action_api() -> ParentHandler { "run", from_fn_async(run_action) .with_display_serializable() - .with_custom_display_fn(|args, res| Ok(display_action_result(args.params, res))) + .with_custom_display_fn(|args, res| display_action_result(args.params, res)) .with_call_remote::(), ) .subcommand("create-task", from_fn_async(create_task).no_cli()) diff --git a/core/startos/src/service/effects/subcontainer/sync.rs b/core/startos/src/service/effects/subcontainer/sync.rs index 19fee0705..d088516d5 100644 --- a/core/startos/src/service/effects/subcontainer/sync.rs +++ b/core/startos/src/service/effects/subcontainer/sync.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::ffi::{c_int, OsStr, OsString}; use std::fs::File; use std::io::{IsTerminal, Read}; -use std::os::unix::process::CommandExt; +use std::os::unix::process::{CommandExt, ExitStatusExt}; use std::path::{Path, PathBuf}; use std::process::{Command as StdCommand, Stdio}; use std::sync::Arc; @@ -330,7 +330,7 @@ pub fn launch( if let Some(code) = exit.code() { drop(raw); std::process::exit(code); - } else if exit.success() { + } else if exit.success() || exit.signal() == Some(15) { Ok(()) } else { Err(Error::new( @@ -380,7 +380,7 @@ pub fn launch( nix::mount::umount(&chroot.join("proc")) .with_ctx(|_| (ErrorKind::Filesystem, "umount procfs"))?; std::process::exit(code); - } else if exit.success() { + } else if exit.success() || exit.signal() == Some(15) { Ok(()) } else { Err(Error::new( diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 063355c6a..d4cee453f 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -48,6 +48,7 @@ use crate::s9pk::S9pk; use crate::service::action::update_tasks; use crate::service::rpc::{ExitParams, InitKind}; use crate::service::service_map::InstallProgressHandles; +use crate::service::uninstall::cleanup; use crate::util::actor::concurrent::ConcurrentActor; use crate::util::io::{create_file, AsyncReadStream, TermSize}; use crate::util::net::WebSocketExt; @@ -111,7 +112,6 @@ impl std::fmt::Display for MiB { #[derive(Clone, Debug, Serialize, Deserialize, Default, TS)] pub struct ServiceStats { pub container_id: Arc, - pub package_id: PackageId, pub memory_usage: MiB, pub memory_limit: MiB, } @@ -307,7 +307,7 @@ impl Service { } } } - // TODO: delete s9pk? + cleanup(ctx, id, false).await.log_err(); ctx.db .mutate(|v| v.as_public_mut().as_package_data_mut().remove(id)) .await @@ -615,7 +615,6 @@ impl Service { .fold((0, 0), |acc, (total, used)| (acc.0 + total, acc.1 + used)); Ok(ServiceStats { container_id: lxc_container.guid.clone(), - package_id: self.seed.id.clone(), memory_limit: MiB::from_MiB(total), memory_usage: MiB::from_MiB(used), }) @@ -735,6 +734,7 @@ pub struct AttachParams { pub command: Vec, pub tty: bool, pub stderr_tty: bool, + pub pty_size: Option, #[ts(skip)] #[serde(rename = "__auth_session")] session: Option, @@ -752,6 +752,7 @@ pub async fn attach( command, tty, stderr_tty, + pty_size, session, subcontainer, image_id, @@ -862,6 +863,7 @@ pub async fn attach( command: Vec, tty: bool, stderr_tty: bool, + pty_size: Option, image_id: ImageId, workdir: Option, root_command: &RootCommand, @@ -898,6 +900,10 @@ pub async fn attach( cmd.arg("--force-stderr-tty"); } + if let Some(pty_size) = pty_size { + cmd.arg(format!("--pty-size={pty_size}")); + } + cmd.arg(&root_path).arg("--"); if command.is_empty() { @@ -1040,6 +1046,7 @@ pub async fn attach( command, tty, stderr_tty, + pty_size, image_id, workdir, &root_command, diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index 37aebdf69..303ff4bc3 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -22,7 +22,9 @@ use crate::disk::mount::guard::GenericMountGuard; use crate::install::PKG_ARCHIVE_DIR; use crate::notifications::{notify, NotificationLevel}; use crate::prelude::*; -use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle, ProgressTrackerWriter}; +use crate::progress::{ + FullProgressTracker, PhaseProgressTrackerHandle, ProgressTrackerWriter, ProgressUnits, +}; use crate::rpc_continuations::Guid; use crate::s9pk::manifest::PackageId; use crate::s9pk::merkle_archive::source::FileSource; @@ -72,6 +74,7 @@ impl ServiceMap { progress.start(); let ids = ctx.db.peek().await.as_public().as_package_data().keys()?; progress.set_total(ids.len() as u64); + progress.set_units(Some(ProgressUnits::Steps)); let mut jobs = FuturesUnordered::new(); for id in &ids { jobs.push(self.load(ctx, id, LoadDisposition::Retry)); diff --git a/core/startos/src/setup.rs b/core/startos/src/setup.rs index ba5477a54..2f3259615 100644 --- a/core/startos/src/setup.rs +++ b/core/startos/src/setup.rs @@ -34,7 +34,7 @@ use crate::disk::REPAIR_DISK_PATH; use crate::init::{init, InitPhases, InitResult}; use crate::net::ssl::root_ca_start_time; use crate::prelude::*; -use crate::progress::{FullProgress, PhaseProgressTrackerHandle}; +use crate::progress::{FullProgress, PhaseProgressTrackerHandle, ProgressUnits}; use crate::rpc_continuations::Guid; use crate::system::sync_kiosk; use crate::util::crypto::EncryptedWire; @@ -547,6 +547,7 @@ async fn migrate( let mut restore_phase = restore_phase.or_not_found("restore progress")?; restore_phase.start(); + restore_phase.set_units(Some(ProgressUnits::Bytes)); let _ = crate::disk::main::import( &old_guid, "/media/startos/migrate", diff --git a/core/startos/src/ssh.rs b/core/startos/src/ssh.rs index 89cabd1cd..dc2f9440a 100644 --- a/core/startos/src/ssh.rs +++ b/core/startos/src/ssh.rs @@ -108,7 +108,7 @@ pub fn ssh() -> ParentHandler { from_fn_async(list) .with_display_serializable() .with_custom_display_fn(|handle, result| { - Ok(display_all_ssh_keys(handle.params, result)) + display_all_ssh_keys(handle.params, result) }) .with_about("List ssh keys") .with_call_remote::(), @@ -177,7 +177,10 @@ pub async fn remove( sync_pubkeys(&keys, SSH_DIR).await } -fn display_all_ssh_keys(params: WithIoFormat, result: Vec) { +fn display_all_ssh_keys( + params: WithIoFormat, + result: Vec, +) -> Result<(), Error> { use prettytable::*; if let Some(format) = params.format { @@ -200,7 +203,9 @@ fn display_all_ssh_keys(params: WithIoFormat, result: Vec ]; table.add_row(row); } - table.print_tty(false).unwrap(); + table.print_tty(false)?; + + Ok(()) } #[instrument(skip_all)] diff --git a/core/startos/src/system.rs b/core/startos/src/system.rs index 96d4bb00b..996fd334b 100644 --- a/core/startos/src/system.rs +++ b/core/startos/src/system.rs @@ -46,7 +46,7 @@ pub fn experimental() -> ParentHandler { from_fn_async(governor) .with_display_serializable() .with_custom_display_fn(|handle, result| { - Ok(display_governor_info(handle.params, result)) + display_governor_info(handle.params, result) }) .with_about("Show current and available CPU governors") .with_call_remote::(), @@ -125,7 +125,10 @@ pub struct GovernorInfo { available: BTreeSet, } -fn display_governor_info(params: WithIoFormat, result: GovernorInfo) { +fn display_governor_info( + params: WithIoFormat, + result: GovernorInfo, +) -> Result<(), Error> { use prettytable::*; if let Some(format) = params.format { @@ -141,7 +144,8 @@ fn display_governor_info(params: WithIoFormat, result: GovernorI table.add_row(row![entry]); } } - table.print_tty(false).unwrap(); + table.print_tty(false)?; + Ok(()) } #[derive(Deserialize, Serialize, Parser, TS)] @@ -191,7 +195,7 @@ pub struct TimeInfo { uptime: u64, } -pub fn display_time(params: WithIoFormat, arg: TimeInfo) { +pub fn display_time(params: WithIoFormat, arg: TimeInfo) -> Result<(), Error> { use std::fmt::Write; use prettytable::*; @@ -230,7 +234,8 @@ pub fn display_time(params: WithIoFormat, arg: TimeInfo) { let mut table = Table::new(); table.add_row(row![bc -> "NOW", &arg.now]); table.add_row(row![bc -> "UPTIME", &uptime_string]); - table.print_tty(false).unwrap(); + table.print_tty(false)?; + Ok(()) } pub async fn time(ctx: RpcContext, _: Empty) -> Result { diff --git a/core/startos/src/update/mod.rs b/core/startos/src/update/mod.rs index fe046168d..8826f78dc 100644 --- a/core/startos/src/update/mod.rs +++ b/core/startos/src/update/mod.rs @@ -26,7 +26,9 @@ use crate::disk::mount::filesystem::MountType; use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; use crate::notifications::{notify, NotificationLevel}; use crate::prelude::*; -use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar}; +use crate::progress::{ + FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar, ProgressUnits, +}; use crate::registry::asset::RegistryAsset; use crate::registry::context::{RegistryContext, RegistryUrlParams}; use crate::registry::os::index::OsVersionInfo; @@ -195,9 +197,9 @@ pub async fn cli_update_system( } if let Some(mut prev) = prev { for phase in &mut prev.phases { - phase.progress.complete(); + phase.progress.set_complete(); } - prev.overall.complete(); + prev.overall.set_complete(); progress.update(&prev); } } else { @@ -265,6 +267,7 @@ async fn maybe_do_update( let prune_phase = progress.add_phase("Pruning Old OS Images".into(), Some(2)); let mut download_phase = progress.add_phase("Downloading File".into(), Some(100)); download_phase.set_total(asset.commitment.size); + download_phase.set_units(Some(ProgressUnits::Bytes)); let reverify_phase = progress.add_phase("Reverifying File".into(), Some(10)); let sync_boot_phase = progress.add_phase("Syncing Boot Files".into(), Some(1)); let finalize_phase = progress.add_phase("Finalizing Update".into(), Some(1)); @@ -399,6 +402,9 @@ async fn do_update( .arg(asset.commitment.size.to_string()) .invoke(ErrorKind::Filesystem) .await?; + Command::new("/usr/lib/startos/scripts/prune-boot") + .invoke(ErrorKind::Filesystem) + .await?; prune_phase.complete(); download_phase.start(); diff --git a/core/startos/src/upload.rs b/core/startos/src/upload.rs index b54091735..d7428bb44 100644 --- a/core/startos/src/upload.rs +++ b/core/startos/src/upload.rs @@ -18,7 +18,7 @@ use tokio::sync::watch; use crate::context::RpcContext; use crate::prelude::*; -use crate::progress::PhaseProgressTrackerHandle; +use crate::progress::{PhaseProgressTrackerHandle, ProgressUnits}; use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::s9pk::merkle_archive::source::multi_cursor_file::{FileCursor, MultiCursorFile}; use crate::s9pk::merkle_archive::source::ArchiveSource; @@ -176,7 +176,10 @@ pub struct UploadingFile { progress: watch::Receiver, } impl UploadingFile { - pub async fn new(progress: PhaseProgressTrackerHandle) -> Result<(UploadHandle, Self), Error> { + pub async fn new( + mut progress: PhaseProgressTrackerHandle, + ) -> Result<(UploadHandle, Self), Error> { + progress.set_units(Some(ProgressUnits::Bytes)); let progress = watch::channel(Progress { tracker: progress, expected_size: None, diff --git a/core/startos/src/util/io.rs b/core/startos/src/util/io.rs index afacde836..dc4c1eebd 100644 --- a/core/startos/src/util/io.rs +++ b/core/startos/src/util/io.rs @@ -26,10 +26,10 @@ use tokio::io::{ use tokio::net::TcpStream; use tokio::sync::{Notify, OwnedMutexGuard}; use tokio::time::{Instant, Sleep}; +use ts_rs::TS; use crate::prelude::*; use crate::util::sync::SyncMutex; -use crate::{CAP_1_KiB, CAP_1_MiB}; pub trait AsyncReadSeek: AsyncRead + AsyncSeek {} impl AsyncReadSeek for T {} @@ -1426,7 +1426,7 @@ impl std::io::Read for SharedIO { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct TermSize { pub size: (u16, u16), pub pixels: Option<(u16, u16)>, @@ -1464,6 +1464,15 @@ impl FromStr for TermSize { .ok_or_else(|| Error::new(eyre!("invalid pty size"), ErrorKind::ParseNumber)) } } +impl std::fmt::Display for TermSize { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.size.0, self.size.1)?; + if let Some(pixels) = self.pixels { + write!(f, ":{}:{}", pixels.0, pixels.1)?; + } + Ok(()) + } +} impl ValueParserFactory for TermSize { type Parser = FromStrParser; fn value_parser() -> Self::Parser { diff --git a/core/startos/src/util/serde.rs b/core/startos/src/util/serde.rs index 5b785ce9a..7e5721884 100644 --- a/core/startos/src/util/serde.rs +++ b/core/startos/src/util/serde.rs @@ -398,13 +398,12 @@ impl IoFormat { } } -pub fn display_serializable(format: IoFormat, result: T) { - format - .to_writer(std::io::stdout(), &result) - .expect("Error serializing result to stdout"); +pub fn display_serializable(format: IoFormat, result: T) -> Result<(), Error> { + format.to_writer(std::io::stdout(), &result)?; if format == IoFormat::JsonPretty { println!() } + Ok(()) } #[derive(Deserialize, Serialize)] @@ -523,13 +522,14 @@ impl, C: Context> HandlerFor for DisplaySerializable { impl PrintCliResult for DisplaySerializable where T::Ok: Serialize, + Self::Err: From, { fn print( &self, HandlerArgs { params, .. }: HandlerArgsFor, result: Self::Ok, ) -> Result<(), Self::Err> { - display_serializable(params.format.unwrap_or_default(), result); + display_serializable(params.format.unwrap_or_default(), result)?; Ok(()) } } diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index eaa3a59a9..d0a1318f7 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -46,8 +46,9 @@ mod v0_4_0_alpha_3; mod v0_4_0_alpha_4; mod v0_4_0_alpha_5; mod v0_4_0_alpha_6; +mod v0_4_0_alpha_7; -pub type Current = v0_4_0_alpha_6::Version; // VERSION_BUMP +pub type Current = v0_4_0_alpha_7::Version; // VERSION_BUMP impl Current { #[instrument(skip(self, db))] @@ -157,7 +158,8 @@ enum Version { V0_4_0_alpha_3(Wrapper), V0_4_0_alpha_4(Wrapper), V0_4_0_alpha_5(Wrapper), - V0_4_0_alpha_6(Wrapper), // VERSION_BUMP + V0_4_0_alpha_6(Wrapper), + V0_4_0_alpha_7(Wrapper), // VERSION_BUMP Other(exver::Version), } @@ -206,7 +208,8 @@ impl Version { Self::V0_4_0_alpha_3(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_4(v) => DynVersion(Box::new(v.0)), Self::V0_4_0_alpha_5(v) => DynVersion(Box::new(v.0)), - Self::V0_4_0_alpha_6(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP + Self::V0_4_0_alpha_6(v) => DynVersion(Box::new(v.0)), + Self::V0_4_0_alpha_7(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP Self::Other(v) => { return Err(Error::new( eyre!("unknown version {v}"), @@ -247,7 +250,8 @@ impl Version { Version::V0_4_0_alpha_3(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_4(Wrapper(x)) => x.semver(), Version::V0_4_0_alpha_5(Wrapper(x)) => x.semver(), - Version::V0_4_0_alpha_6(Wrapper(x)) => x.semver(), // VERSION_BUMP + Version::V0_4_0_alpha_6(Wrapper(x)) => x.semver(), + Version::V0_4_0_alpha_7(Wrapper(x)) => x.semver(), // VERSION_BUMP Version::Other(x) => x.clone(), } } diff --git a/core/startos/src/version/v0_4_0_alpha_7.rs b/core/startos/src/version/v0_4_0_alpha_7.rs new file mode 100644 index 000000000..fdadced3d --- /dev/null +++ b/core/startos/src/version/v0_4_0_alpha_7.rs @@ -0,0 +1,37 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_4_0_alpha_6, VersionT}; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_4_0_alpha_7: exver::Version = exver::Version::new( + [0, 4, 0], + [PreReleaseSegment::String("alpha".into()), 7.into()] + ); +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_4_0_alpha_6::Version; + type PreUpRes = (); + + async fn pre_up(self) -> Result { + Ok(()) + } + fn semver(self) -> exver::Version { + V0_4_0_alpha_7.clone() + } + fn compat(self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + #[instrument] + fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<(), Error> { + Ok(()) + } + fn down(self, _db: &mut Value) -> Result<(), Error> { + Ok(()) + } +} diff --git a/debian/postinst b/debian/postinst index 2404ac5b3..c47ed493e 100755 --- a/debian/postinst +++ b/debian/postinst @@ -25,12 +25,33 @@ if [ -f /etc/default/grub ]; then sed -i '/\(^\|#\)GRUB_TERMINAL=/c\GRUB_TERMINAL="serial"\nGRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1"' /etc/default/grub fi +VERSION="$(cat /usr/lib/startos/VERSION.txt)" +ENVIRONMENT=$(cat /usr/lib/startos/ENVIRONMENT.txt) +VERSION_ENV="${VERSION}" +if [ -n "${ENVIRONMENT}" ]; then + VERSION_ENV="${VERSION} (${ENVIRONMENT})" +fi + +# set /etc/os-release +cat << EOF > /etc/os-release +NAME=StartOS +VERSION="${VERSION_ENV}" +ID=start-os +VERSION_ID="${VERSION}" +PRETTY_NAME="StartOS v${VERSION_ENV}" +HOME_URL="https://start9.com/" +SUPPORT_URL="https://docs.start9.com/0.3.5.x/support" +BUG_REPORT_URL="https://github.com/Start9Labs/start-os/issues" +VARIANT="${ENVIRONMENT}" +VARIANT_ID="${ENVIRONMENT}" +EOF + # set local and remote login prompt cat << EOF > /etc/issue -StartOS v$(cat /usr/lib/startos/VERSION.txt) [\\m] on \\n.local (\\l) +StartOS v${VERSION} [\\m] on \\n.local (\\l) EOF cat << EOF > /etc/issue.net -StartOS v$(cat /usr/lib/startos/VERSION.txt) +StartOS v${VERSION} EOF # change timezone diff --git a/sdk/base/lib/osBindings/GetOsVersionParams.ts b/sdk/base/lib/osBindings/GetOsVersionParams.ts index 155819758..65796eb6c 100644 --- a/sdk/base/lib/osBindings/GetOsVersionParams.ts +++ b/sdk/base/lib/osBindings/GetOsVersionParams.ts @@ -3,7 +3,6 @@ export type GetOsVersionParams = { sourceVersion: string | null targetVersion: string | null - includePrerelease: boolean | null serverId: string | null platform: string | null } diff --git a/sdk/base/lib/osBindings/Manifest.ts b/sdk/base/lib/osBindings/Manifest.ts index f65daeea8..5a489465b 100644 --- a/sdk/base/lib/osBindings/Manifest.ts +++ b/sdk/base/lib/osBindings/Manifest.ts @@ -32,4 +32,5 @@ export type Manifest = { hardwareRequirements: HardwareRequirements gitHash?: GitHash osVersion: string + sdkVersion: string | null } diff --git a/sdk/base/lib/osBindings/Progress.ts b/sdk/base/lib/osBindings/Progress.ts index 9509ed7e1..1ed91afdb 100644 --- a/sdk/base/lib/osBindings/Progress.ts +++ b/sdk/base/lib/osBindings/Progress.ts @@ -1,3 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ProgressUnits } from "./ProgressUnits" -export type Progress = null | boolean | { done: number; total: number | null } +export type Progress = + | null + | boolean + | { done: number; total: number | null; units: ProgressUnits | null } diff --git a/sdk/base/lib/osBindings/ProgressUnits.ts b/sdk/base/lib/osBindings/ProgressUnits.ts new file mode 100644 index 000000000..44f81fc73 --- /dev/null +++ b/sdk/base/lib/osBindings/ProgressUnits.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ProgressUnits = "bytes" | "steps" diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts index e801f3e57..e6e3a7bff 100644 --- a/sdk/base/lib/osBindings/index.ts +++ b/sdk/base/lib/osBindings/index.ts @@ -157,6 +157,7 @@ export { PathOrUrl } from "./PathOrUrl" export { Percentage } from "./Percentage" export { ProcedureId } from "./ProcedureId" export { Progress } from "./Progress" +export { ProgressUnits } from "./ProgressUnits" export { Public } from "./Public" export { RecoverySource } from "./RecoverySource" export { RegistryAsset } from "./RegistryAsset" diff --git a/sdk/base/lib/types.ts b/sdk/base/lib/types.ts index b1d74f2e1..e6a9af811 100644 --- a/sdk/base/lib/types.ts +++ b/sdk/base/lib/types.ts @@ -106,7 +106,7 @@ export class UseEntrypoint { export function isUseEntrypoint( command: CommandType, ): command is UseEntrypoint { - return typeof command === "object" && "ENTRYPOINT" in command + return typeof command === "object" && "USE_ENTRYPOINT" in command } export type CommandType = string | [string, ...string[]] | UseEntrypoint diff --git a/sdk/package/lib/StartSdk.ts b/sdk/package/lib/StartSdk.ts index 44cb53ed1..356258e2c 100644 --- a/sdk/package/lib/StartSdk.ts +++ b/sdk/package/lib/StartSdk.ts @@ -17,7 +17,6 @@ import * as patterns from "../../base/lib/util/patterns" import { BackupSync, Backups } from "./backup/Backups" import { smtpInputSpec } from "../../base/lib/actions/input/inputSpecConstants" import { Daemon, Daemons } from "./mainFn/Daemons" -import { HealthCheck } from "./health/HealthCheck" import { checkPortListening } from "./health/checkFns/checkPortListening" import { checkWebUrl, runHealthScript } from "./health/checkFns" import { List } from "../../base/lib/actions/input/builder/list" @@ -25,10 +24,7 @@ import { SetupBackupsParams, setupBackups } from "./backup/setupBackups" import { setupMain } from "./mainFn" import { defaultTrigger } from "./trigger/defaultTrigger" import { changeOnFirstSuccess, cooldownTrigger } from "./trigger" -import { - UpdateServiceInterfaces, - setupServiceInterfaces, -} from "../../base/lib/interfaces/setupInterfaces" +import { setupServiceInterfaces } from "../../base/lib/interfaces/setupInterfaces" import { successFailure } from "./trigger/successFailure" import { MultiHost, Scheme } from "../../base/lib/interfaces/Host" import { ServiceInterfaceBuilder } from "../../base/lib/interfaces/ServiceInterfaceBuilder" @@ -45,17 +41,13 @@ import { splitCommand } from "./util" import { Mounts } from "./mainFn/Mounts" import { setupDependencies } from "../../base/lib/dependencies/setupDependencies" import * as T from "../../base/lib/types" -import { - ExtendedVersion, - testTypeVersion, - VersionRange, -} from "../../base/lib/exver" +import { testTypeVersion } from "../../base/lib/exver" import { CheckDependencies, checkDependencies, } from "../../base/lib/dependencies/dependencies" import { GetSslCertificate } from "./util" -import { getDataVersion, setDataVersion, VersionGraph } from "./version" +import { getDataVersion, setDataVersion } from "./version" import { MaybeFn } from "../../base/lib/actions/setupActions" import { GetInput } from "../../base/lib/actions/setupActions" import { Run } from "../../base/lib/actions/setupActions" @@ -68,7 +60,7 @@ import { setupOnUninit, } from "../../base/lib/inits" -export const OSVersion = testTypeVersion("0.4.0-alpha.6") +export const OSVersion = testTypeVersion("0.4.0-alpha.7") // prettier-ignore type AnyNeverCond = @@ -95,7 +87,7 @@ export class StartSdk { | "clearServiceInterfaces" | "bind" | "getHostInfo" - type MainUsedEffects = "setMainStatus" | "setHealth" + type MainUsedEffects = "setMainStatus" type CallbackEffects = | "child" | "constRetry" @@ -129,6 +121,7 @@ export class StartSdk { shutdown: (effects, ...args) => effects.shutdown(...args), getDependencies: (effects, ...args) => effects.getDependencies(...args), getStatus: (effects, ...args) => effects.getStatus(...args), + setHealth: (effects, ...args) => effects.setHealth(...args), } return { @@ -454,7 +447,6 @@ export class StartSdk { hostnames: string[], algorithm?: T.Algorithm, ) => new GetSslCertificate(effects, hostnames, algorithm), - HealthCheck, healthCheck: { checkPortListening, checkWebUrl, @@ -652,19 +644,12 @@ export class StartSdk { successFailure, }, Mounts: { - of() { - return Mounts.of() - }, + of: Mounts.of, }, Backups: { - volumes: ( - ...volumeNames: Array - ) => Backups.withVolumes(...volumeNames), - addSets: ( - ...options: BackupSync[] - ) => Backups.withSyncs(...options), - withOptions: (options?: Partial) => - Backups.withOptions(options), + ofVolumes: Backups.ofVolumes, + ofSyncs: Backups.ofSyncs, + withOptions: Backups.withOptions, }, InputSpec: { /** @@ -705,10 +690,11 @@ export class StartSdk { Daemons: { of( effects: Effects, - started: (onTerm: () => PromiseLike) => PromiseLike, - healthChecks: HealthCheck[], + started: + | ((onTerm: () => PromiseLike) => PromiseLike) + | null, ) { - return Daemons.of({ effects, started, healthChecks }) + return Daemons.of({ effects, started }) }, }, SubContainer: { diff --git a/sdk/package/lib/backup/Backups.ts b/sdk/package/lib/backup/Backups.ts index f43495bca..6784ad51c 100644 --- a/sdk/package/lib/backup/Backups.ts +++ b/sdk/package/lib/backup/Backups.ts @@ -31,10 +31,10 @@ export class Backups implements InitScript { private postRestore = async (effects: BackupEffects) => {}, ) {} - static withVolumes( + static ofVolumes( ...volumeNames: Array ): Backups { - return Backups.withSyncs( + return Backups.ofSyncs( ...volumeNames.map((srcVolume) => ({ dataPath: `/media/startos/volumes/${srcVolume}/` as const, backupPath: `/media/startos/backup/volumes/${srcVolume}/` as const, @@ -42,7 +42,7 @@ export class Backups implements InitScript { ) } - static withSyncs( + static ofSyncs( ...syncs: BackupSync[] ) { return syncs.reduce((acc, x) => acc.addSync(x), new Backups()) @@ -112,11 +112,9 @@ export class Backups implements InitScript { ...options, }) } + addSync(sync: BackupSync) { - this.backupSet.push({ - ...sync, - options: { ...this.options, ...sync.options }, - }) + this.backupSet.push(sync) return this } diff --git a/sdk/package/lib/backup/setupBackups.ts b/sdk/package/lib/backup/setupBackups.ts index ef7275cf2..2dbe0c5e0 100644 --- a/sdk/package/lib/backup/setupBackups.ts +++ b/sdk/package/lib/backup/setupBackups.ts @@ -19,7 +19,7 @@ export function setupBackups( if (options instanceof Function) { backupsFactory = options } else { - backupsFactory = async () => Backups.withVolumes(...options) + backupsFactory = async () => Backups.ofVolumes(...options) } const answer: SetupBackupsRes = { get createBackup() { diff --git a/sdk/package/lib/health/HealthCheck.ts b/sdk/package/lib/health/HealthCheck.ts index 54c68e977..786565b8d 100644 --- a/sdk/package/lib/health/HealthCheck.ts +++ b/sdk/package/lib/health/HealthCheck.ts @@ -12,7 +12,6 @@ export type HealthCheckParams = { trigger?: Trigger gracePeriod?: number fn(): Promise | HealthCheckResult - onFirstSuccess?: () => unknown | Promise } export class HealthCheck extends Drop { @@ -32,13 +31,6 @@ export class HealthCheck extends Drop { const getCurrentValue = () => this.currentValue const gracePeriod = o.gracePeriod ?? 10_000 const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue) - const triggerFirstSuccess = once(() => - Promise.resolve( - "onFirstSuccess" in o && o.onFirstSuccess - ? o.onFirstSuccess() - : undefined, - ), - ) const checkStarted = () => [ this.started, @@ -78,9 +70,6 @@ export class HealthCheck extends Drop { message: message || "", }) this.currentValue.lastResult = result - await triggerFirstSuccess().catch((err) => { - console.error(asError(err)) - }) } catch (e) { await effects.setHealth({ name: o.name, diff --git a/sdk/package/lib/mainFn/CommandController.ts b/sdk/package/lib/mainFn/CommandController.ts index ea58f08f9..7fba975f4 100644 --- a/sdk/package/lib/mainFn/CommandController.ts +++ b/sdk/package/lib/mainFn/CommandController.ts @@ -2,44 +2,49 @@ import { DEFAULT_SIGTERM_TIMEOUT } from "." import { NO_TIMEOUT, SIGTERM } from "../../../base/lib/types" import * as T from "../../../base/lib/types" -import { MountOptions, SubContainer } from "../util/SubContainer" +import { SubContainer } from "../util/SubContainer" import { Drop, splitCommand } from "../util" import * as cp from "child_process" import * as fs from "node:fs/promises" -import { Mounts } from "./Mounts" -import { DaemonCommandType } from "./Daemons" +import { DaemonCommandType, ExecCommandOptions, ExecFnOptions } from "./Daemons" -export class CommandController extends Drop { +export class CommandController< + Manifest extends T.SDKManifest, + C extends SubContainer | null, +> extends Drop { private constructor( readonly runningAnswer: Promise, private state: { exited: boolean }, - private readonly subcontainer: SubContainer, + private readonly subcontainer: C, private process: cp.ChildProcess | AbortController, readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, ) { super() } - static of() { + static of< + Manifest extends T.SDKManifest, + C extends SubContainer | null, + >() { return async ( effects: T.Effects, - subcontainer: SubContainer, - exec: DaemonCommandType, + subcontainer: C, + exec: DaemonCommandType, ) => { try { if ("fn" in exec) { const abort = new AbortController() - const cell: { ctrl: CommandController } = { - ctrl: new CommandController( + const cell: { ctrl: CommandController } = { + ctrl: new CommandController( exec.fn(subcontainer, abort).then(async (command) => { - if (command && !abort.signal.aborted) { - Object.assign( - cell.ctrl, - await CommandController.of()( - effects, - subcontainer, - command, - ), - ) + if (subcontainer && command && !abort.signal.aborted) { + const newCtrl = ( + await CommandController.of< + Manifest, + SubContainer + >()(effects, subcontainer, command as ExecCommandOptions) + ).leak() + + Object.assign(cell.ctrl, newCtrl) return await cell.ctrl.runningAnswer } else { cell.ctrl.state.exited = true @@ -57,7 +62,7 @@ export class CommandController extends Drop { let commands: string[] if (T.isUseEntrypoint(exec.command)) { const imageMeta: T.ImageMetadata = await fs - .readFile(`/media/startos/images/${subcontainer.imageId}.json`, { + .readFile(`/media/startos/images/${subcontainer!.imageId}.json`, { encoding: "utf8", }) .catch(() => "{}") @@ -70,11 +75,11 @@ export class CommandController extends Drop { let childProcess: cp.ChildProcess if (exec.runAsInit) { - childProcess = await subcontainer.launch(commands, { + childProcess = await subcontainer!.launch(commands, { env: exec.env, }) } else { - childProcess = await subcontainer.spawn(commands, { + childProcess = await subcontainer!.spawn(commands, { env: exec.env, stdio: exec.onStdout || exec.onStderr ? "pipe" : "inherit", }) @@ -108,7 +113,7 @@ export class CommandController extends Drop { }) }) - return new CommandController( + return new CommandController( answer, state, subcontainer, @@ -116,7 +121,7 @@ export class CommandController extends Drop { exec.sigtermTimeout, ) } catch (e) { - await subcontainer.destroy() + await subcontainer?.destroy() throw e } } @@ -144,7 +149,7 @@ export class CommandController extends Drop { if (this.process instanceof AbortController) this.process.abort() else this.process.kill("SIGKILL") } - await this.subcontainer.destroy() + await this.subcontainer?.destroy() } } async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) { @@ -178,7 +183,7 @@ export class CommandController extends Drop { ]) else await this.runningAnswer } finally { - await this.subcontainer.destroy() + await this.subcontainer?.destroy() } } onDrop(): void { diff --git a/sdk/package/lib/mainFn/Daemon.ts b/sdk/package/lib/mainFn/Daemon.ts index 171b9b5dd..e4ee36e5f 100644 --- a/sdk/package/lib/mainFn/Daemon.ts +++ b/sdk/package/lib/mainFn/Daemon.ts @@ -17,14 +17,17 @@ const MAX_TIMEOUT_MS = 30000 * and the others state of running, where it will keep a living running command */ -export class Daemon extends Drop { - private commandController: CommandController | null = null +export class Daemon< + Manifest extends T.SDKManifest, + C extends SubContainer | null = SubContainer | null, +> extends Drop { + private commandController: CommandController | null = null private shouldBeRunning = false protected exitedSuccess = false private onExitFns: ((success: boolean) => void)[] = [] protected constructor( - private subcontainer: SubContainer, - private startCommand: (() => Promise>) | null, + private subcontainer: C, + private startCommand: () => Promise>, readonly oneshot: boolean = false, ) { super() @@ -33,17 +36,20 @@ export class Daemon extends Drop { return this.oneshot } static of() { - return async ( + return async | null>( effects: T.Effects, - subcontainer: SubContainer, - exec: DaemonCommandType | null, + subcontainer: C, + exec: DaemonCommandType, ) => { - if (subcontainer.isOwned()) subcontainer = subcontainer.rc() - const startCommand = exec - ? () => - CommandController.of()(effects, subcontainer.rc(), exec) - : null - return new Daemon(subcontainer, startCommand) + let subc: SubContainer | null = subcontainer + if (subcontainer && subcontainer.isOwned()) subc = subcontainer.rc() + const startCommand = () => + CommandController.of()( + effects, + (subc?.rc() ?? null) as C, + exec, + ) + return new Daemon(subc, startCommand) } } async start() { @@ -53,7 +59,7 @@ export class Daemon extends Drop { this.shouldBeRunning = true let timeoutCounter = 0 ;(async () => { - while (this.startCommand && this.shouldBeRunning) { + while (this.shouldBeRunning) { if (this.commandController) await this.commandController .term({}) @@ -106,10 +112,10 @@ export class Daemon extends Drop { .catch((e) => console.error(asError(e))) this.commandController = null this.onExitFns = [] - await this.subcontainer.destroy() + await this.subcontainer?.destroy() } - subcontainerRc(): SubContainerRc { - return this.subcontainer.rc() + subcontainerRc(): SubContainerRc | null { + return this.subcontainer?.rc() ?? null } onExit(fn: (success: boolean) => void) { this.onExitFns.push(fn) diff --git a/sdk/package/lib/mainFn/Daemons.ts b/sdk/package/lib/mainFn/Daemons.ts index adc3c758a..01b61150c 100644 --- a/sdk/package/lib/mainFn/Daemons.ts +++ b/sdk/package/lib/mainFn/Daemons.ts @@ -37,9 +37,7 @@ export type Ready = { }) * ``` */ - fn: ( - subcontainer: SubContainer, - ) => Promise | HealthCheckResult + fn: () => Promise | HealthCheckResult /** * A duration in milliseconds to treat a failing health check as "starting" * @@ -65,30 +63,40 @@ export type ExecCommandOptions = { onStderr?: (chunk: Buffer | string | any) => void } -export type ExecFnOptions = { +export type ExecFnOptions< + Manifest extends T.SDKManifest, + C extends SubContainer | null, +> = { fn: ( - subcontainer: SubContainer, + subcontainer: C, abort: AbortController, - ) => Promise + ) => Promise // Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms sigtermTimeout?: number } -export type DaemonCommandType = ExecCommandOptions | ExecFnOptions +export type DaemonCommandType< + Manifest extends T.SDKManifest, + C extends SubContainer | null, +> = ExecFnOptions | (C extends null ? never : ExecCommandOptions) -type NewDaemonParams = { +type NewDaemonParams< + Manifest extends T.SDKManifest, + C extends SubContainer | null, +> = { /** What to run as the daemon: either an async fn or a commandline command to run in the subcontainer */ - exec: DaemonCommandType | null - /** Information about the subcontainer in which the daemon runs */ - subcontainer: SubContainer + exec: DaemonCommandType + /** The subcontainer in which the daemon runs */ + subcontainer: C } type AddDaemonParams< Manifest extends T.SDKManifest, Ids extends string, Id extends string, + C extends SubContainer | null, > = ( - | NewDaemonParams + | NewDaemonParams | { daemon: Daemon } @@ -102,8 +110,15 @@ type AddOneshotParams< Manifest extends T.SDKManifest, Ids extends string, Id extends string, -> = NewDaemonParams & { - exec: DaemonCommandType + C extends SubContainer | null, +> = NewDaemonParams & { + exec: DaemonCommandType + /** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */ + requires: Exclude[] +} + +type AddHealthCheckParams = { + ready: Ready /** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */ requires: Exclude[] } @@ -111,7 +126,7 @@ type AddOneshotParams< type ErrorDuplicateId = `The id '${Id}' is already used` export const runCommand = () => - CommandController.of() + CommandController.of>() /** * A class for defining and controlling the service daemons @@ -141,11 +156,12 @@ export class Daemons { private constructor( readonly effects: T.Effects, - readonly started: (onTerm: () => PromiseLike) => PromiseLike, + readonly started: + | ((onTerm: () => PromiseLike) => PromiseLike) + | null, readonly daemons: Promise>[], readonly ids: Ids[], readonly healthDaemons: HealthDaemon[], - readonly healthChecks: HealthCheck[], ) {} /** * Returns an empty new Daemons class with the provided inputSpec. @@ -154,13 +170,18 @@ export class Daemons * * Daemons run in the order they are defined, with latter daemons being capable of * depending on prior daemons - * @param options + * + * @param effects + * + * @param started * @returns */ static of(options: { effects: T.Effects - started: (onTerm: () => PromiseLike) => PromiseLike - healthChecks: HealthCheck[] + /** + * A closure to run once the system is launched. If you are in main, provide the `started` argument you receive from the function arguments + */ + started: ((onTerm: () => PromiseLike) => PromiseLike) | null }) { return new Daemons( options.effects, @@ -168,7 +189,6 @@ export class Daemons [], [], [], - options.healthChecks, ) } /** @@ -177,19 +197,19 @@ export class Daemons * @param options * @returns a new Daemons object */ - addDaemon( + addDaemon | null>( // prettier-ignore id: "" extends Id ? never : ErrorDuplicateId extends Id ? never : Id extends Ids ? ErrorDuplicateId : Id, - options: AddDaemonParams, + options: AddDaemonParams, ) { const daemon = "daemon" in options ? Promise.resolve(options.daemon) - : Daemon.of()( + : Daemon.of()( this.effects, options.subcontainer, options.exec, @@ -201,11 +221,10 @@ export class Daemons .filter((x) => x >= 0) .map((id) => this.healthDaemons[id]), id, - this.ids, options.ready, this.effects, ) - const daemons = this.daemons.concat(daemon) + const daemons = [...this.daemons, daemon] const ids = [...this.ids, id] as (Ids | Id)[] const healthDaemons = [...this.healthDaemons, healthDaemon] return new Daemons( @@ -214,7 +233,6 @@ export class Daemons daemons, ids, healthDaemons, - this.healthChecks, ) } @@ -225,7 +243,7 @@ export class Daemons * @param options * @returns a new Daemons object */ - addOneshot( + addOneshot | null>( id: "" extends Id ? never : ErrorDuplicateId extends Id @@ -233,9 +251,9 @@ export class Daemons : Id extends Ids ? ErrorDuplicateId : Id, - options: AddOneshotParams, + options: AddOneshotParams, ) { - const daemon = Oneshot.of()( + const daemon = Oneshot.of()( this.effects, options.subcontainer, options.exec, @@ -247,11 +265,10 @@ export class Daemons .filter((x) => x >= 0) .map((id) => this.healthDaemons[id]), id, - this.ids, "EXIT_SUCCESS", this.effects, ) - const daemons = this.daemons.concat(daemon) + const daemons = [...this.daemons, daemon] const ids = [...this.ids, id] as (Ids | Id)[] const healthDaemons = [...this.healthDaemons, healthDaemon] return new Daemons( @@ -260,13 +277,95 @@ export class Daemons daemons, ids, healthDaemons, - this.healthChecks, ) } + /** + * Returns the complete list of daemons, including a new HealthCheck defined here + * @param id + * @param options + * @returns a new Daemons object + */ + addHealthCheck( + id: "" extends Id + ? never + : ErrorDuplicateId extends Id + ? never + : Id extends Ids + ? ErrorDuplicateId + : Id, + options: AddHealthCheckParams, + ) { + const healthDaemon = new HealthDaemon( + null, + options.requires + .map((x) => this.ids.indexOf(x)) + .filter((x) => x >= 0) + .map((id) => this.healthDaemons[id]), + id, + options.ready, + this.effects, + ) + const daemons = [...this.daemons] + const ids = [...this.ids, id] as (Ids | Id)[] + const healthDaemons = [...this.healthDaemons, healthDaemon] + return new Daemons( + this.effects, + this.started, + daemons, + ids, + healthDaemons, + ) + } + + /** + * Runs the entire system until all daemons have returned `ready`. + * @param id + * @param options + * @returns a new Daemons object + */ + async runUntilSuccess(timeout: number | null) { + let resolve = (_: void) => {} + const res = new Promise((res, rej) => { + resolve = res + if (timeout) + setTimeout(() => { + const notReady = this.healthDaemons + .filter((d) => !d.isReady) + .map((d) => d.id) + rej(new Error(`Timed out waiting for ${notReady}`)) + }, timeout) + }) + const daemon = Oneshot.of()(this.effects, null, { + fn: async () => { + resolve() + return null + }, + }) + const healthDaemon = new HealthDaemon( + daemon, + [...this.healthDaemons], + "__RUN_UNTIL_SUCCESS", + "EXIT_SUCCESS", + this.effects, + ) + const daemons = await new Daemons( + this.effects, + this.started, + [...this.daemons, daemon], + this.ids, + [...this.healthDaemons, healthDaemon], + ).build() + try { + await res + } finally { + await daemons.term() + } + return null + } + async term() { try { - this.healthChecks.forEach((health) => health.stop()) for (let result of await Promise.allSettled( this.healthDaemons.map((x) => x.term()), )) { @@ -283,10 +382,7 @@ export class Daemons for (const daemon of this.healthDaemons) { await daemon.init() } - for (const health of this.healthChecks) { - health.start() - } - this.started(() => this.term()) + this.started?.(() => this.term()) return this } } diff --git a/sdk/package/lib/mainFn/HealthDaemon.ts b/sdk/package/lib/mainFn/HealthDaemon.ts index c1d9e6c2c..287f8f497 100644 --- a/sdk/package/lib/mainFn/HealthDaemon.ts +++ b/sdk/package/lib/mainFn/HealthDaemon.ts @@ -6,6 +6,7 @@ import { SetHealth, Effects, SDKManifest } from "../../../base/lib/types" import { DEFAULT_SIGTERM_TIMEOUT } from "." import { asError } from "../../../base/lib/util/asError" import { Oneshot } from "./Oneshot" +import { SubContainer } from "../util/SubContainer" const oncePromise = () => { let resolve: (value: T) => void @@ -30,16 +31,22 @@ export class HealthDaemon { private running = false private started?: number private resolveReady: (() => void) | undefined + private resolvedReady: boolean = false private readyPromise: Promise constructor( - private readonly daemon: Promise>, + private readonly daemon: Promise> | null, private readonly dependencies: HealthDaemon[], readonly id: string, - readonly ids: string[], readonly ready: Ready | typeof EXIT_SUCCESS, readonly effects: Effects, ) { - this.readyPromise = new Promise((resolve) => (this.resolveReady = resolve)) + this.readyPromise = new Promise( + (resolve) => + (this.resolveReady = () => { + resolve() + this.resolvedReady = true + }), + ) this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus())) } @@ -52,7 +59,7 @@ export class HealthDaemon { this.running = false this.healthCheckCleanup?.() - await this.daemon.then((d) => + await this.daemon?.then((d) => d.term({ ...termOptions, }), @@ -74,11 +81,13 @@ export class HealthDaemon { this.running = newStatus if (newStatus) { - ;(await this.daemon).start() - this.started = performance.now() + console.debug(`Launching ${this.id}...`) this.setupHealthCheck() + ;(await this.daemon)?.start() + this.started = performance.now() } else { - ;(await this.daemon).stop() + console.debug(`Stopping ${this.id}...`) + ;(await this.daemon)?.stop() this.turnOffHealthCheck() this.setHealth({ result: "starting", message: null }) @@ -88,10 +97,19 @@ export class HealthDaemon { private healthCheckCleanup: (() => null) | null = null private turnOffHealthCheck() { this.healthCheckCleanup?.() + + this.resolvedReady = false + this.readyPromise = new Promise( + (resolve) => + (this.resolveReady = () => { + resolve() + this.resolvedReady = true + }), + ) } private async setupHealthCheck() { const daemon = await this.daemon - daemon.onExit((success) => { + daemon?.onExit((success) => { if (success && this.ready === "EXIT_SUCCESS") { this.setHealth({ result: "success", message: null }) } else if (!success) { @@ -122,28 +140,17 @@ export class HealthDaemon { !res.done; res = await Promise.race([status, trigger.next()]) ) { - const handle = (await this.daemon).subcontainerRc() - - try { - const response: HealthCheckResult = await Promise.resolve( - this.ready.fn(handle), - ).catch((err) => { - console.error(asError(err)) - return { - result: "failure", - message: "message" in err ? err.message : String(err), - } - }) - if ( - this.resolveReady && - (response.result === "success" || response.result === "disabled") - ) { - this.resolveReady() + const response: HealthCheckResult = await Promise.resolve( + this.ready.fn(), + ).catch((err) => { + console.error(asError(err)) + return { + result: "failure", + message: "message" in err ? err.message : String(err), } - await this.setHealth(response) - } finally { - await handle.destroy() - } + }) + + await this.setHealth(response) } }).catch((err) => console.error(`Daemon ${this.id} failed: ${err}`)) @@ -158,10 +165,18 @@ export class HealthDaemon { return this.readyPromise } + get isReady() { + return this.resolvedReady + } + private async setHealth(health: HealthCheckResult) { + const changed = this._health.result !== health.result this._health = health + if (this.resolveReady && health.result === "success") { + this.resolveReady() + } + if (changed) this.healthWatchers.forEach((watcher) => watcher()) if (this.ready === "EXIT_SUCCESS") return - this.healthWatchers.forEach((watcher) => watcher()) const display = this.ready.display if (!display) { return @@ -182,8 +197,18 @@ export class HealthDaemon { } async updateStatus() { - const healths = this.dependencies.map((d) => d.running && d._health) - this.changeRunning(healths.every((x) => x && x.result === "success")) + const healths = this.dependencies.map((d) => ({ + health: d.running && d._health, + id: d.id, + })) + const waitingOn = healths.filter( + (h) => !h.health || h.health.result !== "success", + ) + if (waitingOn.length) + console.debug( + `daemon ${this.id} waiting on ${waitingOn.map((w) => w.id)}`, + ) + this.changeRunning(!waitingOn.length) } async init() { diff --git a/sdk/package/lib/mainFn/Oneshot.ts b/sdk/package/lib/mainFn/Oneshot.ts index 33200c4c1..b210b868b 100644 --- a/sdk/package/lib/mainFn/Oneshot.ts +++ b/sdk/package/lib/mainFn/Oneshot.ts @@ -10,19 +10,25 @@ import { DaemonCommandType } from "./Daemons" * unlike Daemon, does not restart on success */ -export class Oneshot extends Daemon { +export class Oneshot< + Manifest extends T.SDKManifest, + C extends SubContainer | null = SubContainer | null, +> extends Daemon { static of() { - return async ( + return async | null>( effects: T.Effects, - subcontainer: SubContainer, - exec: DaemonCommandType | null, + subcontainer: C, + exec: DaemonCommandType, ) => { - if (subcontainer.isOwned()) subcontainer = subcontainer.rc() - const startCommand = exec - ? () => - CommandController.of()(effects, subcontainer.rc(), exec) - : null - return new Oneshot(subcontainer, startCommand, true) + let subc: SubContainer | null = subcontainer + if (subcontainer && subcontainer.isOwned()) subc = subcontainer.rc() + const startCommand = () => + CommandController.of()( + effects, + (subc?.rc() ?? null) as C, + exec, + ) + return new Oneshot(subcontainer, startCommand, true) } } } diff --git a/sdk/package/lib/manifest/setupManifest.ts b/sdk/package/lib/manifest/setupManifest.ts index 0995b3f51..17d7d7d3a 100644 --- a/sdk/package/lib/manifest/setupManifest.ts +++ b/sdk/package/lib/manifest/setupManifest.ts @@ -6,7 +6,7 @@ import { } from "../../../base/lib/types/ManifestTypes" import { OSVersion } from "../StartSdk" import { VersionGraph } from "../version/VersionGraph" -import { execSync } from "child_process" +import { version as sdkVersion } from "../../package.json" /** * @description Use this function to define critical information about your package @@ -55,6 +55,7 @@ export function buildManifest< return { ...manifest, osVersion: manifest.osVersion ?? OSVersion, + sdkVersion, version: versions.current.options.version, releaseNotes: versions.current.options.releaseNotes, satisfies: versions.current.options.satisfies || [], diff --git a/sdk/package/lib/util/Drop.ts b/sdk/package/lib/util/Drop.ts index bb8dd53ef..1cd12b516 100644 --- a/sdk/package/lib/util/Drop.ts +++ b/sdk/package/lib/util/Drop.ts @@ -5,18 +5,18 @@ export abstract class Drop { if (weak) weak.drop() }) private static idCtr: number = 0 - private id: number - private ref: { id: number } | WeakRef<{ id: number }> + private dropId?: number + private dropRef?: { id: number } | WeakRef<{ id: number }> protected constructor() { - this.id = Drop.idCtr++ - this.ref = { id: this.id } + this.dropId = Drop.idCtr++ + this.dropRef = { id: this.dropId } const weak = this.weak() - Drop.weak[this.id] = weak - Drop.registry.register(this.ref, this.id, this.ref) + Drop.weak[this.dropId] = weak + Drop.registry.register(this.dropRef, this.dropId, this.dropRef) return new Proxy(this, { set(target: any, prop, value) { - if (prop === "ref") return false + if (prop === "dropRef" || prop == "dropId") return false target[prop] = value ;(weak as any)[prop] = value return true @@ -26,13 +26,21 @@ export abstract class Drop { protected register() {} protected weak(): this { const weak = Object.assign(Object.create(Object.getPrototypeOf(this)), this) - weak.ref = new WeakRef(this.ref) + if (this.dropRef) weak.ref = new WeakRef(this.dropRef) return weak } abstract onDrop(): void drop(): void { + if (!this.dropRef || !this.dropId) return this.onDrop() - Drop.registry.unregister(this.ref) - delete Drop.weak[this.id] + this.leak() + } + leak(): this { + if (!this.dropRef || !this.dropId) return this + Drop.registry.unregister(this.dropRef) + delete Drop.weak[this.dropId] + delete this.dropRef + delete this.dropId + return this } } diff --git a/sdk/package/lib/util/fileHelper.ts b/sdk/package/lib/util/fileHelper.ts index 06ebfb4ff..4bf1e4047 100644 --- a/sdk/package/lib/util/fileHelper.ts +++ b/sdk/package/lib/util/fileHelper.ts @@ -340,9 +340,17 @@ export class FileHelper { /** * Accepts full structured data and overwrites the existing file on disk if it exists. */ - async write(effects: T.Effects, data: T.AllowReadonly | A) { + async write( + effects: T.Effects, + data: T.AllowReadonly | A, + options: { allowWriteAfterConst?: boolean } = {}, + ) { await this.writeFile(this.validate(data)) - if (effects.constRetry && this.consts.includes(effects.constRetry)) + if ( + !options.allowWriteAfterConst && + effects.constRetry && + this.consts.includes(effects.constRetry) + ) throw new Error(`Canceled: write after const: ${this.path}`) return null } @@ -350,7 +358,11 @@ export class FileHelper { /** * Accepts partial structured data and performs a merge with the existing file on disk. */ - async merge(effects: T.Effects, data: T.AllowReadonly>) { + async merge( + effects: T.Effects, + data: T.AllowReadonly>, + options: { allowWriteAfterConst?: boolean } = {}, + ) { const fileDataRaw = await this.readFileRaw() let fileData: any = fileDataRaw === null ? null : this.readData(fileDataRaw) try { @@ -360,7 +372,11 @@ export class FileHelper { const toWrite = this.writeData(mergeData) if (toWrite !== fileDataRaw) { this.writeFile(mergeData) - if (effects.constRetry && this.consts.includes(effects.constRetry)) { + if ( + !options.allowWriteAfterConst && + effects.constRetry && + this.consts.includes(effects.constRetry) + ) { const diff = partialDiff(fileData, mergeData as any) if (!diff) { return null diff --git a/sdk/package/lib/version/VersionGraph.ts b/sdk/package/lib/version/VersionGraph.ts index 06cb8f272..64dec0683 100644 --- a/sdk/package/lib/version/VersionGraph.ts +++ b/sdk/package/lib/version/VersionGraph.ts @@ -7,6 +7,7 @@ import { InitScriptOrFn, UninitFn, UninitScript, + UninitScriptOrFn, } from "../../../base/lib/inits" import { Graph, Vertex, once } from "../util" import { IMPOSSIBLE, VersionInfo } from "./VersionInfo" @@ -171,11 +172,11 @@ export class VersionGraph /** * A script to run only on fresh install */ - preInstall?: InitScript | InitFn + preInstall?: InitScriptOrFn<"install"> /** * A script to run only on uninstall */ - uninstall?: UninitScript | UninitFn + uninstall?: UninitScriptOrFn }) { return new VersionGraph( options.current, diff --git a/sdk/package/package-lock.json b/sdk/package/package-lock.json index 0fcc2ee8e..af340b6ec 100644 --- a/sdk/package/package-lock.json +++ b/sdk/package/package-lock.json @@ -1,12 +1,12 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.27", + "version": "0.4.0-beta.30", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.27", + "version": "0.4.0-beta.30", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/sdk/package/package.json b/sdk/package/package.json index 1e6351f57..d9df3c7cf 100644 --- a/sdk/package/package.json +++ b/sdk/package/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.27", + "version": "0.4.0-beta.30", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./package/lib/index.js", "types": "./package/lib/index.d.ts", @@ -22,14 +22,14 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/Start9Labs/start-sdk.git" + "url": "git+https://github.com/Start9Labs/start-os.git" }, "author": "Start9 Labs", "license": "MIT", "bugs": { - "url": "https://github.com/Start9Labs/start-sdk/issues" + "url": "https://github.com/Start9Labs/start-os/issues" }, - "homepage": "https://github.com/Start9Labs/start-sdk#readme", + "homepage": "https://github.com/Start9Labs/start-os#readme", "dependencies": { "isomorphic-fetch": "^3.0.0", "mime": "^4.0.7", diff --git a/sdk/package/tsconfig.json b/sdk/package/tsconfig.json index 20f7f60f7..2e903eaab 100644 --- a/sdk/package/tsconfig.json +++ b/sdk/package/tsconfig.json @@ -12,7 +12,8 @@ "skipLibCheck": true, "module": "commonjs", "outDir": "../dist", - "target": "es2021" + "target": "es2021", + "resolveJsonModule": true }, "include": ["lib/**/*", "../base/lib/util/Hostname.ts"], "exclude": ["lib/**/*.spec.ts", "lib/**/*.gen.ts", "list", "node_modules"] diff --git a/web/package-lock.json b/web/package-lock.json index 6a60bf3d7..61cf30989 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "startos-ui", - "version": "0.4.0-alpha.6", + "version": "0.4.0-alpha.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "startos-ui", - "version": "0.4.0-alpha.6", + "version": "0.4.0-alpha.7", "license": "MIT", "dependencies": { "@angular/animations": "^19.2.11", diff --git a/web/package.json b/web/package.json index e20389397..024f7f6a8 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "startos-ui", - "version": "0.4.0-alpha.6", + "version": "0.4.0-alpha.7", "author": "Start9 Labs, Inc", "homepage": "https://start9.com/", "license": "MIT", diff --git a/web/projects/install-wizard/src/index.html b/web/projects/install-wizard/src/index.html index ab121c687..8e7a2fd52 100644 --- a/web/projects/install-wizard/src/index.html +++ b/web/projects/install-wizard/src/index.html @@ -1,4 +1,4 @@ - + @@ -14,7 +14,14 @@ - + + + diff --git a/web/projects/setup-wizard/src/index.html b/web/projects/setup-wizard/src/index.html index 9c6e49a64..492dd25af 100644 --- a/web/projects/setup-wizard/src/index.html +++ b/web/projects/setup-wizard/src/index.html @@ -1,4 +1,4 @@ - + @@ -15,7 +15,14 @@ - + + + diff --git a/web/projects/shared/assets/icons/site.webmanifest b/web/projects/shared/assets/icons/site.webmanifest deleted file mode 100644 index 71d508e3d..000000000 --- a/web/projects/shared/assets/icons/site.webmanifest +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "StartOS", - "short_name": "StartOS", - "icons": [ - { - "src": "/assets/icons/web-app-manifest-192x192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "/assets/icons/web-app-manifest-512x512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} \ No newline at end of file diff --git a/web/projects/shared/assets/icons/startos-192x192.png b/web/projects/shared/assets/icons/startos-192x192.png new file mode 100644 index 000000000..76383057e Binary files /dev/null and b/web/projects/shared/assets/icons/startos-192x192.png differ diff --git a/web/projects/shared/assets/icons/startos-512x512.png b/web/projects/shared/assets/icons/startos-512x512.png new file mode 100644 index 000000000..25fa6919f Binary files /dev/null and b/web/projects/shared/assets/icons/startos-512x512.png differ diff --git a/web/projects/shared/assets/icons/web-app-manifest-192x192.png b/web/projects/shared/assets/icons/web-app-manifest-192x192.png deleted file mode 100644 index 24d2c68eb..000000000 Binary files a/web/projects/shared/assets/icons/web-app-manifest-192x192.png and /dev/null differ diff --git a/web/projects/shared/assets/icons/web-app-manifest-512x512.png b/web/projects/shared/assets/icons/web-app-manifest-512x512.png deleted file mode 100644 index 8d9af255a..000000000 Binary files a/web/projects/shared/assets/icons/web-app-manifest-512x512.png and /dev/null differ diff --git a/web/projects/shared/assets/img/background_light.jpeg b/web/projects/shared/assets/img/background_light.jpeg deleted file mode 100644 index 0795145f9..000000000 Binary files a/web/projects/shared/assets/img/background_light.jpeg and /dev/null differ diff --git a/web/projects/shared/assets/img/icon_apple_touch.png b/web/projects/shared/assets/img/icon_apple_touch.png deleted file mode 100644 index 3ca45d443..000000000 Binary files a/web/projects/shared/assets/img/icon_apple_touch.png and /dev/null differ diff --git a/web/projects/shared/assets/img/icon_transparent.svg b/web/projects/shared/assets/img/icon_transparent.svg deleted file mode 100644 index 93d58edf3..000000000 --- a/web/projects/shared/assets/img/icon_transparent.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/web/projects/shared/assets/img/icons/home.svg b/web/projects/shared/assets/img/icons/home.svg deleted file mode 100644 index 90547de82..000000000 --- a/web/projects/shared/assets/img/icons/home.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/web/projects/shared/assets/img/icons/logo-bitcoin.svg b/web/projects/shared/assets/img/icons/logo-bitcoin.svg deleted file mode 100644 index 89958abb0..000000000 --- a/web/projects/shared/assets/img/icons/logo-bitcoin.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/projects/shared/assets/img/temp/one.png b/web/projects/shared/assets/img/temp/one.png deleted file mode 100644 index c7e9a4398..000000000 Binary files a/web/projects/shared/assets/img/temp/one.png and /dev/null differ diff --git a/web/projects/shared/assets/img/temp/three.png b/web/projects/shared/assets/img/temp/three.png deleted file mode 100644 index ec09d41a1..000000000 Binary files a/web/projects/shared/assets/img/temp/three.png and /dev/null differ diff --git a/web/projects/shared/assets/img/temp/two.png b/web/projects/shared/assets/img/temp/two.png deleted file mode 100644 index 3aa62e509..000000000 Binary files a/web/projects/shared/assets/img/temp/two.png and /dev/null differ diff --git a/web/projects/shared/src/i18n/dictionaries/de.ts b/web/projects/shared/src/i18n/dictionaries/de.ts index 3439cb7c4..68ae72751 100644 --- a/web/projects/shared/src/i18n/dictionaries/de.ts +++ b/web/projects/shared/src/i18n/dictionaries/de.ts @@ -521,4 +521,5 @@ export default { 519: 'Um Clearnet-Domains zu veröffentlichen, musst du oben auf „Öffentlich machen“ klicken.', 520: 'Update verfügbar', 521: 'Um das Problem zu beheben, siehe', + 522: 'SDK Version', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/en.ts b/web/projects/shared/src/i18n/dictionaries/en.ts index d097f4694..e72520606 100644 --- a/web/projects/shared/src/i18n/dictionaries/en.ts +++ b/web/projects/shared/src/i18n/dictionaries/en.ts @@ -520,4 +520,5 @@ export const ENGLISH = { 'To publish clearnet domains, you must click "Make Public", above.': 519, 'Update available': 520, 'To resolve the issue, refer to': 521, + 'SDK Version': 522, } as const diff --git a/web/projects/shared/src/i18n/dictionaries/es.ts b/web/projects/shared/src/i18n/dictionaries/es.ts index bcac39cf0..5e4411b3b 100644 --- a/web/projects/shared/src/i18n/dictionaries/es.ts +++ b/web/projects/shared/src/i18n/dictionaries/es.ts @@ -521,4 +521,5 @@ export default { 519: 'Para publicar dominios en clearnet, debes hacer clic en "Hacer público" arriba.', 520: 'Actualización disponible', 521: 'Para resolver el problema, consulta', + 522: 'Versión de SDK', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/fr.ts b/web/projects/shared/src/i18n/dictionaries/fr.ts index c3af56e08..235c4c57e 100644 --- a/web/projects/shared/src/i18n/dictionaries/fr.ts +++ b/web/projects/shared/src/i18n/dictionaries/fr.ts @@ -521,4 +521,5 @@ export default { 519: 'Pour publier des domaines clearnet, vous devez cliquer sur « Rendre public » ci-dessus.', 520: 'Mise à jour disponible', 521: 'Pour résoudre le problème, consultez', + 522: 'Version de SDK', } satisfies i18n diff --git a/web/projects/shared/src/i18n/dictionaries/pl.ts b/web/projects/shared/src/i18n/dictionaries/pl.ts index 2d81b99d9..af3433c42 100644 --- a/web/projects/shared/src/i18n/dictionaries/pl.ts +++ b/web/projects/shared/src/i18n/dictionaries/pl.ts @@ -521,4 +521,5 @@ export default { 519: 'Aby opublikować domeny w clearnet, kliknij „Upublicznij” powyżej.', 520: 'Aktualizacja dostępna', 521: 'Aby rozwiązać problem, zapoznaj się z', + 522: 'Wersja SDK', } satisfies i18n diff --git a/web/projects/shared/src/util/format-progress.ts b/web/projects/shared/src/util/format-progress.ts index 6b974516e..ba4045048 100644 --- a/web/projects/shared/src/util/format-progress.ts +++ b/web/projects/shared/src/util/format-progress.ts @@ -12,15 +12,10 @@ export function formatProgress({ phases, overall }: T.FullProgress): { p, ): p is { name: string - progress: - | false - | { - done: number - total: number | null - } + progress: false | ProgressDetails } => p.progress !== true && p.progress !== null, ) - .map(p => `${p.name}${getPhaseBytes(p.progress)}`) + .map(p => `${p.name}${getDetails(p.progress)}`) .join(', '), } } @@ -35,13 +30,14 @@ function getDecimal(progress: T.Progress): number { } } -function getPhaseBytes( - progress: - | false - | { - done: number - total: number | null - }, -) { - return progress ? `: ${progress.done}/${progress.total}` : '' +function getDetails(progress: false | ProgressDetails) { + return progress + ? `: ${progress.done}/${progress.total} ${progress.units || ''}` + : '' +} + +type ProgressDetails = { + done: number + total: number | null + units: T.ProgressUnits | null } diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts index 58b7718ac..4a96649a2 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/clearnet.component.ts @@ -202,6 +202,11 @@ export class InterfaceClearnetComponent { } const loader = this.loader.open('Removing').subscribe() + + if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url)) { + url = 'http://' + url + } + const params = { domain: new URL(url).hostname } try { diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/dependencies.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/dependencies.component.ts index 6d4c2c524..42bf44de3 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/dependencies.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/dependencies.component.ts @@ -32,7 +32,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' {{ error | i18n }} @if (getHealthCheckName(d.key); as healthCheckName) { - : {{ getHealthCheckName }} + : {{ healthCheckName }} } } @else { diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/about.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/about.component.ts index c3029d048..5fad11bbe 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/about.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/about.component.ts @@ -81,9 +81,17 @@ export default class ServiceAboutRoute { icon: '@tui.copy', action: () => this.copyService.copy(manifest.version), }, + { + name: 'SDK Version', + value: manifest.sdkVersion || '-', + icon: manifest.sdkVersion ? '@tui.copy' : '', + action: () => + manifest.sdkVersion && + this.copyService.copy(manifest.sdkVersion), + }, { name: 'Git Hash', - value: manifest.gitHash || 'Unknown', + value: manifest.gitHash || '-', icon: manifest.gitHash ? '@tui.copy' : '', action: () => manifest.gitHash && this.copyService.copy(manifest.gitHash), diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index c15036de9..0742d1796 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -110,7 +110,7 @@ export namespace Mock { squashfs: { aarch64: { publishedAt: '2025-04-21T20:58:48.140749883Z', - url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.6/startos-0.4.0-alpha.6-33ae46f~dev_aarch64.squashfs', + url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.7/startos-0.4.0-alpha.7-33ae46f~dev_aarch64.squashfs', commitment: { hash: '4elBFVkd/r8hNadKmKtLIs42CoPltMvKe2z3LRqkphk=', size: 1343500288, @@ -122,7 +122,7 @@ export namespace Mock { }, 'aarch64-nonfree': { publishedAt: '2025-04-21T21:07:00.249285116Z', - url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.6/startos-0.4.0-alpha.6-33ae46f~dev_aarch64-nonfree.squashfs', + url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.7/startos-0.4.0-alpha.7-33ae46f~dev_aarch64-nonfree.squashfs', commitment: { hash: 'MrCEi4jxbmPS7zAiGk/JSKlMsiuKqQy6RbYOxlGHOIQ=', size: 1653075968, @@ -134,7 +134,7 @@ export namespace Mock { }, raspberrypi: { publishedAt: '2025-04-21T21:16:12.933319237Z', - url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.6/startos-0.4.0-alpha.6-33ae46f~dev_raspberrypi.squashfs', + url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.7/startos-0.4.0-alpha.7-33ae46f~dev_raspberrypi.squashfs', commitment: { hash: '/XTVQRCqY3RK544PgitlKu7UplXjkmzWoXUh2E4HCw0=', size: 1490731008, @@ -146,7 +146,7 @@ export namespace Mock { }, x86_64: { publishedAt: '2025-04-21T21:14:20.246908903Z', - url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.6/startos-0.4.0-alpha.6-33ae46f~dev_x86_64.squashfs', + url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.7/startos-0.4.0-alpha.7-33ae46f~dev_x86_64.squashfs', commitment: { hash: '/6romKTVQGSaOU7FqSZdw0kFyd7P+NBSYNwM3q7Fe44=', size: 1411657728, @@ -158,7 +158,7 @@ export namespace Mock { }, 'x86_64-nonfree': { publishedAt: '2025-04-21T21:15:17.955265284Z', - url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.6/startos-0.4.0-alpha.6-33ae46f~dev_x86_64-nonfree.squashfs', + url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.7/startos-0.4.0-alpha.7-33ae46f~dev_x86_64-nonfree.squashfs', commitment: { hash: 'HCRq9sr/0t85pMdrEgNBeM4x11zVKHszGnD1GDyZbSE=', size: 1731035136, @@ -226,6 +226,7 @@ export namespace Mock { stop: null, }, osVersion: '0.2.12', + sdkVersion: '0.4.0', dependencies: {}, images: { main: { @@ -270,6 +271,7 @@ export namespace Mock { stop: null, }, osVersion: '0.2.12', + sdkVersion: '0.4.0', dependencies: { bitcoind: { description: 'LND needs bitcoin to live.', @@ -325,6 +327,7 @@ export namespace Mock { stop: null, }, osVersion: '0.2.12', + sdkVersion: '0.4.0', dependencies: { bitcoind: { description: 'Bitcoin Proxy requires a Bitcoin node.', diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index b8da90531..ef123ecc8 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -32,6 +32,7 @@ const PROGRESS: T.FullProgress = { overall: { done: 0, total: 120, + units: 'bytes', }, phases: [ { @@ -39,6 +40,7 @@ const PROGRESS: T.FullProgress = { progress: { done: 0, total: 40, + units: 'bytes', }, }, { diff --git a/web/projects/ui/src/main.ts b/web/projects/ui/src/main.ts index 52e051587..21499c3cd 100644 --- a/web/projects/ui/src/main.ts +++ b/web/projects/ui/src/main.ts @@ -1,11 +1,8 @@ import { enableProdMode } from '@angular/core' import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' - import { AppModule } from './app/app.module' import { environment } from './environments/environment' -; (window as any).global = window - if (environment.production) { enableProdMode() } diff --git a/web/projects/ui/src/manifest.webmanifest b/web/projects/ui/src/manifest.webmanifest index 9dca24b6a..9f4969adb 100644 --- a/web/projects/ui/src/manifest.webmanifest +++ b/web/projects/ui/src/manifest.webmanifest @@ -1,24 +1,16 @@ { "name": "StartOS", - "short_name": "StartOS", - "theme_color": "#ff5b71", - "background_color": "#1e1e1e", + "background_color": "#f0f0f0", "display": "standalone", - "scope": ".", - "start_url": "/?version=036", - "id": "/?version=036", + "start_url": "./", "icons": [ { - "src": "/assets/icons/web-app-manifest-192x192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "any" + "src": "/assets/icons/startos-192x192.png", + "sizes": "192x192" }, { - "src": "/assets/icons/web-app-manifest-512x512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "any" + "src": "/assets/icons/startos-512x512.png", + "sizes": "512x512" } ] }