stop service if critical task activated (#2966)

filter out union lists instead of erroring
This commit is contained in:
Aiden McClelland
2025-06-18 22:03:27 +00:00
committed by GitHub
parent 28f31be36f
commit dbf08a6cf8
10 changed files with 314 additions and 58 deletions

View File

@@ -0,0 +1,153 @@
export default {
nodes: {
type: "list",
subtype: "union",
name: "Lightning Nodes",
description: "List of Lightning Network node instances to manage",
range: "[1,*)",
default: ["lnd"],
spec: {
type: "string",
"display-as": "{{name}}",
"unique-by": "name",
name: "Node Implementation",
tag: {
id: "type",
name: "Type",
description:
"- LND: Lightning Network Daemon from Lightning Labs\n- CLN: Core Lightning from Blockstream\n",
"variant-names": {
lnd: "Lightning Network Daemon (LND)",
"c-lightning": "Core Lightning (CLN)",
},
},
default: "lnd",
variants: {
lnd: {
name: {
type: "string",
name: "Node Name",
description: "Name of this node in the list",
default: "StartOS LND",
nullable: false,
},
"connection-settings": {
type: "union",
name: "Connection Settings",
description: "The Lightning Network Daemon node to connect to.",
tag: {
id: "type",
name: "Type",
description:
"- Internal: The Lightning Network Daemon service installed to your StartOS server.\n- External: A Lightning Network Daemon instance running on a remote device (advanced).\n",
"variant-names": {
internal: "Internal",
external: "External",
},
},
default: "internal",
variants: {
internal: {},
external: {
address: {
type: "string",
name: "Public Address",
description:
"The public address of your LND REST server\nNOTE: RTL does not support a .onion URL here\n",
nullable: false,
},
"rest-port": {
type: "number",
name: "REST Port",
description:
"The port that your Lightning Network Daemon REST server is bound to",
nullable: false,
range: "[0,65535]",
integral: true,
default: 8080,
},
macaroon: {
type: "string",
name: "Macaroon",
description:
'Your admin.macaroon file, Base64URL encoded. This is the same as the value after "macaroon=" in your lndconnect URL.',
nullable: false,
masked: true,
pattern: "[=A-Za-z0-9_-]+",
"pattern-description":
"Macaroon must be encoded in Base64URL format (only A-Z, a-z, 0-9, _, - and = allowed)",
},
},
},
},
},
"c-lightning": {
name: {
type: "string",
name: "Node Name",
description: "Name of this node in the list",
default: "StartOS CLN",
nullable: false,
},
"connection-settings": {
type: "union",
name: "Connection Settings",
description: "The Core Lightning (CLN) node to connect to.",
tag: {
id: "type",
name: "Type",
description:
"- Internal: The Core Lightning (CLN) service installed to your StartOS server.\n- External: A Core Lightning (CLN) instance running on a remote device (advanced).\n",
"variant-names": {
internal: "Internal",
external: "External",
},
},
default: "internal",
variants: {
internal: {},
external: {
address: {
type: "string",
name: "Public Address",
description:
"The public address of your CLNRest server\nNOTE: RTL does not support a .onion URL here\n",
nullable: false,
},
"rest-port": {
type: "number",
name: "CLNRest Port",
description: "The port that your CLNRest server is bound to",
nullable: false,
range: "[0,65535]",
integral: true,
default: 3010,
},
macaroon: {
type: "string",
name: "Rune",
description:
"Your CLNRest unrestricted Rune, Base64URL encoded.",
nullable: false,
masked: true,
},
},
},
},
},
},
},
},
password: {
type: "string",
name: "Password",
description: "The password for your Ride the Lightning dashboard",
nullable: false,
copyable: true,
masked: true,
default: {
charset: "a-z,A-Z,0-9",
len: 22,
},
},
}

View File

@@ -1,5 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`transformConfigSpec transformConfigSpec(RTL) 1`] = `
{
"password": {
"default": {
"charset": "a-z,A-Z,0-9",
"len": 22,
},
"description": "The password for your Ride the Lightning dashboard",
"disabled": false,
"generate": null,
"immutable": false,
"inputmode": "text",
"masked": true,
"maxLength": null,
"minLength": null,
"name": "Password",
"patterns": [],
"placeholder": null,
"required": true,
"type": "text",
"warning": null,
},
}
`;
exports[`transformConfigSpec transformConfigSpec(bitcoind) 1`] = ` exports[`transformConfigSpec transformConfigSpec(bitcoind) 1`] = `
{ {
"advanced": { "advanced": {

View File

@@ -1,5 +1,10 @@
import { matchOldConfigSpec, transformConfigSpec } from "./transformConfigSpec" import {
import fixtureEmbasyPagesConfig from "./__fixtures__/embasyPagesConfig" matchOldConfigSpec,
matchOldValueSpecList,
transformConfigSpec,
} from "./transformConfigSpec"
import fixtureEmbassyPagesConfig from "./__fixtures__/embassyPagesConfig"
import fixtureRTLConfig from "./__fixtures__/rtlConfig"
import searNXG from "./__fixtures__/searNXG" import searNXG from "./__fixtures__/searNXG"
import bitcoind from "./__fixtures__/bitcoind" import bitcoind from "./__fixtures__/bitcoind"
import nostr from "./__fixtures__/nostr" import nostr from "./__fixtures__/nostr"
@@ -8,14 +13,25 @@ import nostrConfig2 from "./__fixtures__/nostrConfig2"
describe("transformConfigSpec", () => { describe("transformConfigSpec", () => {
test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => { test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => {
matchOldConfigSpec.unsafeCast( matchOldConfigSpec.unsafeCast(
fixtureEmbasyPagesConfig.homepage.variants["web-page"], fixtureEmbassyPagesConfig.homepage.variants["web-page"],
) )
}) })
test("matchOldConfigSpec(embassyPages)", () => { test("matchOldConfigSpec(embassyPages)", () => {
matchOldConfigSpec.unsafeCast(fixtureEmbasyPagesConfig) matchOldConfigSpec.unsafeCast(fixtureEmbassyPagesConfig)
}) })
test("transformConfigSpec(embassyPages)", () => { test("transformConfigSpec(embassyPages)", () => {
const spec = matchOldConfigSpec.unsafeCast(fixtureEmbasyPagesConfig) const spec = matchOldConfigSpec.unsafeCast(fixtureEmbassyPagesConfig)
expect(transformConfigSpec(spec)).toMatchSnapshot()
})
test("matchOldConfigSpec(RTL.nodes)", () => {
matchOldValueSpecList.unsafeCast(fixtureRTLConfig.nodes)
})
test("matchOldConfigSpec(RTL)", () => {
matchOldConfigSpec.unsafeCast(fixtureRTLConfig)
})
test("transformConfigSpec(RTL)", () => {
const spec = matchOldConfigSpec.unsafeCast(fixtureRTLConfig)
expect(transformConfigSpec(spec)).toMatchSnapshot() expect(transformConfigSpec(spec)).toMatchSnapshot()
}) })

View File

@@ -47,6 +47,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec {
immutable: false, immutable: false,
} }
} else if (oldVal.type === "list") { } else if (oldVal.type === "list") {
if (isUnionList(oldVal)) return inputSpec
newVal = getListSpec(oldVal) newVal = getListSpec(oldVal)
} else if (oldVal.type === "number") { } else if (oldVal.type === "number") {
const range = Range.from(oldVal.range) const range = Range.from(oldVal.range)
@@ -177,15 +178,17 @@ export function transformOldConfigToNew(
} }
} }
if (isList(val) && isObjectList(val)) { if (isList(val)) {
if (!config[key]) return obj if (!config[key]) return obj
newVal = (config[key] as object[]).map((obj) => if (isObjectList(val)) {
transformOldConfigToNew( newVal = (config[key] as object[]).map((obj) =>
matchOldConfigSpec.unsafeCast(val.spec.spec), transformOldConfigToNew(
obj, matchOldConfigSpec.unsafeCast(val.spec.spec),
), obj,
) ),
)
} else if (isUnionList(val)) return obj
} }
if (isPointer(val)) { if (isPointer(val)) {
@@ -224,13 +227,15 @@ export function transformNewConfigToOld(
} }
} }
if (isList(val) && isObjectList(val)) { if (isList(val)) {
newVal = (config[key] as object[]).map((obj) => if (isObjectList(val)) {
transformNewConfigToOld( newVal = (config[key] as object[]).map((obj) =>
matchOldConfigSpec.unsafeCast(val.spec.spec), transformNewConfigToOld(
obj, matchOldConfigSpec.unsafeCast(val.spec.spec),
), obj,
) ),
)
} else if (isUnionList(val)) return obj
} }
return { return {
@@ -376,15 +381,17 @@ function isNumberList(
): val is OldValueSpecList & { subtype: "number" } { ): val is OldValueSpecList & { subtype: "number" } {
return val.subtype === "number" return val.subtype === "number"
} }
function isObjectList( function isObjectList(
val: OldValueSpecList, val: OldValueSpecList,
): val is OldValueSpecList & { subtype: "object" } { ): val is OldValueSpecList & { subtype: "object" } {
if (["union"].includes(val.subtype)) {
throw new Error("Invalid list subtype. enum, string, and object permitted.")
}
return val.subtype === "object" return val.subtype === "object"
} }
function isUnionList(
val: OldValueSpecList,
): val is OldValueSpecList & { subtype: "union" } {
return val.subtype === "union"
}
export type OldConfigSpec = Record<string, OldValueSpec> export type OldConfigSpec = Record<string, OldValueSpec>
const [_matchOldConfigSpec, setMatchOldConfigSpec] = deferred<unknown>() const [_matchOldConfigSpec, setMatchOldConfigSpec] = deferred<unknown>()
export const matchOldConfigSpec = _matchOldConfigSpec as Parser< export const matchOldConfigSpec = _matchOldConfigSpec as Parser<
@@ -491,6 +498,12 @@ const matchOldListValueSpecObject = object({
"unique-by": matchOldUniqueBy.nullable().optional(), // indicates whether duplicates can be permitted in the list "unique-by": matchOldUniqueBy.nullable().optional(), // indicates whether duplicates can be permitted in the list
"display-as": string.nullable().optional(), // this should be a handlebars template which can make use of the entire config which corresponds to 'spec' "display-as": string.nullable().optional(), // this should be a handlebars template which can make use of the entire config which corresponds to 'spec'
}) })
const matchOldListValueSpecUnion = object({
"unique-by": matchOldUniqueBy.nullable().optional(),
"display-as": string.nullable().optional(),
tag: matchOldUnionTagSpec,
variants: dictionary([string, _matchOldConfigSpec]),
})
const matchOldListValueSpecString = object({ const matchOldListValueSpecString = object({
masked: boolean.nullable().optional(), masked: boolean.nullable().optional(),
copyable: boolean.nullable().optional(), copyable: boolean.nullable().optional(),
@@ -511,7 +524,7 @@ const matchOldListValueSpecNumber = object({
}) })
// represents a spec for a list // represents a spec for a list
const matchOldValueSpecList = every( export const matchOldValueSpecList = every(
object({ object({
type: literals("list"), type: literals("list"),
range: string, // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules range: string, // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules
@@ -542,6 +555,10 @@ const matchOldValueSpecList = every(
subtype: literals("number"), subtype: literals("number"),
spec: matchOldListValueSpecNumber, spec: matchOldListValueSpecNumber,
}), }),
object({
subtype: literals("union"),
spec: matchOldListValueSpecUnion,
}),
), ),
) )
type OldValueSpecList = typeof matchOldValueSpecList._TYPE type OldValueSpecList = typeof matchOldValueSpecList._TYPE

View File

@@ -24,6 +24,7 @@ use super::setup::CURRENT_SECRET;
use crate::account::AccountInfo; use crate::account::AccountInfo;
use crate::auth::Sessions; use crate::auth::Sessions;
use crate::context::config::ServerConfig; use crate::context::config::ServerConfig;
use crate::db::model::package::TaskSeverity;
use crate::db::model::Database; use crate::db::model::Database;
use crate::disk::OsPartitionInfo; use crate::disk::OsPartitionInfo;
use crate::init::{check_time_is_synchronized, InitResult}; use crate::init::{check_time_is_synchronized, InitResult};
@@ -403,21 +404,46 @@ impl RpcContext {
} }
} }
} }
self.db for id in
.mutate(|db| { self.db
for (package_id, action_input) in &action_input { .mutate::<Vec<PackageId>>(|db| {
for (action_id, input) in action_input { for (package_id, action_input) in &action_input {
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? { for (action_id, input) in action_input {
pde.as_tasks_mut().mutate(|tasks| { for (_, pde) in
Ok(update_tasks(tasks, package_id, action_id, input, false)) 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))
})?;
}
} }
} }
} db.as_public()
Ok(()) .as_package_data()
}) .as_entries()?
.await .into_iter()
.result?; .filter_map(|(id, pkg)| {
(|| {
if pkg.as_tasks().de()?.into_iter().any(|(_, t)| {
t.active && t.task.severity == TaskSeverity::Critical
}) {
Ok(Some(id))
} else {
Ok(None)
}
})()
.transpose()
})
.collect()
})
.await
.result?
{
let svc = self.services.get(&id).await;
if let Some(svc) = &*svc {
svc.stop(procedure_id.clone(), false).await?;
}
}
check_tasks.complete(); check_tasks.complete();
Ok(()) Ok(())

View File

@@ -6,7 +6,7 @@ use models::{ActionId, PackageId, ProcedureName, ReplayId};
use crate::action::{ActionInput, ActionResult}; use crate::action::{ActionInput, ActionResult};
use crate::db::model::package::{ use crate::db::model::package::{
ActionVisibility, AllowedStatuses, TaskCondition, TaskEntry, TaskInput, ActionVisibility, AllowedStatuses, TaskCondition, TaskEntry, TaskInput, TaskSeverity,
}; };
use crate::prelude::*; use crate::prelude::*;
use crate::rpc_continuations::Guid; use crate::rpc_continuations::Guid;
@@ -78,7 +78,8 @@ pub fn update_tasks(
action_id: &ActionId, action_id: &ActionId,
input: &Value, input: &Value,
was_run: bool, was_run: bool,
) { ) -> bool {
let mut critical_activated = false;
tasks.retain(|_, v| { tasks.retain(|_, v| {
if &v.task.package_id != package_id || &v.task.action_id != action_id { if &v.task.package_id != package_id || &v.task.action_id != action_id {
return true; return true;
@@ -95,6 +96,9 @@ pub fn update_tasks(
} }
} else { } else {
v.active = true; v.active = true;
if v.task.severity == TaskSeverity::Critical {
critical_activated = true;
}
} }
} }
None => { None => {
@@ -106,7 +110,8 @@ pub fn update_tasks(
} else { } else {
!was_run !was_run
} }
}) });
critical_activated
} }
pub(super) struct RunAction { pub(super) struct RunAction {
@@ -125,7 +130,7 @@ impl Handler<RunAction> for ServiceActor {
id: ref action_id, id: ref action_id,
input, input,
}: RunAction, }: RunAction,
_: &BackgroundJobQueue, jobs: &BackgroundJobQueue,
) -> Self::Response { ) -> Self::Response {
let container = &self.0.persistent_container; let container = &self.0.persistent_container;
let package_id = &self.0.id; let package_id = &self.0.id;
@@ -162,7 +167,7 @@ impl Handler<RunAction> for ServiceActor {
} }
let result = container let result = container
.execute::<Option<ActionResult>>( .execute::<Option<ActionResult>>(
id, id.clone(),
ProcedureName::RunAction(action_id.clone()), ProcedureName::RunAction(action_id.clone()),
json!({ json!({
"input": input, "input": input,
@@ -171,19 +176,30 @@ impl Handler<RunAction> for ServiceActor {
) )
.await .await
.with_kind(ErrorKind::Action)?; .with_kind(ErrorKind::Action)?;
self.0 if self
.0
.ctx .ctx
.db .db
.mutate(|db| { .mutate(|db| {
let mut critical_activated = false;
for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? { for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
pde.as_tasks_mut().mutate(|tasks| { critical_activated |= pde.as_tasks_mut().mutate(|tasks| {
Ok(update_tasks(tasks, package_id, action_id, &input, true)) Ok(update_tasks(tasks, package_id, action_id, &input, true))
})?; })?;
} }
Ok(()) Ok(critical_activated)
}) })
.await .await
.result?; .result?
{
<Self as Handler<super::control::Stop>>::handle(
self,
id,
super::control::Stop { wait: false },
jobs,
)
.await;
}
Ok(result) Ok(result)
} }
} }

View File

@@ -28,8 +28,8 @@ impl Service {
} }
} }
struct Stop { pub(super) struct Stop {
wait: bool, pub wait: bool,
} }
impl Handler<Stop> for ServiceActor { impl Handler<Stop> for ServiceActor {
type Response = (); type Response = ();

View File

@@ -35,7 +35,7 @@ use url::Url;
use crate::context::{CliContext, RpcContext}; use crate::context::{CliContext, RpcContext};
use crate::db::model::package::{ use crate::db::model::package::{
InstalledState, ManifestPreference, PackageDataEntry, PackageState, PackageStateMatchModelRef, InstalledState, ManifestPreference, PackageDataEntry, PackageState, PackageStateMatchModelRef,
UpdatingState, TaskSeverity, UpdatingState,
}; };
use crate::disk::mount::filesystem::ReadOnly; use crate::disk::mount::filesystem::ReadOnly;
use crate::disk::mount::guard::{GenericMountGuard, MountGuard}; use crate::disk::mount::guard::{GenericMountGuard, MountGuard};
@@ -526,7 +526,8 @@ impl Service {
} }
} }
} }
ctx.db let has_critical = ctx
.db
.mutate(|db| { .mutate(|db| {
for (action_id, 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()? { for (_, pde) in db.as_public_mut().as_package_data_mut().as_entries_mut()? {
@@ -541,10 +542,12 @@ impl Service {
.as_idx_mut(&manifest.id) .as_idx_mut(&manifest.id)
.or_not_found(&manifest.id)?; .or_not_found(&manifest.id)?;
let actions = entry.as_actions().keys()?; let actions = entry.as_actions().keys()?;
entry.as_tasks_mut().mutate(|t| { let has_critical = entry.as_tasks_mut().mutate(|t| {
Ok(t.retain(|_, v| { t.retain(|_, v| {
v.task.package_id != manifest.id || actions.contains(&v.task.action_id) v.task.package_id != manifest.id || actions.contains(&v.task.action_id)
})) });
Ok(t.iter()
.any(|(_, t)| t.active && t.task.severity == TaskSeverity::Critical))
})?; })?;
entry entry
.as_state_info_mut() .as_state_info_mut()
@@ -553,11 +556,15 @@ impl Service {
entry.as_icon_mut().ser(&icon)?; entry.as_icon_mut().ser(&icon)?;
entry.as_registry_mut().ser(registry)?; entry.as_registry_mut().ser(registry)?;
Ok(()) Ok(has_critical)
}) })
.await .await
.result?; .result?;
if prev_state == Some(StartStop::Start) && !has_critical {
service.start(procedure_id).await?;
}
Ok(service) Ok(service)
} }

View File

@@ -25,7 +25,6 @@ use crate::prelude::*;
use crate::progress::{ use crate::progress::{
FullProgressTracker, PhaseProgressTrackerHandle, ProgressTrackerWriter, ProgressUnits, FullProgressTracker, PhaseProgressTrackerHandle, ProgressTrackerWriter, ProgressUnits,
}; };
use crate::rpc_continuations::Guid;
use crate::s9pk::manifest::PackageId; use crate::s9pk::manifest::PackageId;
use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::merkle_archive::source::FileSource;
use crate::s9pk::S9pk; use crate::s9pk::S9pk;
@@ -344,9 +343,6 @@ impl ServiceMap {
}), }),
) )
.await?; .await?;
if prev == Some(StartStop::Start) {
new_service.start(Guid::new()).await?;
}
*service = Some(new_service.into()); *service = Some(new_service.into());
drop(service); drop(service);