addHealthCheck instead of additionalHealthChecks for Daemons (#2962)

* addHealthCheck on Daemons

* fix bug that prevents domains without protocols from being deleted

* fixes from testing

* version bump

* add sdk version to UI

* fix useEntrypoint

* fix dependency health check error display

* minor fixes

* beta.29

* fixes from testing

* beta.30

* set /etc/os-release (#2918)

* remove check-monitor from kiosk (#2059)

* add units for progress (#2693)

* use new progress type

* alpha.7

* fix up pwa stuff

* fix wormhole-squashfs and prune boot (#2964)

* don't exit on expected errors

* use bash

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Aiden McClelland
2025-06-17 23:50:01 +00:00
committed by GitHub
parent f5688e077a
commit 3ec4db0225
100 changed files with 846 additions and 757 deletions

View File

@@ -179,7 +179,7 @@ wormhole-squashfs: results/$(BASENAME).squashfs
$(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}')) $(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 "Paste the following command into the shell of your StartOS server:"
@echo @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) update: $(ALL_TARGETS)
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi @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_SUM := $(shell b3sum results/$(BASENAME).squashfs))
$(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}')) $(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}'))
$(call ssh,'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE)') $(call ssh,'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE)')
$(call cp,results/$(BASENAME).squashfs,/media/startos/images/$(SQFS_SUM).rootfs) $(call ssh,'/usr/lib/startos/scripts/prune-boot')
$(call ssh,'sudo ln -rsf /media/startos/images/$(SQFS_SUM).rootfs /media/startos/config/current.rootfs') $(call cp,results/$(BASENAME).squashfs,/media/startos/images/next.rootfs)
$(call ssh,'sudo reboot') $(call ssh,'sudo CHECKSUM=$(SQFS_SUM) /usr/lib/startos/scripts/use-img /media/startos/images/next.rootfs')
emulate-reflash: $(ALL_TARGETS) emulate-reflash: $(ALL_TARGETS)
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi

View File

@@ -1,8 +0,0 @@
#!/bin/sh
if cat /sys/class/drm/*/status | grep -qw connected; then
exit 0
else
exit 1
fi

View File

@@ -91,15 +91,6 @@ cat > /home/kiosk/kiosk.sh << 'EOF'
while ! curl "http://localhost" > /dev/null; do while ! curl "http://localhost" > /dev/null; do
sleep 1 sleep 1
done 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 & matchbox-window-manager -use_titlebar no &
cp -r /home/kiosk/fx-profile /home/kiosk/fx-profile-tmp cp -r /home/kiosk/fx-profile /home/kiosk/fx-profile-tmp
firefox-esr http://localhost --profile /home/kiosk/fx-profile-tmp firefox-esr http://localhost --profile /home/kiosk/fx-profile-tmp

35
build/lib/scripts/prune-boot Executable file
View File

@@ -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."

61
build/lib/scripts/use-img Executable file
View File

@@ -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 <SQUASHFS>"
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

View File

@@ -38,7 +38,7 @@
}, },
"../sdk/dist": { "../sdk/dist": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.27", "version": "0.4.0-beta.30",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^3.0.0", "@iarna/toml": "^3.0.0",

View File

@@ -20,7 +20,9 @@ export class MainLoop {
private subcontainerRc?: SubContainerRc<SDKManifest> private subcontainerRc?: SubContainerRc<SDKManifest>
get mainSubContainerHandle() { get mainSubContainerHandle() {
this.subcontainerRc = this.subcontainerRc =
this.subcontainerRc ?? this.mainEvent?.daemon?.subcontainerRc() this.subcontainerRc ??
this.mainEvent?.daemon?.subcontainerRc() ??
undefined
return this.subcontainerRc return this.subcontainerRc
} }
private healthLoops?: { private healthLoops?: {

2
core/Cargo.lock generated
View File

@@ -5975,7 +5975,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "start-os" name = "start-os"
version = "0.4.0-alpha.6" version = "0.4.0-alpha.7"
dependencies = [ dependencies = [
"aes 0.7.5", "aes 0.7.5",
"async-acme", "async-acme",

View File

@@ -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<String>,
UnboundedSender<container_init::Output>,
Option<Duration>,
) -> Pin<Box<dyn Future<Output = Result<RpcId, String>> + '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<Box<dyn Future<Output = Result<(), String>> + 'static>>
+ Send
+ Sync
+ 'static,
>;
pub trait CommandInserter {
fn insert_command(
&self,
command: String,
args: Vec<String>,
sender: UnboundedSender<container_init::Output>,
timeout: Option<Duration>,
) -> Pin<Box<dyn Future<Output = Option<RpcId>>>>;
fn send_signal(&self, id: RpcId, command: u32) -> Pin<Box<dyn Future<Output = ()>>>;
}
pub type ArcCommandInserter = Arc<Mutex<Option<Box<dyn CommandInserter>>>>;
pub struct ExecutingCommand {
rpc_id: RpcId,
/// Will exist until killed
command_inserter: Arc<Mutex<Option<ArcCommandInserter>>>,
owned_futures: Arc<Mutex<Vec<Pin<Box<dyn Future<Output = ()>>>>>>,
}
impl ExecutingCommand {
pub async fn new(
command_inserter: ArcCommandInserter,
command: String,
args: Vec<String>,
timeout: Option<Duration>,
) -> Result<ExecutingCommand, color_eyre::Report> {
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel::<Output>();
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<Output>,
) -> Result<String, (Option<i32>, String)> {
let (process_id_send, process_id_recv) = tokio::sync::oneshot::channel::<ProcessId>();
let mut answer = String::new();
let mut command_error = String::new();
let mut status: Option<i32> = 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;
});
}
}

View File

@@ -14,7 +14,7 @@ keywords = [
name = "start-os" name = "start-os"
readme = "README.md" readme = "README.md"
repository = "https://github.com/Start9Labs/start-os" repository = "https://github.com/Start9Labs/start-os"
version = "0.4.0-alpha.6" # VERSION_BUMP version = "0.4.0-alpha.7" # VERSION_BUMP
license = "MIT" license = "MIT"
[lib] [lib]

View File

@@ -252,14 +252,18 @@ impl fmt::Display for ActionResultV1 {
} }
} }
pub fn display_action_result<T: Serialize>(params: WithIoFormat<T>, result: Option<ActionResult>) { pub fn display_action_result<T: Serialize>(
params: WithIoFormat<T>,
result: Option<ActionResult>,
) -> Result<(), Error> {
let Some(result) = result else { let Some(result) = result else {
return; return Ok(());
}; };
if let Some(format) = params.format { if let Some(format) = params.format {
return display_serializable(format, result); return display_serializable(format, result);
} }
println!("{result}") println!("{result}");
Ok(())
} }
#[derive(Deserialize, Serialize, TS)] #[derive(Deserialize, Serialize, TS)]

View File

@@ -328,9 +328,7 @@ pub fn session<C: Context>() -> ParentHandler<C> {
from_fn_async(list) from_fn_async(list)
.with_metadata("get_session", Value::Bool(true)) .with_metadata("get_session", Value::Bool(true))
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn(|handle, result| { .with_custom_display_fn(|handle, result| display_sessions(handle.params, result))
Ok(display_sessions(handle.params, result))
})
.with_about("Display all server sessions") .with_about("Display all server sessions")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
@@ -343,7 +341,7 @@ pub fn session<C: Context>() -> ParentHandler<C> {
) )
} }
fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) { fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) -> Result<(), Error> {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
@@ -371,7 +369,8 @@ fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) {
} }
table.add_row(row); table.add_row(row);
} }
table.print_tty(false).unwrap(); table.print_tty(false)?;
Ok(())
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]

View File

@@ -20,6 +20,7 @@ use crate::disk::mount::filesystem::ReadWrite;
use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard};
use crate::init::init; use crate::init::init;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::ProgressUnits;
use crate::s9pk::S9pk; use crate::s9pk::S9pk;
use crate::service::service_map::DownloadInstallFuture; use crate::service::service_map::DownloadInstallFuture;
use crate::setup::SetupExecuteProgress; use crate::setup::SetupExecuteProgress;
@@ -136,6 +137,7 @@ pub async fn recover_full_embassy(
.collect(); .collect();
let tasks = restore_packages(&rpc_ctx, backup_guard, ids).await?; let tasks = restore_packages(&rpc_ctx, backup_guard, ids).await?;
restore_phase.set_total(tasks.len() as u64); restore_phase.set_total(tasks.len() as u64);
restore_phase.set_units(Some(ProgressUnits::Steps));
let restore_phase = Arc::new(Mutex::new(restore_phase)); let restore_phase = Arc::new(Mutex::new(restore_phase));
stream::iter(tasks) stream::iter(tasks)
.for_each_concurrent(5, |(id, res)| { .for_each_concurrent(5, |(id, res)| {

View File

@@ -157,7 +157,7 @@ pub fn target<C: Context>() -> ParentHandler<C> {
from_fn_async(info) from_fn_async(info)
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn::<CliContext, _>(|params, info| { .with_custom_display_fn::<CliContext, _>(|params, info| {
Ok(display_backup_info(params.params, info)) display_backup_info(params.params, info)
}) })
.with_about("Display package backup information") .with_about("Display package backup information")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
@@ -227,7 +227,7 @@ pub struct PackageBackupInfo {
pub timestamp: DateTime<Utc>, pub timestamp: DateTime<Utc>,
} }
fn display_backup_info(params: WithIoFormat<InfoParams>, info: BackupInfo) { fn display_backup_info(params: WithIoFormat<InfoParams>, info: BackupInfo) -> Result<(), Error> {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
@@ -260,7 +260,8 @@ fn display_backup_info(params: WithIoFormat<InfoParams>, info: BackupInfo) {
]; ];
table.add_row(row); table.add_row(row);
} }
table.print_tty(false).unwrap(); table.print_tty(false)?;
Ok(())
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]

View File

@@ -48,9 +48,7 @@ pub fn disk<C: Context>() -> ParentHandler<C> {
"list", "list",
from_fn_async(list) from_fn_async(list)
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn(|handle, result| { .with_custom_display_fn(|handle, result| display_disk_info(handle.params, result))
Ok(display_disk_info(handle.params, result))
})
.with_about("List disk info") .with_about("List disk info")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
@@ -65,7 +63,7 @@ pub fn disk<C: Context>() -> ParentHandler<C> {
) )
} }
fn display_disk_info(params: WithIoFormat<Empty>, args: Vec<DiskInfo>) { fn display_disk_info(params: WithIoFormat<Empty>, args: Vec<DiskInfo>) -> Result<(), Error> {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
@@ -124,7 +122,8 @@ fn display_disk_info(params: WithIoFormat<Empty>, args: Vec<DiskInfo>) {
table.add_row(row); table.add_row(row);
} }
} }
table.print_tty(false).unwrap(); table.print_tty(false)?;
Ok(())
} }
// #[command(display(display_disk_info))] // #[command(display(display_disk_info))]

View File

@@ -32,7 +32,7 @@ use crate::net::utils::find_wifi_iface;
use crate::net::web_server::{UpgradableListener, WebServerAcceptorSetter}; use crate::net::web_server::{UpgradableListener, WebServerAcceptorSetter};
use crate::prelude::*; use crate::prelude::*;
use crate::progress::{ use crate::progress::{
FullProgress, FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar, FullProgress, FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar, ProgressUnits,
}; };
use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::rpc_continuations::{Guid, RpcContinuation};
use crate::s9pk::v2::pack::{CONTAINER_DATADIR, CONTAINER_TOOL}; use crate::s9pk::v2::pack::{CONTAINER_DATADIR, CONTAINER_TOOL};
@@ -259,6 +259,7 @@ pub async fn run_script<P: AsRef<Path>>(path: P, mut progress: PhaseProgressTrac
if let Err(e) = async { if let Err(e) = async {
let script = tokio::fs::read_to_string(script).await?; 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_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())); 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); reader.post_read(|buf| progress += buf.iter().filter(|b| **b == b'\n').count() as u64);
Command::new("/bin/bash") Command::new("/bin/bash")

View File

@@ -89,7 +89,7 @@ use crate::context::{
use crate::disk::fsck::RequiresReboot; use crate::disk::fsck::RequiresReboot;
use crate::registry::context::{RegistryContext, RegistryUrlParams}; use crate::registry::context::{RegistryContext, RegistryUrlParams};
use crate::system::kiosk; use crate::system::kiosk;
use crate::util::serde::{HandlerExtSerde, WithIoFormat}; use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -201,15 +201,6 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
if &*PLATFORM != "raspberrypi" { if &*PLATFORM != "raspberrypi" {
api = api.subcommand("kiosk", kiosk::<C>()); api = api.subcommand("kiosk", kiosk::<C>());
} }
#[cfg(feature = "dev")]
{
api = api.subcommand(
"lxc",
lxc::dev::lxc::<C>().with_about(
"Commands related to lxc containers i.e. create, list, remove, connect",
),
);
}
api api
} }
@@ -220,7 +211,7 @@ pub fn server<C: Context>() -> ParentHandler<C> {
from_fn_async(system::time) from_fn_async(system::time)
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn(|handle, result| { .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_about("Display current time and server uptime")
.with_call_remote::<CliContext>() .with_call_remote::<CliContext>()
@@ -416,6 +407,46 @@ pub fn package<C: Context>() -> ParentHandler<C> {
.with_about("Rebuild service container") .with_about("Rebuild service container")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
.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::<CliContext>(),
)
.subcommand("logs", logs::package_logs()) .subcommand("logs", logs::package_logs())
.subcommand( .subcommand(
"logs", "logs",

View File

@@ -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<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"create",
from_fn_async(create)
.with_about("Create lxc container")
.with_call_remote::<CliContext>(),
)
.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::<CliContext>(),
)
.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::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove)
.no_display()
.with_about("Remove lxc container")
.with_call_remote::<CliContext>(),
)
.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<ContainerId, Error> {
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<Vec<ContainerId>, Error> {
Ok(ctx.dev.lxc.lock().await.keys().cloned().collect())
}
pub async fn stats(ctx: RpcContext) -> Result<Vec<ServiceStats>, 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<Option<crate::service::ServiceRef>> =
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<Guid, Error> {
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<CliContext, ConnectParams>,
) -> Result<(), Error> {
let ctx = context.clone();
let guid = CallRemoteHandler::<CliContext, _, _>::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
}

View File

@@ -1,4 +1,4 @@
use std::collections::BTreeSet; use std::collections::{BTreeMap, BTreeSet};
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use std::path::Path; use std::path::Path;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
@@ -7,7 +7,7 @@ use std::time::Duration;
use clap::builder::ValueParserFactory; use clap::builder::ValueParserFactory;
use futures::{AsyncWriteExt, StreamExt}; use futures::{AsyncWriteExt, StreamExt};
use imbl_value::{InOMap, InternedString}; use imbl_value::{InOMap, InternedString};
use models::{FromStrParser, InvalidId}; use models::{FromStrParser, InvalidId, PackageId};
use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{GenericRpcMethod, RpcRequest, RpcResponse}; use rpc_toolkit::{GenericRpcMethod, RpcRequest, RpcResponse};
use rustyline_async::{ReadlineEvent, SharedWriter}; 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::disk::mount::util::unmount;
use crate::prelude::*; use crate::prelude::*;
use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::rpc_continuations::{Guid, RpcContinuation};
use crate::service::ServiceStats;
use crate::util::io::open_file; use crate::util::io::open_file;
use crate::util::rpc_client::UnixRpcClient; use crate::util::rpc_client::UnixRpcClient;
use crate::util::{new_guid, Invoke}; use crate::util::{new_guid, Invoke};
// #[cfg(feature = "dev")]
pub mod dev;
const LXC_CONTAINER_DIR: &str = "/var/lib/lxc"; const LXC_CONTAINER_DIR: &str = "/var/lib/lxc";
const RPC_DIR: &str = "media/startos/rpc"; // must not be absolute path 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 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(()) Ok(())
} }
pub async fn stats(ctx: RpcContext) -> Result<BTreeMap<PackageId, Option<ServiceStats>>, 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<Option<crate::service::ServiceRef>> =
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)
}

View File

@@ -159,7 +159,7 @@ pub fn binding<C: Context, Kind: HostApiKind>(
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
return Ok(display_serializable(format, res)); return display_serializable(format, res);
} }
let mut table = Table::new(); let mut table = Table::new();
@@ -182,7 +182,7 @@ pub fn binding<C: Context, Kind: HostApiKind>(
]); ]);
} }
table.print_tty(false).unwrap(); table.print_tty(false)?;
Ok(()) Ok(())
}) })

View File

@@ -47,7 +47,7 @@ pub fn network_interface_api<C: Context>() -> ParentHandler<C> {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
return Ok(display_serializable(format, res)); return display_serializable(format, res);
} }
let mut table = Table::new(); let mut table = Table::new();
@@ -78,7 +78,7 @@ pub fn network_interface_api<C: Context>() -> ParentHandler<C> {
]); ]);
} }
table.print_tty(false).unwrap(); table.print_tty(false)?;
Ok(()) Ok(())
}) })

View File

@@ -90,9 +90,7 @@ pub fn tor<C: Context>() -> ParentHandler<C> {
"list-services", "list-services",
from_fn_async(list_services) from_fn_async(list_services)
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn(|handle, result| { .with_custom_display_fn(|handle, result| display_services(handle.params, result))
Ok(display_services(handle.params, result))
})
.with_about("Display Tor V3 Onion Addresses") .with_about("Display Tor V3 Onion Addresses")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
@@ -210,7 +208,10 @@ pub async fn reset(
.await .await
} }
pub fn display_services(params: WithIoFormat<Empty>, services: Vec<OnionAddressV3>) { pub fn display_services(
params: WithIoFormat<Empty>,
services: Vec<OnionAddressV3>,
) -> Result<(), Error> {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
@@ -222,7 +223,8 @@ pub fn display_services(params: WithIoFormat<Empty>, services: Vec<OnionAddressV
let row = row![&service.to_string()]; let row = row![&service.to_string()];
table.add_row(row); table.add_row(row);
} }
table.print_tty(false).unwrap(); table.print_tty(false)?;
Ok(())
} }
pub async fn list_services(ctx: RpcContext, _: Empty) -> Result<Vec<OnionAddressV3>, Error> { pub async fn list_services(ctx: RpcContext, _: Empty) -> Result<Vec<OnionAddressV3>, Error> {

View File

@@ -70,9 +70,7 @@ pub fn wifi<C: Context>() -> ParentHandler<C> {
"get", "get",
from_fn_async(get) from_fn_async(get)
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn(|handle, result| { .with_custom_display_fn(|handle, result| display_wifi_info(handle.params, result))
Ok(display_wifi_info(handle.params, result))
})
.with_about("List wifi info") .with_about("List wifi info")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
@@ -134,7 +132,7 @@ pub fn available<C: Context>() -> ParentHandler<C> {
"get", "get",
from_fn_async(get_available) from_fn_async(get_available)
.with_display_serializable() .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_about("List available wifi networks")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
@@ -363,7 +361,7 @@ pub struct WifiListOut {
security: Vec<String>, security: Vec<String>,
} }
pub type WifiList = HashMap<Ssid, WifiListInfoLow>; pub type WifiList = HashMap<Ssid, WifiListInfoLow>;
fn display_wifi_info(params: WithIoFormat<Empty>, info: WifiListInfo) { fn display_wifi_info(params: WithIoFormat<Empty>, info: WifiListInfo) -> Result<(), Error> {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
@@ -424,10 +422,11 @@ fn display_wifi_info(params: WithIoFormat<Empty>, info: WifiListInfo) {
]); ]);
} }
table_global.print_tty(false).unwrap(); table_global.print_tty(false)?;
Ok(())
} }
fn display_wifi_list(params: WithIoFormat<Empty>, info: Vec<WifiListOut>) { fn display_wifi_list(params: WithIoFormat<Empty>, info: Vec<WifiListOut>) -> Result<(), Error> {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
@@ -448,7 +447,8 @@ fn display_wifi_list(params: WithIoFormat<Empty>, info: Vec<WifiListOut>) {
]); ]);
} }
table_global.print_tty(false).unwrap(); table_global.print_tty(false)?;
Ok(())
} }
// #[command(display(display_wifi_info))] // #[command(display(display_wifi_info))]

View File

@@ -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(); 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)] #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, TS)]
#[serde(untagged)] #[serde(untagged)]
pub enum Progress { pub enum Progress {
@@ -34,13 +41,14 @@ pub enum Progress {
done: u64, done: u64,
#[ts(type = "number | null")] #[ts(type = "number | null")]
total: Option<u64>, total: Option<u64>,
units: Option<ProgressUnits>,
}, },
} }
impl Progress { impl Progress {
pub fn new() -> Self { pub fn new() -> Self {
Progress::NotStarted(()) Progress::NotStarted(())
} }
pub fn update_bar(self, bar: &ProgressBar, bytes: bool) { pub fn update_bar(self, bar: &ProgressBar) {
match self { match self {
Self::NotStarted(()) => { Self::NotStarted(()) => {
bar.set_style(SPINNER.clone()); bar.set_style(SPINNER.clone());
@@ -52,8 +60,12 @@ impl Progress {
Self::Complete(true) => { Self::Complete(true) => {
bar.finish(); bar.finish();
} }
Self::Progress { done, total: None } => { Self::Progress {
if bytes { done,
total: None,
units,
} => {
if units == Some(ProgressUnits::Bytes) {
bar.set_style(BYTES.clone()); bar.set_style(BYTES.clone());
} else { } else {
bar.set_style(STEPS.clone()); bar.set_style(STEPS.clone());
@@ -64,8 +76,9 @@ impl Progress {
Self::Progress { Self::Progress {
done, done,
total: Some(total), total: Some(total),
units,
} => { } => {
if bytes { if units == Some(ProgressUnits::Bytes) {
bar.set_style(PERCENTAGE_BYTES.clone()); bar.set_style(PERCENTAGE_BYTES.clone());
} else { } else {
bar.set_style(PERCENTAGE.clone()); bar.set_style(PERCENTAGE.clone());
@@ -84,14 +97,22 @@ impl Progress {
} }
pub fn set_done(&mut self, done: u64) { pub fn set_done(&mut self, done: u64) {
*self = match *self { *self = match *self {
Self::Complete(false) | Self::NotStarted(()) => Self::Progress { done, total: None }, Self::Complete(false) | Self::NotStarted(()) => Self::Progress {
Self::Progress { mut done, total } => { done,
total: None,
units: None,
},
Self::Progress {
mut done,
total,
units,
} => {
if let Some(total) = total { if let Some(total) = total {
if done > total { if done > total {
done = total; done = total;
} }
} }
Self::Progress { done, total } Self::Progress { done, total, units }
} }
Self::Complete(true) => Self::Complete(true), Self::Complete(true) => Self::Complete(true),
}; };
@@ -101,10 +122,12 @@ impl Progress {
Self::Complete(false) | Self::NotStarted(()) => Self::Progress { Self::Complete(false) | Self::NotStarted(()) => Self::Progress {
done: 0, done: 0,
total: Some(total), total: Some(total),
units: None,
}, },
Self::Progress { done, .. } => Self::Progress { Self::Progress { done, units, .. } => Self::Progress {
done, done,
total: Some(total), total: Some(total),
units,
}, },
Self::Complete(true) => Self::Complete(true), Self::Complete(true) => Self::Complete(true),
} }
@@ -113,17 +136,30 @@ impl Progress {
if let Self::Progress { if let Self::Progress {
done, done,
total: Some(old), total: Some(old),
units,
} = *self } = *self
{ {
*self = Self::Progress { *self = Self::Progress {
done, done,
total: Some(old + total), total: Some(old + total),
units,
}; };
} else { } else {
self.set_total(total) self.set_total(total)
} }
} }
pub fn complete(&mut self) { pub fn set_units(&mut self, units: Option<ProgressUnits>) {
*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); *self = Self::Complete(true);
} }
pub fn is_complete(&self) -> bool { pub fn is_complete(&self) -> bool {
@@ -137,15 +173,16 @@ impl std::ops::Add<u64> for Progress {
Self::Complete(false) | Self::NotStarted(()) => Self::Progress { Self::Complete(false) | Self::NotStarted(()) => Self::Progress {
done: rhs, done: rhs,
total: None, total: None,
units: None,
}, },
Self::Progress { done, total } => { Self::Progress { done, total, units } => {
let mut done = done + rhs; let mut done = done + rhs;
if let Some(total) = total { if let Some(total) = total {
if done > total { if done > total {
done = total; done = total;
} }
} }
Self::Progress { done, total } Self::Progress { done, total, units }
} }
Self::Complete(true) => Self::Complete(true), Self::Complete(true) => Self::Complete(true),
} }
@@ -337,7 +374,7 @@ impl FullProgressTracker {
} }
} }
pub fn complete(&self) { 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 { Progress::Progress {
done, done,
total: Some(total), total: Some(total),
..
} => ((done as f64 / total as f64) * overall_contribution as f64) as u64, } => ((done as f64 / total as f64) * overall_contribution as f64) as u64,
_ => 0, _ => 0,
}; };
@@ -380,8 +418,11 @@ impl PhaseProgressTrackerHandle {
self.progress.send_modify(|p| p.add_total(total)); self.progress.send_modify(|p| p.add_total(total));
self.update_overall(); self.update_overall();
} }
pub fn set_units(&mut self, units: Option<ProgressUnits>) {
self.progress.send_modify(|p| p.set_units(units));
}
pub fn complete(&mut self) { pub fn complete(&mut self) {
self.progress.send_modify(|p| p.complete()); self.progress.send_modify(|p| p.set_complete());
self.update_overall(); self.update_overall();
} }
pub fn writer<W>(self, writer: W) -> ProgressTrackerWriter<W> { pub fn writer<W>(self, writer: W) -> ProgressTrackerWriter<W> {
@@ -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() { for (name, bar) in self.phases.iter() {
if let Some(progress) = progress.phases.iter().find_map(|p| { if let Some(progress) = progress.phases.iter().find_map(|p| {
if &p.name == name { if &p.name == name {
@@ -510,7 +551,7 @@ impl PhasedProgressBar {
None None
} }
}) { }) {
progress.update_bar(bar, true); progress.update_bar(bar);
} }
} }
} }

View File

@@ -47,7 +47,7 @@ pub fn admin_api<C: Context>() -> ParentHandler<C> {
from_fn_async(list_admins) from_fn_async(list_admins)
.with_metadata("admin", Value::Bool(true)) .with_metadata("admin", Value::Bool(true))
.with_display_serializable() .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_about("List admin signers")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
@@ -60,7 +60,7 @@ fn signers_api<C: Context>() -> ParentHandler<C> {
from_fn_async(list_signers) from_fn_async(list_signers)
.with_metadata("admin", Value::Bool(true)) .with_metadata("admin", Value::Bool(true))
.with_display_serializable() .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_about("List signers")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
@@ -133,7 +133,10 @@ pub async fn list_signers(ctx: RegistryContext) -> Result<BTreeMap<Guid, SignerI
ctx.db.peek().await.into_index().into_signers().de() ctx.db.peek().await.into_index().into_signers().de()
} }
pub fn display_signers<T>(params: WithIoFormat<T>, signers: BTreeMap<Guid, SignerInfo>) { pub fn display_signers<T>(
params: WithIoFormat<T>,
signers: BTreeMap<Guid, SignerInfo>,
) -> Result<(), Error> {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
@@ -155,7 +158,8 @@ pub fn display_signers<T>(params: WithIoFormat<T>, signers: BTreeMap<Guid, Signe
&info.keys.into_iter().join("\n"), &info.keys.into_iter().join("\n"),
]); ]);
} }
table.print_tty(false).unwrap(); table.print_tty(false)?;
Ok(())
} }
pub async fn add_signer(ctx: RegistryContext, signer: SignerInfo) -> Result<Guid, Error> { pub async fn add_signer(ctx: RegistryContext, signer: SignerInfo) -> Result<Guid, Error> {

View File

@@ -14,7 +14,7 @@ use url::Url;
use crate::context::CliContext; use crate::context::CliContext;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::FullProgressTracker; use crate::progress::{FullProgressTracker, ProgressUnits};
use crate::registry::asset::RegistryAsset; use crate::registry::asset::RegistryAsset;
use crate::registry::context::RegistryContext; use crate::registry::context::RegistryContext;
use crate::registry::os::index::OsVersionInfo; use crate::registry::os::index::OsVersionInfo;
@@ -246,6 +246,7 @@ pub async fn cli_add_asset(
if let Some(size) = src.size().await { if let Some(size) = src.size().await {
verify_phase.set_total(size); verify_phase.set_total(size);
} }
verify_phase.set_units(Some(ProgressUnits::Bytes));
let mut writer = verify_phase.writer(VerifyingWriter::new( let mut writer = verify_phase.writer(VerifyingWriter::new(
tokio::io::sink(), tokio::io::sink(),
Some((blake3::Hash::from_bytes(*commitment.hash), commitment.size)), Some((blake3::Hash::from_bytes(*commitment.hash), commitment.size)),

View File

@@ -13,7 +13,7 @@ use ts_rs::TS;
use crate::context::CliContext; use crate::context::CliContext;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::FullProgressTracker; use crate::progress::{FullProgressTracker, ProgressUnits};
use crate::registry::asset::RegistryAsset; use crate::registry::asset::RegistryAsset;
use crate::registry::context::RegistryContext; use crate::registry::context::RegistryContext;
use crate::registry::os::index::OsVersionInfo; use crate::registry::os::index::OsVersionInfo;
@@ -167,6 +167,7 @@ async fn cli_get_os_asset(
let mut download_phase = let mut download_phase =
progress.add_phase(InternedString::intern("Downloading File"), Some(100)); progress.add_phase(InternedString::intern("Downloading File"), Some(100));
download_phase.set_total(res.commitment.size); download_phase.set_total(res.commitment.size);
download_phase.set_units(Some(ProgressUnits::Bytes));
let reverify_phase = if reverify { let reverify_phase = if reverify {
Some(progress.add_phase(InternedString::intern("Reverifying File"), Some(10))) Some(progress.add_phase(InternedString::intern("Reverifying File"), Some(10)))
} else { } else {

View File

@@ -49,7 +49,7 @@ pub fn version_api<C: Context>() -> ParentHandler<C> {
.with_metadata("get_device_info", Value::Bool(true)) .with_metadata("get_device_info", Value::Bool(true))
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn(|handle, result| { .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_about("Get OS versions and related version info")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
@@ -197,7 +197,10 @@ pub async fn get_version(
.collect() .collect()
} }
pub fn display_version_info<T>(params: WithIoFormat<T>, info: BTreeMap<Version, OsVersionInfo>) { pub fn display_version_info<T>(
params: WithIoFormat<T>,
info: BTreeMap<Version, OsVersionInfo>,
) -> Result<(), Error> {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
@@ -223,5 +226,6 @@ pub fn display_version_info<T>(params: WithIoFormat<T>, info: BTreeMap<Version,
&info.squashfs.keys().into_iter().join(", "), &info.squashfs.keys().into_iter().join(", "),
]); ]);
} }
table.print_tty(false).unwrap(); table.print_tty(false)?;
Ok(())
} }

View File

@@ -36,7 +36,7 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
"list", "list",
from_fn_async(list_version_signers) from_fn_async(list_version_signers)
.with_display_serializable() .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_about("List version signers and related signer info")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )

View File

@@ -12,7 +12,7 @@ use url::Url;
use crate::context::CliContext; use crate::context::CliContext;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::{FullProgressTracker, ProgressTrackerWriter}; use crate::progress::{FullProgressTracker, ProgressTrackerWriter, ProgressUnits};
use crate::registry::context::RegistryContext; use crate::registry::context::RegistryContext;
use crate::registry::package::index::PackageVersionInfo; use crate::registry::package::index::PackageVersionInfo;
use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment;
@@ -135,6 +135,7 @@ pub async fn cli_add_package(
if let Some(len) = len { if let Some(len) = len {
verify_phase.set_total(len); verify_phase.set_total(len);
} }
verify_phase.set_units(Some(ProgressUnits::Bytes));
let mut verify_writer = ProgressTrackerWriter::new(tokio::io::sink(), verify_phase); let mut verify_writer = ProgressTrackerWriter::new(tokio::io::sink(), verify_phase);
src.serialize(&mut TrackingIO::new(0, &mut verify_writer), true) src.serialize(&mut TrackingIO::new(0, &mut verify_writer), true)
.await?; .await?;

View File

@@ -52,7 +52,7 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
from_fn_async(list_categories) from_fn_async(list_categories)
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn(|params, categories| { .with_custom_display_fn(|params, categories| {
Ok(display_categories(params.params, categories)) display_categories(params.params, categories)
}) })
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )
@@ -182,7 +182,7 @@ pub async fn list_categories(
pub fn display_categories<T>( pub fn display_categories<T>(
params: WithIoFormat<T>, params: WithIoFormat<T>,
categories: BTreeMap<InternedString, Category>, categories: BTreeMap<InternedString, Category>,
) { ) -> Result<(), Error> {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
@@ -197,5 +197,6 @@ pub fn display_categories<T>(
for (id, info) in categories { for (id, info) in categories {
table.add_row(row![&*id, &info.name]); table.add_row(row![&*id, &info.name]);
} }
table.print_tty(false).unwrap(); table.print_tty(false)?;
Ok(())
} }

View File

@@ -36,7 +36,7 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
"list", "list",
from_fn_async(list_package_signers) from_fn_async(list_package_signers)
.with_display_serializable() .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_about("List package signers and related signer info")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
) )

View File

@@ -4,7 +4,7 @@ use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use exver::{ExtendedVersion, VersionRange}; use exver::{ExtendedVersion, VersionRange};
use models::{Id, ImageId, VolumeId}; use models::{ImageId, VolumeId};
use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt}; use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt};
use tokio::process::Command; use tokio::process::Command;
@@ -259,6 +259,7 @@ impl TryFrom<ManifestV1> for Manifest {
}, },
git_hash: value.git_hash, git_hash: value.git_hash,
os_version: value.eos_version, os_version: value.eos_version,
sdk_version: None,
}) })
} }
} }

View File

@@ -66,6 +66,8 @@ pub struct Manifest {
#[serde(default = "current_version")] #[serde(default = "current_version")]
#[ts(type = "string")] #[ts(type = "string")]
pub os_version: Version, pub os_version: Version,
#[ts(type = "string | null")]
pub sdk_version: Option<Version>,
} }
impl Manifest { impl Manifest {
pub fn validate_for<'a, T: Clone>( pub fn validate_for<'a, T: Clone>(

View File

@@ -31,7 +31,7 @@ pub fn action_api<C: Context>() -> ParentHandler<C> {
"run", "run",
from_fn_async(run_action) from_fn_async(run_action)
.with_display_serializable() .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::<ContainerCliContext>(), .with_call_remote::<ContainerCliContext>(),
) )
.subcommand("create-task", from_fn_async(create_task).no_cli()) .subcommand("create-task", from_fn_async(create_task).no_cli())

View File

@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
use std::ffi::{c_int, OsStr, OsString}; use std::ffi::{c_int, OsStr, OsString};
use std::fs::File; use std::fs::File;
use std::io::{IsTerminal, Read}; 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::path::{Path, PathBuf};
use std::process::{Command as StdCommand, Stdio}; use std::process::{Command as StdCommand, Stdio};
use std::sync::Arc; use std::sync::Arc;
@@ -330,7 +330,7 @@ pub fn launch(
if let Some(code) = exit.code() { if let Some(code) = exit.code() {
drop(raw); drop(raw);
std::process::exit(code); std::process::exit(code);
} else if exit.success() { } else if exit.success() || exit.signal() == Some(15) {
Ok(()) Ok(())
} else { } else {
Err(Error::new( Err(Error::new(
@@ -380,7 +380,7 @@ pub fn launch(
nix::mount::umount(&chroot.join("proc")) nix::mount::umount(&chroot.join("proc"))
.with_ctx(|_| (ErrorKind::Filesystem, "umount procfs"))?; .with_ctx(|_| (ErrorKind::Filesystem, "umount procfs"))?;
std::process::exit(code); std::process::exit(code);
} else if exit.success() { } else if exit.success() || exit.signal() == Some(15) {
Ok(()) Ok(())
} else { } else {
Err(Error::new( Err(Error::new(

View File

@@ -48,6 +48,7 @@ use crate::s9pk::S9pk;
use crate::service::action::update_tasks; use crate::service::action::update_tasks;
use crate::service::rpc::{ExitParams, InitKind}; use crate::service::rpc::{ExitParams, InitKind};
use crate::service::service_map::InstallProgressHandles; use crate::service::service_map::InstallProgressHandles;
use crate::service::uninstall::cleanup;
use crate::util::actor::concurrent::ConcurrentActor; use crate::util::actor::concurrent::ConcurrentActor;
use crate::util::io::{create_file, AsyncReadStream, TermSize}; use crate::util::io::{create_file, AsyncReadStream, TermSize};
use crate::util::net::WebSocketExt; use crate::util::net::WebSocketExt;
@@ -111,7 +112,6 @@ impl std::fmt::Display for MiB {
#[derive(Clone, Debug, Serialize, Deserialize, Default, TS)] #[derive(Clone, Debug, Serialize, Deserialize, Default, TS)]
pub struct ServiceStats { pub struct ServiceStats {
pub container_id: Arc<ContainerId>, pub container_id: Arc<ContainerId>,
pub package_id: PackageId,
pub memory_usage: MiB, pub memory_usage: MiB,
pub memory_limit: MiB, pub memory_limit: MiB,
} }
@@ -307,7 +307,7 @@ impl Service {
} }
} }
} }
// TODO: delete s9pk? cleanup(ctx, id, false).await.log_err();
ctx.db ctx.db
.mutate(|v| v.as_public_mut().as_package_data_mut().remove(id)) .mutate(|v| v.as_public_mut().as_package_data_mut().remove(id))
.await .await
@@ -615,7 +615,6 @@ impl Service {
.fold((0, 0), |acc, (total, used)| (acc.0 + total, acc.1 + used)); .fold((0, 0), |acc, (total, used)| (acc.0 + total, acc.1 + used));
Ok(ServiceStats { Ok(ServiceStats {
container_id: lxc_container.guid.clone(), container_id: lxc_container.guid.clone(),
package_id: self.seed.id.clone(),
memory_limit: MiB::from_MiB(total), memory_limit: MiB::from_MiB(total),
memory_usage: MiB::from_MiB(used), memory_usage: MiB::from_MiB(used),
}) })
@@ -735,6 +734,7 @@ pub struct AttachParams {
pub command: Vec<OsString>, pub command: Vec<OsString>,
pub tty: bool, pub tty: bool,
pub stderr_tty: bool, pub stderr_tty: bool,
pub pty_size: Option<TermSize>,
#[ts(skip)] #[ts(skip)]
#[serde(rename = "__auth_session")] #[serde(rename = "__auth_session")]
session: Option<InternedString>, session: Option<InternedString>,
@@ -752,6 +752,7 @@ pub async fn attach(
command, command,
tty, tty,
stderr_tty, stderr_tty,
pty_size,
session, session,
subcontainer, subcontainer,
image_id, image_id,
@@ -862,6 +863,7 @@ pub async fn attach(
command: Vec<OsString>, command: Vec<OsString>,
tty: bool, tty: bool,
stderr_tty: bool, stderr_tty: bool,
pty_size: Option<TermSize>,
image_id: ImageId, image_id: ImageId,
workdir: Option<String>, workdir: Option<String>,
root_command: &RootCommand, root_command: &RootCommand,
@@ -898,6 +900,10 @@ pub async fn attach(
cmd.arg("--force-stderr-tty"); cmd.arg("--force-stderr-tty");
} }
if let Some(pty_size) = pty_size {
cmd.arg(format!("--pty-size={pty_size}"));
}
cmd.arg(&root_path).arg("--"); cmd.arg(&root_path).arg("--");
if command.is_empty() { if command.is_empty() {
@@ -1040,6 +1046,7 @@ pub async fn attach(
command, command,
tty, tty,
stderr_tty, stderr_tty,
pty_size,
image_id, image_id,
workdir, workdir,
&root_command, &root_command,

View File

@@ -22,7 +22,9 @@ use crate::disk::mount::guard::GenericMountGuard;
use crate::install::PKG_ARCHIVE_DIR; use crate::install::PKG_ARCHIVE_DIR;
use crate::notifications::{notify, NotificationLevel}; use crate::notifications::{notify, NotificationLevel};
use crate::prelude::*; use crate::prelude::*;
use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle, ProgressTrackerWriter}; use crate::progress::{
FullProgressTracker, PhaseProgressTrackerHandle, ProgressTrackerWriter, ProgressUnits,
};
use crate::rpc_continuations::Guid; use crate::rpc_continuations::Guid;
use crate::s9pk::manifest::PackageId; use crate::s9pk::manifest::PackageId;
use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::merkle_archive::source::FileSource;
@@ -72,6 +74,7 @@ impl ServiceMap {
progress.start(); progress.start();
let ids = ctx.db.peek().await.as_public().as_package_data().keys()?; let ids = ctx.db.peek().await.as_public().as_package_data().keys()?;
progress.set_total(ids.len() as u64); progress.set_total(ids.len() as u64);
progress.set_units(Some(ProgressUnits::Steps));
let mut jobs = FuturesUnordered::new(); let mut jobs = FuturesUnordered::new();
for id in &ids { for id in &ids {
jobs.push(self.load(ctx, id, LoadDisposition::Retry)); jobs.push(self.load(ctx, id, LoadDisposition::Retry));

View File

@@ -34,7 +34,7 @@ use crate::disk::REPAIR_DISK_PATH;
use crate::init::{init, InitPhases, InitResult}; use crate::init::{init, InitPhases, InitResult};
use crate::net::ssl::root_ca_start_time; use crate::net::ssl::root_ca_start_time;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::{FullProgress, PhaseProgressTrackerHandle}; use crate::progress::{FullProgress, PhaseProgressTrackerHandle, ProgressUnits};
use crate::rpc_continuations::Guid; use crate::rpc_continuations::Guid;
use crate::system::sync_kiosk; use crate::system::sync_kiosk;
use crate::util::crypto::EncryptedWire; use crate::util::crypto::EncryptedWire;
@@ -547,6 +547,7 @@ async fn migrate(
let mut restore_phase = restore_phase.or_not_found("restore progress")?; let mut restore_phase = restore_phase.or_not_found("restore progress")?;
restore_phase.start(); restore_phase.start();
restore_phase.set_units(Some(ProgressUnits::Bytes));
let _ = crate::disk::main::import( let _ = crate::disk::main::import(
&old_guid, &old_guid,
"/media/startos/migrate", "/media/startos/migrate",

View File

@@ -108,7 +108,7 @@ pub fn ssh<C: Context>() -> ParentHandler<C> {
from_fn_async(list) from_fn_async(list)
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn(|handle, result| { .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_about("List ssh keys")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
@@ -177,7 +177,10 @@ pub async fn remove(
sync_pubkeys(&keys, SSH_DIR).await sync_pubkeys(&keys, SSH_DIR).await
} }
fn display_all_ssh_keys(params: WithIoFormat<Empty>, result: Vec<SshKeyResponse>) { fn display_all_ssh_keys(
params: WithIoFormat<Empty>,
result: Vec<SshKeyResponse>,
) -> Result<(), Error> {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
@@ -200,7 +203,9 @@ fn display_all_ssh_keys(params: WithIoFormat<Empty>, result: Vec<SshKeyResponse>
]; ];
table.add_row(row); table.add_row(row);
} }
table.print_tty(false).unwrap(); table.print_tty(false)?;
Ok(())
} }
#[instrument(skip_all)] #[instrument(skip_all)]

View File

@@ -46,7 +46,7 @@ pub fn experimental<C: Context>() -> ParentHandler<C> {
from_fn_async(governor) from_fn_async(governor)
.with_display_serializable() .with_display_serializable()
.with_custom_display_fn(|handle, result| { .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_about("Show current and available CPU governors")
.with_call_remote::<CliContext>(), .with_call_remote::<CliContext>(),
@@ -125,7 +125,10 @@ pub struct GovernorInfo {
available: BTreeSet<Governor>, available: BTreeSet<Governor>,
} }
fn display_governor_info(params: WithIoFormat<GovernorParams>, result: GovernorInfo) { fn display_governor_info(
params: WithIoFormat<GovernorParams>,
result: GovernorInfo,
) -> Result<(), Error> {
use prettytable::*; use prettytable::*;
if let Some(format) = params.format { if let Some(format) = params.format {
@@ -141,7 +144,8 @@ fn display_governor_info(params: WithIoFormat<GovernorParams>, result: GovernorI
table.add_row(row![entry]); table.add_row(row![entry]);
} }
} }
table.print_tty(false).unwrap(); table.print_tty(false)?;
Ok(())
} }
#[derive(Deserialize, Serialize, Parser, TS)] #[derive(Deserialize, Serialize, Parser, TS)]
@@ -191,7 +195,7 @@ pub struct TimeInfo {
uptime: u64, uptime: u64,
} }
pub fn display_time(params: WithIoFormat<Empty>, arg: TimeInfo) { pub fn display_time(params: WithIoFormat<Empty>, arg: TimeInfo) -> Result<(), Error> {
use std::fmt::Write; use std::fmt::Write;
use prettytable::*; use prettytable::*;
@@ -230,7 +234,8 @@ pub fn display_time(params: WithIoFormat<Empty>, arg: TimeInfo) {
let mut table = Table::new(); let mut table = Table::new();
table.add_row(row![bc -> "NOW", &arg.now]); table.add_row(row![bc -> "NOW", &arg.now]);
table.add_row(row![bc -> "UPTIME", &uptime_string]); 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<TimeInfo, Error> { pub async fn time(ctx: RpcContext, _: Empty) -> Result<TimeInfo, Error> {

View File

@@ -26,7 +26,9 @@ use crate::disk::mount::filesystem::MountType;
use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard};
use crate::notifications::{notify, NotificationLevel}; use crate::notifications::{notify, NotificationLevel};
use crate::prelude::*; use crate::prelude::*;
use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar}; use crate::progress::{
FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar, ProgressUnits,
};
use crate::registry::asset::RegistryAsset; use crate::registry::asset::RegistryAsset;
use crate::registry::context::{RegistryContext, RegistryUrlParams}; use crate::registry::context::{RegistryContext, RegistryUrlParams};
use crate::registry::os::index::OsVersionInfo; use crate::registry::os::index::OsVersionInfo;
@@ -195,9 +197,9 @@ pub async fn cli_update_system(
} }
if let Some(mut prev) = prev { if let Some(mut prev) = prev {
for phase in &mut prev.phases { for phase in &mut prev.phases {
phase.progress.complete(); phase.progress.set_complete();
} }
prev.overall.complete(); prev.overall.set_complete();
progress.update(&prev); progress.update(&prev);
} }
} else { } else {
@@ -265,6 +267,7 @@ async fn maybe_do_update(
let prune_phase = progress.add_phase("Pruning Old OS Images".into(), Some(2)); 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)); let mut download_phase = progress.add_phase("Downloading File".into(), Some(100));
download_phase.set_total(asset.commitment.size); 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 reverify_phase = progress.add_phase("Reverifying File".into(), Some(10));
let sync_boot_phase = progress.add_phase("Syncing Boot Files".into(), Some(1)); let sync_boot_phase = progress.add_phase("Syncing Boot Files".into(), Some(1));
let finalize_phase = progress.add_phase("Finalizing Update".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()) .arg(asset.commitment.size.to_string())
.invoke(ErrorKind::Filesystem) .invoke(ErrorKind::Filesystem)
.await?; .await?;
Command::new("/usr/lib/startos/scripts/prune-boot")
.invoke(ErrorKind::Filesystem)
.await?;
prune_phase.complete(); prune_phase.complete();
download_phase.start(); download_phase.start();

View File

@@ -18,7 +18,7 @@ use tokio::sync::watch;
use crate::context::RpcContext; use crate::context::RpcContext;
use crate::prelude::*; use crate::prelude::*;
use crate::progress::PhaseProgressTrackerHandle; use crate::progress::{PhaseProgressTrackerHandle, ProgressUnits};
use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::rpc_continuations::{Guid, RpcContinuation};
use crate::s9pk::merkle_archive::source::multi_cursor_file::{FileCursor, MultiCursorFile}; use crate::s9pk::merkle_archive::source::multi_cursor_file::{FileCursor, MultiCursorFile};
use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::s9pk::merkle_archive::source::ArchiveSource;
@@ -176,7 +176,10 @@ pub struct UploadingFile {
progress: watch::Receiver<Progress>, progress: watch::Receiver<Progress>,
} }
impl UploadingFile { 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 { let progress = watch::channel(Progress {
tracker: progress, tracker: progress,
expected_size: None, expected_size: None,

View File

@@ -26,10 +26,10 @@ use tokio::io::{
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio::sync::{Notify, OwnedMutexGuard}; use tokio::sync::{Notify, OwnedMutexGuard};
use tokio::time::{Instant, Sleep}; use tokio::time::{Instant, Sleep};
use ts_rs::TS;
use crate::prelude::*; use crate::prelude::*;
use crate::util::sync::SyncMutex; use crate::util::sync::SyncMutex;
use crate::{CAP_1_KiB, CAP_1_MiB};
pub trait AsyncReadSeek: AsyncRead + AsyncSeek {} pub trait AsyncReadSeek: AsyncRead + AsyncSeek {}
impl<T: AsyncRead + AsyncSeek> AsyncReadSeek for T {} impl<T: AsyncRead + AsyncSeek> AsyncReadSeek for T {}
@@ -1426,7 +1426,7 @@ impl<T: std::io::Read> std::io::Read for SharedIO<T> {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
pub struct TermSize { pub struct TermSize {
pub size: (u16, u16), pub size: (u16, u16),
pub pixels: Option<(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)) .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 { impl ValueParserFactory for TermSize {
type Parser = FromStrParser<Self>; type Parser = FromStrParser<Self>;
fn value_parser() -> Self::Parser { fn value_parser() -> Self::Parser {

View File

@@ -398,13 +398,12 @@ impl IoFormat {
} }
} }
pub fn display_serializable<T: Serialize>(format: IoFormat, result: T) { pub fn display_serializable<T: Serialize>(format: IoFormat, result: T) -> Result<(), Error> {
format format.to_writer(std::io::stdout(), &result)?;
.to_writer(std::io::stdout(), &result)
.expect("Error serializing result to stdout");
if format == IoFormat::JsonPretty { if format == IoFormat::JsonPretty {
println!() println!()
} }
Ok(())
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
@@ -523,13 +522,14 @@ impl<T: HandlerFor<C>, C: Context> HandlerFor<C> for DisplaySerializable<T> {
impl<T: HandlerTypes, C: Context> PrintCliResult<C> for DisplaySerializable<T> impl<T: HandlerTypes, C: Context> PrintCliResult<C> for DisplaySerializable<T>
where where
T::Ok: Serialize, T::Ok: Serialize,
Self::Err: From<Error>,
{ {
fn print( fn print(
&self, &self,
HandlerArgs { params, .. }: HandlerArgsFor<C, Self>, HandlerArgs { params, .. }: HandlerArgsFor<C, Self>,
result: Self::Ok, result: Self::Ok,
) -> Result<(), Self::Err> { ) -> Result<(), Self::Err> {
display_serializable(params.format.unwrap_or_default(), result); display_serializable(params.format.unwrap_or_default(), result)?;
Ok(()) Ok(())
} }
} }

View File

@@ -46,8 +46,9 @@ mod v0_4_0_alpha_3;
mod v0_4_0_alpha_4; mod v0_4_0_alpha_4;
mod v0_4_0_alpha_5; mod v0_4_0_alpha_5;
mod v0_4_0_alpha_6; 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 { impl Current {
#[instrument(skip(self, db))] #[instrument(skip(self, db))]
@@ -157,7 +158,8 @@ enum Version {
V0_4_0_alpha_3(Wrapper<v0_4_0_alpha_3::Version>), V0_4_0_alpha_3(Wrapper<v0_4_0_alpha_3::Version>),
V0_4_0_alpha_4(Wrapper<v0_4_0_alpha_4::Version>), V0_4_0_alpha_4(Wrapper<v0_4_0_alpha_4::Version>),
V0_4_0_alpha_5(Wrapper<v0_4_0_alpha_5::Version>), V0_4_0_alpha_5(Wrapper<v0_4_0_alpha_5::Version>),
V0_4_0_alpha_6(Wrapper<v0_4_0_alpha_6::Version>), // VERSION_BUMP V0_4_0_alpha_6(Wrapper<v0_4_0_alpha_6::Version>),
V0_4_0_alpha_7(Wrapper<v0_4_0_alpha_7::Version>), // VERSION_BUMP
Other(exver::Version), 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_3(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_4(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_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) => { Self::Other(v) => {
return Err(Error::new( return Err(Error::new(
eyre!("unknown version {v}"), 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_3(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_4(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_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(), Version::Other(x) => x.clone(),
} }
} }

View File

@@ -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<Self::PreUpRes, Error> {
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(())
}
}

25
debian/postinst vendored
View File

@@ -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 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 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 # set local and remote login prompt
cat << EOF > /etc/issue 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 EOF
cat << EOF > /etc/issue.net cat << EOF > /etc/issue.net
StartOS v$(cat /usr/lib/startos/VERSION.txt) StartOS v${VERSION}
EOF EOF
# change timezone # change timezone

View File

@@ -3,7 +3,6 @@
export type GetOsVersionParams = { export type GetOsVersionParams = {
sourceVersion: string | null sourceVersion: string | null
targetVersion: string | null targetVersion: string | null
includePrerelease: boolean | null
serverId: string | null serverId: string | null
platform: string | null platform: string | null
} }

View File

@@ -32,4 +32,5 @@ export type Manifest = {
hardwareRequirements: HardwareRequirements hardwareRequirements: HardwareRequirements
gitHash?: GitHash gitHash?: GitHash
osVersion: string osVersion: string
sdkVersion: string | null
} }

View File

@@ -1,3 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
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 }

View File

@@ -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"

View File

@@ -157,6 +157,7 @@ export { PathOrUrl } from "./PathOrUrl"
export { Percentage } from "./Percentage" export { Percentage } from "./Percentage"
export { ProcedureId } from "./ProcedureId" export { ProcedureId } from "./ProcedureId"
export { Progress } from "./Progress" export { Progress } from "./Progress"
export { ProgressUnits } from "./ProgressUnits"
export { Public } from "./Public" export { Public } from "./Public"
export { RecoverySource } from "./RecoverySource" export { RecoverySource } from "./RecoverySource"
export { RegistryAsset } from "./RegistryAsset" export { RegistryAsset } from "./RegistryAsset"

View File

@@ -106,7 +106,7 @@ export class UseEntrypoint {
export function isUseEntrypoint( export function isUseEntrypoint(
command: CommandType, command: CommandType,
): command is UseEntrypoint { ): 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 export type CommandType = string | [string, ...string[]] | UseEntrypoint

View File

@@ -17,7 +17,6 @@ import * as patterns from "../../base/lib/util/patterns"
import { BackupSync, Backups } from "./backup/Backups" import { BackupSync, Backups } from "./backup/Backups"
import { smtpInputSpec } from "../../base/lib/actions/input/inputSpecConstants" import { smtpInputSpec } from "../../base/lib/actions/input/inputSpecConstants"
import { Daemon, Daemons } from "./mainFn/Daemons" import { Daemon, Daemons } from "./mainFn/Daemons"
import { HealthCheck } from "./health/HealthCheck"
import { checkPortListening } from "./health/checkFns/checkPortListening" import { checkPortListening } from "./health/checkFns/checkPortListening"
import { checkWebUrl, runHealthScript } from "./health/checkFns" import { checkWebUrl, runHealthScript } from "./health/checkFns"
import { List } from "../../base/lib/actions/input/builder/list" import { List } from "../../base/lib/actions/input/builder/list"
@@ -25,10 +24,7 @@ import { SetupBackupsParams, setupBackups } from "./backup/setupBackups"
import { setupMain } from "./mainFn" import { setupMain } from "./mainFn"
import { defaultTrigger } from "./trigger/defaultTrigger" import { defaultTrigger } from "./trigger/defaultTrigger"
import { changeOnFirstSuccess, cooldownTrigger } from "./trigger" import { changeOnFirstSuccess, cooldownTrigger } from "./trigger"
import { import { setupServiceInterfaces } from "../../base/lib/interfaces/setupInterfaces"
UpdateServiceInterfaces,
setupServiceInterfaces,
} from "../../base/lib/interfaces/setupInterfaces"
import { successFailure } from "./trigger/successFailure" import { successFailure } from "./trigger/successFailure"
import { MultiHost, Scheme } from "../../base/lib/interfaces/Host" import { MultiHost, Scheme } from "../../base/lib/interfaces/Host"
import { ServiceInterfaceBuilder } from "../../base/lib/interfaces/ServiceInterfaceBuilder" import { ServiceInterfaceBuilder } from "../../base/lib/interfaces/ServiceInterfaceBuilder"
@@ -45,17 +41,13 @@ import { splitCommand } from "./util"
import { Mounts } from "./mainFn/Mounts" import { Mounts } from "./mainFn/Mounts"
import { setupDependencies } from "../../base/lib/dependencies/setupDependencies" import { setupDependencies } from "../../base/lib/dependencies/setupDependencies"
import * as T from "../../base/lib/types" import * as T from "../../base/lib/types"
import { import { testTypeVersion } from "../../base/lib/exver"
ExtendedVersion,
testTypeVersion,
VersionRange,
} from "../../base/lib/exver"
import { import {
CheckDependencies, CheckDependencies,
checkDependencies, checkDependencies,
} from "../../base/lib/dependencies/dependencies" } from "../../base/lib/dependencies/dependencies"
import { GetSslCertificate } from "./util" import { GetSslCertificate } from "./util"
import { getDataVersion, setDataVersion, VersionGraph } from "./version" import { getDataVersion, setDataVersion } from "./version"
import { MaybeFn } from "../../base/lib/actions/setupActions" import { MaybeFn } from "../../base/lib/actions/setupActions"
import { GetInput } from "../../base/lib/actions/setupActions" import { GetInput } from "../../base/lib/actions/setupActions"
import { Run } from "../../base/lib/actions/setupActions" import { Run } from "../../base/lib/actions/setupActions"
@@ -68,7 +60,7 @@ import {
setupOnUninit, setupOnUninit,
} from "../../base/lib/inits" } from "../../base/lib/inits"
export const OSVersion = testTypeVersion("0.4.0-alpha.6") export const OSVersion = testTypeVersion("0.4.0-alpha.7")
// prettier-ignore // prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> = type AnyNeverCond<T extends any[], Then, Else> =
@@ -95,7 +87,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
| "clearServiceInterfaces" | "clearServiceInterfaces"
| "bind" | "bind"
| "getHostInfo" | "getHostInfo"
type MainUsedEffects = "setMainStatus" | "setHealth" type MainUsedEffects = "setMainStatus"
type CallbackEffects = type CallbackEffects =
| "child" | "child"
| "constRetry" | "constRetry"
@@ -129,6 +121,7 @@ export class StartSdk<Manifest extends T.SDKManifest> {
shutdown: (effects, ...args) => effects.shutdown(...args), shutdown: (effects, ...args) => effects.shutdown(...args),
getDependencies: (effects, ...args) => effects.getDependencies(...args), getDependencies: (effects, ...args) => effects.getDependencies(...args),
getStatus: (effects, ...args) => effects.getStatus(...args), getStatus: (effects, ...args) => effects.getStatus(...args),
setHealth: (effects, ...args) => effects.setHealth(...args),
} }
return { return {
@@ -454,7 +447,6 @@ export class StartSdk<Manifest extends T.SDKManifest> {
hostnames: string[], hostnames: string[],
algorithm?: T.Algorithm, algorithm?: T.Algorithm,
) => new GetSslCertificate(effects, hostnames, algorithm), ) => new GetSslCertificate(effects, hostnames, algorithm),
HealthCheck,
healthCheck: { healthCheck: {
checkPortListening, checkPortListening,
checkWebUrl, checkWebUrl,
@@ -652,19 +644,12 @@ export class StartSdk<Manifest extends T.SDKManifest> {
successFailure, successFailure,
}, },
Mounts: { Mounts: {
of() { of: Mounts.of<Manifest>,
return Mounts.of<Manifest>()
},
}, },
Backups: { Backups: {
volumes: ( ofVolumes: Backups.ofVolumes<Manifest>,
...volumeNames: Array<Manifest["volumes"][number] & string> ofSyncs: Backups.ofSyncs<Manifest>,
) => Backups.withVolumes<Manifest>(...volumeNames), withOptions: Backups.withOptions<Manifest>,
addSets: (
...options: BackupSync<Manifest["volumes"][number] & string>[]
) => Backups.withSyncs<Manifest>(...options),
withOptions: (options?: Partial<SyncOptions>) =>
Backups.withOptions<Manifest>(options),
}, },
InputSpec: { InputSpec: {
/** /**
@@ -705,10 +690,11 @@ export class StartSdk<Manifest extends T.SDKManifest> {
Daemons: { Daemons: {
of( of(
effects: Effects, effects: Effects,
started: (onTerm: () => PromiseLike<void>) => PromiseLike<null>, started:
healthChecks: HealthCheck[], | ((onTerm: () => PromiseLike<void>) => PromiseLike<null>)
| null,
) { ) {
return Daemons.of<Manifest>({ effects, started, healthChecks }) return Daemons.of<Manifest>({ effects, started })
}, },
}, },
SubContainer: { SubContainer: {

View File

@@ -31,10 +31,10 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
private postRestore = async (effects: BackupEffects) => {}, private postRestore = async (effects: BackupEffects) => {},
) {} ) {}
static withVolumes<M extends T.SDKManifest = never>( static ofVolumes<M extends T.SDKManifest = never>(
...volumeNames: Array<M["volumes"][number]> ...volumeNames: Array<M["volumes"][number]>
): Backups<M> { ): Backups<M> {
return Backups.withSyncs( return Backups.ofSyncs(
...volumeNames.map((srcVolume) => ({ ...volumeNames.map((srcVolume) => ({
dataPath: `/media/startos/volumes/${srcVolume}/` as const, dataPath: `/media/startos/volumes/${srcVolume}/` as const,
backupPath: `/media/startos/backup/volumes/${srcVolume}/` as const, backupPath: `/media/startos/backup/volumes/${srcVolume}/` as const,
@@ -42,7 +42,7 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
) )
} }
static withSyncs<M extends T.SDKManifest = never>( static ofSyncs<M extends T.SDKManifest = never>(
...syncs: BackupSync<M["volumes"][number]>[] ...syncs: BackupSync<M["volumes"][number]>[]
) { ) {
return syncs.reduce((acc, x) => acc.addSync(x), new Backups<M>()) return syncs.reduce((acc, x) => acc.addSync(x), new Backups<M>())
@@ -112,11 +112,9 @@ export class Backups<M extends T.SDKManifest> implements InitScript {
...options, ...options,
}) })
} }
addSync(sync: BackupSync<M["volumes"][0]>) { addSync(sync: BackupSync<M["volumes"][0]>) {
this.backupSet.push({ this.backupSet.push(sync)
...sync,
options: { ...this.options, ...sync.options },
})
return this return this
} }

View File

@@ -19,7 +19,7 @@ export function setupBackups<M extends T.SDKManifest>(
if (options instanceof Function) { if (options instanceof Function) {
backupsFactory = options backupsFactory = options
} else { } else {
backupsFactory = async () => Backups.withVolumes(...options) backupsFactory = async () => Backups.ofVolumes(...options)
} }
const answer: SetupBackupsRes = { const answer: SetupBackupsRes = {
get createBackup() { get createBackup() {

View File

@@ -12,7 +12,6 @@ export type HealthCheckParams = {
trigger?: Trigger trigger?: Trigger
gracePeriod?: number gracePeriod?: number
fn(): Promise<HealthCheckResult> | HealthCheckResult fn(): Promise<HealthCheckResult> | HealthCheckResult
onFirstSuccess?: () => unknown | Promise<unknown>
} }
export class HealthCheck extends Drop { export class HealthCheck extends Drop {
@@ -32,13 +31,6 @@ export class HealthCheck extends Drop {
const getCurrentValue = () => this.currentValue const getCurrentValue = () => this.currentValue
const gracePeriod = o.gracePeriod ?? 10_000 const gracePeriod = o.gracePeriod ?? 10_000
const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue) const trigger = (o.trigger ?? defaultTrigger)(getCurrentValue)
const triggerFirstSuccess = once(() =>
Promise.resolve(
"onFirstSuccess" in o && o.onFirstSuccess
? o.onFirstSuccess()
: undefined,
),
)
const checkStarted = () => const checkStarted = () =>
[ [
this.started, this.started,
@@ -78,9 +70,6 @@ export class HealthCheck extends Drop {
message: message || "", message: message || "",
}) })
this.currentValue.lastResult = result this.currentValue.lastResult = result
await triggerFirstSuccess().catch((err) => {
console.error(asError(err))
})
} catch (e) { } catch (e) {
await effects.setHealth({ await effects.setHealth({
name: o.name, name: o.name,

View File

@@ -2,44 +2,49 @@ import { DEFAULT_SIGTERM_TIMEOUT } from "."
import { NO_TIMEOUT, SIGTERM } from "../../../base/lib/types" import { NO_TIMEOUT, SIGTERM } from "../../../base/lib/types"
import * as T 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 { Drop, splitCommand } from "../util"
import * as cp from "child_process" import * as cp from "child_process"
import * as fs from "node:fs/promises" import * as fs from "node:fs/promises"
import { Mounts } from "./Mounts" import { DaemonCommandType, ExecCommandOptions, ExecFnOptions } from "./Daemons"
import { DaemonCommandType } from "./Daemons"
export class CommandController<Manifest extends T.SDKManifest> extends Drop { export class CommandController<
Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null,
> extends Drop {
private constructor( private constructor(
readonly runningAnswer: Promise<null>, readonly runningAnswer: Promise<null>,
private state: { exited: boolean }, private state: { exited: boolean },
private readonly subcontainer: SubContainer<Manifest>, private readonly subcontainer: C,
private process: cp.ChildProcess | AbortController, private process: cp.ChildProcess | AbortController,
readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT,
) { ) {
super() super()
} }
static of<Manifest extends T.SDKManifest>() { static of<
Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null,
>() {
return async ( return async (
effects: T.Effects, effects: T.Effects,
subcontainer: SubContainer<Manifest>, subcontainer: C,
exec: DaemonCommandType, exec: DaemonCommandType<Manifest, C>,
) => { ) => {
try { try {
if ("fn" in exec) { if ("fn" in exec) {
const abort = new AbortController() const abort = new AbortController()
const cell: { ctrl: CommandController<Manifest> } = { const cell: { ctrl: CommandController<Manifest, C> } = {
ctrl: new CommandController( ctrl: new CommandController<Manifest, C>(
exec.fn(subcontainer, abort).then(async (command) => { exec.fn(subcontainer, abort).then(async (command) => {
if (command && !abort.signal.aborted) { if (subcontainer && command && !abort.signal.aborted) {
Object.assign( const newCtrl = (
cell.ctrl, await CommandController.of<
await CommandController.of<Manifest>()( Manifest,
effects, SubContainer<Manifest>
subcontainer, >()(effects, subcontainer, command as ExecCommandOptions)
command, ).leak()
),
) Object.assign(cell.ctrl, newCtrl)
return await cell.ctrl.runningAnswer return await cell.ctrl.runningAnswer
} else { } else {
cell.ctrl.state.exited = true cell.ctrl.state.exited = true
@@ -57,7 +62,7 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
let commands: string[] let commands: string[]
if (T.isUseEntrypoint(exec.command)) { if (T.isUseEntrypoint(exec.command)) {
const imageMeta: T.ImageMetadata = await fs const imageMeta: T.ImageMetadata = await fs
.readFile(`/media/startos/images/${subcontainer.imageId}.json`, { .readFile(`/media/startos/images/${subcontainer!.imageId}.json`, {
encoding: "utf8", encoding: "utf8",
}) })
.catch(() => "{}") .catch(() => "{}")
@@ -70,11 +75,11 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
let childProcess: cp.ChildProcess let childProcess: cp.ChildProcess
if (exec.runAsInit) { if (exec.runAsInit) {
childProcess = await subcontainer.launch(commands, { childProcess = await subcontainer!.launch(commands, {
env: exec.env, env: exec.env,
}) })
} else { } else {
childProcess = await subcontainer.spawn(commands, { childProcess = await subcontainer!.spawn(commands, {
env: exec.env, env: exec.env,
stdio: exec.onStdout || exec.onStderr ? "pipe" : "inherit", stdio: exec.onStdout || exec.onStderr ? "pipe" : "inherit",
}) })
@@ -108,7 +113,7 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
}) })
}) })
return new CommandController( return new CommandController<Manifest, C>(
answer, answer,
state, state,
subcontainer, subcontainer,
@@ -116,7 +121,7 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
exec.sigtermTimeout, exec.sigtermTimeout,
) )
} catch (e) { } catch (e) {
await subcontainer.destroy() await subcontainer?.destroy()
throw e throw e
} }
} }
@@ -144,7 +149,7 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
if (this.process instanceof AbortController) this.process.abort() if (this.process instanceof AbortController) this.process.abort()
else this.process.kill("SIGKILL") else this.process.kill("SIGKILL")
} }
await this.subcontainer.destroy() await this.subcontainer?.destroy()
} }
} }
async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) { async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) {
@@ -178,7 +183,7 @@ export class CommandController<Manifest extends T.SDKManifest> extends Drop {
]) ])
else await this.runningAnswer else await this.runningAnswer
} finally { } finally {
await this.subcontainer.destroy() await this.subcontainer?.destroy()
} }
} }
onDrop(): void { onDrop(): void {

View File

@@ -17,14 +17,17 @@ const MAX_TIMEOUT_MS = 30000
* and the others state of running, where it will keep a living running command * and the others state of running, where it will keep a living running command
*/ */
export class Daemon<Manifest extends T.SDKManifest> extends Drop { export class Daemon<
private commandController: CommandController<Manifest> | null = null Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null = SubContainer<Manifest> | null,
> extends Drop {
private commandController: CommandController<Manifest, C> | null = null
private shouldBeRunning = false private shouldBeRunning = false
protected exitedSuccess = false protected exitedSuccess = false
private onExitFns: ((success: boolean) => void)[] = [] private onExitFns: ((success: boolean) => void)[] = []
protected constructor( protected constructor(
private subcontainer: SubContainer<Manifest>, private subcontainer: C,
private startCommand: (() => Promise<CommandController<Manifest>>) | null, private startCommand: () => Promise<CommandController<Manifest, C>>,
readonly oneshot: boolean = false, readonly oneshot: boolean = false,
) { ) {
super() super()
@@ -33,17 +36,20 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
return this.oneshot return this.oneshot
} }
static of<Manifest extends T.SDKManifest>() { static of<Manifest extends T.SDKManifest>() {
return async ( return async <C extends SubContainer<Manifest> | null>(
effects: T.Effects, effects: T.Effects,
subcontainer: SubContainer<Manifest>, subcontainer: C,
exec: DaemonCommandType | null, exec: DaemonCommandType<Manifest, C>,
) => { ) => {
if (subcontainer.isOwned()) subcontainer = subcontainer.rc() let subc: SubContainer<Manifest> | null = subcontainer
const startCommand = exec if (subcontainer && subcontainer.isOwned()) subc = subcontainer.rc()
? () => const startCommand = () =>
CommandController.of<Manifest>()(effects, subcontainer.rc(), exec) CommandController.of<Manifest, C>()(
: null effects,
return new Daemon(subcontainer, startCommand) (subc?.rc() ?? null) as C,
exec,
)
return new Daemon(subc, startCommand)
} }
} }
async start() { async start() {
@@ -53,7 +59,7 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
this.shouldBeRunning = true this.shouldBeRunning = true
let timeoutCounter = 0 let timeoutCounter = 0
;(async () => { ;(async () => {
while (this.startCommand && this.shouldBeRunning) { while (this.shouldBeRunning) {
if (this.commandController) if (this.commandController)
await this.commandController await this.commandController
.term({}) .term({})
@@ -106,10 +112,10 @@ export class Daemon<Manifest extends T.SDKManifest> extends Drop {
.catch((e) => console.error(asError(e))) .catch((e) => console.error(asError(e)))
this.commandController = null this.commandController = null
this.onExitFns = [] this.onExitFns = []
await this.subcontainer.destroy() await this.subcontainer?.destroy()
} }
subcontainerRc(): SubContainerRc<Manifest> { subcontainerRc(): SubContainerRc<Manifest> | null {
return this.subcontainer.rc() return this.subcontainer?.rc() ?? null
} }
onExit(fn: (success: boolean) => void) { onExit(fn: (success: boolean) => void) {
this.onExitFns.push(fn) this.onExitFns.push(fn)

View File

@@ -37,9 +37,7 @@ export type Ready = {
}) })
* ``` * ```
*/ */
fn: ( fn: () => Promise<HealthCheckResult> | HealthCheckResult
subcontainer: SubContainer<Manifest>,
) => Promise<HealthCheckResult> | HealthCheckResult
/** /**
* A duration in milliseconds to treat a failing health check as "starting" * 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 onStderr?: (chunk: Buffer | string | any) => void
} }
export type ExecFnOptions = { export type ExecFnOptions<
Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null,
> = {
fn: ( fn: (
subcontainer: SubContainer<Manifest>, subcontainer: C,
abort: AbortController, abort: AbortController,
) => Promise<ExecCommandOptions | null> ) => Promise<C extends null ? null : ExecCommandOptions | null>
// Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms // Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms
sigtermTimeout?: number sigtermTimeout?: number
} }
export type DaemonCommandType = ExecCommandOptions | ExecFnOptions export type DaemonCommandType<
Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null,
> = ExecFnOptions<Manifest, C> | (C extends null ? never : ExecCommandOptions)
type NewDaemonParams<Manifest extends T.SDKManifest> = { type NewDaemonParams<
Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null,
> = {
/** What to run as the daemon: either an async fn or a commandline command to run in the subcontainer */ /** What to run as the daemon: either an async fn or a commandline command to run in the subcontainer */
exec: DaemonCommandType | null exec: DaemonCommandType<Manifest, C>
/** Information about the subcontainer in which the daemon runs */ /** The subcontainer in which the daemon runs */
subcontainer: SubContainer<Manifest> subcontainer: C
} }
type AddDaemonParams< type AddDaemonParams<
Manifest extends T.SDKManifest, Manifest extends T.SDKManifest,
Ids extends string, Ids extends string,
Id extends string, Id extends string,
C extends SubContainer<Manifest> | null,
> = ( > = (
| NewDaemonParams<Manifest> | NewDaemonParams<Manifest, C>
| { | {
daemon: Daemon<Manifest> daemon: Daemon<Manifest>
} }
@@ -102,8 +110,15 @@ type AddOneshotParams<
Manifest extends T.SDKManifest, Manifest extends T.SDKManifest,
Ids extends string, Ids extends string,
Id extends string, Id extends string,
> = NewDaemonParams<Manifest> & { C extends SubContainer<Manifest> | null,
exec: DaemonCommandType > = NewDaemonParams<Manifest, C> & {
exec: DaemonCommandType<Manifest, C>
/** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */
requires: Exclude<Ids, Id>[]
}
type AddHealthCheckParams<Ids extends string, Id extends string> = {
ready: Ready
/** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */ /** An array of IDs of prior daemons whose successful initializations are required before this daemon will initialize */
requires: Exclude<Ids, Id>[] requires: Exclude<Ids, Id>[]
} }
@@ -111,7 +126,7 @@ type AddOneshotParams<
type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used` type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used`
export const runCommand = <Manifest extends T.SDKManifest>() => export const runCommand = <Manifest extends T.SDKManifest>() =>
CommandController.of<Manifest>() CommandController.of<Manifest, SubContainer<Manifest>>()
/** /**
* A class for defining and controlling the service daemons * A class for defining and controlling the service daemons
@@ -141,11 +156,12 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
{ {
private constructor( private constructor(
readonly effects: T.Effects, readonly effects: T.Effects,
readonly started: (onTerm: () => PromiseLike<void>) => PromiseLike<null>, readonly started:
| ((onTerm: () => PromiseLike<void>) => PromiseLike<null>)
| null,
readonly daemons: Promise<Daemon<Manifest>>[], readonly daemons: Promise<Daemon<Manifest>>[],
readonly ids: Ids[], readonly ids: Ids[],
readonly healthDaemons: HealthDaemon<Manifest>[], readonly healthDaemons: HealthDaemon<Manifest>[],
readonly healthChecks: HealthCheck[],
) {} ) {}
/** /**
* Returns an empty new Daemons class with the provided inputSpec. * Returns an empty new Daemons class with the provided inputSpec.
@@ -154,13 +170,18 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
* *
* Daemons run in the order they are defined, with latter daemons being capable of * Daemons run in the order they are defined, with latter daemons being capable of
* depending on prior daemons * depending on prior daemons
* @param options *
* @param effects
*
* @param started
* @returns * @returns
*/ */
static of<Manifest extends T.SDKManifest>(options: { static of<Manifest extends T.SDKManifest>(options: {
effects: T.Effects effects: T.Effects
started: (onTerm: () => PromiseLike<void>) => PromiseLike<null> /**
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<void>) => PromiseLike<null>) | null
}) { }) {
return new Daemons<Manifest, never>( return new Daemons<Manifest, never>(
options.effects, options.effects,
@@ -168,7 +189,6 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
[], [],
[], [],
[], [],
options.healthChecks,
) )
} }
/** /**
@@ -177,19 +197,19 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
* @param options * @param options
* @returns a new Daemons object * @returns a new Daemons object
*/ */
addDaemon<Id extends string>( addDaemon<Id extends string, C extends SubContainer<Manifest> | null>(
// prettier-ignore // prettier-ignore
id: id:
"" extends Id ? never : "" extends Id ? never :
ErrorDuplicateId<Id> extends Id ? never : ErrorDuplicateId<Id> extends Id ? never :
Id extends Ids ? ErrorDuplicateId<Id> : Id extends Ids ? ErrorDuplicateId<Id> :
Id, Id,
options: AddDaemonParams<Manifest, Ids, Id>, options: AddDaemonParams<Manifest, Ids, Id, C>,
) { ) {
const daemon = const daemon =
"daemon" in options "daemon" in options
? Promise.resolve(options.daemon) ? Promise.resolve(options.daemon)
: Daemon.of<Manifest>()( : Daemon.of<Manifest>()<C>(
this.effects, this.effects,
options.subcontainer, options.subcontainer,
options.exec, options.exec,
@@ -201,11 +221,10 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
.filter((x) => x >= 0) .filter((x) => x >= 0)
.map((id) => this.healthDaemons[id]), .map((id) => this.healthDaemons[id]),
id, id,
this.ids,
options.ready, options.ready,
this.effects, this.effects,
) )
const daemons = this.daemons.concat(daemon) const daemons = [...this.daemons, daemon]
const ids = [...this.ids, id] as (Ids | Id)[] const ids = [...this.ids, id] as (Ids | Id)[]
const healthDaemons = [...this.healthDaemons, healthDaemon] const healthDaemons = [...this.healthDaemons, healthDaemon]
return new Daemons<Manifest, Ids | Id>( return new Daemons<Manifest, Ids | Id>(
@@ -214,7 +233,6 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
daemons, daemons,
ids, ids,
healthDaemons, healthDaemons,
this.healthChecks,
) )
} }
@@ -225,7 +243,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
* @param options * @param options
* @returns a new Daemons object * @returns a new Daemons object
*/ */
addOneshot<Id extends string>( addOneshot<Id extends string, C extends SubContainer<Manifest> | null>(
id: "" extends Id id: "" extends Id
? never ? never
: ErrorDuplicateId<Id> extends Id : ErrorDuplicateId<Id> extends Id
@@ -233,9 +251,9 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
: Id extends Ids : Id extends Ids
? ErrorDuplicateId<Id> ? ErrorDuplicateId<Id>
: Id, : Id,
options: AddOneshotParams<Manifest, Ids, Id>, options: AddOneshotParams<Manifest, Ids, Id, C>,
) { ) {
const daemon = Oneshot.of<Manifest>()( const daemon = Oneshot.of<Manifest>()<C>(
this.effects, this.effects,
options.subcontainer, options.subcontainer,
options.exec, options.exec,
@@ -247,11 +265,10 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
.filter((x) => x >= 0) .filter((x) => x >= 0)
.map((id) => this.healthDaemons[id]), .map((id) => this.healthDaemons[id]),
id, id,
this.ids,
"EXIT_SUCCESS", "EXIT_SUCCESS",
this.effects, this.effects,
) )
const daemons = this.daemons.concat(daemon) const daemons = [...this.daemons, daemon]
const ids = [...this.ids, id] as (Ids | Id)[] const ids = [...this.ids, id] as (Ids | Id)[]
const healthDaemons = [...this.healthDaemons, healthDaemon] const healthDaemons = [...this.healthDaemons, healthDaemon]
return new Daemons<Manifest, Ids | Id>( return new Daemons<Manifest, Ids | Id>(
@@ -260,13 +277,95 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
daemons, daemons,
ids, ids,
healthDaemons, 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 string>(
id: "" extends Id
? never
: ErrorDuplicateId<Id> extends Id
? never
: Id extends Ids
? ErrorDuplicateId<Id>
: Id,
options: AddHealthCheckParams<Ids, Id>,
) {
const healthDaemon = new HealthDaemon<Manifest>(
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<Manifest, Ids | Id>(
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<void>((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<Manifest>(
daemon,
[...this.healthDaemons],
"__RUN_UNTIL_SUCCESS",
"EXIT_SUCCESS",
this.effects,
)
const daemons = await new Daemons<Manifest, Ids>(
this.effects,
this.started,
[...this.daemons, daemon],
this.ids,
[...this.healthDaemons, healthDaemon],
).build()
try {
await res
} finally {
await daemons.term()
}
return null
}
async term() { async term() {
try { try {
this.healthChecks.forEach((health) => health.stop())
for (let result of await Promise.allSettled( for (let result of await Promise.allSettled(
this.healthDaemons.map((x) => x.term()), this.healthDaemons.map((x) => x.term()),
)) { )) {
@@ -283,10 +382,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
for (const daemon of this.healthDaemons) { for (const daemon of this.healthDaemons) {
await daemon.init() await daemon.init()
} }
for (const health of this.healthChecks) { this.started?.(() => this.term())
health.start()
}
this.started(() => this.term())
return this return this
} }
} }

View File

@@ -6,6 +6,7 @@ import { SetHealth, Effects, SDKManifest } from "../../../base/lib/types"
import { DEFAULT_SIGTERM_TIMEOUT } from "." import { DEFAULT_SIGTERM_TIMEOUT } from "."
import { asError } from "../../../base/lib/util/asError" import { asError } from "../../../base/lib/util/asError"
import { Oneshot } from "./Oneshot" import { Oneshot } from "./Oneshot"
import { SubContainer } from "../util/SubContainer"
const oncePromise = <T>() => { const oncePromise = <T>() => {
let resolve: (value: T) => void let resolve: (value: T) => void
@@ -30,16 +31,22 @@ export class HealthDaemon<Manifest extends SDKManifest> {
private running = false private running = false
private started?: number private started?: number
private resolveReady: (() => void) | undefined private resolveReady: (() => void) | undefined
private resolvedReady: boolean = false
private readyPromise: Promise<void> private readyPromise: Promise<void>
constructor( constructor(
private readonly daemon: Promise<Daemon<Manifest>>, private readonly daemon: Promise<Daemon<Manifest>> | null,
private readonly dependencies: HealthDaemon<Manifest>[], private readonly dependencies: HealthDaemon<Manifest>[],
readonly id: string, readonly id: string,
readonly ids: string[],
readonly ready: Ready | typeof EXIT_SUCCESS, readonly ready: Ready | typeof EXIT_SUCCESS,
readonly effects: Effects, 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())) this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus()))
} }
@@ -52,7 +59,7 @@ export class HealthDaemon<Manifest extends SDKManifest> {
this.running = false this.running = false
this.healthCheckCleanup?.() this.healthCheckCleanup?.()
await this.daemon.then((d) => await this.daemon?.then((d) =>
d.term({ d.term({
...termOptions, ...termOptions,
}), }),
@@ -74,11 +81,13 @@ export class HealthDaemon<Manifest extends SDKManifest> {
this.running = newStatus this.running = newStatus
if (newStatus) { if (newStatus) {
;(await this.daemon).start() console.debug(`Launching ${this.id}...`)
this.started = performance.now()
this.setupHealthCheck() this.setupHealthCheck()
;(await this.daemon)?.start()
this.started = performance.now()
} else { } else {
;(await this.daemon).stop() console.debug(`Stopping ${this.id}...`)
;(await this.daemon)?.stop()
this.turnOffHealthCheck() this.turnOffHealthCheck()
this.setHealth({ result: "starting", message: null }) this.setHealth({ result: "starting", message: null })
@@ -88,10 +97,19 @@ export class HealthDaemon<Manifest extends SDKManifest> {
private healthCheckCleanup: (() => null) | null = null private healthCheckCleanup: (() => null) | null = null
private turnOffHealthCheck() { private turnOffHealthCheck() {
this.healthCheckCleanup?.() this.healthCheckCleanup?.()
this.resolvedReady = false
this.readyPromise = new Promise(
(resolve) =>
(this.resolveReady = () => {
resolve()
this.resolvedReady = true
}),
)
} }
private async setupHealthCheck() { private async setupHealthCheck() {
const daemon = await this.daemon const daemon = await this.daemon
daemon.onExit((success) => { daemon?.onExit((success) => {
if (success && this.ready === "EXIT_SUCCESS") { if (success && this.ready === "EXIT_SUCCESS") {
this.setHealth({ result: "success", message: null }) this.setHealth({ result: "success", message: null })
} else if (!success) { } else if (!success) {
@@ -122,28 +140,17 @@ export class HealthDaemon<Manifest extends SDKManifest> {
!res.done; !res.done;
res = await Promise.race([status, trigger.next()]) res = await Promise.race([status, trigger.next()])
) { ) {
const handle = (await this.daemon).subcontainerRc() const response: HealthCheckResult = await Promise.resolve(
this.ready.fn(),
try { ).catch((err) => {
const response: HealthCheckResult = await Promise.resolve( console.error(asError(err))
this.ready.fn(handle), return {
).catch((err) => { result: "failure",
console.error(asError(err)) message: "message" in err ? err.message : String(err),
return {
result: "failure",
message: "message" in err ? err.message : String(err),
}
})
if (
this.resolveReady &&
(response.result === "success" || response.result === "disabled")
) {
this.resolveReady()
} }
await this.setHealth(response) })
} finally {
await handle.destroy() await this.setHealth(response)
}
} }
}).catch((err) => console.error(`Daemon ${this.id} failed: ${err}`)) }).catch((err) => console.error(`Daemon ${this.id} failed: ${err}`))
@@ -158,10 +165,18 @@ export class HealthDaemon<Manifest extends SDKManifest> {
return this.readyPromise return this.readyPromise
} }
get isReady() {
return this.resolvedReady
}
private async setHealth(health: HealthCheckResult) { private async setHealth(health: HealthCheckResult) {
const changed = this._health.result !== health.result
this._health = health this._health = health
if (this.resolveReady && health.result === "success") {
this.resolveReady()
}
if (changed) this.healthWatchers.forEach((watcher) => watcher())
if (this.ready === "EXIT_SUCCESS") return if (this.ready === "EXIT_SUCCESS") return
this.healthWatchers.forEach((watcher) => watcher())
const display = this.ready.display const display = this.ready.display
if (!display) { if (!display) {
return return
@@ -182,8 +197,18 @@ export class HealthDaemon<Manifest extends SDKManifest> {
} }
async updateStatus() { async updateStatus() {
const healths = this.dependencies.map((d) => d.running && d._health) const healths = this.dependencies.map((d) => ({
this.changeRunning(healths.every((x) => x && x.result === "success")) 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() { async init() {

View File

@@ -10,19 +10,25 @@ import { DaemonCommandType } from "./Daemons"
* unlike Daemon, does not restart on success * unlike Daemon, does not restart on success
*/ */
export class Oneshot<Manifest extends T.SDKManifest> extends Daemon<Manifest> { export class Oneshot<
Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null = SubContainer<Manifest> | null,
> extends Daemon<Manifest, C> {
static of<Manifest extends T.SDKManifest>() { static of<Manifest extends T.SDKManifest>() {
return async ( return async <C extends SubContainer<Manifest> | null>(
effects: T.Effects, effects: T.Effects,
subcontainer: SubContainer<Manifest>, subcontainer: C,
exec: DaemonCommandType | null, exec: DaemonCommandType<Manifest, C>,
) => { ) => {
if (subcontainer.isOwned()) subcontainer = subcontainer.rc() let subc: SubContainer<Manifest> | null = subcontainer
const startCommand = exec if (subcontainer && subcontainer.isOwned()) subc = subcontainer.rc()
? () => const startCommand = () =>
CommandController.of<Manifest>()(effects, subcontainer.rc(), exec) CommandController.of<Manifest, C>()(
: null effects,
return new Oneshot(subcontainer, startCommand, true) (subc?.rc() ?? null) as C,
exec,
)
return new Oneshot<Manifest, C>(subcontainer, startCommand, true)
} }
} }
} }

View File

@@ -6,7 +6,7 @@ import {
} from "../../../base/lib/types/ManifestTypes" } from "../../../base/lib/types/ManifestTypes"
import { OSVersion } from "../StartSdk" import { OSVersion } from "../StartSdk"
import { VersionGraph } from "../version/VersionGraph" 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 * @description Use this function to define critical information about your package
@@ -55,6 +55,7 @@ export function buildManifest<
return { return {
...manifest, ...manifest,
osVersion: manifest.osVersion ?? OSVersion, osVersion: manifest.osVersion ?? OSVersion,
sdkVersion,
version: versions.current.options.version, version: versions.current.options.version,
releaseNotes: versions.current.options.releaseNotes, releaseNotes: versions.current.options.releaseNotes,
satisfies: versions.current.options.satisfies || [], satisfies: versions.current.options.satisfies || [],

View File

@@ -5,18 +5,18 @@ export abstract class Drop {
if (weak) weak.drop() if (weak) weak.drop()
}) })
private static idCtr: number = 0 private static idCtr: number = 0
private id: number private dropId?: number
private ref: { id: number } | WeakRef<{ id: number }> private dropRef?: { id: number } | WeakRef<{ id: number }>
protected constructor() { protected constructor() {
this.id = Drop.idCtr++ this.dropId = Drop.idCtr++
this.ref = { id: this.id } this.dropRef = { id: this.dropId }
const weak = this.weak() const weak = this.weak()
Drop.weak[this.id] = weak Drop.weak[this.dropId] = weak
Drop.registry.register(this.ref, this.id, this.ref) Drop.registry.register(this.dropRef, this.dropId, this.dropRef)
return new Proxy(this, { return new Proxy(this, {
set(target: any, prop, value) { set(target: any, prop, value) {
if (prop === "ref") return false if (prop === "dropRef" || prop == "dropId") return false
target[prop] = value target[prop] = value
;(weak as any)[prop] = value ;(weak as any)[prop] = value
return true return true
@@ -26,13 +26,21 @@ export abstract class Drop {
protected register() {} protected register() {}
protected weak(): this { protected weak(): this {
const weak = Object.assign(Object.create(Object.getPrototypeOf(this)), 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 return weak
} }
abstract onDrop(): void abstract onDrop(): void
drop(): void { drop(): void {
if (!this.dropRef || !this.dropId) return
this.onDrop() this.onDrop()
Drop.registry.unregister(this.ref) this.leak()
delete Drop.weak[this.id] }
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
} }
} }

View File

@@ -340,9 +340,17 @@ export class FileHelper<A> {
/** /**
* Accepts full structured data and overwrites the existing file on disk if it exists. * Accepts full structured data and overwrites the existing file on disk if it exists.
*/ */
async write(effects: T.Effects, data: T.AllowReadonly<A> | A) { async write(
effects: T.Effects,
data: T.AllowReadonly<A> | A,
options: { allowWriteAfterConst?: boolean } = {},
) {
await this.writeFile(this.validate(data)) 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}`) throw new Error(`Canceled: write after const: ${this.path}`)
return null return null
} }
@@ -350,7 +358,11 @@ export class FileHelper<A> {
/** /**
* Accepts partial structured data and performs a merge with the existing file on disk. * Accepts partial structured data and performs a merge with the existing file on disk.
*/ */
async merge(effects: T.Effects, data: T.AllowReadonly<T.DeepPartial<A>>) { async merge(
effects: T.Effects,
data: T.AllowReadonly<T.DeepPartial<A>>,
options: { allowWriteAfterConst?: boolean } = {},
) {
const fileDataRaw = await this.readFileRaw() const fileDataRaw = await this.readFileRaw()
let fileData: any = fileDataRaw === null ? null : this.readData(fileDataRaw) let fileData: any = fileDataRaw === null ? null : this.readData(fileDataRaw)
try { try {
@@ -360,7 +372,11 @@ export class FileHelper<A> {
const toWrite = this.writeData(mergeData) const toWrite = this.writeData(mergeData)
if (toWrite !== fileDataRaw) { if (toWrite !== fileDataRaw) {
this.writeFile(mergeData) 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) const diff = partialDiff(fileData, mergeData as any)
if (!diff) { if (!diff) {
return null return null

View File

@@ -7,6 +7,7 @@ import {
InitScriptOrFn, InitScriptOrFn,
UninitFn, UninitFn,
UninitScript, UninitScript,
UninitScriptOrFn,
} from "../../../base/lib/inits" } from "../../../base/lib/inits"
import { Graph, Vertex, once } from "../util" import { Graph, Vertex, once } from "../util"
import { IMPOSSIBLE, VersionInfo } from "./VersionInfo" import { IMPOSSIBLE, VersionInfo } from "./VersionInfo"
@@ -171,11 +172,11 @@ export class VersionGraph<CurrentVersion extends string>
/** /**
* A script to run only on fresh install * A script to run only on fresh install
*/ */
preInstall?: InitScript | InitFn preInstall?: InitScriptOrFn<"install">
/** /**
* A script to run only on uninstall * A script to run only on uninstall
*/ */
uninstall?: UninitScript | UninitFn uninstall?: UninitScriptOrFn
}) { }) {
return new VersionGraph( return new VersionGraph(
options.current, options.current,

View File

@@ -1,12 +1,12 @@
{ {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.27", "version": "0.4.0-beta.30",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.4.0-beta.27", "version": "0.4.0-beta.30",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^3.0.0", "@iarna/toml": "^3.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@start9labs/start-sdk", "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", "description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js", "main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts", "types": "./package/lib/index.d.ts",
@@ -22,14 +22,14 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/Start9Labs/start-sdk.git" "url": "git+https://github.com/Start9Labs/start-os.git"
}, },
"author": "Start9 Labs", "author": "Start9 Labs",
"license": "MIT", "license": "MIT",
"bugs": { "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": { "dependencies": {
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"mime": "^4.0.7", "mime": "^4.0.7",

View File

@@ -12,7 +12,8 @@
"skipLibCheck": true, "skipLibCheck": true,
"module": "commonjs", "module": "commonjs",
"outDir": "../dist", "outDir": "../dist",
"target": "es2021" "target": "es2021",
"resolveJsonModule": true
}, },
"include": ["lib/**/*", "../base/lib/util/Hostname.ts"], "include": ["lib/**/*", "../base/lib/util/Hostname.ts"],
"exclude": ["lib/**/*.spec.ts", "lib/**/*.gen.ts", "list", "node_modules"] "exclude": ["lib/**/*.spec.ts", "lib/**/*.gen.ts", "list", "node_modules"]

4
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "startos-ui", "name": "startos-ui",
"version": "0.4.0-alpha.6", "version": "0.4.0-alpha.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "startos-ui", "name": "startos-ui",
"version": "0.4.0-alpha.6", "version": "0.4.0-alpha.7",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@angular/animations": "^19.2.11", "@angular/animations": "^19.2.11",

View File

@@ -1,6 +1,6 @@
{ {
"name": "startos-ui", "name": "startos-ui",
"version": "0.4.0-alpha.6", "version": "0.4.0-alpha.7",
"author": "Start9 Labs, Inc", "author": "Start9 Labs, Inc",
"homepage": "https://start9.com/", "homepage": "https://start9.com/",
"license": "MIT", "license": "MIT",

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
@@ -14,7 +14,14 @@
<meta name="format-detection" content="telephone=no" /> <meta name="format-detection" content="telephone=no" />
<meta name="msapplication-tap-highlight" content="no" /> <meta name="msapplication-tap-highlight" content="no" />
<link rel="icon" type="image/png" href="assets/icon/favicon.ico" /> <link
rel="icon"
type="image/png"
href="/assets/icons/favicon-96x96.png"
sizes="96x96"
/>
<link rel="icon" type="image/svg+xml" href="/assets/icons/favicon.svg" />
<link rel="shortcut icon" href="/assets/icons/favicon.ico" />
</head> </head>
<body> <body>

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
@@ -15,7 +15,14 @@
<script> <script>
var global = window var global = window
</script> </script>
<link rel="icon" type="image/x-icon" href="assets/icon/favicon.ico" /> <link
rel="icon"
type="image/png"
href="/assets/icons/favicon-96x96.png"
sizes="96x96"
/>
<link rel="icon" type="image/svg+xml" href="/assets/icons/favicon.svg" />
<link rel="shortcut icon" href="/assets/icons/favicon.ico" />
</head> </head>
<body> <body>

View File

@@ -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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -1,5 +0,0 @@
<svg width="1369" height="1369" viewBox="0 0 1369 1369" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M245.229 1154.11C218.012 1203.36 279.4 1235.04 304.523 1188.79C315.173 1169.19 641.573 388.267 681.128 296.285L1061.75 1185.9C1087.02 1239.85 1150.2 1200.35 1126.87 1149.29L719.161 220.202C699.384 177.981 658.308 177.981 640.052 221.71C638.53 224.726 260.899 1125.74 245.229 1154.11Z" fill="#F0F0F0"/>
<path d="M173.678 1079.24C99.1497 984.791 52.8134 871.532 39.9236 752.308C27.0337 633.084 48.106 512.665 100.751 404.708C153.396 296.75 235.507 205.573 337.773 141.516C440.038 77.4585 558.367 43.0845 679.34 42.2916C800.313 41.4988 919.089 74.3187 1022.2 137.03C1125.31 199.741 1208.63 289.834 1262.71 397.092C1316.79 504.35 1339.47 624.482 1328.17 743.864C1316.88 863.247 1272.05 977.103 1198.79 1072.52L1197.54 1071.58C1270.62 976.389 1315.33 862.81 1326.6 743.718C1337.87 624.627 1315.25 504.788 1261.3 397.791C1207.35 290.794 1124.24 200.921 1021.38 138.362C918.516 75.804 800.028 43.064 679.35 43.8549C558.672 44.6459 440.632 78.9361 338.615 142.837C236.599 206.738 154.688 297.693 102.171 405.388C49.6544 513.082 28.6335 633.208 41.4919 752.141C54.3504 871.075 100.574 984.058 174.92 1078.28L173.678 1079.24Z" stroke="#F0F0F0" stroke-width="65.4186" stroke-linejoin="round"/>
<path d="M393.951 1258.16C484.768 1302.62 583.891 1326.42 685.181 1325.58C786.471 1324.73 886.158 1302.21 976.206 1256.23L976.206 1254.3C886.377 1300.17 786.211 1323.17 685.167 1324.01C584.124 1324.86 484.546 1300.59 393.951 1256.23L393.951 1258.16Z" stroke="#F0F0F0" stroke-width="65.4186" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,7 +0,0 @@
<svg width="32" height="32" viewBox="0 0 34 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="4.78958" width="26.4999" height="26.4999" rx="3.53333" stroke="black" stroke-width="1.76666" stroke-linejoin="round"/>
<rect x="8.47266" y="9.2063" width="7" height="7" rx="0.883332" fill="black"/>
<rect x="8.47266" y="18.9231" width="7" height="7.94998" rx="0.883332" fill="black"/>
<rect x="19.0723" y="22.4563" width="7" height="4.41666" rx="0.883332" fill="black"/>
<rect x="19.0723" y="9.2063" width="7" height="10.6" rx="0.883332" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 580 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M410.47 279.2c-5-11.5-12.7-21.6-28.1-30.1a98.15 98.15 0 00-25.4-10 62.22 62.22 0 0016.3-11 56.37 56.37 0 0015.6-23.3 77.11 77.11 0 003.5-28.2c-1.1-16.8-4.4-33.1-13.2-44.8s-21.2-20.7-37.6-27c-12.6-4.8-25.5-7.8-45.5-8.9V32h-40v64h-32V32h-41v64H96v48h27.87c8.7 0 14.6.8 17.6 2.3a13.22 13.22 0 016.5 6c1.3 2.5 1.9 8.4 1.9 17.5V343c0 9-.6 14.8-1.9 17.4s-2 4.9-5.1 6.3-3.2 1.3-11.8 1.3h-26.4L96 416h87v64h41v-64h32v64h40v-64.4c26-1.3 44.5-4.7 59.4-10.3 19.3-7.2 34.1-17.7 44.7-31.5s14-34.9 14.93-51.2c.67-14.5-.03-33.2-4.56-43.4zM224 150h32v74h-32zm0 212v-90h32v90zm72-208.1c6 2.5 9.9 7.5 13.8 12.7 4.3 5.7 6.5 13.3 6.5 21.4 0 7.8-2.9 14.5-7.5 20.5-3.8 4.9-6.8 8.3-12.8 11.1zm28.8 186.7c-7.8 6.9-12.3 10.1-22.1 13.8a56.06 56.06 0 01-6.7 1.9v-82.8a40.74 40.74 0 0111.3 3.4c7.8 3.3 15.2 6.9 19.8 13.2a43.82 43.82 0 018 24.7c-.03 10.9-2.83 19.2-10.33 25.8z"/></svg>

Before

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -521,4 +521,5 @@ export default {
519: 'Um Clearnet-Domains zu veröffentlichen, musst du oben auf „Öffentlich machen“ klicken.', 519: 'Um Clearnet-Domains zu veröffentlichen, musst du oben auf „Öffentlich machen“ klicken.',
520: 'Update verfügbar', 520: 'Update verfügbar',
521: 'Um das Problem zu beheben, siehe', 521: 'Um das Problem zu beheben, siehe',
522: 'SDK Version',
} satisfies i18n } satisfies i18n

View File

@@ -520,4 +520,5 @@ export const ENGLISH = {
'To publish clearnet domains, you must click "Make Public", above.': 519, 'To publish clearnet domains, you must click "Make Public", above.': 519,
'Update available': 520, 'Update available': 520,
'To resolve the issue, refer to': 521, 'To resolve the issue, refer to': 521,
'SDK Version': 522,
} as const } as const

View File

@@ -521,4 +521,5 @@ export default {
519: 'Para publicar dominios en clearnet, debes hacer clic en "Hacer público" arriba.', 519: 'Para publicar dominios en clearnet, debes hacer clic en "Hacer público" arriba.',
520: 'Actualización disponible', 520: 'Actualización disponible',
521: 'Para resolver el problema, consulta', 521: 'Para resolver el problema, consulta',
522: 'Versión de SDK',
} satisfies i18n } satisfies i18n

View File

@@ -521,4 +521,5 @@ export default {
519: 'Pour publier des domaines clearnet, vous devez cliquer sur « Rendre public » ci-dessus.', 519: 'Pour publier des domaines clearnet, vous devez cliquer sur « Rendre public » ci-dessus.',
520: 'Mise à jour disponible', 520: 'Mise à jour disponible',
521: 'Pour résoudre le problème, consultez', 521: 'Pour résoudre le problème, consultez',
522: 'Version de SDK',
} satisfies i18n } satisfies i18n

View File

@@ -521,4 +521,5 @@ export default {
519: 'Aby opublikować domeny w clearnet, kliknij „Upublicznij” powyżej.', 519: 'Aby opublikować domeny w clearnet, kliknij „Upublicznij” powyżej.',
520: 'Aktualizacja dostępna', 520: 'Aktualizacja dostępna',
521: 'Aby rozwiązać problem, zapoznaj się z', 521: 'Aby rozwiązać problem, zapoznaj się z',
522: 'Wersja SDK',
} satisfies i18n } satisfies i18n

View File

@@ -12,15 +12,10 @@ export function formatProgress({ phases, overall }: T.FullProgress): {
p, p,
): p is { ): p is {
name: string name: string
progress: progress: false | ProgressDetails
| false
| {
done: number
total: number | null
}
} => p.progress !== true && p.progress !== null, } => p.progress !== true && p.progress !== null,
) )
.map(p => `<b>${p.name}</b>${getPhaseBytes(p.progress)}`) .map(p => `<b>${p.name}</b>${getDetails(p.progress)}`)
.join(', '), .join(', '),
} }
} }
@@ -35,13 +30,14 @@ function getDecimal(progress: T.Progress): number {
} }
} }
function getPhaseBytes( function getDetails(progress: false | ProgressDetails) {
progress: return progress
| false ? `: ${progress.done}/${progress.total} ${progress.units || ''}`
| { : ''
done: number }
total: number | null
}, type ProgressDetails = {
) { done: number
return progress ? `: ${progress.done}/${progress.total}` : '' total: number | null
units: T.ProgressUnits | null
} }

View File

@@ -202,6 +202,11 @@ export class InterfaceClearnetComponent {
} }
const loader = this.loader.open('Removing').subscribe() 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 } const params = { domain: new URL(url).hostname }
try { try {

View File

@@ -32,7 +32,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
<span tuiSubtitle class="g-warning"> <span tuiSubtitle class="g-warning">
{{ error | i18n }} {{ error | i18n }}
@if (getHealthCheckName(d.key); as healthCheckName) { @if (getHealthCheckName(d.key); as healthCheckName) {
: {{ getHealthCheckName }} : {{ healthCheckName }}
} }
</span> </span>
} @else { } @else {

View File

@@ -81,9 +81,17 @@ export default class ServiceAboutRoute {
icon: '@tui.copy', icon: '@tui.copy',
action: () => this.copyService.copy(manifest.version), 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', name: 'Git Hash',
value: manifest.gitHash || 'Unknown', value: manifest.gitHash || '-',
icon: manifest.gitHash ? '@tui.copy' : '', icon: manifest.gitHash ? '@tui.copy' : '',
action: () => action: () =>
manifest.gitHash && this.copyService.copy(manifest.gitHash), manifest.gitHash && this.copyService.copy(manifest.gitHash),

View File

@@ -110,7 +110,7 @@ export namespace Mock {
squashfs: { squashfs: {
aarch64: { aarch64: {
publishedAt: '2025-04-21T20:58:48.140749883Z', 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: { commitment: {
hash: '4elBFVkd/r8hNadKmKtLIs42CoPltMvKe2z3LRqkphk=', hash: '4elBFVkd/r8hNadKmKtLIs42CoPltMvKe2z3LRqkphk=',
size: 1343500288, size: 1343500288,
@@ -122,7 +122,7 @@ export namespace Mock {
}, },
'aarch64-nonfree': { 'aarch64-nonfree': {
publishedAt: '2025-04-21T21:07:00.249285116Z', 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: { commitment: {
hash: 'MrCEi4jxbmPS7zAiGk/JSKlMsiuKqQy6RbYOxlGHOIQ=', hash: 'MrCEi4jxbmPS7zAiGk/JSKlMsiuKqQy6RbYOxlGHOIQ=',
size: 1653075968, size: 1653075968,
@@ -134,7 +134,7 @@ export namespace Mock {
}, },
raspberrypi: { raspberrypi: {
publishedAt: '2025-04-21T21:16:12.933319237Z', 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: { commitment: {
hash: '/XTVQRCqY3RK544PgitlKu7UplXjkmzWoXUh2E4HCw0=', hash: '/XTVQRCqY3RK544PgitlKu7UplXjkmzWoXUh2E4HCw0=',
size: 1490731008, size: 1490731008,
@@ -146,7 +146,7 @@ export namespace Mock {
}, },
x86_64: { x86_64: {
publishedAt: '2025-04-21T21:14:20.246908903Z', 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: { commitment: {
hash: '/6romKTVQGSaOU7FqSZdw0kFyd7P+NBSYNwM3q7Fe44=', hash: '/6romKTVQGSaOU7FqSZdw0kFyd7P+NBSYNwM3q7Fe44=',
size: 1411657728, size: 1411657728,
@@ -158,7 +158,7 @@ export namespace Mock {
}, },
'x86_64-nonfree': { 'x86_64-nonfree': {
publishedAt: '2025-04-21T21:15:17.955265284Z', 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: { commitment: {
hash: 'HCRq9sr/0t85pMdrEgNBeM4x11zVKHszGnD1GDyZbSE=', hash: 'HCRq9sr/0t85pMdrEgNBeM4x11zVKHszGnD1GDyZbSE=',
size: 1731035136, size: 1731035136,
@@ -226,6 +226,7 @@ export namespace Mock {
stop: null, stop: null,
}, },
osVersion: '0.2.12', osVersion: '0.2.12',
sdkVersion: '0.4.0',
dependencies: {}, dependencies: {},
images: { images: {
main: { main: {
@@ -270,6 +271,7 @@ export namespace Mock {
stop: null, stop: null,
}, },
osVersion: '0.2.12', osVersion: '0.2.12',
sdkVersion: '0.4.0',
dependencies: { dependencies: {
bitcoind: { bitcoind: {
description: 'LND needs bitcoin to live.', description: 'LND needs bitcoin to live.',
@@ -325,6 +327,7 @@ export namespace Mock {
stop: null, stop: null,
}, },
osVersion: '0.2.12', osVersion: '0.2.12',
sdkVersion: '0.4.0',
dependencies: { dependencies: {
bitcoind: { bitcoind: {
description: 'Bitcoin Proxy requires a Bitcoin node.', description: 'Bitcoin Proxy requires a Bitcoin node.',

View File

@@ -32,6 +32,7 @@ const PROGRESS: T.FullProgress = {
overall: { overall: {
done: 0, done: 0,
total: 120, total: 120,
units: 'bytes',
}, },
phases: [ phases: [
{ {
@@ -39,6 +40,7 @@ const PROGRESS: T.FullProgress = {
progress: { progress: {
done: 0, done: 0,
total: 40, total: 40,
units: 'bytes',
}, },
}, },
{ {

View File

@@ -1,11 +1,8 @@
import { enableProdMode } from '@angular/core' import { enableProdMode } from '@angular/core'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { AppModule } from './app/app.module' import { AppModule } from './app/app.module'
import { environment } from './environments/environment' import { environment } from './environments/environment'
; (window as any).global = window
if (environment.production) { if (environment.production) {
enableProdMode() enableProdMode()
} }

View File

@@ -1,24 +1,16 @@
{ {
"name": "StartOS", "name": "StartOS",
"short_name": "StartOS", "background_color": "#f0f0f0",
"theme_color": "#ff5b71",
"background_color": "#1e1e1e",
"display": "standalone", "display": "standalone",
"scope": ".", "start_url": "./",
"start_url": "/?version=036",
"id": "/?version=036",
"icons": [ "icons": [
{ {
"src": "/assets/icons/web-app-manifest-192x192.png", "src": "/assets/icons/startos-192x192.png",
"sizes": "192x192", "sizes": "192x192"
"type": "image/png",
"purpose": "any"
}, },
{ {
"src": "/assets/icons/web-app-manifest-512x512.png", "src": "/assets/icons/startos-512x512.png",
"sizes": "512x512", "sizes": "512x512"
"type": "image/png",
"purpose": "any"
} }
] ]
} }