misc bugfixes

This commit is contained in:
Aiden McClelland
2026-03-02 18:02:20 -07:00
parent 011a3f9d9f
commit f004c46977
5 changed files with 135 additions and 154 deletions

View File

@@ -3684,6 +3684,13 @@ help.arg.s9pk-file-path:
fr_FR: "Chemin vers le fichier de paquet s9pk"
pl_PL: "Ścieżka do pliku pakietu s9pk"
help.arg.s9pk-file-paths:
en_US: "Paths to s9pk package files"
de_DE: "Pfade zu s9pk-Paketdateien"
es_ES: "Rutas a los archivos de paquete s9pk"
fr_FR: "Chemins vers les fichiers de paquet s9pk"
pl_PL: "Ścieżki do plików pakietów s9pk"
help.arg.session-ids:
en_US: "Session identifiers"
de_DE: "Sitzungskennungen"
@@ -4980,6 +4987,13 @@ about.publish-s9pk:
fr_FR: "Publier s9pk dans le bucket S3 et indexer dans le registre"
pl_PL: "Opublikuj s9pk do bucketu S3 i zindeksuj w rejestrze"
about.select-s9pk-for-device:
en_US: "Select the best compatible s9pk for a target device"
de_DE: "Das beste kompatible s9pk für ein Zielgerät auswählen"
es_ES: "Seleccionar el s9pk más compatible para un dispositivo destino"
fr_FR: "Sélectionner le meilleur s9pk compatible pour un appareil cible"
pl_PL: "Wybierz najlepiej kompatybilny s9pk dla urządzenia docelowego"
about.rebuild-service-container:
en_US: "Rebuild service container"
de_DE: "Dienst-Container neu erstellen"

View File

@@ -10,7 +10,6 @@ use std::time::Duration;
use chrono::{TimeDelta, Utc};
use imbl::OrdMap;
use imbl_value::InternedString;
use itertools::Itertools;
use josekit::jwk::Jwk;
use reqwest::{Client, Proxy};
use rpc_toolkit::yajrc::RpcError;
@@ -25,7 +24,6 @@ use crate::account::AccountInfo;
use crate::auth::Sessions;
use crate::context::config::ServerConfig;
use crate::db::model::Database;
use crate::db::model::package::TaskSeverity;
use crate::disk::OsPartitionInfo;
use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::block_dev::BlockDev;
@@ -44,7 +42,6 @@ use crate::prelude::*;
use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle};
use crate::rpc_continuations::{Guid, OpenAuthedContinuations, RpcContinuations};
use crate::service::ServiceMap;
use crate::service::action::update_tasks;
use crate::service::effects::callbacks::ServiceCallbacks;
use crate::service::effects::subcontainer::NVIDIA_OVERLAY_PATH;
use crate::shutdown::Shutdown;
@@ -53,7 +50,7 @@ use crate::util::future::NonDetachingJoinHandle;
use crate::util::io::{TmpDir, delete_file};
use crate::util::lshw::LshwDevice;
use crate::util::sync::{SyncMutex, SyncRwLock, Watch};
use crate::{ActionId, DATA_DIR, PLATFORM, PackageId};
use crate::{DATA_DIR, PLATFORM, PackageId};
pub struct RpcContextSeed {
is_closed: AtomicBool,
@@ -114,7 +111,6 @@ pub struct CleanupInitPhases {
cleanup_sessions: PhaseProgressTrackerHandle,
init_services: PhaseProgressTrackerHandle,
prune_s9pks: PhaseProgressTrackerHandle,
check_tasks: PhaseProgressTrackerHandle,
}
impl CleanupInitPhases {
pub fn new(handle: &FullProgressTracker) -> Self {
@@ -122,7 +118,6 @@ impl CleanupInitPhases {
cleanup_sessions: handle.add_phase("Cleaning up sessions".into(), Some(1)),
init_services: handle.add_phase("Initializing services".into(), Some(10)),
prune_s9pks: handle.add_phase("Pruning S9PKs".into(), Some(1)),
check_tasks: handle.add_phase("Checking action requests".into(), Some(1)),
}
}
}
@@ -173,7 +168,7 @@ impl RpcContext {
init_net_ctrl.complete();
tracing::info!("{}", t!("context.rpc.initialized-net-controller"));
if PLATFORM.ends_with("-nonfree") {
if PLATFORM.ends_with("-nvidia") {
if let Err(e) = Command::new("nvidia-smi")
.invoke(ErrorKind::ParseSysInfo)
.await
@@ -411,7 +406,6 @@ impl RpcContext {
mut cleanup_sessions,
mut init_services,
mut prune_s9pks,
mut check_tasks,
}: CleanupInitPhases,
) -> Result<(), Error> {
cleanup_sessions.start();
@@ -503,76 +497,6 @@ impl RpcContext {
}
prune_s9pks.complete();
check_tasks.start();
let mut action_input: OrdMap<PackageId, BTreeMap<ActionId, Value>> = OrdMap::new();
let tasks: BTreeSet<_> = peek
.as_public()
.as_package_data()
.as_entries()?
.into_iter()
.map(|(_, pde)| {
Ok(pde
.as_tasks()
.as_entries()?
.into_iter()
.map(|(_, r)| {
let t = r.as_task();
Ok::<_, Error>(if t.as_input().transpose_ref().is_some() {
Some((t.as_package_id().de()?, t.as_action_id().de()?))
} else {
None
})
})
.filter_map_ok(|a| a))
})
.flatten_ok()
.map(|a| a.and_then(|a| a))
.try_collect()?;
let procedure_id = Guid::new();
for (package_id, action_id) in tasks {
if let Some(service) = self.services.get(&package_id).await.as_ref() {
if let Some(input) = service
.get_action_input(procedure_id.clone(), action_id.clone(), Value::Null)
.await
.log_err()
.flatten()
.and_then(|i| i.value)
{
action_input
.entry(package_id)
.or_default()
.insert(action_id, input);
}
}
}
self.db
.mutate(|db| {
for (package_id, action_input) in &action_input {
for (action_id, input) in action_input {
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
pde.as_tasks_mut().mutate(|tasks| {
Ok(update_tasks(tasks, package_id, action_id, input, false))
})?;
}
}
}
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
if pde
.as_tasks()
.de()?
.into_iter()
.any(|(_, t)| t.active && t.task.severity == TaskSeverity::Critical)
{
pde.as_status_info_mut().stop()?;
}
}
Ok(())
})
.await
.result?;
check_tasks.complete();
Ok(())
}
pub async fn call_remote<RemoteContext>(

View File

@@ -251,11 +251,12 @@ async fn create_task(
.get(&task.package_id)
.await
.as_ref()
.filter(|s| s.is_initialized())
{
let Some(prev) = service
let prev = service
.get_action_input(procedure_id.clone(), task.action_id.clone(), Value::Null)
.await?
else {
.await?;
let Some(prev) = prev else {
return Err(Error::new(
eyre!(
"{}",
@@ -278,7 +279,9 @@ async fn create_task(
true
}
} else {
true // update when service is installed
// Service not installed or not yet initialized — assume active.
// Will be retested when service init completes (Service::recheck_tasks).
true
}
}
},

View File

@@ -215,6 +215,84 @@ pub struct Service {
seed: Arc<ServiceActorSeed>,
}
impl Service {
pub fn is_initialized(&self) -> bool {
self.seed.persistent_container.state.borrow().rt_initialized
}
/// Re-evaluate all tasks that reference this service's actions.
/// Called after every service init to update task active state.
#[instrument(skip_all)]
async fn recheck_tasks(&self) -> Result<(), Error> {
let service_id = &self.seed.id;
let peek = self.seed.ctx.db.peek().await;
let mut action_input: BTreeMap<ActionId, Value> = BTreeMap::new();
let tasks: BTreeSet<_> = peek
.as_public()
.as_package_data()
.as_entries()?
.into_iter()
.map(|(_, pde)| {
Ok(pde
.as_tasks()
.as_entries()?
.into_iter()
.map(|(_, r)| {
let t = r.as_task();
Ok::<_, Error>(
if t.as_package_id().de()? == *service_id
&& t.as_input().transpose_ref().is_some()
{
Some(t.as_action_id().de()?)
} else {
None
},
)
})
.filter_map_ok(|a| a))
})
.flatten_ok()
.map(|a| a.and_then(|a| a))
.try_collect()?;
let procedure_id = Guid::new();
for action_id in tasks {
if let Some(input) = self
.get_action_input(procedure_id.clone(), action_id.clone(), Value::Null)
.await
.log_err()
.flatten()
.and_then(|i| i.value)
{
action_input.insert(action_id, input);
}
}
self.seed
.ctx
.db
.mutate(|db| {
for (action_id, input) in &action_input {
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
pde.as_tasks_mut().mutate(|tasks| {
Ok(update_tasks(tasks, service_id, action_id, input, false))
})?;
}
}
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
if pde
.as_tasks()
.de()?
.into_iter()
.any(|(_, t)| t.active && t.task.severity == TaskSeverity::Critical)
{
pde.as_status_info_mut().stop()?;
}
}
Ok(())
})
.await
.result?;
Ok(())
}
#[instrument(skip_all)]
async fn new(
ctx: RpcContext,
@@ -263,6 +341,7 @@ impl Service {
.persistent_container
.init(service.weak(), procedure_id, init_kind)
.await?;
service.recheck_tasks().await?;
if let Some(recovery_guard) = recovery_guard {
recovery_guard.unmount(true).await?;
}
@@ -489,70 +568,8 @@ impl Service {
)
.await?;
if let Some(mut progress) = progress {
progress.finalization_progress.complete();
progress.progress.complete();
tokio::task::yield_now().await;
}
let peek = ctx.db.peek().await;
let mut action_input: BTreeMap<ActionId, Value> = BTreeMap::new();
let tasks: BTreeSet<_> = peek
.as_public()
.as_package_data()
.as_entries()?
.into_iter()
.map(|(_, pde)| {
Ok(pde
.as_tasks()
.as_entries()?
.into_iter()
.map(|(_, r)| {
let t = r.as_task();
Ok::<_, Error>(
if t.as_package_id().de()? == manifest.id
&& t.as_input().transpose_ref().is_some()
{
Some(t.as_action_id().de()?)
} else {
None
},
)
})
.filter_map_ok(|a| a))
})
.flatten_ok()
.map(|a| a.and_then(|a| a))
.try_collect()?;
for action_id in tasks {
if peek
.as_public()
.as_package_data()
.as_idx(&manifest.id)
.or_not_found(&manifest.id)?
.as_actions()
.contains_key(&action_id)?
{
if let Some(input) = service
.get_action_input(procedure_id.clone(), action_id.clone(), Value::Null)
.await
.log_err()
.flatten()
.and_then(|i| i.value)
{
action_input.insert(action_id, input);
}
}
}
ctx.db
.mutate(|db| {
for (action_id, input) in &action_input {
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
pde.as_tasks_mut().mutate(|tasks| {
Ok(update_tasks(tasks, &manifest.id, action_id, input, false))
})?;
}
}
let entry = db
.as_public_mut()
.as_package_data_mut()
@@ -594,6 +611,12 @@ impl Service {
.await
.result?;
if let Some(mut progress) = progress {
progress.finalization_progress.complete();
progress.progress.complete();
tokio::task::yield_now().await;
}
// Trigger manifest callbacks after successful installation
let manifest = service.seed.persistent_container.s9pk.as_manifest();
if let Some(callbacks) = ctx.callbacks.get_service_manifest(&manifest.id) {

View File

@@ -74,21 +74,38 @@ declare module 'zod' {
}
// Override z.object to produce loose objects by default (extra keys are preserved, not stripped).
// Patches the source module in require.cache where 'object' is a writable property;
const _origObject = _z.object
const _patchedObject = (...args: Parameters<typeof _z.object>) =>
_origObject(...args).loose()
// In CJS (Node.js), patch the source module in require.cache where 'object' is a writable property;
// the CJS getter chain (index → external → schemas) then relays the patched version.
// We walk only the zod entry module's dependency tree and match by identity (=== origObject).
const _origObject = _z.object
const _zodModule = require.cache[require.resolve('zod')]
for (const child of _zodModule?.children ?? []) {
for (const grandchild of child.children ?? []) {
const desc = Object.getOwnPropertyDescriptor(grandchild.exports, 'object')
if (desc?.value === _origObject && desc.writable) {
grandchild.exports.object = (...args: Parameters<typeof _z.object>) =>
_origObject(...args).loose()
try {
const _zodModule = require.cache[require.resolve('zod')]
for (const child of _zodModule?.children ?? []) {
for (const grandchild of child.children ?? []) {
const desc = Object.getOwnPropertyDescriptor(grandchild.exports, 'object')
if (desc?.value === _origObject && desc.writable) {
grandchild.exports.object = _patchedObject
}
}
}
} catch (_) {
// Not in CJS/Node environment (e.g. browser) — require.cache unavailable
}
export { _z as z }
// z.object is a non-configurable getter on the zod namespace, so we can't override it directly.
// Shadow it by exporting a new object with _z as prototype and our patched object on the instance.
const z: typeof _z = Object.create(_z, {
object: {
value: _patchedObject,
writable: true,
configurable: true,
enumerable: true,
},
})
export { z }
export * as utils from './util'