Feature/callbacks (#2678)

* wip

* initialize callbacks

* wip

* smtp

* list_service_interfaces

* wip

* wip

* fix domains

* fix hostname handling in NetService

* misc fixes

* getInstalledPackages

* misc fixes

* publish v6 lib

* refactor service effects

* fix import

* fix container runtime

* fix tests

* apply suggestions from review
This commit is contained in:
Aiden McClelland
2024-07-25 11:44:51 -06:00
committed by GitHub
parent ab465a755e
commit b36b62c68e
113 changed files with 4853 additions and 2517 deletions

View File

@@ -28,9 +28,6 @@ set +e
fail=
echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\""
if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features cli,daemon,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl)"; then
fail=true
fi
if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features container-runtime,$FEATURES --locked --bin containerbox --target=$ARCH-unknown-linux-musl)"; then
fail=true
fi

42
core/build-startbox.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/bash
cd "$(dirname "${BASH_SOURCE[0]}")"
set -e
shopt -s expand_aliases
if [ -z "$ARCH" ]; then
ARCH=$(uname -m)
fi
USE_TTY=
if tty -s; then
USE_TTY="-it"
fi
cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi
alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl'
set +e
fail=
echo "FEATURES=\"$FEATURES\""
echo "RUSTFLAGS=\"$RUSTFLAGS\""
if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features cli,daemon,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl)"; then
fail=true
fi
set -e
cd core
sudo chown -R $USER target
sudo chown -R $USER ~/.cargo
if [ -n "$fail" ]; then
exit 1
fi

View File

@@ -61,6 +61,11 @@ impl Borrow<str> for PackageId {
self.0.as_ref()
}
}
impl<'a> Borrow<str> for &'a PackageId {
fn borrow(&self) -> &str {
self.0.as_ref()
}
}
impl AsRef<Path> for PackageId {
fn as_ref(&self) -> &Path {
self.0.as_ref().as_ref()

View File

@@ -4,8 +4,6 @@ use crate::{ActionId, PackageId};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ProcedureName {
StartMain,
StopMain,
GetConfig,
SetConfig,
CreateBackup,
@@ -25,8 +23,6 @@ impl ProcedureName {
match self {
ProcedureName::Init => "/init".to_string(),
ProcedureName::Uninit => "/uninit".to_string(),
ProcedureName::StartMain => "/main/start".to_string(),
ProcedureName::StopMain => "/main/stop".to_string(),
ProcedureName::SetConfig => "/config/set".to_string(),
ProcedureName::GetConfig => "/config/get".to_string(),
ProcedureName::CreateBackup => "/backup/create".to_string(),

View File

@@ -319,6 +319,7 @@ fn display_sessions(params: WithIoFormat<ListParams>, arg: SessionList) {
pub struct ListParams {
#[arg(skip)]
#[ts(skip)]
#[serde(rename = "__auth_session")] // from Auth middleware
session: InternedString,
}

View File

@@ -1,3 +1,4 @@
use imbl_value::InternedString;
use openssl::pkey::{PKey, Private};
use openssl::x509::X509;
use patch_db::Value;
@@ -97,7 +98,7 @@ impl OsBackupV0 {
#[serde(rename = "kebab-case")]
struct OsBackupV1 {
server_id: String, // uuidv4
hostname: String, // embassy-<adjective>-<noun>
hostname: InternedString, // embassy-<adjective>-<noun>
net_key: Base64<[u8; 32]>, // Ed25519 Secret Key
root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key
root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate
@@ -127,7 +128,7 @@ impl OsBackupV1 {
struct OsBackupV2 {
server_id: String, // uuidv4
hostname: String, // <adjective>-<noun>
hostname: InternedString, // <adjective>-<noun>
root_ca_key: Pem<PKey<Private>>, // PEM Encoded OpenSSL Key
root_ca_cert: Pem<X509>, // PEM Encoded OpenSSL X509 Certificate
ssh_key: Pem<ssh_key::PrivateKey>, // PEM Encoded OpenSSH Key

View File

@@ -15,7 +15,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
EmbassyLogger::init();
if let Err(e) = CliApp::new(
|cfg: ContainerClientConfig| Ok(ContainerCliContext::init(cfg)),
crate::service::service_effect_handler::service_effect_handler(),
crate::service::effects::handler(),
)
.run(args)
{

View File

@@ -29,6 +29,7 @@ use crate::net::wifi::WpaCli;
use crate::prelude::*;
use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle};
use crate::rpc_continuations::{OpenAuthedContinuations, RpcContinuations};
use crate::service::effects::callbacks::ServiceCallbacks;
use crate::service::ServiceMap;
use crate::shutdown::Shutdown;
use crate::system::get_mem_info;
@@ -52,6 +53,7 @@ pub struct RpcContextSeed {
pub lxc_manager: Arc<LxcManager>,
pub open_authed_continuations: OpenAuthedContinuations<InternedString>,
pub rpc_continuations: RpcContinuations,
pub callbacks: ServiceCallbacks,
pub wifi_manager: Option<Arc<RwLock<WpaCli>>>,
pub current_secret: Arc<Jwk>,
pub client: Client,
@@ -225,6 +227,7 @@ impl RpcContext {
lxc_manager: Arc::new(LxcManager::new()),
open_authed_continuations: OpenAuthedContinuations::new(),
rpc_continuations: RpcContinuations::new(),
callbacks: Default::default(),
wifi_manager: wifi_interface
.clone()
.map(|i| Arc::new(RwLock::new(WpaCli::init(i)))),

View File

@@ -5,6 +5,7 @@ use std::time::Duration;
use futures::{Future, StreamExt};
use helpers::NonDetachingJoinHandle;
use imbl_value::InternedString;
use josekit::jwk::Jwk;
use patch_db::PatchDb;
use rpc_toolkit::Context;
@@ -40,7 +41,8 @@ lazy_static::lazy_static! {
#[ts(export)]
pub struct SetupResult {
pub tor_address: String,
pub lan_address: String,
#[ts(type = "string")]
pub lan_address: InternedString,
pub root_ca: String,
}
impl TryFrom<&AccountInfo> for SetupResult {

View File

@@ -20,6 +20,7 @@ use crate::db::model::package::AllPackageData;
use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr};
use crate::prelude::*;
use crate::progress::FullProgress;
use crate::system::SmtpValue;
use crate::util::cpupower::Governor;
use crate::version::{Current, VersionT};
use crate::{ARCH, PLATFORM};
@@ -107,7 +108,8 @@ pub struct ServerInfo {
#[ts(type = "string")]
pub platform: InternedString,
pub id: String,
pub hostname: String,
#[ts(type = "string")]
pub hostname: InternedString,
#[ts(type = "string")]
pub version: Version,
#[ts(type = "string | null")]
@@ -135,7 +137,7 @@ pub struct ServerInfo {
#[serde(default)]
pub zram: bool,
pub governor: Option<Governor>,
pub smtp: Option<String>,
pub smtp: Option<SmtpValue>,
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]

View File

@@ -1,3 +1,5 @@
use imbl_value::InternedString;
use lazy_format::lazy_format;
use rand::{thread_rng, Rng};
use tokio::process::Command;
use tracing::instrument;
@@ -5,7 +7,7 @@ use tracing::instrument;
use crate::util::Invoke;
use crate::{Error, ErrorKind};
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct Hostname(pub String);
pub struct Hostname(pub InternedString);
lazy_static::lazy_static! {
static ref ADJECTIVES: Vec<String> = include_str!("./assets/adjectives.txt").lines().map(|x| x.to_string()).collect();
@@ -18,15 +20,16 @@ impl AsRef<str> for Hostname {
}
impl Hostname {
pub fn lan_address(&self) -> String {
format!("https://{}.local", self.0)
pub fn lan_address(&self) -> InternedString {
InternedString::from_display(&lazy_format!("https://{}.local", self.0))
}
pub fn local_domain_name(&self) -> String {
format!("{}.local", self.0)
pub fn local_domain_name(&self) -> InternedString {
InternedString::from_display(&lazy_format!("{}.local", self.0))
}
pub fn no_dot_host_name(&self) -> String {
self.0.to_owned()
pub fn no_dot_host_name(&self) -> InternedString {
self.0.clone()
}
}
@@ -34,7 +37,9 @@ pub fn generate_hostname() -> Hostname {
let mut rng = thread_rng();
let adjective = &ADJECTIVES[rng.gen_range(0..ADJECTIVES.len())];
let noun = &NOUNS[rng.gen_range(0..NOUNS.len())];
Hostname(format!("{adjective}-{noun}"))
Hostname(InternedString::from_display(&lazy_format!(
"{adjective}-{noun}"
)))
}
pub fn generate_id() -> String {
@@ -48,12 +53,12 @@ pub async fn get_current_hostname() -> Result<Hostname, Error> {
.invoke(ErrorKind::ParseSysInfo)
.await?;
let out_string = String::from_utf8(out)?;
Ok(Hostname(out_string.trim().to_owned()))
Ok(Hostname(out_string.trim().into()))
}
#[instrument(skip_all)]
pub async fn set_hostname(hostname: &Hostname) -> Result<(), Error> {
let hostname: &String = &hostname.0;
let hostname = &*hostname.0;
Command::new("hostnamectl")
.arg("--static")
.arg("set-hostname")

View File

@@ -224,6 +224,18 @@ pub fn server<C: Context>() -> ParentHandler<C> {
})
.with_call_remote::<CliContext>(),
)
.subcommand(
"set-smtp",
from_fn_async(system::set_system_smtp)
.no_display()
.with_call_remote::<CliContext>(),
)
.subcommand(
"clear-smtp",
from_fn_async(system::clear_system_smtp)
.no_display()
.with_call_remote::<CliContext>(),
)
}
pub fn package<C: Context>() -> ParentHandler<C> {

View File

@@ -34,7 +34,7 @@ struct Resolver {
impl Resolver {
async fn resolve(&self, name: &Name) -> Option<Vec<Ipv4Addr>> {
match name.iter().next_back() {
Some(b"embassy") => {
Some(b"embassy") | Some(b"startos") => {
if let Some(pkg) = name.iter().rev().skip(1).next() {
if let Some(ip) = self.services.read().await.get(&Some(
std::str::from_utf8(pkg)

View File

@@ -1,7 +1,13 @@
use std::fmt;
use std::str::FromStr;
use imbl_value::InternedString;
use serde::{Deserialize, Serialize};
use torut::onion::OnionAddressV3;
use ts_rs::TS;
use crate::prelude::*;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, TS)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "kind")]
@@ -11,4 +17,32 @@ pub enum HostAddress {
#[ts(type = "string")]
address: OnionAddressV3,
},
Domain {
#[ts(type = "string")]
address: InternedString,
},
}
impl FromStr for HostAddress {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(addr) = s.strip_suffix(".onion") {
Ok(HostAddress::Onion {
address: addr
.parse::<OnionAddressV3>()
.with_kind(ErrorKind::ParseUrl)?,
})
} else {
Ok(HostAddress::Domain { address: s.into() })
}
}
}
impl fmt::Display for HostAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Onion { address } => write!(f, "{address}"),
Self::Domain { address } => write!(f, "{address}"),
}
}
}

View File

@@ -40,6 +40,10 @@ impl Host {
hostname_info: BTreeMap::new(),
}
}
pub fn addresses(&self) -> impl Iterator<Item = &HostAddress> {
// TODO: handle primary
self.addresses.iter()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]

View File

@@ -4,6 +4,7 @@ use std::sync::{Arc, Weak};
use color_eyre::eyre::eyre;
use imbl::OrdMap;
use imbl_value::InternedString;
use models::{HostId, OptionExt, PackageId};
use torut::onion::{OnionAddressV3, TorSecretKeyV3};
use tracing::instrument;
@@ -67,6 +68,14 @@ impl PreInitNetController {
alpn.clone(),
)
.await?;
self.vhost
.add(
Some("startos".into()),
443,
([127, 0, 0, 1], 80).into(),
alpn.clone(),
)
.await?;
// LAN IP
self.os_bindings.push(
@@ -113,7 +122,9 @@ impl PreInitNetController {
self.os_bindings.push(
self.vhost
.add(
Some(tor_key.public().get_onion_address().to_string()),
Some(InternedString::from_display(
&tor_key.public().get_onion_address(),
)),
443,
([127, 0, 0, 1], 80).into(),
alpn.clone(),
@@ -189,7 +200,15 @@ impl NetController {
#[derive(Default, Debug)]
struct HostBinds {
lan: BTreeMap<u16, (LanInfo, Option<AddSslOptions>, Vec<Arc<()>>)>,
lan: BTreeMap<
u16,
(
LanInfo,
Option<AddSslOptions>,
BTreeSet<InternedString>,
Vec<Arc<()>>,
),
>,
tor: BTreeMap<OnionAddressV3, (OrdMap<u16, SocketAddr>, Vec<Arc<()>>)>,
}
@@ -234,20 +253,35 @@ impl NetService {
.await?;
self.update(id, host).await
}
pub async fn clear_bindings(&mut self) -> Result<(), Error> {
// TODO BLUJ
Ok(())
let ctrl = self.net_controller()?;
let mut errors = ErrorCollection::new();
for (_, binds) in std::mem::take(&mut self.binds) {
for (_, (lan, _, _, rc)) in binds.lan {
drop(rc);
if let Some(external) = lan.assigned_ssl_port {
ctrl.vhost.gc(None, external).await?;
}
if let Some(external) = lan.assigned_port {
ctrl.forward.gc(external).await?;
}
}
for (addr, (_, rcs)) in binds.tor {
drop(rcs);
errors.handle(ctrl.tor.gc(Some(addr), None).await);
}
}
std::mem::take(&mut self.dns);
errors.handle(ctrl.dns.gc(Some(self.id.clone()), self.ip).await);
errors.into_result()
}
async fn update(&mut self, id: HostId, host: Host) -> Result<(), Error> {
let ctrl = self.net_controller()?;
let mut hostname_info = BTreeMap::new();
let binds = {
if !self.binds.contains_key(&id) {
self.binds.insert(id.clone(), Default::default());
}
self.binds.get_mut(&id).unwrap()
};
let binds = self.binds.entry(id.clone()).or_default();
let peek = ctrl.db.peek().await;
// LAN
@@ -256,37 +290,71 @@ impl NetService {
let hostname = server_info.as_hostname().de()?;
for (port, bind) in &host.bindings {
let old_lan_bind = binds.lan.remove(port);
let old_lan_port = old_lan_bind.as_ref().map(|(external, _, _)| *external);
let lan_bind = old_lan_bind
.filter(|(external, ssl, _)| ssl == &bind.options.add_ssl && bind.lan == *external); // only keep existing binding if relevant details match
.as_ref()
.filter(|(external, ssl, _, _)| {
ssl == &bind.options.add_ssl && bind.lan == *external
})
.cloned(); // only keep existing binding if relevant details match
if bind.lan.assigned_port.is_some() || bind.lan.assigned_ssl_port.is_some() {
let new_lan_bind = if let Some(b) = lan_bind {
b
} else {
let mut rcs = Vec::with_capacity(2);
let mut rcs = Vec::with_capacity(2 + host.addresses.len());
let mut hostnames = BTreeSet::new();
if let Some(ssl) = &bind.options.add_ssl {
let external = bind
.lan
.assigned_ssl_port
.or_not_found("assigned ssl port")?;
let target = (self.ip, *port).into();
let connect_ssl = if let Some(alpn) = ssl.alpn.clone() {
Err(alpn)
} else {
if bind.options.secure.as_ref().map_or(false, |s| s.ssl) {
Ok(())
} else {
Err(AlpnInfo::Reflect)
}
};
rcs.push(
ctrl.vhost
.add(
None,
external,
(self.ip, *port).into(),
if let Some(alpn) = ssl.alpn.clone() {
Err(alpn)
} else {
if bind.options.secure.as_ref().map_or(false, |s| s.ssl) {
Ok(())
} else {
Err(AlpnInfo::Reflect)
}
},
)
.add(None, external, target, connect_ssl.clone())
.await?,
);
for address in host.addresses() {
match address {
HostAddress::Onion { address } => {
let hostname = InternedString::from_display(address);
if hostnames.insert(hostname.clone()) {
rcs.push(
ctrl.vhost
.add(
Some(hostname),
external,
target,
connect_ssl.clone(),
)
.await?,
);
}
}
HostAddress::Domain { address } => {
if hostnames.insert(address.clone()) {
rcs.push(
ctrl.vhost
.add(
Some(address.clone()),
external,
target,
connect_ssl.clone(),
)
.await?,
);
}
}
}
}
}
if let Some(security) = bind.options.secure {
if bind.options.add_ssl.is_some() && security.ssl {
@@ -297,7 +365,7 @@ impl NetService {
rcs.push(ctrl.forward.add(external, (self.ip, *port).into()).await?);
}
}
(bind.lan, bind.options.add_ssl.clone(), rcs)
(bind.lan, bind.options.add_ssl.clone(), hostnames, rcs)
};
let mut bind_hostname_info: Vec<HostnameInfo> =
hostname_info.remove(port).unwrap_or_default();
@@ -337,9 +405,12 @@ impl NetService {
hostname_info.insert(*port, bind_hostname_info);
binds.lan.insert(*port, new_lan_bind);
}
if let Some(lan) = old_lan_port {
if let Some((lan, _, hostnames, _)) = old_lan_bind {
if let Some(external) = lan.assigned_ssl_port {
ctrl.vhost.gc(None, external).await?;
for hostname in hostnames {
ctrl.vhost.gc(Some(hostname), external).await?;
}
}
if let Some(external) = lan.assigned_port {
ctrl.forward.gc(external).await?;
@@ -347,18 +418,21 @@ impl NetService {
}
}
let mut removed = BTreeSet::new();
binds.lan.retain(|internal, (external, _, _)| {
binds.lan.retain(|internal, (external, _, hostnames, _)| {
if host.bindings.contains_key(internal) {
true
} else {
removed.insert(*external);
removed.insert((*external, std::mem::take(hostnames)));
false
}
});
for lan in removed {
for (lan, hostnames) in removed {
if let Some(external) = lan.assigned_ssl_port {
ctrl.vhost.gc(None, external).await?;
for hostname in hostnames {
ctrl.vhost.gc(Some(hostname), external).await?;
}
}
if let Some(external) = lan.assigned_port {
ctrl.forward.gc(external).await?;
@@ -401,48 +475,44 @@ impl NetService {
);
}
}
let mut keep_tor_addrs = BTreeSet::new();
for addr in match host.kind {
HostKind::Multi => {
// itertools::Either::Left(
host.addresses.iter()
// )
} // HostKind::Single | HostKind::Static => itertools::Either::Right(&host.primary),
} {
match addr {
HostAddress::Onion { address } => {
keep_tor_addrs.insert(address);
let old_tor_bind = binds.tor.remove(address);
let tor_bind = old_tor_bind.filter(|(ports, _)| ports == &tor_binds);
let new_tor_bind = if let Some(tor_bind) = tor_bind {
tor_bind
} else {
let key = peek
.as_private()
.as_key_store()
.as_onion()
.get_key(address)?;
let rcs = ctrl
.tor
.add(key, tor_binds.clone().into_iter().collect())
.await?;
(tor_binds.clone(), rcs)
};
for (internal, ports) in &tor_hostname_ports {
let mut bind_hostname_info =
hostname_info.remove(internal).unwrap_or_default();
bind_hostname_info.push(HostnameInfo::Onion {
hostname: OnionHostname {
value: address.to_string(),
port: ports.non_ssl,
ssl_port: ports.ssl,
},
});
hostname_info.insert(*internal, bind_hostname_info);
}
binds.tor.insert(address.clone(), new_tor_bind);
}
for tor_addr in host.addresses().filter_map(|a| {
if let HostAddress::Onion { address } = a {
Some(address)
} else {
None
}
}) {
keep_tor_addrs.insert(tor_addr);
let old_tor_bind = binds.tor.remove(tor_addr);
let tor_bind = old_tor_bind.filter(|(ports, _)| ports == &tor_binds);
let new_tor_bind = if let Some(tor_bind) = tor_bind {
tor_bind
} else {
let key = peek
.as_private()
.as_key_store()
.as_onion()
.get_key(tor_addr)?;
let rcs = ctrl
.tor
.add(key, tor_binds.clone().into_iter().collect())
.await?;
(tor_binds.clone(), rcs)
};
for (internal, ports) in &tor_hostname_ports {
let mut bind_hostname_info = hostname_info.remove(internal).unwrap_or_default();
bind_hostname_info.push(HostnameInfo::Onion {
hostname: OnionHostname {
value: tor_addr.to_string(),
port: ports.non_ssl,
ssl_port: ports.ssl,
},
});
hostname_info.insert(*internal, bind_hostname_info);
}
binds.tor.insert(tor_addr.clone(), new_tor_bind);
}
for addr in binds.tor.keys() {
if !keep_tor_addrs.contains(addr) {
@@ -462,26 +532,8 @@ impl NetService {
pub async fn remove_all(mut self) -> Result<(), Error> {
self.shutdown = true;
let mut errors = ErrorCollection::new();
if let Some(ctrl) = Weak::upgrade(&self.controller) {
for (_, binds) in std::mem::take(&mut self.binds) {
for (_, (lan, _, rc)) in binds.lan {
drop(rc);
if let Some(external) = lan.assigned_ssl_port {
ctrl.vhost.gc(None, external).await?;
}
if let Some(external) = lan.assigned_port {
ctrl.forward.gc(external).await?;
}
}
for (addr, (_, rcs)) in binds.tor {
drop(rcs);
errors.handle(ctrl.tor.gc(Some(addr), None).await);
}
}
std::mem::take(&mut self.dns);
errors.handle(ctrl.dns.gc(Some(self.id.clone()), self.ip).await);
errors.into_result()
self.clear_bindings().await
} else {
tracing::warn!("NetService dropped after NetController is shutdown");
Err(Error::new(
@@ -495,11 +547,11 @@ impl NetService {
self.ip
}
pub fn get_ext_port(&self, host_id: HostId, internal_port: u16) -> Result<LanInfo, Error> {
pub fn get_lan_port(&self, host_id: HostId, internal_port: u16) -> Result<LanInfo, Error> {
let host_id_binds = self.binds.get_key_value(&host_id);
match host_id_binds {
Some((_, binds)) => {
if let Some((lan, _, _)) = binds.lan.get(&internal_port) {
if let Some((lan, _, _, _)) = binds.lan.get(&internal_port) {
Ok(*lan)
} else {
Err(Error::new(

View File

@@ -1,13 +1,13 @@
use std::cmp::Ordering;
use std::cmp::{min, Ordering};
use std::collections::{BTreeMap, BTreeSet};
use std::net::IpAddr;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use futures::FutureExt;
use imbl_value::InternedString;
use libc::time_t;
use openssl::asn1::{Asn1Integer, Asn1Time};
use openssl::asn1::{Asn1Integer, Asn1Time, Asn1TimeRef};
use openssl::bn::{BigNum, MsbOption};
use openssl::ec::{EcGroup, EcKey};
use openssl::hash::MessageDigest;
@@ -17,6 +17,7 @@ use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509};
use openssl::*;
use patch_db::HasModel;
use serde::{Deserialize, Serialize};
use tokio::time::Instant;
use tracing::instrument;
use crate::account::AccountInfo;
@@ -126,12 +127,18 @@ impl Model<CertStore> {
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct CertData {
pub keys: PKeyPair,
pub certs: CertPair,
}
impl CertData {
pub fn expiration(&self) -> Result<SystemTime, Error> {
self.certs.expiration()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FullchainCertData {
pub root: X509,
pub int: X509,
@@ -144,6 +151,16 @@ impl FullchainCertData {
pub fn fullchain_nistp256(&self) -> Vec<&X509> {
vec![&self.leaf.certs.nistp256, &self.int, &self.root]
}
pub fn expiration(&self) -> Result<SystemTime, Error> {
[
asn1_time_to_system_time(self.root.not_after())?,
asn1_time_to_system_time(self.int.not_after())?,
self.leaf.expiration()?,
]
.into_iter()
.min()
.ok_or_else(|| Error::new(eyre!("unreachable"), ErrorKind::Unknown))
}
}
static CERTIFICATE_VERSION: i32 = 2; // X509 version 3 is actually encoded as '2' in the cert because fuck you.
@@ -155,6 +172,26 @@ fn unix_time(time: SystemTime) -> time_t {
.unwrap_or_default()
}
lazy_static::lazy_static! {
static ref ASN1_UNIX_EPOCH: Asn1Time = Asn1Time::from_unix(0).unwrap();
}
fn asn1_time_to_system_time(time: &Asn1TimeRef) -> Result<SystemTime, Error> {
let diff = time.diff(&**ASN1_UNIX_EPOCH)?;
let mut res = UNIX_EPOCH;
if diff.days >= 0 {
res += Duration::from_secs(diff.days as u64 * 86400);
} else {
res -= Duration::from_secs((-1 * diff.days) as u64 * 86400);
}
if diff.secs >= 0 {
res += Duration::from_secs(diff.secs as u64);
} else {
res -= Duration::from_secs((-1 * diff.secs) as u64);
}
Ok(res)
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PKeyPair {
#[serde(with = "crate::util::serde::pem")]
@@ -162,6 +199,12 @@ pub struct PKeyPair {
#[serde(with = "crate::util::serde::pem")]
pub nistp256: PKey<Private>,
}
impl PartialEq for PKeyPair {
fn eq(&self, other: &Self) -> bool {
self.ed25519.public_eq(&other.ed25519) && self.nistp256.public_eq(&other.nistp256)
}
}
impl Eq for PKeyPair {}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
pub struct CertPair {
@@ -170,6 +213,14 @@ pub struct CertPair {
#[serde(with = "crate::util::serde::pem")]
pub nistp256: X509,
}
impl CertPair {
pub fn expiration(&self) -> Result<SystemTime, Error> {
Ok(min(
asn1_time_to_system_time(self.ed25519.not_after())?,
asn1_time_to_system_time(self.nistp256.not_after())?,
))
}
}
pub async fn root_ca_start_time() -> Result<SystemTime, Error> {
Ok(if check_time_is_synchronized().await? {

View File

@@ -46,7 +46,7 @@ impl VHostController {
#[instrument(skip_all)]
pub async fn add(
&self,
hostname: Option<String>,
hostname: Option<InternedString>,
external: u16,
target: SocketAddr,
connect_ssl: Result<(), AlpnInfo>, // Ok: yes, connect using ssl, pass through alpn; Err: connect tcp, use provided strategy for alpn
@@ -70,7 +70,7 @@ impl VHostController {
Ok(rc?)
}
#[instrument(skip_all)]
pub async fn gc(&self, hostname: Option<String>, external: u16) -> Result<(), Error> {
pub async fn gc(&self, hostname: Option<InternedString>, external: u16) -> Result<(), Error> {
let mut writable = self.servers.lock().await;
if let Some(server) = writable.remove(&external) {
server.gc(hostname).await?;
@@ -102,7 +102,7 @@ impl Default for AlpnInfo {
}
struct VHostServer {
mapping: Weak<RwLock<BTreeMap<Option<String>, BTreeMap<TargetInfo, Weak<()>>>>>,
mapping: Weak<RwLock<BTreeMap<Option<InternedString>, BTreeMap<TargetInfo, Weak<()>>>>>,
_thread: NonDetachingJoinHandle<()>,
}
impl VHostServer {
@@ -179,7 +179,7 @@ impl VHostServer {
}
};
let target_name =
mid.client_hello().server_name().map(|s| s.to_owned());
mid.client_hello().server_name().map(|s| s.into());
let target = {
let mapping = mapping.read().await;
mapping
@@ -208,9 +208,7 @@ impl VHostServer {
let mut tcp_stream =
TcpStream::connect(target.addr).await?;
let hostnames = target_name
.as_ref()
.into_iter()
.map(InternedString::intern)
.chain(
db.peek()
.await
@@ -405,7 +403,11 @@ impl VHostServer {
.into(),
})
}
async fn add(&self, hostname: Option<String>, target: TargetInfo) -> Result<Arc<()>, Error> {
async fn add(
&self,
hostname: Option<InternedString>,
target: TargetInfo,
) -> Result<Arc<()>, Error> {
if let Some(mapping) = Weak::upgrade(&self.mapping) {
let mut writable = mapping.write().await;
let mut targets = writable.remove(&hostname).unwrap_or_default();
@@ -424,7 +426,7 @@ impl VHostServer {
))
}
}
async fn gc(&self, hostname: Option<String>) -> Result<(), Error> {
async fn gc(&self, hostname: Option<InternedString>) -> Result<(), Error> {
if let Some(mapping) = Weak::upgrade(&self.mapping) {
let mut writable = mapping.write().await;
let mut targets = writable.remove(&hostname).unwrap_or_default();

View File

@@ -4,6 +4,7 @@ use std::path::PathBuf;
use chrono::Utc;
use clap::Parser;
use exver::Version;
use imbl_value::InternedString;
use itertools::Itertools;
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
@@ -27,7 +28,6 @@ use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::s9pk::merkle_archive::source::ArchiveSource;
use crate::util::io::open_file;
use crate::util::serde::Base64;
use crate::util::VersionString;
pub fn add_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
@@ -55,7 +55,8 @@ pub fn add_api<C: Context>() -> ParentHandler<C> {
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct AddAssetParams {
pub version: VersionString,
#[ts(type = "string")]
pub version: Version,
#[ts(type = "string")]
pub platform: InternedString,
#[ts(type = "string")]
@@ -154,7 +155,7 @@ pub struct CliAddAssetParams {
#[arg(short = 'p', long = "platform")]
pub platform: InternedString,
#[arg(short = 'v', long = "version")]
pub version: VersionString,
pub version: Version,
pub file: PathBuf,
pub url: Url,
}
@@ -209,11 +210,18 @@ pub async fn cli_add_asset(
hash: Base64(*blake3.as_bytes()),
size,
};
let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?;
let signature = AnySignature::Ed25519(Ed25519.sign_commitment(
ctx.developer_key()?,
&commitment,
SIG_CONTEXT,
)?);
sign_phase.complete();
verify_phase.start();
let src = HttpSource::new(ctx.client.clone(), url.clone()).await?;
if let Some(size) = src.size().await {
verify_phase.set_total(size);
}
let mut writer = verify_phase.writer(VerifyingWriter::new(
tokio::io::sink(),
Some((blake3::Hash::from_bytes(*commitment.hash), commitment.size)),

View File

@@ -3,6 +3,7 @@ use std::panic::UnwindSafe;
use std::path::{Path, PathBuf};
use clap::Parser;
use exver::Version;
use helpers::AtomicFile;
use imbl_value::{json, InternedString};
use itertools::Itertools;
@@ -21,7 +22,6 @@ use crate::registry::signer::commitment::blake3::Blake3Commitment;
use crate::registry::signer::commitment::Commitment;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::util::io::open_file;
use crate::util::VersionString;
pub fn get_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
@@ -37,7 +37,8 @@ pub fn get_api<C: Context>() -> ParentHandler<C> {
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct GetOsAssetParams {
pub version: VersionString,
#[ts(type = "string")]
pub version: Version,
#[ts(type = "string")]
pub platform: InternedString,
}
@@ -91,7 +92,7 @@ pub async fn get_squashfs(
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliGetOsAssetParams {
pub version: VersionString,
pub version: Version,
pub platform: InternedString,
#[arg(long = "download", short = 'd')]
pub download: Option<PathBuf>,

View File

@@ -3,6 +3,7 @@ use std::panic::UnwindSafe;
use std::path::PathBuf;
use clap::Parser;
use exver::Version;
use imbl_value::InternedString;
use itertools::Itertools;
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
@@ -23,7 +24,6 @@ use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::s9pk::merkle_archive::source::ArchiveSource;
use crate::util::io::open_file;
use crate::util::serde::Base64;
use crate::util::VersionString;
pub fn sign_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
@@ -51,7 +51,8 @@ pub fn sign_api<C: Context>() -> ParentHandler<C> {
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct SignAssetParams {
version: VersionString,
#[ts(type = "string")]
version: Version,
#[ts(type = "string")]
platform: InternedString,
#[ts(skip)]
@@ -137,7 +138,7 @@ pub struct CliSignAssetParams {
#[arg(short = 'p', long = "platform")]
pub platform: InternedString,
#[arg(short = 'v', long = "version")]
pub version: VersionString,
pub version: Version,
pub file: PathBuf,
}
@@ -189,7 +190,11 @@ pub async fn cli_sign_asset(
hash: Base64(*blake3.as_bytes()),
size,
};
let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?;
let signature = AnySignature::Ed25519(Ed25519.sign_commitment(
ctx.developer_key()?,
&commitment,
SIG_CONTEXT,
)?);
sign_phase.complete();
index_phase.start();

View File

@@ -1,6 +1,6 @@
use std::collections::{BTreeMap, BTreeSet};
use exver::VersionRange;
use exver::{Version, VersionRange};
use imbl_value::InternedString;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
@@ -10,14 +10,28 @@ use crate::registry::asset::RegistryAsset;
use crate::registry::context::RegistryContext;
use crate::registry::signer::commitment::blake3::Blake3Commitment;
use crate::rpc_continuations::Guid;
use crate::util::VersionString;
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct OsIndex {
pub versions: BTreeMap<VersionString, OsVersionInfo>,
pub versions: OsVersionInfoMap,
}
#[derive(Debug, Default, Deserialize, Serialize, TS)]
pub struct OsVersionInfoMap(
#[ts(as = "BTreeMap::<String, OsVersionInfo>")] pub BTreeMap<Version, OsVersionInfo>,
);
impl Map for OsVersionInfoMap {
type Key = Version;
type Value = OsVersionInfo;
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
Ok(InternedString::from_display(key))
}
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
Ok(InternedString::from_display(key))
}
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]

View File

@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
use chrono::Utc;
use clap::Parser;
use exver::VersionRange;
use exver::{Version, VersionRange};
use itertools::Itertools;
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
@@ -15,7 +15,6 @@ use crate::registry::context::RegistryContext;
use crate::registry::os::index::OsVersionInfo;
use crate::registry::signer::sign::AnyVerifyingKey;
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
use crate::util::VersionString;
pub mod signer;
@@ -53,7 +52,8 @@ pub fn version_api<C: Context>() -> ParentHandler<C> {
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct AddVersionParams {
pub version: VersionString,
#[ts(type = "string")]
pub version: Version,
pub headline: String,
pub release_notes: String,
#[ts(type = "string")]
@@ -99,7 +99,8 @@ pub async fn add_version(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemoveVersionParams {
pub version: VersionString,
#[ts(type = "string")]
pub version: Version,
}
pub async fn remove_version(
@@ -124,7 +125,7 @@ pub async fn remove_version(
pub struct GetOsVersionParams {
#[ts(type = "string | null")]
#[arg(long = "src")]
pub source: Option<VersionString>,
pub source: Option<Version>,
#[ts(type = "string | null")]
#[arg(long = "target")]
pub target: Option<VersionRange>,
@@ -144,7 +145,7 @@ pub async fn get_version(
server_id,
arch,
}: GetOsVersionParams,
) -> Result<BTreeMap<VersionString, OsVersionInfo>, Error> {
) -> Result<BTreeMap<Version, OsVersionInfo>, Error> {
if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, arch) {
let created_at = Utc::now();
@@ -176,10 +177,7 @@ pub async fn get_version(
.collect()
}
pub fn display_version_info<T>(
params: WithIoFormat<T>,
info: BTreeMap<VersionString, OsVersionInfo>,
) {
pub fn display_version_info<T>(params: WithIoFormat<T>, info: BTreeMap<Version, OsVersionInfo>) {
use prettytable::*;
if let Some(format) = params.format {
@@ -197,7 +195,7 @@ pub fn display_version_info<T>(
]);
for (version, info) in &info {
table.add_row(row![
version.as_str(),
&version.to_string(),
&info.headline,
&info.release_notes,
&info.iso.keys().into_iter().join(", "),

View File

@@ -1,6 +1,7 @@
use std::collections::BTreeMap;
use clap::Parser;
use exver::Version;
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
@@ -12,7 +13,6 @@ use crate::registry::context::RegistryContext;
use crate::registry::signer::SignerInfo;
use crate::rpc_continuations::Guid;
use crate::util::serde::HandlerExtSerde;
use crate::util::VersionString;
pub fn signer_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
@@ -44,7 +44,8 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct VersionSignerParams {
pub version: VersionString,
#[ts(type = "string")]
pub version: Version,
pub signer: Guid,
}
@@ -104,7 +105,8 @@ pub async fn remove_version_signer(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct ListVersionSignersParams {
pub version: VersionString,
#[ts(type = "string")]
pub version: Version,
}
pub async fn list_version_signers(

View File

@@ -9,7 +9,7 @@ use rpc_toolkit::{call_remote_socket, yajrc, CallRemote, Context, Empty};
use tokio::runtime::Runtime;
use crate::lxc::HOST_RPC_SERVER_SOCKET;
use crate::service::service_effect_handler::EffectContext;
use crate::service::effects::context::EffectContext;
#[derive(Debug, Default, Parser)]
pub struct ContainerClientConfig {

View File

@@ -0,0 +1,101 @@
use std::collections::BTreeMap;
use models::{ActionId, PackageId};
use crate::action::ActionResult;
use crate::db::model::package::ActionMetadata;
use crate::rpc_continuations::Guid;
use crate::service::effects::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ExportActionParams {
#[ts(optional)]
package_id: Option<PackageId>,
id: ActionId,
metadata: ActionMetadata,
}
pub async fn export_action(context: EffectContext, data: ExportActionParams) -> Result<(), Error> {
let context = context.deref()?;
let package_id = context.seed.id.clone();
context
.seed
.ctx
.db
.mutate(|db| {
let model = db
.as_public_mut()
.as_package_data_mut()
.as_idx_mut(&package_id)
.or_not_found(&package_id)?
.as_actions_mut();
let mut value = model.de()?;
value
.insert(data.id, data.metadata)
.map(|_| ())
.unwrap_or_default();
model.ser(&value)
})
.await?;
Ok(())
}
pub async fn clear_actions(context: EffectContext) -> Result<(), Error> {
let context = context.deref()?;
let package_id = context.seed.id.clone();
context
.seed
.ctx
.db
.mutate(|db| {
db.as_public_mut()
.as_package_data_mut()
.as_idx_mut(&package_id)
.or_not_found(&package_id)?
.as_actions_mut()
.ser(&BTreeMap::new())
})
.await?;
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct ExecuteAction {
#[serde(default)]
#[ts(skip)]
procedure_id: Guid,
#[ts(optional)]
package_id: Option<PackageId>,
action_id: ActionId,
#[ts(type = "any")]
input: Value,
}
pub async fn execute_action(
context: EffectContext,
ExecuteAction {
procedure_id,
package_id,
action_id,
input,
}: ExecuteAction,
) -> Result<ActionResult, Error> {
let context = context.deref()?;
if let Some(package_id) = package_id {
context
.seed
.ctx
.services
.get(&package_id)
.await
.as_ref()
.or_not_found(&package_id)?
.action(procedure_id, action_id, input)
.await
} else {
context.action(procedure_id, action_id, input).await
}
}

View File

@@ -0,0 +1,311 @@
use std::cmp::min;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::{Arc, Mutex, Weak};
use std::time::{Duration, SystemTime};
use futures::future::join_all;
use helpers::NonDetachingJoinHandle;
use imbl::{vector, Vector};
use imbl_value::InternedString;
use models::{HostId, PackageId, ServiceInterfaceId};
use patch_db::json_ptr::JsonPointer;
use tracing::warn;
use crate::net::ssl::FullchainCertData;
use crate::prelude::*;
use crate::service::effects::context::EffectContext;
use crate::service::effects::net::ssl::Algorithm;
use crate::service::rpc::CallbackHandle;
use crate::service::{Service, ServiceActorSeed};
use crate::util::collections::EqMap;
#[derive(Default)]
pub struct ServiceCallbacks(Mutex<ServiceCallbackMap>);
#[derive(Default)]
struct ServiceCallbackMap {
get_service_interface: BTreeMap<(PackageId, ServiceInterfaceId), Vec<CallbackHandler>>,
list_service_interfaces: BTreeMap<PackageId, Vec<CallbackHandler>>,
get_system_smtp: Vec<CallbackHandler>,
get_host_info: BTreeMap<(PackageId, HostId), Vec<CallbackHandler>>,
get_ssl_certificate: EqMap<
(BTreeSet<InternedString>, FullchainCertData, Algorithm),
(NonDetachingJoinHandle<()>, Vec<CallbackHandler>),
>,
get_store: BTreeMap<PackageId, BTreeMap<JsonPointer, Vec<CallbackHandler>>>,
}
impl ServiceCallbacks {
fn mutate<T>(&self, f: impl FnOnce(&mut ServiceCallbackMap) -> T) -> T {
let mut this = self.0.lock().unwrap();
f(&mut *this)
}
pub fn gc(&self) {
self.mutate(|this| {
this.get_service_interface.retain(|_, v| {
v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
!v.is_empty()
});
this.list_service_interfaces.retain(|_, v| {
v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
!v.is_empty()
});
this.get_system_smtp
.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
this.get_host_info.retain(|_, v| {
v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
!v.is_empty()
});
this.get_ssl_certificate.retain(|_, (_, v)| {
v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
!v.is_empty()
});
this.get_store.retain(|_, v| {
v.retain(|_, v| {
v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0);
!v.is_empty()
});
!v.is_empty()
});
})
}
pub(super) fn add_get_service_interface(
&self,
package_id: PackageId,
service_interface_id: ServiceInterfaceId,
handler: CallbackHandler,
) {
self.mutate(|this| {
this.get_service_interface
.entry((package_id, service_interface_id))
.or_default()
.push(handler);
})
}
#[must_use]
pub fn get_service_interface(
&self,
id: &(PackageId, ServiceInterfaceId),
) -> Option<CallbackHandlers> {
self.mutate(|this| {
Some(CallbackHandlers(
this.get_service_interface.remove(id).unwrap_or_default(),
))
.filter(|cb| !cb.0.is_empty())
})
}
pub(super) fn add_list_service_interfaces(
&self,
package_id: PackageId,
handler: CallbackHandler,
) {
self.mutate(|this| {
this.list_service_interfaces
.entry(package_id)
.or_default()
.push(handler);
})
}
#[must_use]
pub fn list_service_interfaces(&self, id: &PackageId) -> Option<CallbackHandlers> {
self.mutate(|this| {
Some(CallbackHandlers(
this.list_service_interfaces.remove(id).unwrap_or_default(),
))
.filter(|cb| !cb.0.is_empty())
})
}
pub(super) fn add_get_system_smtp(&self, handler: CallbackHandler) {
self.mutate(|this| {
this.get_system_smtp.push(handler);
})
}
#[must_use]
pub fn get_system_smtp(&self) -> Option<CallbackHandlers> {
self.mutate(|this| {
Some(CallbackHandlers(std::mem::take(&mut this.get_system_smtp)))
.filter(|cb| !cb.0.is_empty())
})
}
pub(super) fn add_get_host_info(
&self,
package_id: PackageId,
host_id: HostId,
handler: CallbackHandler,
) {
self.mutate(|this| {
this.get_host_info
.entry((package_id, host_id))
.or_default()
.push(handler);
})
}
#[must_use]
pub fn get_host_info(&self, id: &(PackageId, HostId)) -> Option<CallbackHandlers> {
self.mutate(|this| {
Some(CallbackHandlers(
this.get_host_info.remove(id).unwrap_or_default(),
))
.filter(|cb| !cb.0.is_empty())
})
}
pub(super) fn add_get_ssl_certificate(
&self,
ctx: EffectContext,
hostnames: BTreeSet<InternedString>,
cert: FullchainCertData,
algorithm: Algorithm,
handler: CallbackHandler,
) {
self.mutate(|this| {
this.get_ssl_certificate
.entry((hostnames.clone(), cert.clone(), algorithm))
.or_insert_with(|| {
(
tokio::spawn(async move {
if let Err(e) = async {
loop {
match cert
.expiration()
.ok()
.and_then(|e| e.duration_since(SystemTime::now()).ok())
{
Some(d) => {
tokio::time::sleep(min(Duration::from_secs(86400), d))
.await
}
_ => break,
}
}
let Ok(ctx) = ctx.deref() else {
return Ok(());
};
if let Some((_, callbacks)) =
ctx.seed.ctx.callbacks.mutate(|this| {
this.get_ssl_certificate
.remove(&(hostnames, cert, algorithm))
})
{
CallbackHandlers(callbacks).call(vector![]).await?;
}
Ok::<_, Error>(())
}
.await
{
tracing::error!(
"Error in callback handler for getSslCertificate: {e}"
);
tracing::debug!("{e:?}");
}
})
.into(),
Vec::new(),
)
})
.1
.push(handler);
})
}
pub(super) fn add_get_store(
&self,
package_id: PackageId,
path: JsonPointer,
handler: CallbackHandler,
) {
self.mutate(|this| {
this.get_store
.entry(package_id)
.or_default()
.entry(path)
.or_default()
.push(handler)
})
}
#[must_use]
pub fn get_store(
&self,
package_id: &PackageId,
path: &JsonPointer,
) -> Option<CallbackHandlers> {
self.mutate(|this| {
if let Some(watched) = this.get_store.get_mut(package_id) {
let mut res = Vec::new();
watched.retain(|ptr, cbs| {
if ptr.starts_with(path) || path.starts_with(ptr) {
res.append(cbs);
false
} else {
true
}
});
Some(CallbackHandlers(res))
} else {
None
}
.filter(|cb| !cb.0.is_empty())
})
}
}
pub struct CallbackHandler {
handle: CallbackHandle,
seed: Weak<ServiceActorSeed>,
}
impl CallbackHandler {
pub fn new(service: &Service, handle: CallbackHandle) -> Self {
Self {
handle,
seed: Arc::downgrade(&service.seed),
}
}
pub async fn call(mut self, args: Vector<Value>) -> Result<(), Error> {
if let Some(seed) = self.seed.upgrade() {
seed.persistent_container
.callback(self.handle.take(), args)
.await?;
}
Ok(())
}
}
impl Drop for CallbackHandler {
fn drop(&mut self) {
if self.handle.is_active() {
warn!("Callback handler dropped while still active!");
}
}
}
pub struct CallbackHandlers(Vec<CallbackHandler>);
impl CallbackHandlers {
pub async fn call(self, args: Vector<Value>) -> Result<(), Error> {
let mut err = ErrorCollection::new();
for res in join_all(self.0.into_iter().map(|cb| cb.call(args.clone()))).await {
err.handle(res);
}
err.into_result()
}
}
pub(super) fn clear_callbacks(context: EffectContext) -> Result<(), Error> {
let context = context.deref()?;
context
.seed
.persistent_container
.state
.send_if_modified(|s| !std::mem::take(&mut s.callbacks).is_empty());
context.seed.ctx.callbacks.gc();
Ok(())
}

View File

@@ -0,0 +1,53 @@
use models::PackageId;
use crate::service::effects::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct GetConfiguredParams {
#[ts(optional)]
package_id: Option<PackageId>,
}
pub async fn get_configured(context: EffectContext) -> Result<bool, Error> {
let context = context.deref()?;
let peeked = context.seed.ctx.db.peek().await;
let package_id = &context.seed.id;
peeked
.as_public()
.as_package_data()
.as_idx(package_id)
.or_not_found(package_id)?
.as_status()
.as_configured()
.de()
}
#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct SetConfigured {
configured: bool,
}
pub async fn set_configured(
context: EffectContext,
SetConfigured { configured }: SetConfigured,
) -> Result<(), Error> {
let context = context.deref()?;
let package_id = &context.seed.id;
context
.seed
.ctx
.db
.mutate(|db| {
db.as_public_mut()
.as_package_data_mut()
.as_idx_mut(package_id)
.or_not_found(package_id)?
.as_status_mut()
.as_configured_mut()
.ser(&configured)
})
.await?;
Ok(())
}

View File

@@ -0,0 +1,27 @@
use std::sync::{Arc, Weak};
use rpc_toolkit::Context;
use crate::prelude::*;
use crate::service::Service;
#[derive(Clone)]
pub(in crate::service) struct EffectContext(Weak<Service>);
impl EffectContext {
pub fn new(service: Weak<Service>) -> Self {
Self(service)
}
}
impl Context for EffectContext {}
impl EffectContext {
pub(super) fn deref(&self) -> Result<Arc<Service>, Error> {
if let Some(seed) = Weak::upgrade(&self.0) {
Ok(seed)
} else {
Err(Error::new(
eyre!("Service has already been destroyed"),
ErrorKind::InvalidRequest,
))
}
}
}

View File

@@ -0,0 +1,66 @@
use std::str::FromStr;
use clap::builder::ValueParserFactory;
use crate::service::effects::prelude::*;
use crate::util::clap::FromStrParser;
pub async fn restart(
context: EffectContext,
ProcedureId { procedure_id }: ProcedureId,
) -> Result<(), Error> {
let context = context.deref()?;
context.restart(procedure_id).await?;
Ok(())
}
pub async fn shutdown(
context: EffectContext,
ProcedureId { procedure_id }: ProcedureId,
) -> Result<(), Error> {
let context = context.deref()?;
context.stop(procedure_id).await?;
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub enum SetMainStatusStatus {
Running,
Stopped,
}
impl FromStr for SetMainStatusStatus {
type Err = color_eyre::eyre::Report;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"running" => Ok(Self::Running),
"stopped" => Ok(Self::Stopped),
_ => Err(eyre!("unknown status {s}")),
}
}
}
impl ValueParserFactory for SetMainStatusStatus {
type Parser = FromStrParser<Self>;
fn value_parser() -> Self::Parser {
FromStrParser::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct SetMainStatus {
status: SetMainStatusStatus,
}
pub async fn set_main_status(
context: EffectContext,
SetMainStatus { status }: SetMainStatus,
) -> Result<(), Error> {
let context = context.deref()?;
match status {
SetMainStatusStatus::Running => context.seed.started(),
SetMainStatusStatus::Stopped => context.seed.stopped(),
}
Ok(())
}

View File

@@ -0,0 +1,371 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::str::FromStr;
use clap::builder::ValueParserFactory;
use exver::VersionRange;
use itertools::Itertools;
use models::{HealthCheckId, PackageId, VolumeId};
use patch_db::json_ptr::JsonPointer;
use crate::db::model::package::{
CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, ManifestPreference,
};
use crate::rpc_continuations::Guid;
use crate::service::effects::prelude::*;
use crate::status::health_check::HealthCheckResult;
use crate::util::clap::FromStrParser;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct MountTarget {
package_id: PackageId,
volume_id: VolumeId,
subpath: Option<PathBuf>,
readonly: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct MountParams {
location: String,
target: MountTarget,
}
pub async fn mount(
context: EffectContext,
MountParams {
location,
target:
MountTarget {
package_id,
volume_id,
subpath,
readonly,
},
}: MountParams,
) -> Result<(), Error> {
// TODO
todo!()
}
pub async fn get_installed_packages(context: EffectContext) -> Result<Vec<PackageId>, Error> {
context
.deref()?
.seed
.ctx
.db
.peek()
.await
.into_public()
.into_package_data()
.keys()
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct ExposeForDependentsParams {
#[ts(type = "string[]")]
paths: Vec<JsonPointer>,
}
pub async fn expose_for_dependents(
context: EffectContext,
ExposeForDependentsParams { paths }: ExposeForDependentsParams,
) -> Result<(), Error> {
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub enum DependencyKind {
Exists,
Running,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase", tag = "kind")]
#[serde(rename_all_fields = "camelCase")]
#[ts(export)]
pub enum DependencyRequirement {
Running {
id: PackageId,
health_checks: BTreeSet<HealthCheckId>,
#[ts(type = "string")]
version_range: VersionRange,
},
Exists {
id: PackageId,
#[ts(type = "string")]
version_range: VersionRange,
},
}
// filebrowser:exists,bitcoind:running:foo+bar+baz
impl FromStr for DependencyRequirement {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once(':') {
Some((id, "e")) | Some((id, "exists")) => Ok(Self::Exists {
id: id.parse()?,
version_range: "*".parse()?, // TODO
}),
Some((id, rest)) => {
let health_checks = match rest.split_once(':') {
Some(("r", rest)) | Some(("running", rest)) => rest
.split('+')
.map(|id| id.parse().map_err(Error::from))
.collect(),
Some((kind, _)) => Err(Error::new(
eyre!("unknown dependency kind {kind}"),
ErrorKind::InvalidRequest,
)),
None => match rest {
"r" | "running" => Ok(BTreeSet::new()),
kind => Err(Error::new(
eyre!("unknown dependency kind {kind}"),
ErrorKind::InvalidRequest,
)),
},
}?;
Ok(Self::Running {
id: id.parse()?,
health_checks,
version_range: "*".parse()?, // TODO
})
}
None => Ok(Self::Running {
id: s.parse()?,
health_checks: BTreeSet::new(),
version_range: "*".parse()?, // TODO
}),
}
}
}
impl ValueParserFactory for DependencyRequirement {
type Parser = FromStrParser<Self>;
fn value_parser() -> Self::Parser {
FromStrParser::new()
}
}
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[command(rename_all = "camelCase")]
#[ts(export)]
pub struct SetDependenciesParams {
#[serde(default)]
procedure_id: Guid,
dependencies: Vec<DependencyRequirement>,
}
pub async fn set_dependencies(
context: EffectContext,
SetDependenciesParams {
procedure_id,
dependencies,
}: SetDependenciesParams,
) -> Result<(), Error> {
let context = context.deref()?;
let id = &context.seed.id;
let mut deps = BTreeMap::new();
for dependency in dependencies {
let (dep_id, kind, version_range) = match dependency {
DependencyRequirement::Exists { id, version_range } => {
(id, CurrentDependencyKind::Exists, version_range)
}
DependencyRequirement::Running {
id,
health_checks,
version_range,
} => (
id,
CurrentDependencyKind::Running { health_checks },
version_range,
),
};
let config_satisfied =
if let Some(dep_service) = &*context.seed.ctx.services.get(&dep_id).await {
context
.dependency_config(
procedure_id.clone(),
dep_id.clone(),
dep_service.get_config(procedure_id.clone()).await?.config,
)
.await?
.is_none()
} else {
true
};
let info = CurrentDependencyInfo {
title: context
.seed
.persistent_container
.s9pk
.dependency_metadata(&dep_id)
.await?
.map(|m| m.title),
icon: context
.seed
.persistent_container
.s9pk
.dependency_icon_data_url(&dep_id)
.await?,
kind,
version_range,
config_satisfied,
};
deps.insert(dep_id, info);
}
context
.seed
.ctx
.db
.mutate(|db| {
db.as_public_mut()
.as_package_data_mut()
.as_idx_mut(id)
.or_not_found(id)?
.as_current_dependencies_mut()
.ser(&CurrentDependencies(deps))
})
.await
}
pub async fn get_dependencies(context: EffectContext) -> Result<Vec<DependencyRequirement>, Error> {
let context = context.deref()?;
let id = &context.seed.id;
let db = context.seed.ctx.db.peek().await;
let data = db
.as_public()
.as_package_data()
.as_idx(id)
.or_not_found(id)?
.as_current_dependencies()
.de()?;
data.0
.into_iter()
.map(|(id, current_dependency_info)| {
let CurrentDependencyInfo {
version_range,
kind,
..
} = current_dependency_info;
Ok::<_, Error>(match kind {
CurrentDependencyKind::Exists => {
DependencyRequirement::Exists { id, version_range }
}
CurrentDependencyKind::Running { health_checks } => {
DependencyRequirement::Running {
id,
health_checks,
version_range,
}
}
})
})
.try_collect()
}
#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CheckDependenciesParam {
#[ts(optional)]
package_ids: Option<Vec<PackageId>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CheckDependenciesResult {
package_id: PackageId,
is_installed: bool,
is_running: bool,
config_satisfied: bool,
health_checks: BTreeMap<HealthCheckId, HealthCheckResult>,
#[ts(type = "string | null")]
version: Option<exver::ExtendedVersion>,
}
pub async fn check_dependencies(
context: EffectContext,
CheckDependenciesParam { package_ids }: CheckDependenciesParam,
) -> Result<Vec<CheckDependenciesResult>, Error> {
let context = context.deref()?;
let db = context.seed.ctx.db.peek().await;
let current_dependencies = db
.as_public()
.as_package_data()
.as_idx(&context.seed.id)
.or_not_found(&context.seed.id)?
.as_current_dependencies()
.de()?;
let package_ids: Vec<_> = package_ids
.unwrap_or_else(|| current_dependencies.0.keys().cloned().collect())
.into_iter()
.filter_map(|x| {
let info = current_dependencies.0.get(&x)?;
Some((x, info))
})
.collect();
let mut results = Vec::with_capacity(package_ids.len());
for (package_id, dependency_info) in package_ids {
let Some(package) = db.as_public().as_package_data().as_idx(&package_id) else {
results.push(CheckDependenciesResult {
package_id,
is_installed: false,
is_running: false,
config_satisfied: false,
health_checks: Default::default(),
version: None,
});
continue;
};
let manifest = package.as_state_info().as_manifest(ManifestPreference::New);
let installed_version = manifest.as_version().de()?.into_version();
let satisfies = manifest.as_satisfies().de()?;
let version = Some(installed_version.clone());
if ![installed_version]
.into_iter()
.chain(satisfies.into_iter().map(|v| v.into_version()))
.any(|v| v.satisfies(&dependency_info.version_range))
{
results.push(CheckDependenciesResult {
package_id,
is_installed: false,
is_running: false,
config_satisfied: false,
health_checks: Default::default(),
version,
});
continue;
}
let is_installed = true;
let status = package.as_status().as_main().de()?;
let is_running = if is_installed {
status.running()
} else {
false
};
let health_checks =
if let CurrentDependencyKind::Running { health_checks } = &dependency_info.kind {
status
.health()
.cloned()
.unwrap_or_default()
.into_iter()
.filter(|(id, _)| health_checks.contains(id))
.collect()
} else {
Default::default()
};
results.push(CheckDependenciesResult {
package_id,
is_installed,
is_running,
config_satisfied: dependency_info.config_satisfied,
health_checks,
version,
});
}
Ok(results)
}

View File

@@ -0,0 +1,46 @@
use models::HealthCheckId;
use crate::service::effects::prelude::*;
use crate::status::health_check::HealthCheckResult;
use crate::status::MainStatus;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct SetHealth {
id: HealthCheckId,
#[serde(flatten)]
result: HealthCheckResult,
}
pub async fn set_health(
context: EffectContext,
SetHealth { id, result }: SetHealth,
) -> Result<(), Error> {
let context = context.deref()?;
let package_id = &context.seed.id;
context
.seed
.ctx
.db
.mutate(move |db| {
db.as_public_mut()
.as_package_data_mut()
.as_idx_mut(package_id)
.or_not_found(package_id)?
.as_status_mut()
.as_main_mut()
.mutate(|main| {
match main {
&mut MainStatus::Running { ref mut health, .. }
| &mut MainStatus::BackingUp { ref mut health, .. } => {
health.insert(id, result);
}
_ => (),
}
Ok(())
})
})
.await?;
Ok(())
}

View File

@@ -0,0 +1,163 @@
use std::ffi::OsString;
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use models::ImageId;
use rpc_toolkit::Context;
use tokio::process::Command;
use crate::disk::mount::filesystem::overlayfs::OverlayGuard;
use crate::rpc_continuations::Guid;
use crate::service::effects::prelude::*;
use crate::util::Invoke;
#[derive(Debug, Clone, Serialize, Deserialize, Parser)]
pub struct ChrootParams {
#[arg(short = 'e', long = "env")]
env: Option<PathBuf>,
#[arg(short = 'w', long = "workdir")]
workdir: Option<PathBuf>,
#[arg(short = 'u', long = "user")]
user: Option<String>,
path: PathBuf,
command: OsString,
args: Vec<OsString>,
}
pub fn chroot<C: Context>(
_: C,
ChrootParams {
env,
workdir,
user,
path,
command,
args,
}: ChrootParams,
) -> Result<(), Error> {
let mut cmd = std::process::Command::new(command);
if let Some(env) = env {
for (k, v) in std::fs::read_to_string(env)?
.lines()
.map(|l| l.trim())
.filter_map(|l| l.split_once("="))
{
cmd.env(k, v);
}
}
nix::unistd::setsid().ok(); // https://stackoverflow.com/questions/25701333/os-setsid-operation-not-permitted
std::os::unix::fs::chroot(path)?;
if let Some(uid) = user.as_deref().and_then(|u| u.parse::<u32>().ok()) {
cmd.uid(uid);
} else if let Some(user) = user {
let (uid, gid) = std::fs::read_to_string("/etc/passwd")?
.lines()
.find_map(|l| {
let mut split = l.trim().split(":");
if user != split.next()? {
return None;
}
split.next(); // throw away x
Some((split.next()?.parse().ok()?, split.next()?.parse().ok()?))
// uid gid
})
.or_not_found(lazy_format!("{user} in /etc/passwd"))?;
cmd.uid(uid);
cmd.gid(gid);
};
if let Some(workdir) = workdir {
cmd.current_dir(workdir);
}
cmd.args(args);
Err(cmd.exec().into())
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct DestroyOverlayedImageParams {
guid: Guid,
}
#[instrument(skip_all)]
pub async fn destroy_overlayed_image(
context: EffectContext,
DestroyOverlayedImageParams { guid }: DestroyOverlayedImageParams,
) -> Result<(), Error> {
let context = context.deref()?;
if context
.seed
.persistent_container
.overlays
.lock()
.await
.remove(&guid)
.is_none()
{
tracing::warn!("Could not find a guard to remove on the destroy overlayed image; assumming that it already is removed and will be skipping");
}
Ok(())
}
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CreateOverlayedImageParams {
image_id: ImageId,
}
#[instrument(skip_all)]
pub async fn create_overlayed_image(
context: EffectContext,
CreateOverlayedImageParams { image_id }: CreateOverlayedImageParams,
) -> Result<(PathBuf, Guid), Error> {
let context = context.deref()?;
if let Some(image) = context
.seed
.persistent_container
.images
.get(&image_id)
.cloned()
{
let guid = Guid::new();
let rootfs_dir = context
.seed
.persistent_container
.lxc_container
.get()
.ok_or_else(|| {
Error::new(
eyre!("PersistentContainer has been destroyed"),
ErrorKind::Incoherent,
)
})?
.rootfs_dir();
let mountpoint = rootfs_dir
.join("media/startos/overlays")
.join(guid.as_ref());
tokio::fs::create_dir_all(&mountpoint).await?;
let container_mountpoint = Path::new("/").join(
mountpoint
.strip_prefix(rootfs_dir)
.with_kind(ErrorKind::Incoherent)?,
);
tracing::info!("Mounting overlay {guid} for {image_id}");
let guard = OverlayGuard::mount(image, &mountpoint).await?;
Command::new("chown")
.arg("100000:100000")
.arg(&mountpoint)
.invoke(ErrorKind::Filesystem)
.await?;
tracing::info!("Mounted overlay {guid} for {image_id}");
context
.seed
.persistent_container
.overlays
.lock()
.await
.insert(guid.clone(), guard);
Ok((container_mountpoint, guid))
} else {
Err(Error::new(
eyre!("image {image_id} not found in s9pk"),
ErrorKind::NotFound,
))
}
}

View File

@@ -0,0 +1,174 @@
use rpc_toolkit::{from_fn, from_fn_async, Context, HandlerExt, ParentHandler};
use crate::echo;
use crate::prelude::*;
use crate::service::cli::ContainerCliContext;
use crate::service::effects::context::EffectContext;
mod action;
pub mod callbacks;
mod config;
pub mod context;
mod control;
mod dependency;
mod health;
mod image;
mod net;
mod prelude;
mod store;
mod system;
pub fn handler<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand("gitInfo", from_fn(|_: C| crate::version::git_info()))
.subcommand(
"echo",
from_fn(echo::<EffectContext>).with_call_remote::<ContainerCliContext>(),
)
// action
.subcommand(
"executeAction",
from_fn_async(action::execute_action).no_cli(),
)
.subcommand(
"exportAction",
from_fn_async(action::export_action).no_cli(),
)
.subcommand(
"clearActions",
from_fn_async(action::clear_actions).no_cli(),
)
// callbacks
.subcommand(
"clearCallbacks",
from_fn(callbacks::clear_callbacks).no_cli(),
)
// config
.subcommand(
"getConfigured",
from_fn_async(config::get_configured).no_cli(),
)
.subcommand(
"setConfigured",
from_fn_async(config::set_configured)
.no_display()
.with_call_remote::<ContainerCliContext>(),
)
// control
.subcommand(
"restart",
from_fn_async(control::restart)
.no_display()
.with_call_remote::<ContainerCliContext>(),
)
.subcommand(
"shutdown",
from_fn_async(control::shutdown)
.no_display()
.with_call_remote::<ContainerCliContext>(),
)
.subcommand(
"setMainStatus",
from_fn_async(control::set_main_status)
.no_display()
.with_call_remote::<ContainerCliContext>(),
)
// dependency
.subcommand(
"setDependencies",
from_fn_async(dependency::set_dependencies)
.no_display()
.with_call_remote::<ContainerCliContext>(),
)
.subcommand(
"getDependencies",
from_fn_async(dependency::get_dependencies)
.no_display()
.with_call_remote::<ContainerCliContext>(),
)
.subcommand(
"checkDependencies",
from_fn_async(dependency::check_dependencies)
.no_display()
.with_call_remote::<ContainerCliContext>(),
)
.subcommand("mount", from_fn_async(dependency::mount).no_cli())
.subcommand(
"getInstalledPackages",
from_fn_async(dependency::get_installed_packages).no_cli(),
)
.subcommand(
"exposeForDependents",
from_fn_async(dependency::expose_for_dependents).no_cli(),
)
// health
.subcommand("setHealth", from_fn_async(health::set_health).no_cli())
// image
.subcommand(
"chroot",
from_fn(image::chroot::<ContainerCliContext>).no_display(),
)
.subcommand(
"createOverlayedImage",
from_fn_async(image::create_overlayed_image)
.with_custom_display_fn(|_, (path, _)| Ok(println!("{}", path.display())))
.with_call_remote::<ContainerCliContext>(),
)
.subcommand(
"destroyOverlayedImage",
from_fn_async(image::destroy_overlayed_image).no_cli(),
)
// net
.subcommand("bind", from_fn_async(net::bind::bind).no_cli())
.subcommand(
"getServicePortForward",
from_fn_async(net::bind::get_service_port_forward).no_cli(),
)
.subcommand(
"clearBindings",
from_fn_async(net::bind::clear_bindings).no_cli(),
)
.subcommand(
"getHostInfo",
from_fn_async(net::host::get_host_info).no_cli(),
)
.subcommand(
"getPrimaryUrl",
from_fn_async(net::host::get_primary_url).no_cli(),
)
.subcommand(
"getContainerIp",
from_fn_async(net::info::get_container_ip).no_cli(),
)
.subcommand(
"exportServiceInterface",
from_fn_async(net::interface::export_service_interface).no_cli(),
)
.subcommand(
"getServiceInterface",
from_fn_async(net::interface::get_service_interface).no_cli(),
)
.subcommand(
"listServiceInterfaces",
from_fn_async(net::interface::list_service_interfaces).no_cli(),
)
.subcommand(
"clearServiceInterfaces",
from_fn_async(net::interface::clear_service_interfaces).no_cli(),
)
.subcommand(
"getSslCertificate",
from_fn_async(net::ssl::get_ssl_certificate).no_cli(),
)
.subcommand("getSslKey", from_fn_async(net::ssl::get_ssl_key).no_cli())
// store
.subcommand("getStore", from_fn_async(store::get_store).no_cli())
.subcommand("setStore", from_fn_async(store::set_store).no_cli())
// system
.subcommand(
"getSystemSmtp",
from_fn_async(system::get_system_smtp).no_cli(),
)
// TODO Callbacks
}

View File

@@ -0,0 +1,56 @@
use models::{HostId, PackageId};
use crate::net::host::binding::{BindOptions, LanInfo};
use crate::net::host::HostKind;
use crate::service::effects::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct BindParams {
kind: HostKind,
id: HostId,
internal_port: u16,
#[serde(flatten)]
options: BindOptions,
}
pub async fn bind(
context: EffectContext,
BindParams {
kind,
id,
internal_port,
options,
}: BindParams,
) -> Result<(), Error> {
let context = context.deref()?;
let mut svc = context.seed.persistent_container.net_service.lock().await;
svc.bind(kind, id, internal_port, options).await
}
pub async fn clear_bindings(context: EffectContext) -> Result<(), Error> {
let context = context.deref()?;
let mut svc = context.seed.persistent_container.net_service.lock().await;
svc.clear_bindings().await?;
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct GetServicePortForwardParams {
#[ts(optional)]
package_id: Option<PackageId>,
host_id: HostId,
internal_port: u32,
}
pub async fn get_service_port_forward(
context: EffectContext,
data: GetServicePortForwardParams,
) -> Result<LanInfo, Error> {
let internal_port = data.internal_port as u16;
let context = context.deref()?;
let net_service = context.seed.persistent_container.net_service.lock().await;
net_service.get_lan_port(data.host_id, internal_port)
}

View File

@@ -0,0 +1,73 @@
use models::{HostId, PackageId};
use crate::net::host::address::HostAddress;
use crate::net::host::Host;
use crate::service::effects::callbacks::CallbackHandler;
use crate::service::effects::prelude::*;
use crate::service::rpc::CallbackId;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct GetPrimaryUrlParams {
#[ts(optional)]
package_id: Option<PackageId>,
host_id: HostId,
#[ts(optional)]
callback: Option<CallbackId>,
}
pub async fn get_primary_url(
context: EffectContext,
GetPrimaryUrlParams {
package_id,
host_id,
callback,
}: GetPrimaryUrlParams,
) -> Result<Option<HostAddress>, Error> {
let context = context.deref()?;
let package_id = package_id.unwrap_or_else(|| context.seed.id.clone());
Ok(None) // TODO
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct GetHostInfoParams {
host_id: HostId,
#[ts(optional)]
package_id: Option<PackageId>,
#[ts(optional)]
callback: Option<CallbackId>,
}
pub async fn get_host_info(
context: EffectContext,
GetHostInfoParams {
host_id,
package_id,
callback,
}: GetHostInfoParams,
) -> Result<Option<Host>, Error> {
let context = context.deref()?;
let db = context.seed.ctx.db.peek().await;
let package_id = package_id.unwrap_or_else(|| context.seed.id.clone());
let res = db
.as_public()
.as_package_data()
.as_idx(&package_id)
.and_then(|m| m.as_hosts().as_idx(&host_id))
.map(|m| m.de())
.transpose()?;
if let Some(callback) = callback {
let callback = callback.register(&context.seed.persistent_container);
context.seed.ctx.callbacks.add_get_host_info(
package_id,
host_id,
CallbackHandler::new(&context, callback),
);
}
Ok(res)
}

View File

@@ -0,0 +1,9 @@
use std::net::Ipv4Addr;
use crate::service::effects::prelude::*;
pub async fn get_container_ip(context: EffectContext) -> Result<Ipv4Addr, Error> {
let context = context.deref()?;
let net_service = context.seed.persistent_container.net_service.lock().await;
Ok(net_service.get_ip())
}

View File

@@ -0,0 +1,188 @@
use std::collections::BTreeMap;
use imbl::vector;
use models::{PackageId, ServiceInterfaceId};
use crate::net::service_interface::{AddressInfo, ServiceInterface, ServiceInterfaceType};
use crate::service::effects::callbacks::CallbackHandler;
use crate::service::effects::prelude::*;
use crate::service::rpc::CallbackId;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ExportServiceInterfaceParams {
id: ServiceInterfaceId,
name: String,
description: String,
has_primary: bool,
disabled: bool,
masked: bool,
address_info: AddressInfo,
r#type: ServiceInterfaceType,
}
pub async fn export_service_interface(
context: EffectContext,
ExportServiceInterfaceParams {
id,
name,
description,
has_primary,
disabled,
masked,
address_info,
r#type,
}: ExportServiceInterfaceParams,
) -> Result<(), Error> {
let context = context.deref()?;
let package_id = context.seed.id.clone();
let service_interface = ServiceInterface {
id: id.clone(),
name,
description,
has_primary,
disabled,
masked,
address_info,
interface_type: r#type,
};
context
.seed
.ctx
.db
.mutate(|db| {
db.as_public_mut()
.as_package_data_mut()
.as_idx_mut(&package_id)
.or_not_found(&package_id)?
.as_service_interfaces_mut()
.insert(&id, &service_interface)?;
Ok(())
})
.await?;
if let Some(callbacks) = context
.seed
.ctx
.callbacks
.get_service_interface(&(package_id.clone(), id))
{
callbacks.call(vector![]).await?;
}
if let Some(callbacks) = context
.seed
.ctx
.callbacks
.list_service_interfaces(&package_id)
{
callbacks.call(vector![]).await?;
}
Ok(())
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct GetServiceInterfaceParams {
#[ts(optional)]
package_id: Option<PackageId>,
service_interface_id: ServiceInterfaceId,
#[ts(optional)]
callback: Option<CallbackId>,
}
pub async fn get_service_interface(
context: EffectContext,
GetServiceInterfaceParams {
package_id,
service_interface_id,
callback,
}: GetServiceInterfaceParams,
) -> Result<Option<ServiceInterface>, Error> {
let context = context.deref()?;
let package_id = package_id.unwrap_or_else(|| context.seed.id.clone());
let db = context.seed.ctx.db.peek().await;
let interface = db
.as_public()
.as_package_data()
.as_idx(&package_id)
.and_then(|m| m.as_service_interfaces().as_idx(&service_interface_id))
.map(|m| m.de())
.transpose()?;
if let Some(callback) = callback {
let callback = callback.register(&context.seed.persistent_container);
context.seed.ctx.callbacks.add_get_service_interface(
package_id,
service_interface_id,
CallbackHandler::new(&context, callback),
);
}
Ok(interface)
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ListServiceInterfacesParams {
#[ts(optional)]
package_id: Option<PackageId>,
#[ts(optional)]
callback: Option<CallbackId>,
}
pub async fn list_service_interfaces(
context: EffectContext,
ListServiceInterfacesParams {
package_id,
callback,
}: ListServiceInterfacesParams,
) -> Result<BTreeMap<ServiceInterfaceId, ServiceInterface>, Error> {
let context = context.deref()?;
let package_id = package_id.unwrap_or_else(|| context.seed.id.clone());
let res = context
.seed
.ctx
.db
.peek()
.await
.into_public()
.into_package_data()
.into_idx(&package_id)
.map(|m| m.into_service_interfaces().de())
.transpose()?
.unwrap_or_default();
if let Some(callback) = callback {
let callback = callback.register(&context.seed.persistent_container);
context
.seed
.ctx
.callbacks
.add_list_service_interfaces(package_id, CallbackHandler::new(&context, callback));
}
Ok(res)
}
pub async fn clear_service_interfaces(context: EffectContext) -> Result<(), Error> {
let context = context.deref()?;
let package_id = context.seed.id.clone();
context
.seed
.ctx
.db
.mutate(|db| {
db.as_public_mut()
.as_package_data_mut()
.as_idx_mut(&package_id)
.or_not_found(&package_id)?
.as_service_interfaces_mut()
.ser(&Default::default())
})
.await
}

View File

@@ -0,0 +1,5 @@
pub mod bind;
pub mod host;
pub mod info;
pub mod interface;
pub mod ssl;

View File

@@ -0,0 +1,169 @@
use std::collections::BTreeSet;
use imbl_value::InternedString;
use itertools::Itertools;
use openssl::pkey::{PKey, Private};
use crate::service::effects::callbacks::CallbackHandler;
use crate::service::effects::prelude::*;
use crate::service::rpc::CallbackId;
use crate::util::serde::Pem;
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, TS, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub enum Algorithm {
Ecdsa,
Ed25519,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct GetSslCertificateParams {
#[ts(type = "string[]")]
hostnames: BTreeSet<InternedString>,
#[ts(optional)]
algorithm: Option<Algorithm>, //"ecdsa" | "ed25519"
#[ts(optional)]
callback: Option<CallbackId>,
}
pub async fn get_ssl_certificate(
ctx: EffectContext,
GetSslCertificateParams {
hostnames,
algorithm,
callback,
}: GetSslCertificateParams,
) -> Result<Vec<String>, Error> {
let context = ctx.deref()?;
let algorithm = algorithm.unwrap_or(Algorithm::Ecdsa);
let cert = context
.seed
.ctx
.db
.mutate(|db| {
let errfn = |h: &str| Error::new(eyre!("unknown hostname: {h}"), ErrorKind::NotFound);
let entries = db.as_public().as_package_data().as_entries()?;
let packages = entries.iter().map(|(k, _)| k).collect::<BTreeSet<_>>();
let allowed_hostnames = entries
.iter()
.map(|(_, m)| m.as_hosts().as_entries())
.flatten_ok()
.map_ok(|(_, m)| m.as_addresses().de())
.map(|a| a.and_then(|a| a))
.flatten_ok()
.map_ok(|a| InternedString::from_display(&a))
.try_collect::<_, BTreeSet<_>, _>()?;
for hostname in &hostnames {
if let Some(internal) = hostname
.strip_suffix(".embassy")
.or_else(|| hostname.strip_suffix(".startos"))
{
if !packages.contains(internal) {
return Err(errfn(&*hostname));
}
} else {
if !allowed_hostnames.contains(hostname) {
return Err(errfn(&*hostname));
}
}
}
db.as_private_mut()
.as_key_store_mut()
.as_local_certs_mut()
.cert_for(&hostnames)
})
.await?;
let fullchain = match algorithm {
Algorithm::Ecdsa => cert.fullchain_nistp256(),
Algorithm::Ed25519 => cert.fullchain_ed25519(),
};
let res = fullchain
.into_iter()
.map(|c| c.to_pem())
.map_ok(String::from_utf8)
.map(|a| Ok::<_, Error>(a??))
.try_collect()?;
if let Some(callback) = callback {
let callback = callback.register(&context.seed.persistent_container);
context.seed.ctx.callbacks.add_get_ssl_certificate(
ctx,
hostnames,
cert,
algorithm,
CallbackHandler::new(&context, callback),
);
}
Ok(res)
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct GetSslKeyParams {
#[ts(type = "string[]")]
hostnames: BTreeSet<InternedString>,
#[ts(optional)]
algorithm: Option<Algorithm>, //"ecdsa" | "ed25519"
}
pub async fn get_ssl_key(
context: EffectContext,
GetSslKeyParams {
hostnames,
algorithm,
}: GetSslKeyParams,
) -> Result<Pem<PKey<Private>>, Error> {
let context = context.deref()?;
let package_id = &context.seed.id;
let algorithm = algorithm.unwrap_or(Algorithm::Ecdsa);
let cert = context
.seed
.ctx
.db
.mutate(|db| {
let errfn = |h: &str| Error::new(eyre!("unknown hostname: {h}"), ErrorKind::NotFound);
let allowed_hostnames = db
.as_public()
.as_package_data()
.as_idx(package_id)
.into_iter()
.map(|m| m.as_hosts().as_entries())
.flatten_ok()
.map_ok(|(_, m)| m.as_addresses().de())
.map(|a| a.and_then(|a| a))
.flatten_ok()
.map_ok(|a| InternedString::from_display(&a))
.try_collect::<_, BTreeSet<_>, _>()?;
for hostname in &hostnames {
if let Some(internal) = hostname
.strip_suffix(".embassy")
.or_else(|| hostname.strip_suffix(".startos"))
{
if internal != &**package_id {
return Err(errfn(&*hostname));
}
} else {
if !allowed_hostnames.contains(hostname) {
return Err(errfn(&*hostname));
}
}
}
db.as_private_mut()
.as_key_store_mut()
.as_local_certs_mut()
.cert_for(&hostnames)
})
.await?;
let key = match algorithm {
Algorithm::Ecdsa => cert.leaf.keys.nistp256,
Algorithm::Ed25519 => cert.leaf.keys.ed25519,
};
Ok(Pem(key))
}

View File

@@ -0,0 +1,16 @@
pub use clap::Parser;
pub use serde::{Deserialize, Serialize};
pub use ts_rs::TS;
pub use crate::prelude::*;
use crate::rpc_continuations::Guid;
pub(super) use crate::service::effects::context::EffectContext;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct ProcedureId {
#[serde(default)]
#[arg(default_value_t, long)]
pub procedure_id: Guid,
}

View File

@@ -0,0 +1,93 @@
use imbl::vector;
use imbl_value::json;
use models::PackageId;
use patch_db::json_ptr::JsonPointer;
use crate::service::effects::callbacks::CallbackHandler;
use crate::service::effects::prelude::*;
use crate::service::rpc::CallbackId;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct GetStoreParams {
#[ts(optional)]
package_id: Option<PackageId>,
#[ts(type = "string")]
path: JsonPointer,
#[ts(optional)]
callback: Option<CallbackId>,
}
pub async fn get_store(
context: EffectContext,
GetStoreParams {
package_id,
path,
callback,
}: GetStoreParams,
) -> Result<Value, Error> {
let context = context.deref()?;
let peeked = context.seed.ctx.db.peek().await;
let package_id = package_id.unwrap_or(context.seed.id.clone());
let value = peeked
.as_private()
.as_package_stores()
.as_idx(&package_id)
.or_not_found(&package_id)?
.de()?;
if let Some(callback) = callback {
let callback = callback.register(&context.seed.persistent_container);
context.seed.ctx.callbacks.add_get_store(
package_id,
path.clone(),
CallbackHandler::new(&context, callback),
);
}
Ok(path
.get(&value)
.ok_or_else(|| Error::new(eyre!("Did not find value at path"), ErrorKind::NotFound))?
.clone())
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct SetStoreParams {
#[ts(type = "any")]
value: Value,
#[ts(type = "string")]
path: JsonPointer,
}
pub async fn set_store(
context: EffectContext,
SetStoreParams { value, path }: SetStoreParams,
) -> Result<(), Error> {
let context = context.deref()?;
let package_id = &context.seed.id;
context
.seed
.ctx
.db
.mutate(|db| {
let model = db
.as_private_mut()
.as_package_stores_mut()
.upsert(package_id, || Ok(json!({})))?;
let mut model_value = model.de()?;
if model_value.is_null() {
model_value = json!({});
}
path.set(&mut model_value, value, true)
.with_kind(ErrorKind::ParseDbField)?;
model.ser(&model_value)
})
.await?;
if let Some(callbacks) = context.seed.ctx.callbacks.get_store(package_id, &path) {
callbacks.call(vector![]).await?;
}
Ok(())
}

View File

@@ -0,0 +1,39 @@
use crate::service::effects::callbacks::CallbackHandler;
use crate::service::effects::prelude::*;
use crate::service::rpc::CallbackId;
use crate::system::SmtpValue;
#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct GetSystemSmtpParams {
#[arg(skip)]
callback: Option<CallbackId>,
}
pub async fn get_system_smtp(
context: EffectContext,
GetSystemSmtpParams { callback }: GetSystemSmtpParams,
) -> Result<Option<SmtpValue>, Error> {
let context = context.deref()?;
let res = context
.seed
.ctx
.db
.peek()
.await
.into_public()
.into_server_info()
.into_smtp()
.de()?;
if let Some(callback) = callback {
let callback = callback.register(&context.seed.persistent_container);
context
.seed
.ctx
.callbacks
.add_get_system_smtp(CallbackHandler::new(&context, callback));
}
Ok(res)
}

View File

@@ -27,10 +27,7 @@ use crate::progress::{NamedProgress, Progress};
use crate::rpc_continuations::Guid;
use crate::s9pk::S9pk;
use crate::service::service_map::InstallProgressHandles;
use crate::service::transition::TransitionKind;
use crate::status::health_check::HealthCheckResult;
use crate::status::MainStatus;
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::actor::concurrent::ConcurrentActor;
use crate::util::actor::Actor;
use crate::util::io::create_file;
@@ -43,11 +40,11 @@ pub mod cli;
mod config;
mod control;
mod dependencies;
pub mod effects;
pub mod persistent_container;
mod properties;
mod rpc;
mod service_actor;
pub mod service_effect_handler;
pub mod service_map;
mod start_stop;
mod transition;

View File

@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use std::sync::{Arc, Weak};
use std::time::Duration;
@@ -6,6 +6,7 @@ use std::time::Duration;
use futures::future::ready;
use futures::{Future, FutureExt};
use helpers::NonDetachingJoinHandle;
use imbl::Vector;
use models::{ImageId, ProcedureName, VolumeId};
use rpc_toolkit::{Empty, Server, ShutdownHandle};
use serde::de::DeserializeOwned;
@@ -13,8 +14,6 @@ use tokio::process::Command;
use tokio::sync::{oneshot, watch, Mutex, OnceCell};
use tracing::instrument;
use super::service_effect_handler::{service_effect_handler, EffectContext};
use super::transition::{TransitionKind, TransitionState};
use crate::context::RpcContext;
use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::idmapped::IdMapped;
@@ -28,7 +27,11 @@ use crate::prelude::*;
use crate::rpc_continuations::Guid;
use crate::s9pk::merkle_archive::source::FileSource;
use crate::s9pk::S9pk;
use crate::service::effects::context::EffectContext;
use crate::service::effects::handler;
use crate::service::rpc::{CallbackHandle, CallbackId, CallbackParams};
use crate::service::start_stop::StartStop;
use crate::service::transition::{TransitionKind, TransitionState};
use crate::service::{rpc, RunningStatus, Service};
use crate::util::io::create_file;
use crate::util::rpc_client::UnixRpcClient;
@@ -42,6 +45,8 @@ const RPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
pub struct ServiceState {
// This contains the start time and health check information for when the service is running. Note: Will be overwritting to the db,
pub(super) running_status: Option<RunningStatus>,
// This tracks references to callbacks registered by the running service:
pub(super) callbacks: BTreeSet<Arc<CallbackId>>,
/// Setting this value causes the service actor to try to bring the service to the specified state. This is done in the background job created in ServiceActor::init
pub(super) desired_state: StartStop,
/// Override the current desired state for the service during a transition (this is protected by a guard that sets this value to null on drop)
@@ -61,6 +66,7 @@ impl ServiceState {
pub fn new(desired_state: StartStop) -> Self {
Self {
running_status: Default::default(),
callbacks: Default::default(),
temp_desired_state: Default::default(),
transition_state: Default::default(),
desired_state,
@@ -308,10 +314,7 @@ impl PersistentContainer {
#[instrument(skip_all)]
pub async fn init(&self, seed: Weak<Service>) -> Result<(), Error> {
let socket_server_context = EffectContext::new(seed);
let server = Server::new(
move || ready(Ok(socket_server_context.clone())),
service_effect_handler(),
);
let server = Server::new(move || ready(Ok(socket_server_context.clone())), handler());
let path = self
.lxc_container
.get()
@@ -430,21 +433,13 @@ impl PersistentContainer {
#[instrument(skip_all)]
pub async fn start(&self) -> Result<(), Error> {
self.execute(
Guid::new(),
ProcedureName::StartMain,
Value::Null,
Some(Duration::from_secs(5)), // TODO
)
.await?;
self.rpc_client.request(rpc::Start, Empty {}).await?;
Ok(())
}
#[instrument(skip_all)]
pub async fn stop(&self) -> Result<(), Error> {
let timeout: Option<crate::util::serde::Duration> = self
.execute(Guid::new(), ProcedureName::StopMain, Value::Null, None)
.await?;
self.rpc_client.request(rpc::Stop, Empty {}).await?;
Ok(())
}
@@ -480,6 +475,19 @@ impl PersistentContainer {
.and_then(from_value)
}
#[instrument(skip_all)]
pub async fn callback(&self, handle: CallbackHandle, args: Vector<Value>) -> Result<(), Error> {
let mut params = None;
self.state.send_if_modified(|s| {
params = handle.params(&mut s.callbacks, args);
params.is_some()
});
if let Some(params) = params {
self._callback(params).await?;
}
Ok(())
}
#[instrument(skip_all)]
async fn _execute(
&self,
@@ -523,6 +531,12 @@ impl PersistentContainer {
fut.await?
})
}
#[instrument(skip_all)]
async fn _callback(&self, params: CallbackParams) -> Result<(), Error> {
self.rpc_client.notify(rpc::Callback, params).await?;
Ok(())
}
}
impl Drop for PersistentContainer {

View File

@@ -1,5 +1,8 @@
use std::collections::BTreeSet;
use std::sync::{Arc, Weak};
use std::time::Duration;
use imbl::Vector;
use imbl_value::Value;
use models::ProcedureName;
use rpc_toolkit::yajrc::RpcMethod;
@@ -8,6 +11,8 @@ use ts_rs::TS;
use crate::prelude::*;
use crate::rpc_continuations::Guid;
use crate::service::persistent_container::PersistentContainer;
use crate::util::Never;
#[derive(Clone)]
pub struct Init;
@@ -27,6 +32,42 @@ impl serde::Serialize for Init {
}
}
#[derive(Clone)]
pub struct Start;
impl RpcMethod for Start {
type Params = Empty;
type Response = ();
fn as_str<'a>(&'a self) -> &'a str {
"start"
}
}
impl serde::Serialize for Start {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
#[derive(Clone)]
pub struct Stop;
impl RpcMethod for Stop {
type Params = Empty;
type Response = ();
fn as_str<'a>(&'a self) -> &'a str {
"stop"
}
}
impl serde::Serialize for Stop {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
#[derive(Clone)]
pub struct Exit;
impl RpcMethod for Exit {
@@ -104,3 +145,74 @@ impl serde::Serialize for Sandbox {
serializer.serialize_str(self.as_str())
}
}
#[derive(
Clone, Copy, Debug, serde::Deserialize, serde::Serialize, TS, PartialEq, Eq, PartialOrd, Ord,
)]
#[ts(type = "number")]
pub struct CallbackId(u64);
impl CallbackId {
pub fn register(self, container: &PersistentContainer) -> CallbackHandle {
let this = Arc::new(self);
let res = Arc::downgrade(&this);
container
.state
.send_if_modified(|s| s.callbacks.insert(this));
CallbackHandle(res)
}
}
pub struct CallbackHandle(Weak<CallbackId>);
impl CallbackHandle {
pub fn is_active(&self) -> bool {
self.0.strong_count() > 0
}
pub fn params(
self,
registered: &mut BTreeSet<Arc<CallbackId>>,
args: Vector<Value>,
) -> Option<CallbackParams> {
if let Some(id) = self.0.upgrade() {
if let Some(strong) = registered.get(&id) {
if Arc::ptr_eq(strong, &id) {
registered.remove(&id);
return Some(CallbackParams::new(&*id, args));
}
}
}
None
}
pub fn take(&mut self) -> Self {
Self(std::mem::take(&mut self.0))
}
}
#[derive(Clone, serde::Deserialize, serde::Serialize, TS)]
pub struct CallbackParams {
id: u64,
#[ts(type = "any[]")]
args: Vector<Value>,
}
impl CallbackParams {
fn new(id: &CallbackId, args: Vector<Value>) -> Self {
Self { id: id.0, args }
}
}
#[derive(Clone)]
pub struct Callback;
impl RpcMethod for Callback {
type Params = CallbackParams;
type Response = Never;
fn as_str<'a>(&'a self) -> &'a str {
"callback"
}
}
impl serde::Serialize for Callback {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ use chrono::Utc;
use clap::Parser;
use color_eyre::eyre::eyre;
use futures::FutureExt;
use imbl::vector;
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tokio::process::Command;
@@ -824,6 +825,51 @@ async fn get_disk_info() -> Result<MetricsDisk, Error> {
})
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct SmtpValue {
#[arg(long)]
pub server: String,
#[arg(long)]
pub port: u16,
#[arg(long)]
pub from: String,
#[arg(long)]
pub login: String,
#[arg(long)]
pub password: Option<String>,
}
pub async fn set_system_smtp(ctx: RpcContext, smtp: SmtpValue) -> Result<(), Error> {
let smtp = Some(smtp);
ctx.db
.mutate(|db| {
db.as_public_mut()
.as_server_info_mut()
.as_smtp_mut()
.ser(&smtp)
})
.await?;
if let Some(callbacks) = ctx.callbacks.get_system_smtp() {
callbacks.call(vector![to_value(&smtp)?]).await?;
}
Ok(())
}
pub async fn clear_system_smtp(ctx: RpcContext) -> Result<(), Error> {
ctx.db
.mutate(|db| {
db.as_public_mut()
.as_server_info_mut()
.as_smtp_mut()
.ser(&None)
})
.await?;
if let Some(callbacks) = ctx.callbacks.get_system_smtp() {
callbacks.call(vector![Value::Null]).await?;
}
Ok(())
}
#[tokio::test]
#[ignore]
pub async fn test_get_temp() {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
pub mod eq_map;
pub use eq_map::EqMap;

View File

@@ -34,8 +34,10 @@ use crate::shutdown::Shutdown;
use crate::util::io::create_file;
use crate::util::serde::{deserialize_from_str, serialize_display};
use crate::{Error, ErrorKind, ResultExt as _};
pub mod actor;
pub mod clap;
pub mod collections;
pub mod cpupower;
pub mod crypto;
pub mod future;

View File

@@ -138,6 +138,31 @@ impl RpcClient {
err.data = Some(json!("RpcClient thread has terminated"));
Err(err)
}
pub async fn notify<T: RpcMethod>(
&mut self,
method: T,
params: T::Params,
) -> Result<(), RpcError>
where
T: Serialize,
T::Params: Serialize,
{
let request = RpcRequest {
id: None,
method,
params,
};
self.writer
.write_all((dbg!(serde_json::to_string(&request))? + "\n").as_bytes())
.await
.map_err(|e| {
let mut err = rpc_toolkit::yajrc::INTERNAL_ERROR.clone();
err.data = Some(json!(e.to_string()));
err
})?;
Ok(())
}
}
#[derive(Clone)]
@@ -224,4 +249,36 @@ impl UnixRpcClient {
};
res
}
pub async fn notify<T: RpcMethod>(&self, method: T, params: T::Params) -> Result<(), RpcError>
where
T: Serialize + Clone,
T::Params: Serialize + Clone,
{
let mut tries = 0;
let res = loop {
let mut client = self.pool.clone().get().await?;
if client.handler.is_finished() {
client.destroy();
continue;
}
let res = client.notify(method.clone(), params.clone()).await;
match &res {
Err(e) if e.code == rpc_toolkit::yajrc::INTERNAL_ERROR.code => {
let mut e = Error::from(e.clone());
e.kind = ErrorKind::Filesystem;
tracing::error!("{e}");
tracing::debug!("{e:?}");
client.destroy();
}
_ => break res,
}
tries += 1;
if tries > MAX_TRIES {
tracing::warn!("Max Tries exceeded");
break res;
}
};
res
}
}