mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
chore: bump sdk to beta.54, add device-info RPC, improve SDK abort handling and InputSpec filtering
- Bump SDK version to 0.4.0-beta.54 - Add `server.device-info` RPC endpoint and `s9pk select` CLI command - Extract `HardwareRequirements::is_compatible()` method, reuse in registry filtering - Add `AbortedError` class with `muteUnhandled` flag, replace generic abort errors - Handle unhandled promise rejections in container-runtime with mute support - Improve `InputSpec.filter()` with `keepByDefault` param and boolean filter values - Accept readonly tuples in `CommandType` and `splitCommand` - Remove `sync_host` calls from host API handlers (binding/address changes) - Filter mDNS hostnames by secure gateway availability - Derive mDNS enabled state from LAN IPs in web UI - Add "Open UI" action to address table, disable mDNS toggle - Hide debug details in service error component - Update rpc-toolkit docs for no-params handlers
This commit is contained in:
@@ -21,6 +21,14 @@ pub async fn my_handler(ctx: RpcContext, params: MyParams) -> Result<MyResponse,
|
||||
from_fn_async(my_handler)
|
||||
```
|
||||
|
||||
If a handler takes no params, simply omit the params argument entirely (no need for `_: Empty`):
|
||||
|
||||
```rust
|
||||
pub async fn no_params_handler(ctx: RpcContext) -> Result<MyResponse, Error> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### `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<Response, Error>`
|
||||
3. Write handler function taking `(Context, Params) -> Result<Response, Error>` (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`
|
||||
|
||||
|
||||
@@ -268,6 +268,18 @@ pub fn server<C: Context>() -> ParentHandler<C> {
|
||||
.with_about("about.display-time-uptime")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"device-info",
|
||||
ParentHandler::<C, WithIoFormat<Empty>>::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::<CliContext>(),
|
||||
),
|
||||
)
|
||||
.subcommand(
|
||||
"experimental",
|
||||
system::experimental::<C>().with_about("about.commands-experimental"),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -204,7 +204,6 @@ pub async fn add_public_domain<Kind: HostApiKind>(
|
||||
})
|
||||
.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<Kind: HostApiKind>(
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
Kind::sync_host(&ctx, inheritance).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -279,7 +277,6 @@ pub async fn add_private_domain<Kind: HostApiKind>(
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
Kind::sync_host(&ctx, inheritance).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -306,7 +303,6 @@ pub async fn remove_private_domain<Kind: HostApiKind>(
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
Kind::sync_host(&ctx, inheritance).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -358,5 +358,5 @@ pub async fn set_address_enabled<Kind: HostApiKind>(
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
Kind::sync_host(&ctx, inheritance).await
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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<Host> {
|
||||
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<Host>, Error>;
|
||||
fn sync_host(
|
||||
ctx: &RpcContext,
|
||||
inheritance: Self::Inheritance,
|
||||
) -> impl Future<Output = Result<(), Error>> + Send;
|
||||
}
|
||||
pub struct ForPackage;
|
||||
impl HostApiKind for ForPackage {
|
||||
@@ -451,12 +457,6 @@ impl HostApiKind for ForPackage {
|
||||
) -> Result<&'a mut Model<Host>, 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<Host>, 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<C: Context>() -> ParentHandler<C, RequiresPackageId> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -255,30 +255,7 @@ impl Model<PackageVersionInfo> {
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -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<CliContext> {
|
||||
.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<PathBuf>,
|
||||
}
|
||||
|
||||
async fn select(
|
||||
HandlerArgs {
|
||||
context,
|
||||
params: SelectParams { s9pks },
|
||||
..
|
||||
}: HandlerArgs<CliContext, SelectParams>,
|
||||
) -> Result<PathBuf, Error> {
|
||||
// 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::<RpcContext>("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)
|
||||
}
|
||||
|
||||
@@ -154,6 +154,32 @@ pub struct HardwareRequirements {
|
||||
pub arch: Option<BTreeSet<InternedString>>,
|
||||
}
|
||||
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) {
|
||||
(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<TimeInfo, Error> {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn device_info(ctx: RpcContext) -> Result<DeviceInfo, Error> {
|
||||
DeviceInfo::load(&ctx).await
|
||||
}
|
||||
|
||||
pub fn display_device_info(
|
||||
params: WithIoFormat<Empty>,
|
||||
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<C: Context + AsRef<RpcContinuations>>() -> ParentHandler<C, LogsParams> {
|
||||
crate::logs::logs(|_: &C, _| async { Ok(LogSource::Unit(SYSTEM_UNIT)) })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user