Compare commits

..

11 Commits

Author SHA1 Message Date
Aiden McClelland
5aa9c045e1 fix live-build resolv.conf (#3035)
* fix live-build resolv.conf

* improved debuggability
2025-09-24 22:44:25 -06:00
Matt Hill
6f1900f3bb limit adding gateway to StartTunnel, better copy around Tor SSL (#3033)
* limit adding gateway to StartTunnel, better copy around Tor SSL

* properly differentiate ssl

* exclude disconnected gateways

* better error handling

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2025-09-24 13:22:26 -06:00
Aiden McClelland
bc62de795e bugfixes for alpha.10 (#3032)
* bugfixes for alpha.10

* bump raspi kernel

* rpi kernel bump

* alpha.11
2025-09-23 22:42:17 +00:00
Alex Inkin
c62ca4b183 fix: make long dropdown options wrap (#3031) 2025-09-21 06:06:33 -06:00
Alex Inkin
876e5bc683 fix: fix overflowing interface table (#3027) 2025-09-20 07:10:58 -06:00
Alex Inkin
b99f3b73cd fix: make logs page take up all space (#3030) 2025-09-20 07:10:28 -06:00
Matt Hill
7eecf29449 fix dep error display, show starting if any health check starting, show disabled health check message, remove loader from service list, animated dots, better color (#3025)
* refector addresses to not need gateways array

* fix dep error display, show starting if any health check starting, show disabled health check message, remove loader from service list, animated dots, better color

* fix: fix action results textfields

---------

Co-authored-by: waterplea <alexander@inkin.ru>
2025-09-17 10:32:20 -06:00
Mariusz Kogen
1d331d7810 Fix file permissions for developer key and auth cookie (#3024)
* fix permissions

* include read for group
2025-09-16 09:09:33 -06:00
Aiden McClelland
68414678d8 sdk updates; beta.39 (#3022)
* sdk updates; beta.39

* beta.40
2025-09-11 15:47:48 -06:00
Aiden McClelland
2f6b9dac26 Bugfix/dns recursion (#3023)
* fix dns recursion and localhost

* additional fix
2025-09-11 15:47:38 -06:00
Aiden McClelland
d1812d875b fix dns recursion and localhost (#3021) 2025-09-11 12:35:12 -06:00
62 changed files with 6396 additions and 3769 deletions

View File

@@ -30,8 +30,10 @@ ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE
echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; \
fi) \
$(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then \
echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; \
echo cargo-deps/$(ARCH)-unknown-linux-musl/release/flamegraph; \
fi') \
$(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)console($$|-) ]]; then \
echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; \
fi')
REBUILD_TYPES = 1
@@ -139,9 +141,11 @@ install: $(ALL_TARGETS)
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-cli)
if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-musl/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then \
$(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); \
$(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/flamegraph,$(DESTDIR)/usr/bin/flamegraph); \
fi
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)console($$|-) ]]'; then \
$(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); \
fi
$(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs,$(DESTDIR)/usr/bin/startos-backup-fs)
$(call ln,/usr/bin/startos-backup-fs,$(DESTDIR)/usr/sbin/mount.backup-fs)

View File

@@ -38,7 +38,7 @@
},
"../sdk/dist": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.38",
"version": "0.4.0-beta.41",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",

34
core/Cargo.lock generated
View File

@@ -881,6 +881,17 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "backtrace-on-stack-overflow"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd2d70527f3737a1ad17355e260706c1badebabd1fa06a7a053407380df841b"
dependencies = [
"backtrace",
"libc",
"nix 0.23.2",
]
[[package]]
name = "barrage"
version = "0.2.3"
@@ -4632,6 +4643,7 @@ dependencies = [
"tokio",
"tracing",
"ts-rs",
"typeid",
"yasi",
"zbus",
]
@@ -4678,6 +4690,19 @@ dependencies = [
"smallvec",
]
[[package]]
name = "nix"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
dependencies = [
"bitflags 1.3.2",
"cc",
"cfg-if",
"libc",
"memoffset 0.6.5",
]
[[package]]
name = "nix"
version = "0.24.3"
@@ -7265,7 +7290,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "start-os"
version = "0.4.0-alpha.10"
version = "0.4.0-alpha.11"
dependencies = [
"aes 0.7.5",
"arti-client",
@@ -7275,6 +7300,7 @@ dependencies = [
"async-trait",
"axum 0.8.4",
"backhand",
"backtrace-on-stack-overflow",
"barrage",
"base32",
"base64 0.22.1",
@@ -9249,6 +9275,12 @@ dependencies = [
"serde",
]
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
version = "1.18.0"

View File

@@ -46,7 +46,7 @@ if [ -n "$FEATURES" ]; then
fi
RUSTFLAGS=""
if [[ "${ENVIRONMENT:-}" =~ (^|-)unstable($|-) ]]; then
if [[ "${ENVIRONMENT:-}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi

View File

@@ -22,7 +22,7 @@ cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi

View File

@@ -22,7 +22,7 @@ cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi

View File

@@ -27,7 +27,7 @@ cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi

View File

@@ -22,7 +22,7 @@ cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi

View File

@@ -22,7 +22,7 @@ cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi

View File

@@ -35,5 +35,6 @@ ts-rs = "9"
thiserror = "2.0"
tokio = { version = "1", features = ["full"] }
tracing = "0.1.39"
typeid = "1"
yasi = { version = "0.1.6", features = ["serde", "ts-rs"] }
zbus = "5"

View File

@@ -188,6 +188,7 @@ impl Display for ErrorKind {
#[derive(Debug)]
pub struct Error {
pub source: color_eyre::eyre::Error,
pub debug: Option<color_eyre::eyre::Error>,
pub kind: ErrorKind,
pub revision: Option<Revision>,
pub task: Option<JoinHandle<()>>,
@@ -199,9 +200,15 @@ impl Display for Error {
}
}
impl Error {
pub fn new<E: Into<color_eyre::eyre::Error>>(source: E, kind: ErrorKind) -> Self {
pub fn new<E: Into<color_eyre::eyre::Error> + std::fmt::Debug + 'static>(
source: E,
kind: ErrorKind,
) -> Self {
let debug = (typeid::of::<E>() == typeid::of::<color_eyre::eyre::Error>())
.then(|| eyre!("{source:?}"));
Error {
source: source.into(),
debug,
kind,
revision: None,
task: None,
@@ -209,11 +216,8 @@ impl Error {
}
pub fn clone_output(&self) -> Self {
Error {
source: ErrorData {
details: format!("{}", self.source),
debug: format!("{:?}", self.source),
}
.into(),
source: eyre!("{}", self.source),
debug: self.debug.as_ref().map(|e| eyre!("{e}")),
kind: self.kind,
revision: self.revision.clone(),
task: None,
@@ -539,25 +543,24 @@ where
impl<T, E> ResultExt<T, E> for Result<T, E>
where
color_eyre::eyre::Error: From<E>,
E: std::fmt::Debug + 'static,
{
fn with_kind(self, kind: ErrorKind) -> Result<T, Error> {
self.map_err(|e| Error {
source: e.into(),
kind,
revision: None,
task: None,
})
self.map_err(|e| Error::new(e, kind))
}
fn with_ctx<F: FnOnce(&E) -> (ErrorKind, D), D: Display>(self, f: F) -> Result<T, Error> {
self.map_err(|e| {
let (kind, ctx) = f(&e);
let debug = (typeid::of::<E>() == typeid::of::<color_eyre::eyre::Error>())
.then(|| eyre!("{ctx}: {e:?}"));
let source = color_eyre::eyre::Error::from(e);
let ctx = format!("{}: {}", ctx, source);
let source = source.wrap_err(ctx);
let with_ctx = format!("{ctx}: {source}");
let source = source.wrap_err(with_ctx);
Error {
kind,
source,
debug,
revision: None,
task: None,
}
@@ -578,25 +581,24 @@ where
}
impl<T> ResultExt<T, Error> for Result<T, Error> {
fn with_kind(self, kind: ErrorKind) -> Result<T, Error> {
self.map_err(|e| Error {
source: e.source,
kind,
revision: e.revision,
task: e.task,
})
self.map_err(|e| Error { kind, ..e })
}
fn with_ctx<F: FnOnce(&Error) -> (ErrorKind, D), D: Display>(self, f: F) -> Result<T, Error> {
self.map_err(|e| {
let (kind, ctx) = f(&e);
let source = e.source;
let ctx = format!("{}: {}", ctx, source);
let source = source.wrap_err(ctx);
let with_ctx = format!("{ctx}: {source}");
let source = source.wrap_err(with_ctx);
let debug = e.debug.map(|e| {
let with_ctx = format!("{ctx}: {e}");
e.wrap_err(with_ctx)
});
Error {
kind,
source,
revision: e.revision,
task: e.task,
debug,
..e
}
})
}

View File

@@ -22,7 +22,7 @@ cd ..
FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')"
RUSTFLAGS=""
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
RUSTFLAGS="--cfg tokio_unstable"
fi

View File

@@ -14,7 +14,7 @@ keywords = [
name = "start-os"
readme = "README.md"
repository = "https://github.com/Start9Labs/start-os"
version = "0.4.0-alpha.10" # VERSION_BUMP
version = "0.4.0-alpha.11" # VERSION_BUMP
license = "MIT"
[lib]
@@ -48,13 +48,14 @@ cli-registry = []
cli-startd = []
cli-tunnel = []
default = ["cli", "startd", "registry", "cli-container", "tunnel"]
dev = []
dev = ["backtrace-on-stack-overflow"]
docker = []
registry = []
startd = ["mail-send"]
test = []
tunnel = []
unstable = ["console-subscriber", "tokio/tracing"]
console = ["console-subscriber", "tokio/tracing"]
unstable = ["backtrace-on-stack-overflow"]
[dependencies]
arti-client = { version = "0.33", features = [
@@ -82,6 +83,7 @@ async-trait = "0.1.74"
axum = { version = "0.8.4", features = ["ws"] }
barrage = "0.2.3"
backhand = "0.21.0"
backtrace-on-stack-overflow = { version = "0.3.0", optional = true }
base32 = "0.5.0"
base64 = "0.22.1"
base64ct = "1.6.0"

View File

@@ -132,8 +132,6 @@ async fn inner_main(
.await?;
rpc_ctx.shutdown().await?;
tracing::info!("RPC Context is dropped");
Ok(shutdown)
}

View File

@@ -76,6 +76,11 @@ pub struct RpcContextSeed {
pub start_time: Instant,
pub crons: SyncMutex<BTreeMap<Guid, NonDetachingJoinHandle<()>>>,
}
impl Drop for RpcContextSeed {
fn drop(&mut self) {
tracing::info!("RpcContext is dropped");
}
}
pub struct Hardware {
pub devices: Vec<LshwDevice>,
@@ -269,7 +274,7 @@ impl RpcContext {
self.crons.mutate(|c| std::mem::take(c));
self.services.shutdown_all().await?;
self.is_closed.store(true, Ordering::SeqCst);
tracing::info!("RPC Context is shutdown");
tracing::info!("RpcContext is shutdown");
Ok(())
}

View File

@@ -1,4 +1,4 @@
use std::collections::{BTreeMap, BTreeSet};
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use chrono::{DateTime, Utc};
@@ -202,8 +202,10 @@ pub struct NetworkInfo {
#[model = "Model<Self>"]
#[ts(export)]
pub struct DnsSettings {
pub dhcp_servers: Vec<SocketAddr>,
pub static_servers: Option<Vec<SocketAddr>>,
#[ts(type = "string[]")]
pub dhcp_servers: VecDeque<SocketAddr>,
#[ts(type = "string[] | null")]
pub static_servers: Option<VecDeque<SocketAddr>>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)]

View File

@@ -31,7 +31,7 @@ pub async fn write_developer_key(
secret_key: secret.to_bytes(),
public_key: Some(PublicKeyBytes(VerifyingKey::from(secret).to_bytes())),
};
let mut file = create_file_mod(path, 0o046).await?;
let mut file = create_file_mod(path, 0o640).await?;
file.write_all(
keypair_bytes
.to_pkcs8_pem(base64ct::LineEnding::default())

View File

@@ -10,8 +10,8 @@ use tracing::instrument;
use super::filesystem::{FileSystem, MountType, ReadOnly, ReadWrite};
use super::util::unmount;
use crate::Error;
use crate::util::{Invoke, Never};
use crate::Error;
pub const TMP_MOUNTPOINT: &'static str = "/media/startos/tmp";
@@ -74,7 +74,7 @@ impl MountGuard {
}
pub async fn unmount(mut self, delete_mountpoint: bool) -> Result<(), Error> {
if self.mounted {
unmount(&self.mountpoint, false).await?;
unmount(&self.mountpoint, !cfg!(feature = "unstable")).await?;
if delete_mountpoint {
match tokio::fs::remove_dir(&self.mountpoint).await {
Err(e) if e.raw_os_error() == Some(39) => Ok(()), // directory not empty

View File

@@ -1,3 +1,7 @@
fn main() {
#[cfg(feature = "backtrace-on-stack-overflow")]
unsafe {
backtrace_on_stack_overflow::enable()
};
startos::bins::startbox()
}

View File

@@ -43,7 +43,7 @@ pub trait AuthContext: SignatureAuthContext {
const LOCAL_AUTH_COOKIE_OWNERSHIP: &str;
fn init_auth_cookie() -> impl Future<Output = Result<(), Error>> + Send {
async {
let mut file = create_file_mod(Self::LOCAL_AUTH_COOKIE_PATH, 0o046).await?;
let mut file = create_file_mod(Self::LOCAL_AUTH_COOKIE_PATH, 0o640).await?;
file.write_all(BASE64.encode(random::<[u8; 32]>()).as_bytes())
.await?;
file.sync_all().await?;

View File

@@ -1,19 +1,20 @@
use std::borrow::Borrow;
use std::collections::BTreeMap;
use std::collections::{BTreeMap, VecDeque};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::str::FromStr;
use std::sync::{Arc, Weak};
use std::time::Duration;
use clap::Parser;
use color_eyre::eyre::eyre;
use futures::future::BoxFuture;
use futures::{FutureExt, StreamExt, TryStreamExt};
use futures::{FutureExt, StreamExt};
use helpers::NonDetachingJoinHandle;
use hickory_client::client::Client;
use hickory_client::proto::runtime::TokioRuntimeProvider;
use hickory_client::proto::tcp::TcpClientStream;
use hickory_client::proto::udp::UdpClientStream;
use hickory_client::proto::xfer::{DnsExchangeBackground, DnsRequestOptions};
use hickory_client::proto::xfer::DnsRequestOptions;
use hickory_client::proto::DnsHandle;
use hickory_server::authority::MessageResponseBuilder;
use hickory_server::proto::op::{Header, ResponseCode};
@@ -24,6 +25,7 @@ use imbl::OrdMap;
use imbl_value::InternedString;
use itertools::Itertools;
use models::{GatewayId, OptionExt, PackageId};
use patch_db::json_ptr::JsonPointer;
use rpc_toolkit::{
from_fn_async, from_fn_blocking, Context, HandlerArgs, HandlerExt, ParentHandler,
};
@@ -36,6 +38,7 @@ use crate::db::model::public::NetworkInterfaceInfo;
use crate::db::model::Database;
use crate::net::gateway::NetworkInterfaceWatcher;
use crate::prelude::*;
use crate::util::actor::background::BackgroundJobQueue;
use crate::util::io::file_string_stream;
use crate::util::serde::{display_serializable, HandlerExtSerde};
use crate::util::sync::{SyncRwLock, Watch};
@@ -161,97 +164,146 @@ impl DnsClient {
Self {
client: client.clone(),
_thread: tokio::spawn(async move {
loop {
if let Err::<(), Error>(e) = async {
let mut stream = file_string_stream("/run/systemd/resolve/resolv.conf")
.filter_map(|a| futures::future::ready(a.transpose()))
.boxed();
let mut conf: String = stream
.next()
.await
.or_not_found("/run/systemd/resolve/resolv.conf")??;
let mut prev_nameservers = Vec::new();
let mut bg = BTreeMap::<SocketAddr, BoxFuture<_>>::new();
loop {
let nameservers = conf
.lines()
.map(|l| l.trim())
.filter_map(|l| l.strip_prefix("nameserver "))
.skip(2)
.map(|n| {
n.parse::<SocketAddr>()
.or_else(|_| n.parse::<IpAddr>().map(|a| (a, 53).into()))
})
.collect::<Result<Vec<_>, _>>()?;
let static_nameservers = db
.mutate(|db| {
let dns = db
.as_public_mut()
.as_server_info_mut()
.as_network_mut()
.as_dns_mut();
dns.as_dhcp_servers_mut().ser(&nameservers)?;
dns.as_static_servers().de()
})
.await
.result?;
let nameservers = static_nameservers.unwrap_or(nameservers);
if nameservers != prev_nameservers {
let mut existing: BTreeMap<_, _> =
client.peek(|c| c.iter().cloned().collect());
let mut new = Vec::with_capacity(nameservers.len());
for addr in &nameservers {
if let Some(existing) = existing.remove(addr) {
new.push((*addr, existing));
} else {
let client = if let Ok((client, bg_thread)) =
Client::connect(
UdpClientStream::builder(
*addr,
TokioRuntimeProvider::new(),
)
.build(),
)
.await
let (bg, mut runner) = BackgroundJobQueue::new();
runner
.run_while(async move {
let dhcp_ns_db = db.clone();
bg.add_job(async move {
loop {
if let Err(e) = async {
let mut stream =
file_string_stream("/run/systemd/resolve/resolv.conf")
.filter_map(|a| futures::future::ready(a.transpose()))
.boxed();
while let Some(conf) = stream.next().await {
let conf: String = conf?;
let mut nameservers = conf
.lines()
.map(|l| l.trim())
.filter_map(|l| l.strip_prefix("nameserver "))
.map(|n| {
n.parse::<SocketAddr>().or_else(|_| {
n.parse::<IpAddr>().map(|a| (a, 53).into())
})
})
.collect::<Result<VecDeque<_>, _>>()?;
if nameservers
.front()
.map_or(false, |addr| addr.ip().is_loopback())
{
bg.insert(*addr, bg_thread.boxed());
client
} else {
let (stream, sender) = TcpClientStream::new(
*addr,
None,
Some(Duration::from_secs(30)),
TokioRuntimeProvider::new(),
);
let (client, bg_thread) =
Client::new(stream, sender, None)
.await
.with_kind(ErrorKind::Network)?;
bg.insert(*addr, bg_thread.boxed());
client
};
new.push((*addr, client));
nameservers.pop_front();
}
if nameservers.front().map_or(false, |addr| {
addr.ip() == IpAddr::from([1, 1, 1, 1])
}) {
nameservers.pop_front();
}
dhcp_ns_db
.mutate(|db| {
let dns = db
.as_public_mut()
.as_server_info_mut()
.as_network_mut()
.as_dns_mut();
dns.as_dhcp_servers_mut().ser(&nameservers)
})
.await
.result?
}
Ok::<_, Error>(())
}
.await
{
tracing::error!("{e}");
tracing::debug!("{e:?}");
tokio::time::sleep(Duration::from_secs(1)).await;
}
bg.retain(|n, _| nameservers.iter().any(|a| a == n));
prev_nameservers = nameservers;
client.replace(new);
}
tokio::select! {
c = stream.next() => conf = c.or_not_found("/run/systemd/resolve/resolv.conf")??,
_ = futures::future::join(
futures::future::join_all(bg.values_mut()),
futures::future::pending::<()>(),
) => (),
});
loop {
if let Err::<(), Error>(e) = async {
let mut static_changed = db
.subscribe(
"/public/serverInfo/network/dns/staticServers"
.parse::<JsonPointer>()
.with_kind(ErrorKind::Database)?,
)
.await;
let mut prev_nameservers = VecDeque::new();
let mut bg = BTreeMap::<SocketAddr, BoxFuture<_>>::new();
loop {
let dns = db
.peek()
.await
.into_public()
.into_server_info()
.into_network()
.into_dns();
let nameservers = dns
.as_static_servers()
.transpose_ref()
.unwrap_or_else(|| dns.as_dhcp_servers())
.de()?;
if nameservers != prev_nameservers {
let mut existing: BTreeMap<_, _> =
client.peek(|c| c.iter().cloned().collect());
let mut new = Vec::with_capacity(nameservers.len());
for addr in &nameservers {
if let Some(existing) = existing.remove(addr) {
new.push((*addr, existing));
} else {
let client = if let Ok((client, bg_thread)) =
Client::connect(
UdpClientStream::builder(
*addr,
TokioRuntimeProvider::new(),
)
.build(),
)
.await
{
bg.insert(*addr, bg_thread.boxed());
client
} else {
let (stream, sender) = TcpClientStream::new(
*addr,
None,
Some(Duration::from_secs(30)),
TokioRuntimeProvider::new(),
);
let (client, bg_thread) =
Client::new(stream, sender, None)
.await
.with_kind(ErrorKind::Network)?;
bg.insert(*addr, bg_thread.boxed());
client
};
new.push((*addr, client));
}
}
bg.retain(|n, _| nameservers.iter().any(|a| a == n));
prev_nameservers = nameservers;
client.replace(new);
}
futures::future::select(
static_changed.recv().boxed(),
futures::future::join(
futures::future::join_all(bg.values_mut()),
futures::future::pending::<()>(),
),
)
.await;
}
}
.await
{
tracing::error!("{e}");
tracing::debug!("{e:?}");
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
.await
{
tracing::error!("{e}");
tracing::debug!("{e:?}");
}
}
})
.await;
})
.into(),
}
@@ -269,6 +321,12 @@ impl DnsClient {
}
}
lazy_static::lazy_static! {
static ref LOCALHOST: Name = Name::from_ascii("localhost").unwrap();
static ref STARTOS: Name = Name::from_ascii("startos").unwrap();
static ref EMBASSY: Name = Name::from_ascii("embassy").unwrap();
}
struct Resolver {
client: DnsClient,
net_iface: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>,
@@ -276,9 +334,12 @@ struct Resolver {
}
impl Resolver {
fn resolve(&self, name: &Name, src: IpAddr) -> Option<Vec<IpAddr>> {
if name.zone_of(&*LOCALHOST) {
return Some(vec![Ipv4Addr::LOCALHOST.into(), Ipv6Addr::LOCALHOST.into()]);
}
self.resolve.peek(|r| {
if r.private_domains
.get(&*name.to_lowercase().to_ascii())
.get(&*name.to_lowercase().to_utf8().trim_end_matches('.'))
.map_or(false, |d| d.strong_count() > 0)
{
if let Some(res) = self.net_iface.peek(|i| {
@@ -295,36 +356,30 @@ impl Resolver {
return Some(res);
}
}
match name.iter().next_back() {
Some(b"embassy") | Some(b"startos") => {
if let Some(pkg) = name.iter().rev().skip(1).next() {
if let Some(ip) = r.services.get(&Some(
std::str::from_utf8(pkg)
.unwrap_or_default()
.parse()
.unwrap_or_default(),
)) {
Some(
ip.iter()
.filter(|(_, rc)| rc.strong_count() > 0)
.map(|(ip, _)| (*ip).into())
.collect(),
)
} else {
None
}
} else if let Some(ip) = r.services.get(&None) {
Some(
ip.iter()
.filter(|(_, rc)| rc.strong_count() > 0)
.map(|(ip, _)| (*ip).into())
.collect(),
)
} else {
None
}
if STARTOS.zone_of(name) || EMBASSY.zone_of(name) {
let Ok(pkg) = name
.trim_to(2)
.iter()
.next()
.map(std::str::from_utf8)
.transpose()
.map_err(|_| ())
.and_then(|s| s.map(PackageId::from_str).transpose().map_err(|_| ()))
else {
return None;
};
if let Some(ip) = r.services.get(&pkg) {
Some(
ip.iter()
.filter(|(_, rc)| rc.strong_count() > 0)
.map(|(ip, _)| (*ip).into())
.collect(),
)
} else {
None
}
_ => None,
} else {
None
}
})
}
@@ -420,16 +475,22 @@ impl RequestHandler for Resolver {
}
} else {
let query = query.original().clone();
let mut streams = self.client.lookup(query, DnsRequestOptions::default());
let mut opt = DnsRequestOptions::default();
opt.recursion_desired = request.recursion_desired();
let mut streams = self.client.lookup(query, opt);
let mut err = None;
for stream in streams.iter_mut() {
match tokio::time::timeout(Duration::from_secs(5), stream.next()).await {
Ok(Some(Err(e))) => err = Some(e),
Ok(Some(Ok(msg))) => {
let mut header = msg.header().clone();
header.set_id(request.id());
header.set_checking_disabled(request.checking_disabled());
header.set_recursion_available(true);
return response_handle
.send_response(
MessageResponseBuilder::from_message_request(&*request).build(
Header::response_from_request(request.header()),
header,
msg.answers(),
msg.name_servers(),
&msg.soa().map(|s| s.to_owned().into_record_of_rdata()),

View File

@@ -7,16 +7,19 @@ use helpers::NonDetachingJoinHandle;
use id_pool::IdPool;
use imbl::OrdMap;
use models::GatewayId;
use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use tokio::sync::mpsc;
use crate::context::{CliContext, RpcContext};
use crate::db::model::public::NetworkInterfaceInfo;
use crate::net::gateway::{DynInterfaceFilter, InterfaceFilter};
use crate::net::gateway::{DynInterfaceFilter, InterfaceFilter, SecureFilter};
use crate::net::utils::ipv6_is_link_local;
use crate::prelude::*;
use crate::util::Invoke;
use crate::util::serde::{display_serializable, HandlerExtSerde};
use crate::util::sync::Watch;
use crate::util::Invoke;
pub const START9_BRIDGE_IFACE: &str = "lxcbr0";
pub const FIRST_DYNAMIC_PRIVATE_PORT: u16 = 49152;
@@ -42,6 +45,42 @@ impl AvailablePorts {
}
}
pub fn forward_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new().subcommand(
"dump-table",
from_fn_async(
|ctx: RpcContext| async move { ctx.net_controller.forward.dump_table().await },
)
.with_display_serializable()
.with_custom_display_fn(|HandlerArgs { params, .. }, res| {
use prettytable::*;
if let Some(format) = params.format {
return display_serializable(format, res);
}
let mut table = Table::new();
table.add_row(row![bc => "FROM", "TO", "FILTER / GATEWAY"]);
for (external, target) in res.0 {
table.add_row(row![external, target.target, target.filter]);
for (source, gateway) in target.gateways {
table.add_row(row![
format!("{}:{}", source, external),
target.target,
gateway
]);
}
}
table.print_tty(false)?;
Ok(())
})
.with_call_remote::<CliContext>(),
)
}
struct ForwardRequest {
external: u16,
target: SocketAddr,
@@ -49,6 +88,7 @@ struct ForwardRequest {
rc: Weak<()>,
}
#[derive(Clone)]
struct ForwardEntry {
external: u16,
target: SocketAddr,
@@ -96,7 +136,7 @@ impl ForwardEntry {
let mut keep = BTreeSet::<SocketAddr>::new();
for (iface, info) in ip_info
.iter()
.chain([NetworkInterfaceInfo::loopback()])
// .chain([NetworkInterfaceInfo::loopback()])
.filter(|(id, info)| filter_ref.filter(*id, *info))
{
if let Some(ip_info) = &info.ip_info {
@@ -155,10 +195,9 @@ impl ForwardEntry {
*self = Self::new(external, target, rc);
self.update(ip_info, Some(filter)).await?;
} else {
if self.prev_filter != filter {
self.update(ip_info, Some(filter)).await?;
}
self.rc = rc;
self.update(ip_info, Some(filter).filter(|f| f != &self.prev_filter))
.await?;
}
Ok(())
}
@@ -174,7 +213,7 @@ impl Drop for ForwardEntry {
}
}
#[derive(Default)]
#[derive(Default, Clone)]
struct ForwardState {
state: BTreeMap<u16, ForwardEntry>,
}
@@ -197,7 +236,7 @@ impl ForwardState {
for entry in self.state.values_mut() {
entry.update(ip_info, None).await?;
}
self.state.retain(|_, fwd| !fwd.forwards.is_empty());
self.state.retain(|_, fwd| fwd.rc.strong_count() > 0);
Ok(())
}
}
@@ -209,28 +248,68 @@ fn err_has_exited<T>(_: T) -> Error {
)
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ForwardTable(pub BTreeMap<u16, ForwardTarget>);
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ForwardTarget {
pub target: SocketAddr,
pub filter: String,
pub gateways: BTreeMap<SocketAddr, GatewayId>,
}
impl From<&ForwardState> for ForwardTable {
fn from(value: &ForwardState) -> Self {
Self(
value
.state
.iter()
.map(|(external, entry)| {
(
*external,
ForwardTarget {
target: entry.target,
filter: format!("{:?}", entry.prev_filter),
gateways: entry.forwards.clone(),
},
)
})
.collect(),
)
}
}
enum ForwardCommand {
Forward(ForwardRequest, oneshot::Sender<Result<(), Error>>),
Sync(oneshot::Sender<Result<(), Error>>),
DumpTable(oneshot::Sender<ForwardTable>),
}
#[test]
fn test() {
assert_ne!(
false.into_dyn(),
SecureFilter { secure: false }.into_dyn().into_dyn()
);
}
pub struct PortForwardController {
req: mpsc::UnboundedSender<(Option<ForwardRequest>, oneshot::Sender<Result<(), Error>>)>,
req: mpsc::UnboundedSender<ForwardCommand>,
_thread: NonDetachingJoinHandle<()>,
}
impl PortForwardController {
pub fn new(mut ip_info: Watch<OrdMap<GatewayId, NetworkInterfaceInfo>>) -> Self {
let (req_send, mut req_recv) = mpsc::unbounded_channel::<(
Option<ForwardRequest>,
oneshot::Sender<Result<(), Error>>,
)>();
let (req_send, mut req_recv) = mpsc::unbounded_channel::<ForwardCommand>();
let thread = NonDetachingJoinHandle::from(tokio::spawn(async move {
let mut state = ForwardState::default();
let mut interfaces = ip_info.read_and_mark_seen();
loop {
tokio::select! {
msg = req_recv.recv() => {
if let Some((msg, re)) = msg {
if let Some(req) = msg {
re.send(state.handle_request(req, &interfaces).await).ok();
} else {
re.send(state.sync(&interfaces).await).ok();
}
if let Some(cmd) = msg {
match cmd {
ForwardCommand::Forward(req, re) => re.send(state.handle_request(req, &interfaces).await).ok(),
ForwardCommand::Sync(re) => re.send(state.sync(&interfaces).await).ok(),
ForwardCommand::DumpTable(re) => re.send((&state).into()).ok(),
};
} else {
break;
}
@@ -250,19 +329,19 @@ impl PortForwardController {
pub async fn add(
&self,
external: u16,
filter: impl InterfaceFilter,
filter: DynInterfaceFilter,
target: SocketAddr,
) -> Result<Arc<()>, Error> {
let rc = Arc::new(());
let (send, recv) = oneshot::channel();
self.req
.send((
Some(ForwardRequest {
.send(ForwardCommand::Forward(
ForwardRequest {
external,
target,
filter: filter.into_dyn(),
filter,
rc: Arc::downgrade(&rc),
}),
},
send,
))
.map_err(err_has_exited)?;
@@ -271,13 +350,25 @@ impl PortForwardController {
}
pub async fn gc(&self) -> Result<(), Error> {
let (send, recv) = oneshot::channel();
self.req.send((None, send)).map_err(err_has_exited)?;
self.req
.send(ForwardCommand::Sync(send))
.map_err(err_has_exited)?;
recv.await.map_err(err_has_exited)?
}
pub async fn dump_table(&self) -> Result<ForwardTable, Error> {
let (req, res) = oneshot::channel();
self.req
.send(ForwardCommand::DumpTable(req))
.map_err(err_has_exited)?;
res.await.map_err(err_has_exited)
}
}
async fn forward(interface: &str, source: SocketAddr, target: SocketAddr) -> Result<(), Error> {
if source.is_ipv6() {
return Ok(()); // TODO: socat? ip6tables?
}
Command::new("/usr/lib/startos/scripts/forward-port")
.env("iiface", interface)
.env("oiface", START9_BRIDGE_IFACE)
@@ -291,6 +382,9 @@ async fn forward(interface: &str, source: SocketAddr, target: SocketAddr) -> Res
}
async fn unforward(interface: &str, source: SocketAddr, target: SocketAddr) -> Result<(), Error> {
if source.is_ipv6() {
return Ok(()); // TODO: socat? ip6tables?
}
Command::new("/usr/lib/startos/scripts/forward-port")
.env("UNDO", "1")
.env("iiface", interface)

View File

@@ -1329,6 +1329,9 @@ impl InterfaceFilter for DynInterfaceFilter {
fn as_any(&self) -> &dyn Any {
self.0.as_any()
}
fn into_dyn(self) -> DynInterfaceFilter {
self
}
}
impl DynInterfaceFilter {
fn new<T: InterfaceFilter>(value: T) -> Self {

View File

@@ -135,11 +135,12 @@ impl BindInfo {
}
impl InterfaceFilter for NetInfo {
fn filter(&self, id: &GatewayId, info: &NetworkInterfaceInfo) -> bool {
if info.public() {
self.public_enabled.contains(id)
} else {
!self.private_disabled.contains(id)
}
info.ip_info.is_some()
&& if info.public() {
self.public_enabled.contains(id)
} else {
!self.private_disabled.contains(id)
}
}
}

View File

@@ -33,6 +33,10 @@ pub fn net_api<C: Context>() -> ParentHandler<C> {
"dns",
dns::dns_api::<C>().with_about("Manage and query DNS"),
)
.subcommand(
"forward",
forward::forward_api::<C>().with_about("Manage port forwards"),
)
.subcommand(
"gateway",
gateway::gateway_api::<C>().with_about("View and edit gateway configurations"),

View File

@@ -3,13 +3,13 @@ use std::fmt;
use std::str::FromStr;
use chrono::{DateTime, Utc};
use clap::Parser;
use clap::builder::ValueParserFactory;
use clap::Parser;
use color_eyre::eyre::eyre;
use helpers::const_true;
use imbl_value::InternedString;
use models::{FromStrParser, PackageId};
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use tracing::instrument;
use ts_rs::TS;
@@ -155,6 +155,16 @@ pub async fn remove(
for id in ids {
n.remove(&id)?;
}
let mut unread = 0;
for (_, n) in n.as_entries()? {
if !n.as_seen().de()? {
unread += 1;
}
}
db.as_public_mut()
.as_server_info_mut()
.as_unread_notification_count_mut()
.ser(&unread)?;
Ok(())
})
.await
@@ -179,6 +189,16 @@ pub async fn remove_before(
for id in n.keys()?.range(..before) {
n.remove(&id)?;
}
let mut unread = 0;
for (_, n) in n.as_entries()? {
if !n.as_seen().de()? {
unread += 1;
}
}
db.as_public_mut()
.as_server_info_mut()
.as_unread_notification_count_mut()
.ser(&unread)?;
Ok(())
})
.await

View File

@@ -15,12 +15,12 @@ use futures::future::BoxFuture;
use futures::stream::FusedStream;
use futures::{FutureExt, SinkExt, StreamExt, TryStreamExt};
use helpers::NonDetachingJoinHandle;
use imbl_value::{InternedString, json};
use imbl_value::{json, InternedString};
use itertools::Itertools;
use models::{ActionId, HostId, ImageId, PackageId};
use nix::sys::signal::Signal;
use persistent_container::{PersistentContainer, Subcontainer};
use rpc_toolkit::{CallRemoteHandler, Empty, HandlerArgs, HandlerFor, from_fn_async};
use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, HandlerArgs, HandlerFor};
use serde::{Deserialize, Serialize};
use service_actor::ServiceActor;
use start_stop::StartStop;
@@ -47,11 +47,11 @@ use crate::service::action::update_tasks;
use crate::service::rpc::{ExitParams, InitKind};
use crate::service::service_map::InstallProgressHandles;
use crate::service::uninstall::cleanup;
use crate::util::Never;
use crate::util::actor::concurrent::ConcurrentActor;
use crate::util::io::{AsyncReadStream, TermSize, create_file, delete_file};
use crate::util::io::{create_file, delete_file, AsyncReadStream, TermSize};
use crate::util::net::WebSocketExt;
use crate::util::serde::Pem;
use crate::util::Never;
use crate::volume::data_dir;
use crate::{CAP_1_KiB, DATA_DIR};

View File

@@ -2,7 +2,7 @@ use std::net::Ipv4Addr;
use clap::Parser;
use ipnet::Ipv4Net;
use rpc_toolkit::{Context, Empty, HandlerExt, ParentHandler, from_fn_async};
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use crate::context::CliContext;
@@ -22,7 +22,7 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
subnet_api::<C>().with_about("Add, remove, or modify subnets"),
)
// .subcommand(
// "forward",
// "port-forward",
// ParentHandler::<C>::new()
// .subcommand(
// "add",
@@ -77,19 +77,19 @@ pub fn subnet_api<C: Context>() -> ParentHandler<C, SubnetParams> {
// .with_call_remote::<CliContext>(),
// )
// .subcommand(
// "add-client",
// from_fn_async(add_client)
// "add-device",
// from_fn_async(add_device)
// .with_metadata("sync_db", Value::Bool(true))
// .no_display()
// .with_about("Add a client to a subnet")
// .with_about("Add a device to a subnet")
// .with_call_remote::<CliContext>(),
// )
// .subcommand(
// "remove-client",
// from_fn_async(remove_client)
// "remove-device",
// from_fn_async(remove_device)
// .with_metadata("sync_db", Value::Bool(true))
// .no_display()
// .with_about("Remove a client from a subnet")
// .with_about("Remove a device from a subnet")
// .with_call_remote::<CliContext>(),
// )
}

View File

@@ -1,7 +1,7 @@
use std::pin::Pin;
use std::task::{Context, Poll};
use futures::future::{BoxFuture, FusedFuture, abortable, pending};
use futures::future::{abortable, pending, BoxFuture, FusedFuture};
use futures::stream::{AbortHandle, Abortable, BoxStream};
use futures::{Future, FutureExt, Stream, StreamExt};
use tokio::sync::watch;

View File

@@ -912,7 +912,7 @@ impl AsRef<Path> for TmpDir {
}
impl Drop for TmpDir {
fn drop(&mut self) {
if self.path.exists() {
if self.path != PathBuf::new() && self.path.exists() {
let path = std::mem::take(&mut self.path);
tokio::spawn(async move {
tokio::fs::remove_dir_all(&path).await.log_err();
@@ -1575,7 +1575,7 @@ pub fn file_string_stream(
loop {
match stream.watches().add(
&path,
WatchMask::MODIFY | WatchMask::MOVE_SELF | WatchMask::MOVED_TO | WatchMask::DELETE_SELF,
WatchMask::MODIFY | WatchMask::CLOSE_WRITE | WatchMask::MOVE_SELF | WatchMask::MOVED_TO | WatchMask::DELETE_SELF,
) {
Ok(_) => break,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {

View File

@@ -59,7 +59,7 @@ impl StartOSLogger {
fn base_subscriber(logfile: LogFile) -> impl Subscriber {
use tracing_error::ErrorLayer;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{EnvFilter, fmt};
use tracing_subscriber::{fmt, EnvFilter};
let filter_layer = || {
EnvFilter::builder()
@@ -80,10 +80,8 @@ impl StartOSLogger {
let sub = tracing_subscriber::registry();
#[cfg(feature = "unstable")]
#[cfg(feature = "console-subscriber")]
let sub = sub.with(console_subscriber::spawn());
#[cfg(not(feature = "unstable"))]
let sub = sub.with(filter_layer());
let sub = sub.with(fmt_layer).with(ErrorLayer::default());

View File

@@ -14,10 +14,10 @@ use ::serde::{Deserialize, Serialize};
use async_trait::async_trait;
use color_eyre::eyre::{self, eyre};
use fd_lock_rs::FdLock;
use futures::FutureExt;
use futures::future::BoxFuture;
pub use helpers::NonDetachingJoinHandle;
use futures::FutureExt;
use helpers::canonicalize;
pub use helpers::NonDetachingJoinHandle;
use imbl_value::InternedString;
use lazy_static::lazy_static;
pub use models::VersionString;
@@ -25,7 +25,7 @@ use pin_project::pin_project;
use sha2::Digest;
use tokio::fs::File;
use tokio::io::{AsyncRead, AsyncReadExt, BufReader};
use tokio::sync::{Mutex, OwnedMutexGuard, RwLock, oneshot};
use tokio::sync::{oneshot, Mutex, OwnedMutexGuard, RwLock};
use tracing::instrument;
use ts_rs::TS;
use url::Url;
@@ -197,17 +197,17 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> {
}
#[instrument(skip_all)]
async fn invoke(&mut self, error_kind: crate::ErrorKind) -> Result<Vec<u8>, Error> {
let cmd_str = self
.cmd
.as_std()
.get_program()
.to_string_lossy()
.into_owned();
self.cmd.kill_on_drop(true);
if self.input.is_some() {
self.cmd.stdin(Stdio::piped());
}
if self.pipe.is_empty() {
let cmd_str = self
.cmd
.as_std()
.get_program()
.to_string_lossy()
.into_owned();
if self.capture {
self.cmd.stdout(Stdio::piped());
self.cmd.stderr(Stdio::piped());
@@ -256,6 +256,7 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> {
.take()
.map(|i| Box::new(i) as Box<dyn AsyncRead + Unpin + Send>);
for (idx, cmd) in IntoIterator::into_iter(cmds).enumerate() {
let cmd_str = cmd.as_std().get_program().to_string_lossy().into_owned();
let last = idx == len - 1;
if self.capture || !last {
cmd.stdout(Stdio::piped());

View File

@@ -5,14 +5,14 @@ use std::panic::{RefUnwindSafe, UnwindSafe};
use color_eyre::eyre::eyre;
use futures::future::BoxFuture;
use futures::{Future, FutureExt};
use imbl_value::{InternedString, to_value};
use imbl_value::{to_value, InternedString};
use patch_db::json_ptr::ROOT;
use crate::Error;
use crate::context::RpcContext;
use crate::db::model::Database;
use crate::prelude::*;
use crate::progress::PhaseProgressTrackerHandle;
use crate::Error;
mod v0_3_5;
mod v0_3_5_1;
@@ -50,8 +50,9 @@ mod v0_4_0_alpha_8;
mod v0_4_0_alpha_9;
mod v0_4_0_alpha_10;
mod v0_4_0_alpha_11;
pub type Current = v0_4_0_alpha_10::Version; // VERSION_BUMP
pub type Current = v0_4_0_alpha_11::Version; // VERSION_BUMP
impl Current {
#[instrument(skip(self, db))]
@@ -164,7 +165,8 @@ enum Version {
V0_4_0_alpha_7(Wrapper<v0_4_0_alpha_7::Version>),
V0_4_0_alpha_8(Wrapper<v0_4_0_alpha_8::Version>),
V0_4_0_alpha_9(Wrapper<v0_4_0_alpha_9::Version>),
V0_4_0_alpha_10(Wrapper<v0_4_0_alpha_10::Version>), // VERSION_BUMP
V0_4_0_alpha_10(Wrapper<v0_4_0_alpha_10::Version>),
V0_4_0_alpha_11(Wrapper<v0_4_0_alpha_11::Version>), // VERSION_BUMP
Other(exver::Version),
}
@@ -217,7 +219,8 @@ impl Version {
Self::V0_4_0_alpha_7(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_8(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_9(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_10(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::V0_4_0_alpha_10(v) => DynVersion(Box::new(v.0)),
Self::V0_4_0_alpha_11(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
Self::Other(v) => {
return Err(Error::new(
eyre!("unknown version {v}"),
@@ -262,7 +265,8 @@ impl Version {
Version::V0_4_0_alpha_7(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_8(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_9(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_10(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::V0_4_0_alpha_10(Wrapper(x)) => x.semver(),
Version::V0_4_0_alpha_11(Wrapper(x)) => x.semver(), // VERSION_BUMP
Version::Other(x) => x.clone(),
}
}

View File

@@ -0,0 +1,37 @@
use exver::{PreReleaseSegment, VersionRange};
use super::v0_3_5::V0_3_0_COMPAT;
use super::{v0_4_0_alpha_10, VersionT};
use crate::prelude::*;
lazy_static::lazy_static! {
static ref V0_4_0_alpha_11: exver::Version = exver::Version::new(
[0, 4, 0],
[PreReleaseSegment::String("alpha".into()), 11.into()]
);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Version;
impl VersionT for Version {
type Previous = v0_4_0_alpha_10::Version;
type PreUpRes = ();
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
Ok(())
}
fn semver(self) -> exver::Version {
V0_4_0_alpha_11.clone()
}
fn compat(self) -> &'static VersionRange {
&V0_3_0_COMPAT
}
#[instrument]
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
Ok(Value::Null)
}
fn down(self, _db: &mut Value) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -61,7 +61,7 @@ PLATFORM_CONFIG_EXTRAS=()
if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
PLATFORM_CONFIG_EXTRAS+=( --firmware-binary false )
PLATFORM_CONFIG_EXTRAS+=( --firmware-chroot false )
PLATFORM_CONFIG_EXTRAS+=( --linux-packages linux-image-6.12.20+rpt )
PLATFORM_CONFIG_EXTRAS+=( --linux-packages linux-image-6.12.47+rpt )
PLATFORM_CONFIG_EXTRAS+=( --linux-flavours "rpi-v8 rpi-2712" )
elif [ "${IB_TARGET_PLATFORM}" = "rockchip64" ]; then
PLATFORM_CONFIG_EXTRAS+=( --linux-flavours rockchip64 )
@@ -204,8 +204,8 @@ if [ "${IB_TARGET_PLATFORM}" = "raspberrypi" ]; then
echo "Configuring raspi kernel '\$v'"
extract-ikconfig "/usr/lib/modules/\$v/kernel/kernel/configs.ko.xz" > /boot/config-\$v
done
mkinitramfs -c gzip -o /boot/initramfs8 6.12.25-v8+
mkinitramfs -c gzip -o /boot/initramfs_2712 6.12.25-v8-16k+
mkinitramfs -c gzip -o /boot/initramfs8 6.12.47-v8+
mkinitramfs -c gzip -o /boot/initramfs_2712 6.12.47-v8-16k+
fi
useradd --shell /bin/bash -G startos -m start9
@@ -231,7 +231,8 @@ lb chroot
lb installer
lb binary_chroot
lb chroot_prep install all mode-apt-install-binary mode-archives-chroot
ln -sf /run/systemd/resolve/stub-resolv.conf chroot/chroot/etc/resolv.conf
echo "nameserver 127.0.0.1" > chroot/chroot/etc/resolv.conf
echo "nameserver 1.1.1.1" >> chroot/chroot/etc/resolv.conf # Cloudflare DNS Fallback
lb binary_rootfs
cp $prep_results_dir/binary/live/filesystem.squashfs $RESULTS_DIR/$IMAGE_BASENAME.squashfs

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DnsSettings = {
dhcpServers: Array<string>
staticServers: Array<string> | null
dhcpServers: string[]
staticServers: string[] | null
}

View File

@@ -21,23 +21,81 @@ type FilterKinds = "onion" | "local" | "domain" | "ip" | "ipv4" | "ipv6"
export type Filter = {
visibility?: "public" | "private"
kind?: FilterKinds | FilterKinds[]
predicate?: (h: HostnameInfo) => boolean
exclude?: Filter
}
type VisibilityFilter<V extends "public" | "private"> = V extends "public"
? (HostnameInfo & { public: true }) | VisibilityFilter<Exclude<V, "public">>
: V extends "private"
?
| (HostnameInfo & { public: false })
| VisibilityFilter<Exclude<V, "private">>
: never
type KindFilter<K extends FilterKinds> = K extends "onion"
? (HostnameInfo & { kind: "onion" }) | KindFilter<Exclude<K, "onion">>
: K extends "local"
?
| (HostnameInfo & { kind: "ip"; hostname: { kind: "local" } })
| KindFilter<Exclude<K, "local">>
: K extends "domain"
?
| (HostnameInfo & { kind: "ip"; hostname: { kind: "domain" } })
| KindFilter<Exclude<K, "domain">>
: K extends "ipv4"
?
| (HostnameInfo & { kind: "ip"; hostname: { kind: "ipv4" } })
| KindFilter<Exclude<K, "ipv4">>
: K extends "ipv6"
?
| (HostnameInfo & { kind: "ip"; hostname: { kind: "ipv6" } })
| KindFilter<Exclude<K, "ipv6">>
: K extends "ip"
? KindFilter<Exclude<K, "ip"> | "ipv4" | "ipv6">
: never
type FilterReturnTy<F extends Filter> = F extends {
visibility: infer V extends "public" | "private"
}
? VisibilityFilter<V> & FilterReturnTy<Omit<F, "visibility">>
: F extends {
kind: (infer K extends FilterKinds) | (infer K extends FilterKinds)[]
}
? KindFilter<K> & FilterReturnTy<Omit<F, "kind">>
: F extends {
predicate: (h: HostnameInfo) => h is infer H extends HostnameInfo
}
? H & FilterReturnTy<Omit<F, "predicate">>
: F extends { exclude: infer E extends Filter } // MUST BE LAST
? HostnameInfo extends FilterReturnTy<E>
? HostnameInfo
: Exclude<HostnameInfo, FilterReturnTy<E>>
: HostnameInfo
type Formats = "hostname-info" | "urlstring" | "url"
type FormatReturnTy<Format extends Formats> = Format extends "hostname-info"
? HostnameInfo
type FormatReturnTy<
F extends Filter,
Format extends Formats,
> = Format extends "hostname-info"
? FilterReturnTy<F> | FormatReturnTy<F, Exclude<Format, "hostname-info">>
: Format extends "url"
? URL
: UrlString
? URL | FormatReturnTy<F, Exclude<Format, "url">>
: Format extends "urlstring"
? UrlString | FormatReturnTy<F, Exclude<Format, "urlstring">>
: never
export type Filled = {
hostnames: HostnameInfo[]
filter: <Format extends Formats = "urlstring">(
filter: Filter,
toUrls: (h: HostnameInfo) => {
url: UrlString | null
sslUrl: UrlString | null
}
filter: <F extends Filter, Format extends Formats = "urlstring">(
filter: F,
format?: Format,
) => FormatReturnTy<Format>[]
) => FormatReturnTy<F, Format>[]
publicHostnames: HostnameInfo[]
onionHostnames: HostnameInfo[]
@@ -83,8 +141,8 @@ const negate =
const unique = <A>(values: A[]) => Array.from(new Set(values))
export const addressHostToUrl = (
{ scheme, sslScheme, username, suffix }: AddressInfo,
host: HostnameInfo,
): UrlString[] => {
hostname: HostnameInfo,
): { url: UrlString | null; sslUrl: UrlString | null } => {
const res = []
const fmt = (scheme: string | null, host: HostnameInfo, port: number) => {
const excludePort =
@@ -109,14 +167,16 @@ export const addressHostToUrl = (
username ? `${username}@` : ""
}${hostname}${excludePort ? "" : `:${port}`}${suffix}`
}
if (host.hostname.sslPort !== null) {
res.push(fmt(sslScheme, host, host.hostname.sslPort))
let url = null
if (hostname.hostname.sslPort !== null) {
url = fmt(sslScheme, hostname, hostname.hostname.sslPort)
}
if (host.hostname.port !== null) {
res.push(fmt(scheme, host, host.hostname.port))
let sslUrl = null
if (hostname.hostname.port !== null) {
sslUrl = fmt(scheme, hostname, hostname.hostname.port)
}
return res
return { url, sslUrl }
}
function filterRec(
@@ -124,6 +184,10 @@ function filterRec(
filter: Filter,
invert: boolean,
): HostnameInfo[] {
if (filter.predicate) {
const pred = filter.predicate
hostnames = hostnames.filter((h) => invert !== pred(h))
}
if (filter.visibility === "public")
hostnames = hostnames.filter(
(h) => invert !== (h.kind === "onion" || h.public),
@@ -164,19 +228,28 @@ export const filledAddress = (
host: Host,
addressInfo: AddressInfo,
): FilledAddressInfo => {
const toUrl = addressHostToUrl.bind(null, addressInfo)
const toUrls = addressHostToUrl.bind(null, addressInfo)
const toUrlArray = (h: HostnameInfo) => {
const u = toUrls(h)
return [u.url, u.sslUrl].filter((u) => u !== null)
}
const hostnames = host.hostnameInfo[addressInfo.internalPort] ?? []
return {
...addressInfo,
hostnames,
filter: <T extends Formats = "urlstring">(filter: Filter, format?: T) => {
const res = filterRec(hostnames, filter, false)
if (format === "hostname-info") return res as FormatReturnTy<T>[]
const urls = res.flatMap(toUrl)
if (format === "url")
return urls.map((u) => new URL(u)) as FormatReturnTy<T>[]
return urls as FormatReturnTy<T>[]
toUrls,
filter: <F extends Filter, Format extends Formats = "urlstring">(
filter: F,
format?: Format,
) => {
const filtered = filterRec(hostnames, filter, false)
let res: FormatReturnTy<F, Format>[] = filtered as any
if (format === "hostname-info") return res
const urls = filtered.flatMap(toUrlArray)
if (format === "url") res = urls.map((u) => new URL(u)) as any
else res = urls as any
return res
},
get publicHostnames() {
return hostnames.filter((h) => h.kind === "onion" || h.public)
@@ -215,28 +288,28 @@ export const filledAddress = (
)
},
get urls() {
return this.hostnames.flatMap(toUrl)
return this.hostnames.flatMap(toUrlArray)
},
get publicUrls() {
return this.publicHostnames.flatMap(toUrl)
return this.publicHostnames.flatMap(toUrlArray)
},
get onionUrls() {
return this.onionHostnames.flatMap(toUrl)
return this.onionHostnames.flatMap(toUrlArray)
},
get localUrls() {
return this.localHostnames.flatMap(toUrl)
return this.localHostnames.flatMap(toUrlArray)
},
get ipUrls() {
return this.ipHostnames.flatMap(toUrl)
return this.ipHostnames.flatMap(toUrlArray)
},
get ipv4Urls() {
return this.ipv4Hostnames.flatMap(toUrl)
return this.ipv4Hostnames.flatMap(toUrlArray)
},
get ipv6Urls() {
return this.ipv6Hostnames.flatMap(toUrl)
return this.ipv6Hostnames.flatMap(toUrlArray)
},
get nonIpUrls() {
return this.nonIpHostnames.flatMap(toUrl)
return this.nonIpHostnames.flatMap(toUrlArray)
},
}
}

View File

@@ -61,7 +61,7 @@ import {
} from "../../base/lib/inits"
import { DropGenerator } from "../../base/lib/util/Drop"
export const OSVersion = testTypeVersion("0.4.0-alpha.10")
export const OSVersion = testTypeVersion("0.4.0-alpha.11")
// prettier-ignore
type AnyNeverCond<T extends any[], Then, Else> =

View File

@@ -11,7 +11,7 @@ import * as CP from "node:child_process"
export { Daemon } from "./Daemon"
export { CommandController } from "./CommandController"
import { HealthDaemon } from "./HealthDaemon"
import { EXIT_SUCCESS, HealthDaemon } from "./HealthDaemon"
import { Daemon } from "./Daemon"
import { CommandController } from "./CommandController"
import { HealthCheck } from "../health/HealthCheck"
@@ -91,6 +91,10 @@ type NewDaemonParams<
subcontainer: C
}
type OptionalParamSync<T> = T | (() => T | null)
type OptionalParamAsync<T> = () => Promise<T | null>
type OptionalParam<T> = OptionalParamSync<T> | OptionalParamAsync<T>
type AddDaemonParams<
Manifest extends T.SDKManifest,
Ids extends string,
@@ -160,7 +164,6 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
readonly started:
| ((onTerm: () => PromiseLike<void>) => PromiseLike<null>)
| null,
readonly daemons: Promise<Daemon<Manifest>>[],
readonly ids: Ids[],
readonly healthDaemons: HealthDaemon<Manifest>[],
) {}
@@ -189,9 +192,37 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
options.started,
[],
[],
[],
)
}
private addDaemonImpl<Id extends string>(
id: Id,
daemon: Promise<
Daemon<Manifest, SubContainer<Manifest, T.Effects> | null>
> | null,
requires: Ids[],
ready: Ready | typeof EXIT_SUCCESS,
) {
const healthDaemon = new HealthDaemon(
daemon,
requires
.map((x) => this.ids.indexOf(x))
.filter((x) => x >= 0)
.map((id) => this.healthDaemons[id]),
id,
ready,
this.effects,
)
const ids = [...this.ids, id] as (Ids | Id)[]
const healthDaemons = [...this.healthDaemons, healthDaemon]
return new Daemons<Manifest, Ids | Id>(
this.effects,
this.started,
ids,
healthDaemons,
)
}
/**
* Returns the complete list of daemons, including the one defined here
* @param id
@@ -205,36 +236,42 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
ErrorDuplicateId<Id> extends Id ? never :
Id extends Ids ? ErrorDuplicateId<Id> :
Id,
options: AddDaemonParams<Manifest, Ids, Id, C>,
options: OptionalParamSync<AddDaemonParams<Manifest, Ids, Id, C>>,
): Daemons<Manifest, Ids | Id>
addDaemon<Id extends string, C extends SubContainer<Manifest> | null>(
// prettier-ignore
id:
"" extends Id ? never :
ErrorDuplicateId<Id> extends Id ? never :
Id extends Ids ? ErrorDuplicateId<Id> :
Id,
options: OptionalParamAsync<AddDaemonParams<Manifest, Ids, Id, C>>,
): Promise<Daemons<Manifest, Ids | Id>>
addDaemon<Id extends string, C extends SubContainer<Manifest> | null>(
id: Id,
options: OptionalParam<AddDaemonParams<Manifest, Ids, Id, C>>,
) {
const daemon =
"daemon" in options
? Promise.resolve(options.daemon)
: Daemon.of<Manifest>()<C>(
this.effects,
options.subcontainer,
options.exec,
)
const healthDaemon = new HealthDaemon(
daemon,
options.requires
.map((x) => this.ids.indexOf(x))
.filter((x) => x >= 0)
.map((id) => this.healthDaemons[id]),
id,
options.ready,
this.effects,
)
const daemons = [...this.daemons, daemon]
const ids = [...this.ids, id] as (Ids | Id)[]
const healthDaemons = [...this.healthDaemons, healthDaemon]
return new Daemons<Manifest, Ids | Id>(
this.effects,
this.started,
daemons,
ids,
healthDaemons,
)
const prev = this
const res = (options: AddDaemonParams<Manifest, Ids, Id, C> | null) => {
if (!options) return prev
const daemon =
"daemon" in options
? Promise.resolve(options.daemon)
: Daemon.of<Manifest>()<C>(
this.effects,
options.subcontainer,
options.exec,
)
return prev.addDaemonImpl(id, daemon, options.requires, options.ready)
}
if (options instanceof Function) {
const opts = options()
if (opts instanceof Promise) {
return opts.then(res)
}
return res(opts)
}
return res(options)
}
/**
@@ -245,40 +282,45 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
* @returns a new Daemons object
*/
addOneshot<Id extends string, C extends SubContainer<Manifest> | null>(
id: "" extends Id
? never
: ErrorDuplicateId<Id> extends Id
? never
: Id extends Ids
? ErrorDuplicateId<Id>
: Id,
options: AddOneshotParams<Manifest, Ids, Id, C>,
// prettier-ignore
id:
"" extends Id ? never :
ErrorDuplicateId<Id> extends Id ? never :
Id extends Ids ? ErrorDuplicateId<Id> :
Id,
options: OptionalParamSync<AddOneshotParams<Manifest, Ids, Id, C>>,
): Daemons<Manifest, Ids | Id>
addOneshot<Id extends string, C extends SubContainer<Manifest> | null>(
// prettier-ignore
id:
"" extends Id ? never :
ErrorDuplicateId<Id> extends Id ? never :
Id extends Ids ? ErrorDuplicateId<Id> :
Id,
options: OptionalParamAsync<AddOneshotParams<Manifest, Ids, Id, C>>,
): Promise<Daemons<Manifest, Ids | Id>>
addOneshot<Id extends string, C extends SubContainer<Manifest> | null>(
id: Id,
options: OptionalParam<AddOneshotParams<Manifest, Ids, Id, C>>,
) {
const daemon = Oneshot.of<Manifest>()<C>(
this.effects,
options.subcontainer,
options.exec,
)
const healthDaemon = new HealthDaemon<Manifest>(
daemon,
options.requires
.map((x) => this.ids.indexOf(x))
.filter((x) => x >= 0)
.map((id) => this.healthDaemons[id]),
id,
"EXIT_SUCCESS",
this.effects,
)
const daemons = [...this.daemons, daemon]
const ids = [...this.ids, id] as (Ids | Id)[]
const healthDaemons = [...this.healthDaemons, healthDaemon]
return new Daemons<Manifest, Ids | Id>(
this.effects,
this.started,
daemons,
ids,
healthDaemons,
)
const prev = this
const res = (options: AddOneshotParams<Manifest, Ids, Id, C> | null) => {
if (!options) return prev
const daemon = Oneshot.of<Manifest>()<C>(
this.effects,
options.subcontainer,
options.exec,
)
return prev.addDaemonImpl(id, daemon, options.requires, EXIT_SUCCESS)
}
if (options instanceof Function) {
const opts = options()
if (opts instanceof Promise) {
return opts.then(res)
}
return res(opts)
}
return res(options)
}
/**
@@ -288,35 +330,40 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
* @returns a new Daemons object
*/
addHealthCheck<Id extends string>(
id: "" extends Id
? never
: ErrorDuplicateId<Id> extends Id
? never
: Id extends Ids
? ErrorDuplicateId<Id>
: Id,
options: AddHealthCheckParams<Ids, Id>,
// prettier-ignore
id:
"" extends Id ? never :
ErrorDuplicateId<Id> extends Id ? never :
Id extends Ids ? ErrorDuplicateId<Id> :
Id,
options: OptionalParamSync<AddHealthCheckParams<Ids, Id>>,
): Daemons<Manifest, Ids | Id>
addHealthCheck<Id extends string>(
// prettier-ignore
id:
"" extends Id ? never :
ErrorDuplicateId<Id> extends Id ? never :
Id extends Ids ? ErrorDuplicateId<Id> :
Id,
options: OptionalParamAsync<AddHealthCheckParams<Ids, Id>>,
): Promise<Daemons<Manifest, Ids | Id>>
addHealthCheck<Id extends string>(
id: Id,
options: OptionalParam<AddHealthCheckParams<Ids, Id>>,
) {
const healthDaemon = new HealthDaemon<Manifest>(
null,
options.requires
.map((x) => this.ids.indexOf(x))
.filter((x) => x >= 0)
.map((id) => this.healthDaemons[id]),
id,
options.ready,
this.effects,
)
const daemons = [...this.daemons]
const ids = [...this.ids, id] as (Ids | Id)[]
const healthDaemons = [...this.healthDaemons, healthDaemon]
return new Daemons<Manifest, Ids | Id>(
this.effects,
this.started,
daemons,
ids,
healthDaemons,
)
const prev = this
const res = (options: AddHealthCheckParams<Ids, Id> | null) => {
if (!options) return prev
return prev.addDaemonImpl(id, null, options.requires, options.ready)
}
if (options instanceof Function) {
const opts = options()
if (opts instanceof Promise) {
return opts.then(res)
}
return res(opts)
}
return res(options)
}
/**
@@ -353,7 +400,6 @@ export class Daemons<Manifest extends T.SDKManifest, Ids extends string>
const daemons = await new Daemons<Manifest, Ids>(
this.effects,
this.started,
[...this.daemons, daemon],
this.ids,
[...this.healthDaemons, healthDaemon],
).build()

View File

@@ -1,12 +1,12 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.38",
"version": "0.4.0-beta.41",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.38",
"version": "0.4.0-beta.41",
"license": "MIT",
"dependencies": {
"@iarna/toml": "^3.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@start9labs/start-sdk",
"version": "0.4.0-beta.38",
"version": "0.4.0-beta.41",
"description": "Software development kit to facilitate packaging services for StartOS",
"main": "./package/lib/index.js",
"types": "./package/lib/index.d.ts",

8693
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "startos-ui",
"version": "0.4.0-alpha.10",
"version": "0.4.0-alpha.11",
"author": "Start9 Labs, Inc",
"homepage": "https://start9.com/",
"license": "MIT",

View File

@@ -34,6 +34,12 @@ import { i18nPipe } from '../../i18n/i18n.pipe'
<logs-window />
`,
styles: `
:host {
display: flex;
flex-direction: column;
height: 100%;
}
section {
border-radius: 0.25rem;
padding: 1rem;
@@ -48,7 +54,7 @@ import { i18nPipe } from '../../i18n/i18n.pipe'
logs-window {
display: flex;
flex-direction: column;
height: 18rem;
flex: 1;
padding: 1rem;
margin: 0 1.5rem auto;
text-align: left;

View File

@@ -502,7 +502,7 @@ export default {
531: 'Fehler beim Initialisieren des Servers',
532: 'Abgeschlossen',
533: 'Gateways',
535: 'Gateway hinzufügen',
535: 'StartTunnel-Gateway hinzufügen',
536: 'Umbenennen',
537: 'Zugriff',
538: 'Öffentliche Domains',
@@ -535,10 +535,8 @@ export default {
569: 'Wählen Sie eine Zertifizierungsstelle aus, um SSL/TLS-Zertifikate für diese Domain auszustellen.',
570: 'Andere',
571: 'Ein Name zur einfachen Identifizierung des Gateways',
572: 'Wählen Sie diese Option, wenn das Gateway für den privaten Zugriff nur für autorisierte Clients konfiguriert ist. StartTunnel ist ein privates Gateway.',
573: 'Wählen Sie diese Option, wenn das Gateway für uneingeschränkten öffentlichen Zugriff konfiguriert ist.',
574: 'Datei',
575: 'Wireguard-Konfigurationsdatei',
575: 'StartTunnel-Konfigurationsdatei',
576: 'Kopieren/Einfügen',
577: 'Dateiinhalt',
578: 'Öffentlicher Schlüssel',
@@ -550,7 +548,7 @@ export default {
584: 'Verbindungen können manchmal langsam oder unzuverlässig sein',
585: 'Öffentlich, wenn Sie die Adresse öffentlich teilen, andernfalls privat',
586: 'Erfordert ein Tor-fähiges Gerät oder einen Browser',
587: 'Nur nützlich für Clients, die HTTPS erzwingen',
587: 'Nur nützlich für Clients, die SSL erzwingen',
588: 'Ideal für anonyme, zensurresistente Bereitstellung und Fernzugriff',
589: 'Ideal für lokalen Zugriff',
590: 'Erfordert die Verbindung mit demselben lokalen Netzwerk (LAN) wie Ihr Server, entweder physisch oder über VPN',
@@ -589,4 +587,5 @@ export default {
623: 'Alternative Implementierungen',
624: 'Versionen',
625: 'Eine andere Version auswählen',
626: 'Hochladen',
} satisfies i18n

View File

@@ -501,7 +501,7 @@ export const ENGLISH = {
'Error initializing server': 531,
'Finished': 532, // an in, complete
'Gateways': 533, // as in, a device or software that connects two different networks
'Add gateway': 535, // as in, add a new network gateway to StartOS
'Add StartTunnel Gateway': 535, // as in, add a new StartTunnel network gateway to StartOS
'Rename': 536,
'Access': 537, // as in, public or private access, almost "permission"
'Public Domains': 538, // as in, internet domains
@@ -534,10 +534,8 @@ export const ENGLISH = {
'Select a Certificate Authority to issue SSL/TLS certificates for this domain': 569,
'Other': 570, // as in, a list option to indicate none of the options listed
'A name to easily identify the gateway': 571,
'select this option if the gateway is configured for private access to authorized clients only. StartTunnel is a private gateway.': 572,
'select this option if the gateway is configured for unfettered public access.': 573,
'File': 574, // as in, a computer file
'Wireguard Config File': 575,
'StartTunnel Config File': 575,
'Copy/Paste': 576,
'File Contents': 577,
'Public Key': 578, // as in, a cryptographic public key
@@ -549,7 +547,7 @@ export const ENGLISH = {
'Connections can be slow or unreliable at times': 584,
'Public if you share the address publicly, otherwise private': 585,
'Requires using a Tor-enabled device or browser': 586,
'Only useful for clients that enforce HTTPS': 587,
'Only useful for clients that require SSL': 587,
'Ideal for anonymous, censorship-resistant hosting and remote access': 588,
'Ideal for local access': 589,
'Requires being connected to the same Local Area Network (LAN) as your server, either physically or via VPN': 590,
@@ -588,4 +586,5 @@ export const ENGLISH = {
'Alternative Implementations': 623,
'Versions': 624,
'Select another version': 625,
'Upload': 626, // as in, upload a file
} as const

View File

@@ -502,7 +502,7 @@ export default {
531: 'Error al inicializar el servidor',
532: 'Finalizado',
533: 'Puertas de enlace',
535: 'Agregar puerta de enlace',
535: 'Agregar puerta de enlace StartTunnel',
536: 'Renombrar',
537: 'Acceso',
538: 'Dominios públicos',
@@ -535,10 +535,8 @@ export default {
569: 'Selecciona una Autoridad Certificadora para emitir certificados SSL/TLS para este dominio.',
570: 'Otro',
571: 'Un nombre para identificar fácilmente la puerta de enlace',
572: 'Selecciona esta opción si la puerta de enlace está configurada para acceso privado solo a clientes autorizados. StartTunnel es una puerta de enlace privada.',
573: 'Selecciona esta opción si la puerta de enlace está configurada para acceso público sin restricciones.',
574: 'Archivo',
575: 'Archivo de configuración de Wireguard',
575: 'Archivo de configuración de StartTunnel',
576: 'Copiar/Pegar',
577: 'Contenido del archivo',
578: 'Clave pública',
@@ -550,7 +548,7 @@ export default {
584: 'Las conexiones pueden ser lentas o poco confiables a veces',
585: 'Público si compartes la dirección públicamente, de lo contrario privado',
586: 'Requiere un dispositivo o navegador habilitado para Tor',
587: 'Solo útil para clientes que imponen HTTPS',
587: 'Solo útil para clientes que imponen SSL',
588: 'Ideal para alojamiento y acceso remoto anónimo y resistente a la censura',
589: 'Ideal para acceso local',
590: 'Requiere estar conectado a la misma red de área local (LAN) que tu servidor, ya sea físicamente o mediante VPN',
@@ -589,4 +587,5 @@ export default {
623: 'Implementaciones alternativas',
624: 'Versiones',
625: 'Seleccionar otra versión',
626: 'Subir',
} satisfies i18n

View File

@@ -502,7 +502,7 @@ export default {
531: "Erreur lors de l'initialisation du serveur",
532: 'Terminé',
533: 'Passerelles',
535: 'Ajouter une passerelle',
535: 'Ajouter une passerelle StartTunnel',
536: 'Renommer',
537: 'Accès',
538: 'Domaines publics',
@@ -535,10 +535,8 @@ export default {
569: 'Sélectionnez une Autorité de Certification pour émettre des certificats SSL/TLS pour ce domaine.',
570: 'Autre',
571: 'Un nom pour identifier facilement la passerelle',
572: 'Sélectionnez cette option si la passerelle est configurée pour un accès privé uniquement aux clients autorisés. StartTunnel est une passerelle privée.',
573: 'Sélectionnez cette option si la passerelle est configurée pour un accès public illimité.',
574: 'Fichier',
575: 'Fichier de configuration Wireguard',
575: 'Fichier de configuration StartTunnel',
576: 'Copier/Coller',
577: 'Contenu du fichier',
578: 'Clé publique',
@@ -550,7 +548,7 @@ export default {
584: 'Les connexions peuvent parfois être lentes ou peu fiables',
585: 'Public si vous partagez ladresse publiquement, sinon privé',
586: 'Nécessite un appareil ou un navigateur compatible Tor',
587: 'Utile uniquement pour les clients qui imposent HTTPS',
587: 'Utile uniquement pour les clients qui imposent SSL',
588: 'Idéal pour lhébergement et laccès à distance anonymes et résistants à la censure',
589: 'Idéal pour un accès local',
590: 'Nécessite dêtre connecté au même réseau local (LAN) que votre serveur, soit physiquement, soit via VPN',
@@ -589,4 +587,5 @@ export default {
623: 'Implémentations alternatives',
624: 'Versions',
625: 'Sélectionner une autre version',
626: 'Téléverser',
} satisfies i18n

View File

@@ -502,7 +502,7 @@ export default {
531: 'Błąd inicjalizacji serwera',
532: 'Zakończono',
533: 'Bramy sieciowe',
535: 'Dodaj bramę',
535: 'Dodaj bramę StartTunnel',
536: 'Zmień nazwę',
537: 'Dostęp',
538: 'Domeny publiczne',
@@ -535,10 +535,8 @@ export default {
569: 'Wybierz Urząd Certyfikacji, aby wystawić certyfikaty SSL/TLS dla tej domeny.',
570: 'Inne',
571: 'Nazwa ułatwiająca identyfikację bramy',
572: 'Wybierz tę opcję, jeśli brama jest skonfigurowana do prywatnego dostępu tylko dla autoryzowanych klientów. StartTunnel to prywatna brama.',
573: 'Wybierz tę opcję, jeśli brama jest skonfigurowana do nieograniczonego publicznego dostępu.',
574: 'Plik',
575: 'Plik konfiguracyjny Wireguard',
575: 'Plik konfiguracyjny StartTunnel',
576: 'Kopiuj/Wklej',
577: 'Zawartość pliku',
578: 'Klucz publiczny',
@@ -550,7 +548,7 @@ export default {
584: 'Połączenia mogą być czasami wolne lub niestabilne',
585: 'Publiczne, jeśli udostępniasz adres publicznie, w przeciwnym razie prywatne',
586: 'Wymaga urządzenia lub przeglądarki obsługującej Tor',
587: 'Przydatne tylko dla klientów wymuszających HTTPS',
587: 'Przydatne tylko dla klientów wymuszających SSL',
588: 'Idealne do anonimowego, odpornego na cenzurę hostingu i zdalnego dostępu',
589: 'Idealne do dostępu lokalnego',
590: 'Wymaga połączenia z tą samą siecią lokalną (LAN) co serwer, fizycznie lub przez VPN',
@@ -589,4 +587,5 @@ export default {
623: 'Alternatywne implementacje',
624: 'Wersje',
625: 'Wybierz inną wersję',
626: 'Prześlij',
} satisfies i18n

View File

@@ -110,6 +110,7 @@ body {
animation-fill-mode: forwards;
text-align: left;
width: 1em;
margin-left: -.3rem;
}
@keyframes ellipsis-dot {

View File

@@ -57,7 +57,7 @@
var(--tui-status-warning) 24%,
transparent
);
--tui-status-info: rgba(128, 89, 229, 1);
--tui-status-info: rgba(53, 96, 240, 1);
--tui-status-info-pale: color-mix(
in hsl,
var(--tui-status-info) 12%,
@@ -168,3 +168,8 @@ tui-textfield [tuiTooltip] {
align-self: center !important;
}
}
// TODO: Remove after migrating to v5
[tuiOption] {
word-break: break-word;
}

View File

@@ -24,7 +24,7 @@ import { StateService } from 'src/app/services/state.service'
<app-initializing [progress]="progress()" />
`,
providers: [provideSetupLogsService(ApiService)],
styles: ':host { padding: 1rem; }',
styles: ':host { height: 100%; }',
imports: [InitializingComponent],
})
export default class InitializingPage {

View File

@@ -24,8 +24,8 @@ import { TuiBadge } from '@taiga-ui/kit'
{{ 'Address details' | i18n }}
</button>
</td>
<td [style.width.rem]="6">{{ address.type }}</td>
<td [style.width.rem]="5">
<td>{{ address.type }}</td>
<td>
@if (address.access === 'public') {
<tui-badge size="s" appearance="primary-success">
{{ 'public' | i18n }}
@@ -38,20 +38,41 @@ import { TuiBadge } from '@taiga-ui/kit'
-
}
</td>
<td [style.width.rem]="10" [style.order]="-1">
{{ address.gatewayName || '-' }}
<td [style.order]="-1">
<div [title]="address.gatewayName">
{{ address.gatewayName || '-' }}
</div>
</td>
<td>
<div [title]="address.url">{{ address.url }}</div>
</td>
<td>{{ address.url }}</td>
<td
actions
[disabled]="!isRunning()"
[href]="address.url"
[style.width.rem]="7"
[style.width.rem]="5"
(onDetails)="viewDetails(address.bullets)"
></td>
}
`,
styles: `
:host {
white-space: nowrap;
td:last-child {
padding-inline-start: 0;
}
}
div {
white-space: normal;
word-break: break-all;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
:host-context(tui-root._mobile) {
td {
width: auto !important;

View File

@@ -7,6 +7,7 @@ import { i18nKey, i18nPipe } from '@start9labs/shared'
type AddressWithInfo = {
url: string
ssl: boolean
info: T.HostnameInfo
gateway?: GatewayPlus
}
@@ -132,12 +133,26 @@ export class InterfaceService {
if (!hostnamesInfos.length) return addresses
const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap(h =>
utils.addressHostToUrl(serviceInterface.addressInfo, h).map(url => ({
url,
info: h,
gateway: gateways.find(g => h.kind === 'ip' && h.gateway.id === g.id),
})),
const allAddressesWithInfo: AddressWithInfo[] = hostnamesInfos.flatMap(
h => {
const { url, sslUrl } = utils.addressHostToUrl(
serviceInterface.addressInfo,
h,
)
const info = h
const gateway =
h.kind === 'ip'
? gateways.find(g => h.gateway.id === g.id)
: undefined
const res = []
if (url) {
res.push({ url, ssl: false, info, gateway })
}
if (sslUrl) {
res.push({ url: sslUrl, ssl: true, info, gateway })
}
return res
},
)
const torAddrs = allAddressesWithInfo.filter(filterTor).sort(cmpTor)
@@ -166,12 +181,10 @@ export class InterfaceService {
}, [] as AddressWithInfo[])
return {
common: bestAddrs.map(a =>
this.toDisplayAddress(a, gateways, host.publicDomains),
),
common: bestAddrs.map(a => this.toDisplayAddress(a, host.publicDomains)),
uncommon: allAddressesWithInfo
.filter(a => !bestAddrs.includes(a))
.map(a => this.toDisplayAddress(a, gateways, host.publicDomains)),
.map(a => this.toDisplayAddress(a, host.publicDomains)),
}
}
@@ -313,8 +326,7 @@ export class InterfaceService {
}
private toDisplayAddress(
{ info, url, gateway }: AddressWithInfo,
gateways: GatewayPlus[],
{ info, ssl, url, gateway }: AddressWithInfo,
publicDomains: Record<string, T.PublicDomainConfig>,
): DisplayAddress {
let access: DisplayAddress['access']
@@ -338,33 +350,29 @@ export class InterfaceService {
),
this.i18n.transform('Requires using a Tor-enabled device or browser'),
]
// Tor (HTTPS)
if (url.startsWith('https:')) {
type = `${type} (HTTPS)`
// Tor (SSL)
if (ssl) {
type = `${type} (SSL)`
bullets = [
this.i18n.transform('Only useful for clients that enforce HTTPS'),
this.i18n.transform('Only useful for clients that require SSL'),
rootCaRequired,
...bullets,
]
// Tor (HTTP)
// Tor (NON-SSL)
} else {
bullets.unshift(
this.i18n.transform(
'Ideal for anonymous, censorship-resistant hosting and remote access',
),
)
if (url.startsWith('http:')) {
type = `${type} (HTTP)`
}
}
// ** Not Tor **
} else {
const port = info.hostname.sslPort || info.hostname.port
const gateway = gateways.find(g => g.id === info.gateway.id)!
gatewayName = gateway.name
gatewayName = info.gateway.name
const gatewayLanIpv4 = gateway.lanIpv4[0]
const isWireguard = gateway.ipInfo.deviceType === 'wireguard'
const gatewayLanIpv4 = gateway?.lanIpv4[0]
const isWireguard = gateway?.ipInfo.deviceType === 'wireguard'
const localIdeal = this.i18n.transform('Ideal for local access')
const lanRequired = this.i18n.transform(
@@ -405,9 +413,9 @@ export class InterfaceService {
),
rootCaRequired,
]
if (!gateway.public) {
if (!info.gateway.public) {
bullets.push(
`${portForwarding} "${gatewayName}": ${port} -> ${gateway.subnets.find(s => s.isIpv4())?.address}:${port}`,
`${portForwarding} "${gatewayName}": ${port} -> ${gateway?.subnets.find(s => s.isIpv4())?.address}:${port}`,
)
}
} else {
@@ -439,12 +447,12 @@ export class InterfaceService {
if (info.public) {
access = 'public'
bullets = [
`${dnsFor} ${info.hostname.value} ${resolvesTo} ${gateway.ipInfo.wanIp}`,
`${dnsFor} ${info.hostname.value} ${resolvesTo} ${gateway?.ipInfo.wanIp}`,
]
if (!gateway.public) {
if (!info.gateway.public) {
bullets.push(
`${portForwarding} "${gatewayName}": ${port} -> ${gateway.subnets.find(s => s.isIpv4())?.address}:${port === 443 ? 5443 : port}`,
`${portForwarding} "${gatewayName}": ${port} -> ${gateway?.subnets.find(s => s.isIpv4())?.address}:${port === 443 ? 5443 : port}`,
)
}

View File

@@ -97,10 +97,8 @@ export class ServiceHealthCheckComponent {
return `${this.i18n.transform('Success')}: ${this.healthCheck.message || 'health check passing'}`
case 'loading':
case 'failure':
return this.healthCheck.message
// disabled
default:
return this.healthCheck.result
case 'disabled':
return this.healthCheck.message || this.healthCheck.result
}
}
}

View File

@@ -6,22 +6,26 @@ import {
} from '@angular/core'
import { i18nKey, i18nPipe } from '@start9labs/shared'
import { tuiPure } from '@taiga-ui/cdk'
import { TuiIcon, TuiLoader } from '@taiga-ui/core'
import { TuiIcon } from '@taiga-ui/core'
import { getProgressText } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
import {
PrimaryRendering,
renderPkgStatus,
} from 'src/app/services/pkg-status-rendering.service'
@Component({
selector: 'td[appStatus]',
template: `
@if (loading) {
<tui-loader size="s" />
} @else {
@if (!healthy) {
<tui-icon icon="@tui.triangle-alert" class="g-warning" />
}
@if (!healthy) {
<tui-icon icon="@tui.triangle-alert" class="g-warning" />
}
<b [style.color]="color">{{ status | i18n }}</b>
@if (showDots) {
<span class="loading-dots g-info"></span>
}
<b [style.color]="color">{{ status | i18n }}{{ dots }}</b>
`,
styles: `
:host {
@@ -37,7 +41,7 @@ import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiIcon, TuiLoader, i18nPipe],
imports: [TuiIcon, i18nPipe],
})
export class StatusComponent {
@Input()
@@ -58,10 +62,6 @@ export class StatusComponent {
)
}
get loading(): boolean {
return this.color === 'var(--tui-status-info)'
}
@tuiPure
getStatus(pkg: PackageDataEntry) {
return renderPkgStatus(pkg)
@@ -72,35 +72,10 @@ export class StatusComponent {
return `${this.i18n.transform('Installing')}... ${this.i18n.transform(getProgressText(this.pkg.stateInfo.installingInfo.progress.overall))}` as i18nKey
}
switch (this.getStatus(this.pkg).primary) {
case 'running':
return 'Running'
case 'stopped':
return 'Stopped'
case 'taskRequired':
return 'Task Required'
case 'updating':
return 'Updating'
case 'stopping':
return 'Stopping'
case 'starting':
return 'Starting'
case 'backingUp':
return 'Backing Up'
case 'restarting':
return 'Restarting'
case 'removing':
return 'Removing'
case 'restoring':
return 'Restoring'
case 'error':
return 'Error'
default:
return 'Unknown'
}
return PrimaryRendering[this.getStatus(this.pkg).primary].display
}
get dots(): '...' | '' {
get showDots() {
switch (this.getStatus(this.pkg).primary) {
case 'updating':
case 'stopping':
@@ -108,9 +83,9 @@ export class StatusComponent {
case 'backingUp':
case 'restarting':
case 'removing':
return '...'
return true
default:
return ''
return false
}
}

View File

@@ -27,7 +27,6 @@ import { QrCodeComponent } from 'ng-qrcode'
tuiTextfield
[readOnly]="true"
[ngModel]="member.value"
[style.border-inline-end-width.rem]="border"
[type]="member.masked && masked ? 'password' : 'text'"
/>
@if (member.masked) {
@@ -129,16 +128,6 @@ export class ActionSuccessMemberComponent {
masked = true
get border(): number {
let border = 0
if (this.member.masked) border += 2
if (this.member.copyable) border += 2
if (this.member.qr) border += 2
return border
}
show(template: TemplateRef<any>) {
const masked = this.masked

View File

@@ -23,7 +23,6 @@ import { SingleResult } from './types'
tuiTextfield
[readOnly]="true"
[ngModel]="single.value"
[style.border-inline-end-width.rem]="border"
[type]="single.masked && masked ? 'password' : 'text'"
/>
@if (single.masked) {
@@ -105,15 +104,6 @@ export class ActionSuccessSingleComponent {
masked = true
get border(): number {
let border = 0
if (this.single.masked) border += 2
if (this.single.copyable) border += 2
return border
}
copy() {
const el = this.input.nativeElement

View File

@@ -83,18 +83,10 @@ export default class GatewaysComponent {
),
required: true,
default: null,
}),
type: ISB.Value.select({
name: this.i18n.transform('Type'),
description: `-**${this.i18n.transform('private')}**: ${this.i18n.transform('select this option if the gateway is configured for private access to authorized clients only. StartTunnel is a private gateway.')}\n-**${this.i18n.transform('public')}**: ${this.i18n.transform('select this option if the gateway is configured for unfettered public access.')}`,
default: 'private',
values: {
private: this.i18n.transform('private'),
public: this.i18n.transform('public'),
},
placeholder: 'StartTunnel 1',
}),
config: ISB.Value.union({
name: this.i18n.transform('Wireguard Config File'),
name: this.i18n.transform('StartTunnel Config File'),
default: 'paste',
variants: ISB.Variants.of({
paste: {
@@ -108,7 +100,7 @@ export default class GatewaysComponent {
}),
},
select: {
name: this.i18n.transform('Select'),
name: this.i18n.transform('Upload'),
spec: ISB.InputSpec.of({
file: ISB.Value.file({
name: this.i18n.transform('File'),
@@ -122,7 +114,7 @@ export default class GatewaysComponent {
})
this.formDialog.open(FormComponent, {
label: 'Add gateway',
label: 'Add StartTunnel Gateway',
data: {
spec: await configBuilderToSpec(spec),
buttons: [
@@ -138,7 +130,7 @@ export default class GatewaysComponent {
input.config.selection === 'paste'
? input.config.value.file
: await (input.config.value.file as any as File).text(),
public: input.type === 'public',
public: false,
})
return true
} catch (e: any) {

View File

@@ -110,7 +110,7 @@ export namespace Mock {
squashfs: {
aarch64: {
publishedAt: '2025-04-21T20:58:48.140749883Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_aarch64.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.11/startos-0.4.0-alpha.11-33ae46f~dev_aarch64.squashfs',
commitment: {
hash: '4elBFVkd/r8hNadKmKtLIs42CoPltMvKe2z3LRqkphk=',
size: 1343500288,
@@ -122,7 +122,7 @@ export namespace Mock {
},
'aarch64-nonfree': {
publishedAt: '2025-04-21T21:07:00.249285116Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_aarch64-nonfree.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.11/startos-0.4.0-alpha.11-33ae46f~dev_aarch64-nonfree.squashfs',
commitment: {
hash: 'MrCEi4jxbmPS7zAiGk/JSKlMsiuKqQy6RbYOxlGHOIQ=',
size: 1653075968,
@@ -134,7 +134,7 @@ export namespace Mock {
},
raspberrypi: {
publishedAt: '2025-04-21T21:16:12.933319237Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_raspberrypi.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.11/startos-0.4.0-alpha.11-33ae46f~dev_raspberrypi.squashfs',
commitment: {
hash: '/XTVQRCqY3RK544PgitlKu7UplXjkmzWoXUh2E4HCw0=',
size: 1490731008,
@@ -146,7 +146,7 @@ export namespace Mock {
},
x86_64: {
publishedAt: '2025-04-21T21:14:20.246908903Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_x86_64.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.11/startos-0.4.0-alpha.11-33ae46f~dev_x86_64.squashfs',
commitment: {
hash: '/6romKTVQGSaOU7FqSZdw0kFyd7P+NBSYNwM3q7Fe44=',
size: 1411657728,
@@ -158,7 +158,7 @@ export namespace Mock {
},
'x86_64-nonfree': {
publishedAt: '2025-04-21T21:15:17.955265284Z',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.10/startos-0.4.0-alpha.10-33ae46f~dev_x86_64-nonfree.squashfs',
url: 'https://alpha-registry-x.start9.com/startos/v0.4.0-alpha.11/startos-0.4.0-alpha.11-33ae46f~dev_x86_64-nonfree.squashfs',
commitment: {
hash: 'HCRq9sr/0t85pMdrEgNBeM4x11zVKHszGnD1GDyZbSE=',
size: 1731035136,
@@ -385,7 +385,7 @@ export namespace Mock {
docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.38',
sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -420,7 +420,7 @@ export namespace Mock {
docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.38',
sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -465,7 +465,7 @@ export namespace Mock {
docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.38',
sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -500,7 +500,7 @@ export namespace Mock {
docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.38',
sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -547,7 +547,7 @@ export namespace Mock {
docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release to 0.17.5',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.38',
sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
@@ -595,7 +595,7 @@ export namespace Mock {
docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release to 0.17.4',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.38',
sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
@@ -647,7 +647,7 @@ export namespace Mock {
docsUrl: 'https://bitcoin.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.38',
sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -682,7 +682,7 @@ export namespace Mock {
docsUrl: 'https://bitcoinknots.org',
releaseNotes: 'Even better support for Bitcoin and wallets!',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.38',
sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash',
icon: BTC_ICON,
sourceVersion: null,
@@ -727,7 +727,7 @@ export namespace Mock {
docsUrl: 'https://lightning.engineering/',
releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.38',
sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash',
icon: LND_ICON,
sourceVersion: null,
@@ -775,7 +775,7 @@ export namespace Mock {
marketingSite: '',
releaseNotes: 'Upstream release and minor fixes.',
osVersion: '0.3.6',
sdkVersion: '0.4.0-beta.38',
sdkVersion: '0.4.0-beta.41',
gitHash: 'fakehash',
icon: PROXY_ICON,
sourceVersion: null,

View File

@@ -126,13 +126,14 @@ export class DepErrorService {
const expected = currentDep?.versionRange || ''
// incorrect version
if (!this.exver.satisfies(depManifest.version, expected)) {
if (depManifest.satisfies.some(v => !this.exver.satisfies(v, expected))) {
return {
expected,
type: 'incorrectVersion',
received: depManifest.version,
}
if (
!this.exver.satisfies(depManifest.version, expected) &&
!depManifest.satisfies.some(v => this.exver.satisfies(v, expected))
) {
return {
expected,
type: 'incorrectVersion',
received: depManifest.version,
}
}

View File

@@ -25,11 +25,21 @@ export function getInstalledPrimaryStatus({
tasks,
status,
}: T.PackageDataEntry): PrimaryStatus {
return Object.values(tasks).some(
t => t.active && t.task.severity === 'critical',
)
? 'taskRequired'
: status.main
if (
Object.values(tasks).some(t => t.active && t.task.severity === 'critical')
) {
return 'taskRequired'
}
if (
Object.values(status.main === 'running' && status.health)
.filter(h => !!h)
.some(h => h.result === 'starting')
) {
return 'starting'
}
return status.main
}
function getHealthStatus(status: T.MainStatus): T.HealthStatus | null {
@@ -43,14 +53,14 @@ function getHealthStatus(status: T.MainStatus): T.HealthStatus | null {
return 'failure'
}
if (values.some(h => h.result === 'loading')) {
return 'loading'
}
if (values.some(h => h.result === 'starting')) {
return 'starting'
}
if (values.some(h => h.result === 'loading')) {
return 'loading'
}
return 'success'
}