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

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
sleep 1
done
while ! /usr/lib/startos/scripts/check-monitor; do
sleep 15
done
(
while /usr/lib/startos/scripts/check-monitor; do
sleep 15
done
killall firefox-esr
) &
matchbox-window-manager -use_titlebar no &
cp -r /home/kiosk/fx-profile /home/kiosk/fx-profile-tmp
firefox-esr http://localhost --profile /home/kiosk/fx-profile-tmp

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": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.27",
"version": "0.4.0-beta.30",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",

View File

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

2
core/Cargo.lock generated
View File

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

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"
readme = "README.md"
repository = "https://github.com/Start9Labs/start-os"
version = "0.4.0-alpha.6" # VERSION_BUMP
version = "0.4.0-alpha.7" # VERSION_BUMP
license = "MIT"
[lib]

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 {
return;
return Ok(());
};
if let Some(format) = params.format {
return display_serializable(format, result);
}
println!("{result}")
println!("{result}");
Ok(())
}
#[derive(Deserialize, Serialize, TS)]

View File

@@ -328,9 +328,7 @@ pub fn session<C: Context>() -> ParentHandler<C> {
from_fn_async(list)
.with_metadata("get_session", Value::Bool(true))
.with_display_serializable()
.with_custom_display_fn(|handle, result| {
Ok(display_sessions(handle.params, result))
})
.with_custom_display_fn(|handle, result| display_sessions(handle.params, result))
.with_about("Display all server sessions")
.with_call_remote::<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::*;
if let Some(format) = params.format {
@@ -371,7 +369,8 @@ fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) {
}
table.add_row(row);
}
table.print_tty(false).unwrap();
table.print_tty(false)?;
Ok(())
}
#[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::init::init;
use crate::prelude::*;
use crate::progress::ProgressUnits;
use crate::s9pk::S9pk;
use crate::service::service_map::DownloadInstallFuture;
use crate::setup::SetupExecuteProgress;
@@ -136,6 +137,7 @@ pub async fn recover_full_embassy(
.collect();
let tasks = restore_packages(&rpc_ctx, backup_guard, ids).await?;
restore_phase.set_total(tasks.len() as u64);
restore_phase.set_units(Some(ProgressUnits::Steps));
let restore_phase = Arc::new(Mutex::new(restore_phase));
stream::iter(tasks)
.for_each_concurrent(5, |(id, res)| {

View File

@@ -157,7 +157,7 @@ pub fn target<C: Context>() -> ParentHandler<C> {
from_fn_async(info)
.with_display_serializable()
.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_call_remote::<CliContext>(),
@@ -227,7 +227,7 @@ pub struct PackageBackupInfo {
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::*;
if let Some(format) = params.format {
@@ -260,7 +260,8 @@ fn display_backup_info(params: WithIoFormat<InfoParams>, info: BackupInfo) {
];
table.add_row(row);
}
table.print_tty(false).unwrap();
table.print_tty(false)?;
Ok(())
}
#[derive(Deserialize, Serialize, Parser, TS)]

View File

@@ -48,9 +48,7 @@ pub fn disk<C: Context>() -> ParentHandler<C> {
"list",
from_fn_async(list)
.with_display_serializable()
.with_custom_display_fn(|handle, result| {
Ok(display_disk_info(handle.params, result))
})
.with_custom_display_fn(|handle, result| display_disk_info(handle.params, result))
.with_about("List disk info")
.with_call_remote::<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::*;
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.print_tty(false).unwrap();
table.print_tty(false)?;
Ok(())
}
// #[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::prelude::*;
use crate::progress::{
FullProgress, FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar,
FullProgress, FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar, ProgressUnits,
};
use crate::rpc_continuations::{Guid, RpcContinuation};
use crate::s9pk::v2::pack::{CONTAINER_DATADIR, CONTAINER_TOOL};
@@ -259,6 +259,7 @@ pub async fn run_script<P: AsRef<Path>>(path: P, mut progress: PhaseProgressTrac
if let Err(e) = async {
let script = tokio::fs::read_to_string(script).await?;
progress.set_total(script.as_bytes().iter().filter(|b| **b == b'\n').count() as u64);
progress.set_units(Some(ProgressUnits::Bytes));
let mut reader = IOHook::new(Cursor::new(script.as_bytes()));
reader.post_read(|buf| progress += buf.iter().filter(|b| **b == b'\n').count() as u64);
Command::new("/bin/bash")

View File

@@ -89,7 +89,7 @@ use crate::context::{
use crate::disk::fsck::RequiresReboot;
use crate::registry::context::{RegistryContext, RegistryUrlParams};
use crate::system::kiosk;
use crate::util::serde::{HandlerExtSerde, WithIoFormat};
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
@@ -201,15 +201,6 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
if &*PLATFORM != "raspberrypi" {
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
}
@@ -220,7 +211,7 @@ pub fn server<C: Context>() -> ParentHandler<C> {
from_fn_async(system::time)
.with_display_serializable()
.with_custom_display_fn(|handle, result| {
Ok(system::display_time(handle.params, result))
system::display_time(handle.params, result)
})
.with_about("Display current time and server uptime")
.with_call_remote::<CliContext>()
@@ -416,6 +407,46 @@ pub fn package<C: Context>() -> ParentHandler<C> {
.with_about("Rebuild service container")
.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",

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::path::Path;
use std::sync::{Arc, Weak};
@@ -7,7 +7,7 @@ use std::time::Duration;
use clap::builder::ValueParserFactory;
use futures::{AsyncWriteExt, StreamExt};
use imbl_value::{InOMap, InternedString};
use models::{FromStrParser, InvalidId};
use models::{FromStrParser, InvalidId, PackageId};
use rpc_toolkit::yajrc::RpcError;
use rpc_toolkit::{GenericRpcMethod, RpcRequest, RpcResponse};
use rustyline_async::{ReadlineEvent, SharedWriter};
@@ -28,13 +28,11 @@ use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard};
use crate::disk::mount::util::unmount;
use crate::prelude::*;
use crate::rpc_continuations::{Guid, RpcContinuation};
use crate::service::ServiceStats;
use crate::util::io::open_file;
use crate::util::rpc_client::UnixRpcClient;
use crate::util::{new_guid, Invoke};
// #[cfg(feature = "dev")]
pub mod dev;
const LXC_CONTAINER_DIR: &str = "/var/lib/lxc";
const RPC_DIR: &str = "media/startos/rpc"; // must not be absolute path
pub const CONTAINER_RPC_SERVER_SOCKET: &str = "service.sock"; // must not be absolute path
@@ -564,3 +562,21 @@ pub async fn connect_cli(ctx: &CliContext, guid: Guid) -> Result<(), Error> {
Ok(())
}
pub async fn stats(ctx: RpcContext) -> Result<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::*;
if let Some(format) = params.format {
return Ok(display_serializable(format, res));
return display_serializable(format, res);
}
let mut table = Table::new();
@@ -182,7 +182,7 @@ pub fn binding<C: Context, Kind: HostApiKind>(
]);
}
table.print_tty(false).unwrap();
table.print_tty(false)?;
Ok(())
})

View File

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

View File

@@ -90,9 +90,7 @@ pub fn tor<C: Context>() -> ParentHandler<C> {
"list-services",
from_fn_async(list_services)
.with_display_serializable()
.with_custom_display_fn(|handle, result| {
Ok(display_services(handle.params, result))
})
.with_custom_display_fn(|handle, result| display_services(handle.params, result))
.with_about("Display Tor V3 Onion Addresses")
.with_call_remote::<CliContext>(),
)
@@ -210,7 +208,10 @@ pub async fn reset(
.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::*;
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()];
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> {

View File

@@ -70,9 +70,7 @@ pub fn wifi<C: Context>() -> ParentHandler<C> {
"get",
from_fn_async(get)
.with_display_serializable()
.with_custom_display_fn(|handle, result| {
Ok(display_wifi_info(handle.params, result))
})
.with_custom_display_fn(|handle, result| display_wifi_info(handle.params, result))
.with_about("List wifi info")
.with_call_remote::<CliContext>(),
)
@@ -134,7 +132,7 @@ pub fn available<C: Context>() -> ParentHandler<C> {
"get",
from_fn_async(get_available)
.with_display_serializable()
.with_custom_display_fn(|handle, result| Ok(display_wifi_list(handle.params, result)))
.with_custom_display_fn(|handle, result| display_wifi_list(handle.params, result))
.with_about("List available wifi networks")
.with_call_remote::<CliContext>(),
)
@@ -363,7 +361,7 @@ pub struct WifiListOut {
security: Vec<String>,
}
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::*;
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::*;
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))]

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

View File

@@ -47,7 +47,7 @@ pub fn admin_api<C: Context>() -> ParentHandler<C> {
from_fn_async(list_admins)
.with_metadata("admin", Value::Bool(true))
.with_display_serializable()
.with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result)))
.with_custom_display_fn(|handle, result| display_signers(handle.params, result))
.with_about("List admin signers")
.with_call_remote::<CliContext>(),
)
@@ -60,7 +60,7 @@ fn signers_api<C: Context>() -> ParentHandler<C> {
from_fn_async(list_signers)
.with_metadata("admin", Value::Bool(true))
.with_display_serializable()
.with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result)))
.with_custom_display_fn(|handle, result| display_signers(handle.params, result))
.with_about("List signers")
.with_call_remote::<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()
}
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::*;
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"),
]);
}
table.print_tty(false).unwrap();
table.print_tty(false)?;
Ok(())
}
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::prelude::*;
use crate::progress::FullProgressTracker;
use crate::progress::{FullProgressTracker, ProgressUnits};
use crate::registry::asset::RegistryAsset;
use crate::registry::context::RegistryContext;
use crate::registry::os::index::OsVersionInfo;
@@ -246,6 +246,7 @@ pub async fn cli_add_asset(
if let Some(size) = src.size().await {
verify_phase.set_total(size);
}
verify_phase.set_units(Some(ProgressUnits::Bytes));
let mut writer = verify_phase.writer(VerifyingWriter::new(
tokio::io::sink(),
Some((blake3::Hash::from_bytes(*commitment.hash), commitment.size)),

View File

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

View File

@@ -49,7 +49,7 @@ pub fn version_api<C: Context>() -> ParentHandler<C> {
.with_metadata("get_device_info", Value::Bool(true))
.with_display_serializable()
.with_custom_display_fn(|handle, result| {
Ok(display_version_info(handle.params, result))
display_version_info(handle.params, result)
})
.with_about("Get OS versions and related version info")
.with_call_remote::<CliContext>(),
@@ -197,7 +197,10 @@ pub async fn get_version(
.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::*;
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(", "),
]);
}
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",
from_fn_async(list_version_signers)
.with_display_serializable()
.with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result)))
.with_custom_display_fn(|handle, result| display_signers(handle.params, result))
.with_about("List version signers and related signer info")
.with_call_remote::<CliContext>(),
)

View File

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

View File

@@ -52,7 +52,7 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
from_fn_async(list_categories)
.with_display_serializable()
.with_custom_display_fn(|params, categories| {
Ok(display_categories(params.params, categories))
display_categories(params.params, categories)
})
.with_call_remote::<CliContext>(),
)
@@ -182,7 +182,7 @@ pub async fn list_categories(
pub fn display_categories<T>(
params: WithIoFormat<T>,
categories: BTreeMap<InternedString, Category>,
) {
) -> Result<(), Error> {
use prettytable::*;
if let Some(format) = params.format {
@@ -197,5 +197,6 @@ pub fn display_categories<T>(
for (id, info) in categories {
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",
from_fn_async(list_package_signers)
.with_display_serializable()
.with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result)))
.with_custom_display_fn(|handle, result| display_signers(handle.params, result))
.with_about("List package signers and related signer info")
.with_call_remote::<CliContext>(),
)

View File

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

View File

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

View File

@@ -31,7 +31,7 @@ pub fn action_api<C: Context>() -> ParentHandler<C> {
"run",
from_fn_async(run_action)
.with_display_serializable()
.with_custom_display_fn(|args, res| Ok(display_action_result(args.params, res)))
.with_custom_display_fn(|args, res| display_action_result(args.params, res))
.with_call_remote::<ContainerCliContext>(),
)
.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::fs::File;
use std::io::{IsTerminal, Read};
use std::os::unix::process::CommandExt;
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::path::{Path, PathBuf};
use std::process::{Command as StdCommand, Stdio};
use std::sync::Arc;
@@ -330,7 +330,7 @@ pub fn launch(
if let Some(code) = exit.code() {
drop(raw);
std::process::exit(code);
} else if exit.success() {
} else if exit.success() || exit.signal() == Some(15) {
Ok(())
} else {
Err(Error::new(
@@ -380,7 +380,7 @@ pub fn launch(
nix::mount::umount(&chroot.join("proc"))
.with_ctx(|_| (ErrorKind::Filesystem, "umount procfs"))?;
std::process::exit(code);
} else if exit.success() {
} else if exit.success() || exit.signal() == Some(15) {
Ok(())
} else {
Err(Error::new(

View File

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

View File

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

View File

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

View File

@@ -108,7 +108,7 @@ pub fn ssh<C: Context>() -> ParentHandler<C> {
from_fn_async(list)
.with_display_serializable()
.with_custom_display_fn(|handle, result| {
Ok(display_all_ssh_keys(handle.params, result))
display_all_ssh_keys(handle.params, result)
})
.with_about("List ssh keys")
.with_call_remote::<CliContext>(),
@@ -177,7 +177,10 @@ pub async fn remove(
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::*;
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.print_tty(false).unwrap();
table.print_tty(false)?;
Ok(())
}
#[instrument(skip_all)]

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ use tokio::sync::watch;
use crate::context::RpcContext;
use crate::prelude::*;
use crate::progress::PhaseProgressTrackerHandle;
use crate::progress::{PhaseProgressTrackerHandle, ProgressUnits};
use crate::rpc_continuations::{Guid, RpcContinuation};
use crate::s9pk::merkle_archive::source::multi_cursor_file::{FileCursor, MultiCursorFile};
use crate::s9pk::merkle_archive::source::ArchiveSource;
@@ -176,7 +176,10 @@ pub struct UploadingFile {
progress: watch::Receiver<Progress>,
}
impl UploadingFile {
pub async fn new(progress: PhaseProgressTrackerHandle) -> Result<(UploadHandle, Self), Error> {
pub async fn new(
mut progress: PhaseProgressTrackerHandle,
) -> Result<(UploadHandle, Self), Error> {
progress.set_units(Some(ProgressUnits::Bytes));
let progress = watch::channel(Progress {
tracker: progress,
expected_size: None,

View File

@@ -26,10 +26,10 @@ use tokio::io::{
use tokio::net::TcpStream;
use tokio::sync::{Notify, OwnedMutexGuard};
use tokio::time::{Instant, Sleep};
use ts_rs::TS;
use crate::prelude::*;
use crate::util::sync::SyncMutex;
use crate::{CAP_1_KiB, CAP_1_MiB};
pub trait AsyncReadSeek: AsyncRead + AsyncSeek {}
impl<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 size: (u16, u16),
pub pixels: Option<(u16, u16)>,
@@ -1464,6 +1464,15 @@ impl FromStr for TermSize {
.ok_or_else(|| Error::new(eyre!("invalid pty size"), ErrorKind::ParseNumber))
}
}
impl std::fmt::Display for TermSize {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.size.0, self.size.1)?;
if let Some(pixels) = self.pixels {
write!(f, ":{}:{}", pixels.0, pixels.1)?;
}
Ok(())
}
}
impl ValueParserFactory for TermSize {
type Parser = FromStrParser<Self>;
fn value_parser() -> Self::Parser {

View File

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

View File

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

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
fi
VERSION="$(cat /usr/lib/startos/VERSION.txt)"
ENVIRONMENT=$(cat /usr/lib/startos/ENVIRONMENT.txt)
VERSION_ENV="${VERSION}"
if [ -n "${ENVIRONMENT}" ]; then
VERSION_ENV="${VERSION} (${ENVIRONMENT})"
fi
# set /etc/os-release
cat << EOF > /etc/os-release
NAME=StartOS
VERSION="${VERSION_ENV}"
ID=start-os
VERSION_ID="${VERSION}"
PRETTY_NAME="StartOS v${VERSION_ENV}"
HOME_URL="https://start9.com/"
SUPPORT_URL="https://docs.start9.com/0.3.5.x/support"
BUG_REPORT_URL="https://github.com/Start9Labs/start-os/issues"
VARIANT="${ENVIRONMENT}"
VARIANT_ID="${ENVIRONMENT}"
EOF
# set local and remote login prompt
cat << EOF > /etc/issue
StartOS v$(cat /usr/lib/startos/VERSION.txt) [\\m] on \\n.local (\\l)
StartOS v${VERSION} [\\m] on \\n.local (\\l)
EOF
cat << EOF > /etc/issue.net
StartOS v$(cat /usr/lib/startos/VERSION.txt)
StartOS v${VERSION}
EOF
# change timezone

View File

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

View File

@@ -32,4 +32,5 @@ export type Manifest = {
hardwareRequirements: HardwareRequirements
gitHash?: GitHash
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.
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 { ProcedureId } from "./ProcedureId"
export { Progress } from "./Progress"
export { ProgressUnits } from "./ProgressUnits"
export { Public } from "./Public"
export { RecoverySource } from "./RecoverySource"
export { RegistryAsset } from "./RegistryAsset"

View File

@@ -106,7 +106,7 @@ export class UseEntrypoint {
export function isUseEntrypoint(
command: CommandType,
): command is UseEntrypoint {
return typeof command === "object" && "ENTRYPOINT" in command
return typeof command === "object" && "USE_ENTRYPOINT" in command
}
export type CommandType = string | [string, ...string[]] | UseEntrypoint

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,9 +37,7 @@ export type Ready = {
})
* ```
*/
fn: (
subcontainer: SubContainer<Manifest>,
) => Promise<HealthCheckResult> | HealthCheckResult
fn: () => Promise<HealthCheckResult> | HealthCheckResult
/**
* A duration in milliseconds to treat a failing health check as "starting"
*
@@ -65,30 +63,40 @@ export type ExecCommandOptions = {
onStderr?: (chunk: Buffer | string | any) => void
}
export type ExecFnOptions = {
export type ExecFnOptions<
Manifest extends T.SDKManifest,
C extends SubContainer<Manifest> | null,
> = {
fn: (
subcontainer: SubContainer<Manifest>,
subcontainer: C,
abort: AbortController,
) => Promise<ExecCommandOptions | null>
) => Promise<C extends null ? null : ExecCommandOptions | null>
// Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms
sigtermTimeout?: number
}
export type DaemonCommandType = ExecCommandOptions | ExecFnOptions
export type DaemonCommandType<
Manifest extends T.SDKManifest,
C extends SubContainer<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 */
exec: DaemonCommandType | null
/** Information about the subcontainer in which the daemon runs */
subcontainer: SubContainer<Manifest>
exec: DaemonCommandType<Manifest, C>
/** The subcontainer in which the daemon runs */
subcontainer: C
}
type AddDaemonParams<
Manifest extends T.SDKManifest,
Ids extends string,
Id extends string,
C extends SubContainer<Manifest> | null,
> = (
| NewDaemonParams<Manifest>
| NewDaemonParams<Manifest, C>
| {
daemon: Daemon<Manifest>
}
@@ -102,8 +110,15 @@ type AddOneshotParams<
Manifest extends T.SDKManifest,
Ids extends string,
Id extends string,
> = NewDaemonParams<Manifest> & {
exec: DaemonCommandType
C extends SubContainer<Manifest> | null,
> = 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 */
requires: Exclude<Ids, Id>[]
}
@@ -111,7 +126,7 @@ type AddOneshotParams<
type ErrorDuplicateId<Id extends string> = `The id '${Id}' is already used`
export const runCommand = <Manifest extends T.SDKManifest>() =>
CommandController.of<Manifest>()
CommandController.of<Manifest, SubContainer<Manifest>>()
/**
* 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(
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 ids: Ids[],
readonly healthDaemons: HealthDaemon<Manifest>[],
readonly healthChecks: HealthCheck[],
) {}
/**
* 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
* depending on prior daemons
* @param options
*
* @param effects
*
* @param started
* @returns
*/
static of<Manifest extends T.SDKManifest>(options: {
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>(
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
* @returns a new Daemons object
*/
addDaemon<Id extends string>(
addDaemon<Id extends string, C extends SubContainer<Manifest> | null>(
// prettier-ignore
id:
"" extends Id ? never :
ErrorDuplicateId<Id> extends Id ? never :
Id extends Ids ? ErrorDuplicateId<Id> :
Id,
options: AddDaemonParams<Manifest, Ids, Id>,
options: AddDaemonParams<Manifest, Ids, Id, C>,
) {
const daemon =
"daemon" in options
? Promise.resolve(options.daemon)
: Daemon.of<Manifest>()(
: Daemon.of<Manifest>()<C>(
this.effects,
options.subcontainer,
options.exec,
@@ -201,11 +221,10 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
.filter((x) => x >= 0)
.map((id) => this.healthDaemons[id]),
id,
this.ids,
options.ready,
this.effects,
)
const daemons = this.daemons.concat(daemon)
const daemons = [...this.daemons, daemon]
const ids = [...this.ids, id] as (Ids | Id)[]
const healthDaemons = [...this.healthDaemons, healthDaemon]
return new Daemons<Manifest, Ids | Id>(
@@ -214,7 +233,6 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
daemons,
ids,
healthDaemons,
this.healthChecks,
)
}
@@ -225,7 +243,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
* @param options
* @returns a new Daemons object
*/
addOneshot<Id extends string>(
addOneshot<Id extends string, C extends SubContainer<Manifest> | null>(
id: "" extends Id
? never
: ErrorDuplicateId<Id> extends Id
@@ -233,9 +251,9 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
: Id extends Ids
? ErrorDuplicateId<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,
options.subcontainer,
options.exec,
@@ -247,11 +265,10 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
.filter((x) => x >= 0)
.map((id) => this.healthDaemons[id]),
id,
this.ids,
"EXIT_SUCCESS",
this.effects,
)
const daemons = this.daemons.concat(daemon)
const daemons = [...this.daemons, daemon]
const ids = [...this.ids, id] as (Ids | Id)[]
const healthDaemons = [...this.healthDaemons, healthDaemon]
return new Daemons<Manifest, Ids | Id>(
@@ -260,13 +277,95 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
daemons,
ids,
healthDaemons,
this.healthChecks,
)
}
/**
* Returns the complete list of daemons, including a new HealthCheck defined here
* @param id
* @param options
* @returns a new Daemons object
*/
addHealthCheck<Id extends 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() {
try {
this.healthChecks.forEach((health) => health.stop())
for (let result of await Promise.allSettled(
this.healthDaemons.map((x) => x.term()),
)) {
@@ -283,10 +382,7 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
for (const daemon of this.healthDaemons) {
await daemon.init()
}
for (const health of this.healthChecks) {
health.start()
}
this.started(() => this.term())
this.started?.(() => this.term())
return this
}
}

View File

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

View File

@@ -10,19 +10,25 @@ import { DaemonCommandType } from "./Daemons"
* 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>() {
return async (
return async <C extends SubContainer<Manifest> | null>(
effects: T.Effects,
subcontainer: SubContainer<Manifest>,
exec: DaemonCommandType | null,
subcontainer: C,
exec: DaemonCommandType<Manifest, C>,
) => {
if (subcontainer.isOwned()) subcontainer = subcontainer.rc()
const startCommand = exec
? () =>
CommandController.of<Manifest>()(effects, subcontainer.rc(), exec)
: null
return new Oneshot(subcontainer, startCommand, true)
let subc: SubContainer<Manifest> | null = subcontainer
if (subcontainer && subcontainer.isOwned()) subc = subcontainer.rc()
const startCommand = () =>
CommandController.of<Manifest, C>()(
effects,
(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"
import { OSVersion } from "../StartSdk"
import { VersionGraph } from "../version/VersionGraph"
import { execSync } from "child_process"
import { version as sdkVersion } from "../../package.json"
/**
* @description Use this function to define critical information about your package
@@ -55,6 +55,7 @@ export function buildManifest<
return {
...manifest,
osVersion: manifest.osVersion ?? OSVersion,
sdkVersion,
version: versions.current.options.version,
releaseNotes: versions.current.options.releaseNotes,
satisfies: versions.current.options.satisfies || [],

View File

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

View File

@@ -340,9 +340,17 @@ export class FileHelper<A> {
/**
* 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))
if (effects.constRetry && this.consts.includes(effects.constRetry))
if (
!options.allowWriteAfterConst &&
effects.constRetry &&
this.consts.includes(effects.constRetry)
)
throw new Error(`Canceled: write after const: ${this.path}`)
return null
}
@@ -350,7 +358,11 @@ export class FileHelper<A> {
/**
* 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()
let fileData: any = fileDataRaw === null ? null : this.readData(fileDataRaw)
try {
@@ -360,7 +372,11 @@ export class FileHelper<A> {
const toWrite = this.writeData(mergeData)
if (toWrite !== fileDataRaw) {
this.writeFile(mergeData)
if (effects.constRetry && this.consts.includes(effects.constRetry)) {
if (
!options.allowWriteAfterConst &&
effects.constRetry &&
this.consts.includes(effects.constRetry)
) {
const diff = partialDiff(fileData, mergeData as any)
if (!diff) {
return null

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.27",
"version": "0.4.0-beta.30",
"description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts",
@@ -22,14 +22,14 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/Start9Labs/start-sdk.git"
"url": "git+https://github.com/Start9Labs/start-os.git"
},
"author": "Start9 Labs",
"license": "MIT",
"bugs": {
"url": "https://github.com/Start9Labs/start-sdk/issues"
"url": "https://github.com/Start9Labs/start-os/issues"
},
"homepage": "https://github.com/Start9Labs/start-sdk#readme",
"homepage": "https://github.com/Start9Labs/start-os#readme",
"dependencies": {
"isomorphic-fetch": "^3.0.0",
"mime": "^4.0.7",

View File

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

4
web/package-lock.json generated
View File

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

View File

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

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
@@ -14,7 +14,14 @@
<meta name="format-detection" content="telephone=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>
<body>

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
@@ -15,7 +15,14 @@
<script>
var global = window
</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>
<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.',
520: 'Update verfügbar',
521: 'Um das Problem zu beheben, siehe',
522: 'SDK Version',
} satisfies i18n

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -202,6 +202,11 @@ export class InterfaceClearnetComponent {
}
const loader = this.loader.open('Removing').subscribe()
if (!/^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(url)) {
url = 'http://' + url
}
const params = { domain: new URL(url).hostname }
try {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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