diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json index 6696ff616..15898b2e9 100644 --- a/container-runtime/package-lock.json +++ b/container-runtime/package-lock.json @@ -37,7 +37,7 @@ }, "../sdk/dist": { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.53", + "version": "0.4.0-beta.54", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/container-runtime/src/index.ts b/container-runtime/src/index.ts index e1e473183..05b0ef601 100644 --- a/container-runtime/src/index.ts +++ b/container-runtime/src/index.ts @@ -1,5 +1,4 @@ import { RpcListener } from "./Adapters/RpcListener" -import { SystemForEmbassy } from "./Adapters/Systems/SystemForEmbassy" import { AllGetDependencies } from "./Interfaces/AllGetDependencies" import { getSystem } from "./Adapters/Systems" @@ -7,6 +6,18 @@ const getDependencies: AllGetDependencies = { system: getSystem, } +process.on("unhandledRejection", (reason) => { + if ( + reason instanceof Error && + "muteUnhandled" in reason && + reason.muteUnhandled + ) { + // mute + } else { + console.error("Unhandled promise rejection", reason) + } +}) + for (let s of ["SIGTERM", "SIGINT", "SIGHUP"]) { process.on(s, (s) => { console.log(`Caught ${s}`) diff --git a/core/rpc-toolkit.md b/core/rpc-toolkit.md index 933c345b6..a1499dc29 100644 --- a/core/rpc-toolkit.md +++ b/core/rpc-toolkit.md @@ -21,6 +21,14 @@ pub async fn my_handler(ctx: RpcContext, params: MyParams) -> Result Result { + // ... +} +``` + ### `from_fn_async_local` - Non-thread-safe async handlers For async functions that are not `Send` (cannot be safely moved between threads). Use when working with non-thread-safe types. @@ -181,9 +189,9 @@ pub struct MyParams { ### Adding a New RPC Endpoint -1. Define params struct with `Deserialize, Serialize, Parser, TS` +1. Define params struct with `Deserialize, Serialize, Parser, TS` (skip if no params needed) 2. Choose handler type based on sync/async and thread-safety -3. Write handler function taking `(Context, Params) -> Result` +3. Write handler function taking `(Context, Params) -> Result` (omit Params if none needed) 4. Add to parent handler with appropriate extensions (display modifiers before `with_about`) 5. TypeScript types auto-generated via `make ts-bindings` diff --git a/core/src/lib.rs b/core/src/lib.rs index 820190d91..fa31d41c3 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -268,6 +268,18 @@ pub fn server() -> ParentHandler { .with_about("about.display-time-uptime") .with_call_remote::(), ) + .subcommand( + "device-info", + ParentHandler::>::new().root_handler( + from_fn_async(system::device_info) + .with_display_serializable() + .with_custom_display_fn(|handle, result| { + system::display_device_info(handle.params, result) + }) + .with_about("about.get-device-info") + .with_call_remote::(), + ), + ) .subcommand( "experimental", system::experimental::().with_about("about.commands-experimental"), diff --git a/core/src/middleware/auth/session.rs b/core/src/middleware/auth/session.rs index 2ff67d372..66ef0ffe4 100644 --- a/core/src/middleware/auth/session.rs +++ b/core/src/middleware/auth/session.rs @@ -20,9 +20,6 @@ use crate::context::RpcContext; use crate::middleware::auth::DbContext; use crate::prelude::*; use crate::rpc_continuations::OpenAuthedContinuations; -use crate::util::Invoke; -use crate::util::io::{create_file_mod, read_file_to_string}; -use crate::util::serde::{BASE64, const_true}; use crate::util::sync::SyncMutex; pub trait SessionAuthContext: DbContext { diff --git a/core/src/net/host/address.rs b/core/src/net/host/address.rs index f7ee40469..0a69d0427 100644 --- a/core/src/net/host/address.rs +++ b/core/src/net/host/address.rs @@ -204,7 +204,6 @@ pub async fn add_public_domain( }) .await .result?; - Kind::sync_host(&ctx, inheritance).await?; tokio::task::spawn_blocking(|| { crate::net::dns::query_dns(ctx, crate::net::dns::QueryDnsParams { fqdn }) @@ -242,7 +241,6 @@ pub async fn remove_public_domain( }) .await .result?; - Kind::sync_host(&ctx, inheritance).await?; Ok(()) } @@ -279,7 +277,6 @@ pub async fn add_private_domain( }) .await .result?; - Kind::sync_host(&ctx, inheritance).await?; Ok(()) } @@ -306,7 +303,6 @@ pub async fn remove_private_domain( }) .await .result?; - Kind::sync_host(&ctx, inheritance).await?; Ok(()) } diff --git a/core/src/net/host/binding.rs b/core/src/net/host/binding.rs index 81c14615f..54020d865 100644 --- a/core/src/net/host/binding.rs +++ b/core/src/net/host/binding.rs @@ -358,5 +358,5 @@ pub async fn set_address_enabled( }) .await .result?; - Kind::sync_host(&ctx, inheritance).await + Ok(()) } diff --git a/core/src/net/host/mod.rs b/core/src/net/host/mod.rs index b26355ff3..aff25ccd5 100644 --- a/core/src/net/host/mod.rs +++ b/core/src/net/host/mod.rs @@ -1,5 +1,4 @@ use std::collections::{BTreeMap, BTreeSet}; -use std::future::Future; use std::net::{IpAddr, SocketAddrV4}; use std::panic::RefUnwindSafe; @@ -182,15 +181,26 @@ impl Model { opt.secure .map_or(true, |s| !(s.ssl && opt.add_ssl.is_some())) }) { - available.insert(HostnameInfo { - ssl: opt.secure.map_or(false, |s| s.ssl), - public: false, - hostname: mdns_host.clone(), - port: Some(port), - metadata: HostnameMetadata::Mdns { - gateways: mdns_gateways.clone(), - }, - }); + let mdns_gateways = if opt.secure.is_some() { + mdns_gateways.clone() + } else { + mdns_gateways + .iter() + .filter(|g| gateways.get(*g).map_or(false, |g| g.secure())) + .cloned() + .collect() + }; + if !mdns_gateways.is_empty() { + available.insert(HostnameInfo { + ssl: opt.secure.map_or(false, |s| s.ssl), + public: false, + hostname: mdns_host.clone(), + port: Some(port), + metadata: HostnameMetadata::Mdns { + gateways: mdns_gateways, + }, + }); + } } if let Some(port) = net.assigned_ssl_port { available.insert(HostnameInfo { @@ -429,10 +439,6 @@ pub trait HostApiKind: 'static { inheritance: &Self::Inheritance, db: &'a mut DatabaseModel, ) -> Result<&'a mut Model, Error>; - fn sync_host( - ctx: &RpcContext, - inheritance: Self::Inheritance, - ) -> impl Future> + Send; } pub struct ForPackage; impl HostApiKind for ForPackage { @@ -451,12 +457,6 @@ impl HostApiKind for ForPackage { ) -> Result<&'a mut Model, Error> { host_for(db, Some(package), host) } - async fn sync_host(ctx: &RpcContext, (package, host): Self::Inheritance) -> Result<(), Error> { - let service = ctx.services.get(&package).await; - let service_ref = service.as_ref().or_not_found(&package)?; - service_ref.sync_host(host).await?; - Ok(()) - } } pub struct ForServer; impl HostApiKind for ForServer { @@ -472,9 +472,6 @@ impl HostApiKind for ForServer { ) -> Result<&'a mut Model, Error> { host_for(db, None, &HostId::default()) } - async fn sync_host(ctx: &RpcContext, _: Self::Inheritance) -> Result<(), Error> { - ctx.os_net_service.sync_host(HostId::default()).await - } } pub fn host_api() -> ParentHandler { diff --git a/core/src/net/net_controller.rs b/core/src/net/net_controller.rs index 608545e2f..dc990ec06 100644 --- a/core/src/net/net_controller.rs +++ b/core/src/net/net_controller.rs @@ -725,13 +725,6 @@ impl NetService { .result } - pub async fn sync_host(&self, _id: HostId) -> Result<(), Error> { - let current = self.synced.peek(|v| *v); - let mut w = self.synced.clone(); - w.wait_for(|v| *v > current).await; - Ok(()) - } - pub async fn remove_all(mut self) -> Result<(), Error> { if Weak::upgrade(&self.data.lock().await.controller).is_none() { self.shutdown = true; diff --git a/core/src/net/static_server.rs b/core/src/net/static_server.rs index d52014afa..0d1fb458c 100644 --- a/core/src/net/static_server.rs +++ b/core/src/net/static_server.rs @@ -9,7 +9,7 @@ use async_compression::tokio::bufread::GzipEncoder; use axum::Router; use axum::body::Body; use axum::extract::{self as x, Request}; -use axum::response::{IntoResponse, Response}; +use axum::response::Response; use axum::routing::{any, get}; use base64::display::Base64Display; use digest::Digest; diff --git a/core/src/registry/package/index.rs b/core/src/registry/package/index.rs index 9ecd996db..e7848b13c 100644 --- a/core/src/registry/package/index.rs +++ b/core/src/registry/package/index.rs @@ -255,30 +255,7 @@ impl Model { } if let Some(hw) = &device_info.hardware { self.as_s9pks_mut().mutate(|s9pks| { - s9pks.retain(|(hw_req, _)| { - if let Some(arch) = &hw_req.arch { - if !arch.contains(&hw.arch) { - return false; - } - } - if let Some(ram) = hw_req.ram { - if hw.ram < ram { - return false; - } - } - if let Some(dev) = &hw.devices { - for device_filter in &hw_req.device { - if !dev - .iter() - .filter(|d| d.class() == &*device_filter.class) - .any(|d| device_filter.matches(d)) - { - return false; - } - } - } - true - }); + s9pks.retain(|(hw_req, _)| hw_req.is_compatible(hw)); if hw.devices.is_some() { s9pks.sort_by_key(|(req, _)| req.specificity_desc()); } else { diff --git a/core/src/s9pk/rpc.rs b/core/src/s9pk/rpc.rs index 3c16d27ba..d4e5ee690 100644 --- a/core/src/s9pk/rpc.rs +++ b/core/src/s9pk/rpc.rs @@ -3,16 +3,17 @@ use std::path::PathBuf; use std::sync::Arc; use clap::Parser; -use rpc_toolkit::{Empty, HandlerExt, ParentHandler, from_fn_async}; +use rpc_toolkit::{Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async}; use serde::{Deserialize, Serialize}; use tokio::process::Command; use ts_rs::TS; use url::Url; use crate::ImageId; -use crate::context::CliContext; +use crate::context::{CliContext, RpcContext}; use crate::prelude::*; -use crate::s9pk::manifest::Manifest; +use crate::registry::device_info::DeviceInfo; +use crate::s9pk::manifest::{HardwareRequirements, Manifest}; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::v2::pack::ImageConfig; @@ -70,6 +71,15 @@ pub fn s9pk() -> ParentHandler { .no_display() .with_about("about.publish-s9pk"), ) + .subcommand( + "select", + from_fn_async(select) + .with_custom_display_fn(|_, path: PathBuf| { + println!("{}", path.display()); + Ok(()) + }) + .with_about("about.select-s9pk-for-device"), + ) } #[derive(Deserialize, Serialize, Parser)] @@ -323,3 +333,93 @@ async fn publish(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Res .await?; crate::registry::package::add::cli_add_package_impl(ctx, s9pk, vec![s3url], false).await } + +#[derive(Deserialize, Serialize, Parser)] +struct SelectParams { + #[arg(help = "help.arg.s9pk-file-paths")] + s9pks: Vec, +} + +async fn select( + HandlerArgs { + context, + params: SelectParams { s9pks }, + .. + }: HandlerArgs, +) -> Result { + // Resolve file list: use provided paths or scan cwd for *.s9pk + let paths = if s9pks.is_empty() { + let mut found = Vec::new(); + let mut entries = tokio::fs::read_dir(".").await?; + while let Some(entry) = entries.next_entry().await? { + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) == Some("s9pk") { + found.push(path); + } + } + if found.is_empty() { + return Err(Error::new( + eyre!("no .s9pk files found in current directory"), + ErrorKind::NotFound, + )); + } + found + } else { + s9pks + }; + + // Fetch DeviceInfo from the target server + let device_info: DeviceInfo = from_value( + context + .call_remote::("server.device-info", imbl_value::json!({})) + .await?, + )?; + + // Filter and rank s9pk files by compatibility + let mut compatible: Vec<(PathBuf, HardwareRequirements)> = Vec::new(); + for path in &paths { + let s9pk = match super::S9pk::open(path, None).await { + Ok(s9pk) => s9pk, + Err(e) => { + tracing::warn!("skipping {}: {e}", path.display()); + continue; + } + }; + let manifest = s9pk.as_manifest(); + + // OS version check: package's required OS version must be in server's compat range + if !manifest.metadata.os_version.satisfies(&device_info.os.compat) { + continue; + } + + let hw_req = &manifest.hardware_requirements; + + if let Some(hw) = &device_info.hardware { + if !hw_req.is_compatible(hw) { + continue; + } + } + + compatible.push((path.clone(), hw_req.clone())); + } + + if compatible.is_empty() { + return Err(Error::new( + eyre!( + "no compatible s9pk found for device (arch: {}, os: {})", + device_info + .hardware + .as_ref() + .map(|h| h.arch.to_string()) + .unwrap_or_else(|| "unknown".into()), + device_info.os.version, + ), + ErrorKind::NotFound, + )); + } + + // Sort by specificity (most specific first) + compatible.sort_by_key(|(_, req)| req.specificity_desc()); + + Ok(compatible.into_iter().next().unwrap().0) +} diff --git a/core/src/s9pk/v2/manifest.rs b/core/src/s9pk/v2/manifest.rs index 39361e341..bc31bec8f 100644 --- a/core/src/s9pk/v2/manifest.rs +++ b/core/src/s9pk/v2/manifest.rs @@ -154,6 +154,32 @@ pub struct HardwareRequirements { pub arch: Option>, } impl HardwareRequirements { + /// Returns true if this s9pk's hardware requirements are satisfied by the given hardware. + pub fn is_compatible(&self, hw: &crate::registry::device_info::HardwareInfo) -> bool { + if let Some(arch) = &self.arch { + if !arch.contains(&hw.arch) { + return false; + } + } + if let Some(ram) = self.ram { + if hw.ram < ram { + return false; + } + } + if let Some(devices) = &hw.devices { + for device_filter in &self.device { + if !devices + .iter() + .filter(|d| d.class() == &*device_filter.class) + .any(|d| device_filter.matches(d)) + { + return false; + } + } + } + true + } + /// returns a value that can be used as a sort key to get most specific requirements first pub fn specificity_desc(&self) -> (u32, u32, u64) { ( diff --git a/core/src/service/mod.rs b/core/src/service/mod.rs index 0e2cde585..f7c458985 100644 --- a/core/src/service/mod.rs +++ b/core/src/service/mod.rs @@ -52,7 +52,7 @@ use crate::util::serde::Pem; use crate::util::sync::SyncMutex; use crate::util::tui::choose; use crate::volume::data_dir; -use crate::{ActionId, CAP_1_KiB, DATA_DIR, HostId, ImageId, PackageId}; +use crate::{ActionId, CAP_1_KiB, DATA_DIR, ImageId, PackageId}; pub mod action; pub mod cli; @@ -683,14 +683,6 @@ impl Service { memory_usage: MiB::from_MiB(used), }) } - - pub async fn sync_host(&self, host_id: HostId) -> Result<(), Error> { - self.seed - .persistent_container - .net_service - .sync_host(host_id) - .await - } } struct ServiceActorSeed { diff --git a/core/src/system/mod.rs b/core/src/system/mod.rs index 08a49079d..c903eac9f 100644 --- a/core/src/system/mod.rs +++ b/core/src/system/mod.rs @@ -20,6 +20,7 @@ use crate::context::{CliContext, RpcContext}; use crate::disk::util::{get_available, get_used}; use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT}; use crate::prelude::*; +use crate::registry::device_info::DeviceInfo; use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations}; use crate::shutdown::Shutdown; use crate::util::Invoke; @@ -249,6 +250,67 @@ pub async fn time(ctx: RpcContext, _: Empty) -> Result { }) } +pub async fn device_info(ctx: RpcContext) -> Result { + DeviceInfo::load(&ctx).await +} + +pub fn display_device_info( + params: WithIoFormat, + info: DeviceInfo, +) -> Result<(), Error> { + use prettytable::*; + + if let Some(format) = params.format { + return display_serializable(format, info); + } + + let mut table = Table::new(); + table.add_row(row![br -> "PLATFORM", &*info.os.platform]); + table.add_row(row![br -> "OS VERSION", info.os.version.to_string()]); + table.add_row(row![br -> "OS COMPAT", info.os.compat.to_string()]); + if let Some(lang) = &info.os.language { + table.add_row(row![br -> "LANGUAGE", &**lang]); + } + if let Some(hw) = &info.hardware { + table.add_row(row![br -> "ARCH", &*hw.arch]); + table.add_row(row![br -> "RAM", format_ram(hw.ram)]); + if let Some(devices) = &hw.devices { + for dev in devices { + let (class, desc) = match dev { + crate::util::lshw::LshwDevice::Processor(p) => ( + "PROCESSOR", + p.product.as_deref().unwrap_or("unknown").to_string(), + ), + crate::util::lshw::LshwDevice::Display(d) => ( + "DISPLAY", + format!( + "{}{}", + d.product.as_deref().unwrap_or("unknown"), + d.driver + .as_deref() + .map(|drv| format!(" ({})", drv)) + .unwrap_or_default() + ), + ), + }; + table.add_row(row![br -> class, desc]); + } + } + } + table.print_tty(false)?; + Ok(()) +} + +fn format_ram(bytes: u64) -> String { + const GIB: u64 = 1024 * 1024 * 1024; + const MIB: u64 = 1024 * 1024; + if bytes >= GIB { + format!("{:.1} GiB", bytes as f64 / GIB as f64) + } else { + format!("{:.1} MiB", bytes as f64 / MIB as f64) + } +} + pub fn logs>() -> ParentHandler { crate::logs::logs(|_: &C, _| async { Ok(LogSource::Unit(SYSTEM_UNIT)) }) } diff --git a/sdk/base/lib/actions/input/builder/inputSpec.ts b/sdk/base/lib/actions/input/builder/inputSpec.ts index 109c3844c..3050a19ac 100644 --- a/sdk/base/lib/actions/input/builder/inputSpec.ts +++ b/sdk/base/lib/actions/input/builder/inputSpec.ts @@ -26,23 +26,39 @@ export type LazyBuild = ( * Defines which keys to keep when filtering an InputSpec. * Use `true` to keep a field as-is, or a nested object to filter sub-fields of an object-typed field. */ -export type FilterKeys = { - [K in keyof T]?: T[K] extends Record - ? true | FilterKeys - : true +export type FilterKeys = { + [K in keyof F]?: F[K] extends Record + ? boolean | FilterKeys + : boolean } +type RetainKey = { + [K in keyof T]: K extends keyof F + ? F[K] extends false + ? never + : K + : Default extends true + ? K + : never +}[keyof T] + /** * Computes the resulting type after applying a {@link FilterKeys} shape to a type. */ -export type ApplyFilter = { - [K in Extract]: F[K] extends true - ? T[K] - : T[K] extends Record - ? F[K] extends FilterKeys - ? ApplyFilter - : T[K] - : T[K] +export type ApplyFilter = { + [K in RetainKey]: K extends keyof F + ? true extends F[K] + ? F[K] extends true + ? T[K] + : T[K] | undefined + : T[K] extends Record + ? F[K] extends FilterKeys + ? ApplyFilter + : undefined + : undefined + : Default extends true + ? T[K] + : undefined } /** @@ -226,14 +242,15 @@ export class InputSpec< * const filtered = full.filter({ name: true, settings: { debug: true } }) * ``` */ - filter>( + filter, Default extends boolean = false>( keys: F, + keepByDefault?: Default, ): InputSpec< - ApplyFilter & ApplyFilter, - ApplyFilter + ApplyFilter & ApplyFilter, + ApplyFilter > { const newSpec: Record> = {} - for (const k of Object.keys(keys)) { + for (const k of Object.keys(this.spec)) { const filterVal = (keys as any)[k] const value = (this.spec as any)[k] as Value | undefined if (!value) continue @@ -242,11 +259,16 @@ export class InputSpec< } else if (typeof filterVal === 'object' && filterVal !== null) { const objectMeta = value._objectSpec if (objectMeta) { - const filteredInner = objectMeta.inputSpec.filter(filterVal) + const filteredInner = objectMeta.inputSpec.filter( + filterVal, + keepByDefault, + ) newSpec[k] = Value.object(objectMeta.params, filteredInner) } else { newSpec[k] = value } + } else if (keepByDefault && filterVal !== false) { + newSpec[k] = value } } const newValidator = z.object( diff --git a/sdk/base/lib/types.ts b/sdk/base/lib/types.ts index 41259d0d2..5d6e3756f 100644 --- a/sdk/base/lib/types.ts +++ b/sdk/base/lib/types.ts @@ -145,7 +145,11 @@ export function isUseEntrypoint( * - An explicit argv array * - A {@link UseEntrypoint} to use the container's built-in entrypoint */ -export type CommandType = string | [string, ...string[]] | UseEntrypoint +export type CommandType = + | string + | [string, ...string[]] + | readonly [string, ...string[]] + | UseEntrypoint /** The return type from starting a daemon — provides `wait()` and `term()` controls. */ export type DaemonReturned = { diff --git a/sdk/base/lib/util/AbortedError.ts b/sdk/base/lib/util/AbortedError.ts new file mode 100644 index 000000000..ca9843a22 --- /dev/null +++ b/sdk/base/lib/util/AbortedError.ts @@ -0,0 +1,10 @@ +export class AbortedError extends Error { + readonly muteUnhandled = true as const + declare cause?: unknown + + constructor(message?: string, options?: { cause?: unknown }) { + super(message) + this.name = 'AbortedError' + if (options?.cause !== undefined) this.cause = options.cause + } +} diff --git a/sdk/base/lib/util/GetOutboundGateway.ts b/sdk/base/lib/util/GetOutboundGateway.ts index 16a8f95da..460bb8b90 100644 --- a/sdk/base/lib/util/GetOutboundGateway.ts +++ b/sdk/base/lib/util/GetOutboundGateway.ts @@ -1,4 +1,5 @@ import { Effects } from '../Effects' +import { AbortedError } from './AbortedError' import { DropGenerator, DropPromise } from './Drop' export class GetOutboundGateway { @@ -38,7 +39,7 @@ export class GetOutboundGateway { }) await waitForNext } - return new Promise((_, rej) => rej(new Error('aborted'))) + return new Promise((_, rej) => rej(new AbortedError())) } /** diff --git a/sdk/base/lib/util/GetSystemSmtp.ts b/sdk/base/lib/util/GetSystemSmtp.ts index 69e6c1279..03cedba6f 100644 --- a/sdk/base/lib/util/GetSystemSmtp.ts +++ b/sdk/base/lib/util/GetSystemSmtp.ts @@ -1,5 +1,6 @@ import { Effects } from '../Effects' import * as T from '../types' +import { AbortedError } from './AbortedError' import { DropGenerator, DropPromise } from './Drop' export class GetSystemSmtp { @@ -39,7 +40,7 @@ export class GetSystemSmtp { }) await waitForNext } - return new Promise((_, rej) => rej(new Error('aborted'))) + return new Promise((_, rej) => rej(new AbortedError())) } /** diff --git a/sdk/base/lib/util/getServiceInterface.ts b/sdk/base/lib/util/getServiceInterface.ts index 7d6975fcb..60fb2a745 100644 --- a/sdk/base/lib/util/getServiceInterface.ts +++ b/sdk/base/lib/util/getServiceInterface.ts @@ -8,6 +8,7 @@ import { HostnameInfo, } from '../types' import { Effects } from '../Effects' +import { AbortedError } from './AbortedError' import { DropGenerator, DropPromise } from './Drop' import { IpAddress, IPV6_LINK_LOCAL } from './ip' import { deepEqual } from './deepEqual' @@ -394,7 +395,7 @@ export class GetServiceInterface { } await waitForNext } - return new Promise((_, rej) => rej(new Error('aborted'))) + return new Promise((_, rej) => rej(new AbortedError())) } /** diff --git a/sdk/base/lib/util/getServiceInterfaces.ts b/sdk/base/lib/util/getServiceInterfaces.ts index ea11d7c65..e6a745d56 100644 --- a/sdk/base/lib/util/getServiceInterfaces.ts +++ b/sdk/base/lib/util/getServiceInterfaces.ts @@ -1,5 +1,6 @@ import { Effects } from '../Effects' import { PackageId } from '../osBindings' +import { AbortedError } from './AbortedError' import { deepEqual } from './deepEqual' import { DropGenerator, DropPromise } from './Drop' import { ServiceInterfaceFilled, filledAddress } from './getServiceInterface' @@ -105,7 +106,7 @@ export class GetServiceInterfaces { } await waitForNext } - return new Promise((_, rej) => rej(new Error('aborted'))) + return new Promise((_, rej) => rej(new AbortedError())) } /** diff --git a/sdk/base/lib/util/index.ts b/sdk/base/lib/util/index.ts index f26338438..bad134501 100644 --- a/sdk/base/lib/util/index.ts +++ b/sdk/base/lib/util/index.ts @@ -22,5 +22,6 @@ export { splitCommand } from './splitCommand' export { nullIfEmpty } from './nullIfEmpty' export { deepMerge, partialDiff } from './deepMerge' export { deepEqual } from './deepEqual' +export { AbortedError } from './AbortedError' export * as regexes from './regexes' export { stringFromStdErrOut } from './stringFromStdErrOut' diff --git a/sdk/base/lib/util/splitCommand.ts b/sdk/base/lib/util/splitCommand.ts index 279af4d12..23e7b56b2 100644 --- a/sdk/base/lib/util/splitCommand.ts +++ b/sdk/base/lib/util/splitCommand.ts @@ -1,3 +1,5 @@ +import { AllowReadonly } from '../types' + /** * Normalizes a command into an argv-style string array. * If given a string, wraps it as `["sh", "-c", command]`. @@ -13,8 +15,8 @@ * ``` */ export const splitCommand = ( - command: string | [string, ...string[]], + command: string | AllowReadonly<[string, ...string[]]>, ): string[] => { if (Array.isArray(command)) return command - return ['sh', '-c', command] + return ['sh', '-c', command as string] } diff --git a/sdk/package/lib/util/GetServiceManifest.ts b/sdk/package/lib/util/GetServiceManifest.ts index 87b7ea5da..9f85570d2 100644 --- a/sdk/package/lib/util/GetServiceManifest.ts +++ b/sdk/package/lib/util/GetServiceManifest.ts @@ -1,5 +1,6 @@ import { Effects } from '../../../base/lib/Effects' import { Manifest, PackageId } from '../../../base/lib/osBindings' +import { AbortedError } from '../../../base/lib/util/AbortedError' import { DropGenerator, DropPromise } from '../../../base/lib/util/Drop' import { deepEqual } from '../../../base/lib/util/deepEqual' @@ -64,7 +65,7 @@ export class GetServiceManifest { } await waitForNext } - return new Promise((_, rej) => rej(new Error('aborted'))) + return new Promise((_, rej) => rej(new AbortedError())) } /** diff --git a/sdk/package/lib/util/GetSslCertificate.ts b/sdk/package/lib/util/GetSslCertificate.ts index df60b3b1f..b9967bf22 100644 --- a/sdk/package/lib/util/GetSslCertificate.ts +++ b/sdk/package/lib/util/GetSslCertificate.ts @@ -1,5 +1,6 @@ import { T } from '..' import { Effects } from '../../../base/lib/Effects' +import { AbortedError } from '../../../base/lib/util/AbortedError' import { DropGenerator, DropPromise } from '../../../base/lib/util/Drop' export class GetSslCertificate { @@ -50,7 +51,7 @@ export class GetSslCertificate { }) await waitForNext } - return new Promise((_, rej) => rej(new Error('aborted'))) + return new Promise((_, rej) => rej(new AbortedError())) } /** diff --git a/sdk/package/lib/util/fileHelper.ts b/sdk/package/lib/util/fileHelper.ts index 13a45922c..6b428edfd 100644 --- a/sdk/package/lib/util/fileHelper.ts +++ b/sdk/package/lib/util/fileHelper.ts @@ -4,7 +4,7 @@ import * as TOML from '@iarna/toml' import * as INI from 'ini' import * as T from '../../../base/lib/types' import * as fs from 'node:fs/promises' -import { asError, deepEqual } from '../../../base/lib/util' +import { AbortedError, asError, deepEqual } from '../../../base/lib/util' import { DropGenerator, DropPromise } from '../../../base/lib/util/Drop' import { PathBase } from './Volume' @@ -289,7 +289,7 @@ export class FileHelper { await onCreated(this.path).catch((e) => console.error(asError(e))) } } - return new Promise((_, rej) => rej(new Error('aborted'))) + return new Promise((_, rej) => rej(new AbortedError())) } private readOnChange( diff --git a/sdk/package/package-lock.json b/sdk/package/package-lock.json index 0b8e98d54..7d6faa579 100644 --- a/sdk/package/package-lock.json +++ b/sdk/package/package-lock.json @@ -1,12 +1,12 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.53", + "version": "0.4.0-beta.54", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.53", + "version": "0.4.0-beta.54", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/sdk/package/package.json b/sdk/package/package.json index c7ad06381..92f7b9622 100644 --- a/sdk/package/package.json +++ b/sdk/package/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.53", + "version": "0.4.0-beta.54", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./package/lib/index.js", "types": "./package/lib/index.d.ts", diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/actions.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/actions.component.ts index 487e0e4cb..47aae1b73 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/actions.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/addresses/actions.component.ts @@ -30,6 +30,19 @@ import { DomainHealthService } from './domain-health.service' selector: 'td[actions]', template: `
+ @if (address().ui) { + + {{ 'Open UI' | i18n }} + + } @if (address().deletable) {