mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
Merge branch 'bugfix/alpha.20' of github.com:Start9Labs/start-os into bugfix/alpha.20
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,3 +22,4 @@ secrets.db
|
||||
tmp
|
||||
web/.i18n-checked
|
||||
docs/USER.md
|
||||
*.s9pk
|
||||
|
||||
9
Makefile
9
Makefile
@@ -15,7 +15,8 @@ IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo
|
||||
WEB_UIS := web/dist/raw/ui/index.html web/dist/raw/setup-wizard/index.html
|
||||
COMPRESSED_WEB_UIS := web/dist/static/ui/index.html web/dist/static/setup-wizard/index.html
|
||||
FIRMWARE_ROMS := build/lib/firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./build/lib/firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json)
|
||||
BUILD_SRC := $(call ls-files, build/lib) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS)
|
||||
TOR_S9PK := build/lib/tor_$(ARCH).s9pk
|
||||
BUILD_SRC := $(call ls-files, build/lib) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS) $(TOR_S9PK)
|
||||
IMAGE_RECIPE_SRC := $(call ls-files, build/image-recipe/)
|
||||
STARTD_SRC := core/startd.service $(BUILD_SRC)
|
||||
CORE_SRC := $(call ls-files, core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE)
|
||||
@@ -188,6 +189,9 @@ install: $(STARTOS_TARGETS)
|
||||
|
||||
$(call mkdir,$(DESTDIR)/lib/systemd/system)
|
||||
$(call cp,core/startd.service,$(DESTDIR)/lib/systemd/system/startd.service)
|
||||
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then \
|
||||
sed -i '/^Environment=/a Environment=RUST_BACKTRACE=full' $(DESTDIR)/lib/systemd/system/startd.service; \
|
||||
fi
|
||||
|
||||
$(call mkdir,$(DESTDIR)/usr/lib)
|
||||
$(call rm,$(DESTDIR)/usr/lib/startos)
|
||||
@@ -312,6 +316,9 @@ build/lib/depends build/lib/conflicts: $(ENVIRONMENT_FILE) $(PLATFORM_FILE) $(sh
|
||||
$(FIRMWARE_ROMS): build/lib/firmware.json ./build/download-firmware.sh $(PLATFORM_FILE)
|
||||
./build/download-firmware.sh $(PLATFORM)
|
||||
|
||||
$(TOR_S9PK): ./build/download-tor-s9pk.sh
|
||||
./build/download-tor-s9pk.sh $(ARCH)
|
||||
|
||||
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox: $(CORE_SRC) $(COMPRESSED_WEB_UIS) web/patchdb-ui-seed.json $(ENVIRONMENT_FILE)
|
||||
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build/build-startbox.sh
|
||||
touch core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox
|
||||
|
||||
14
build/download-tor-s9pk.sh
Executable file
14
build/download-tor-s9pk.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
set -e
|
||||
|
||||
ARCH=$1
|
||||
|
||||
if [ -z "$ARCH" ]; then
|
||||
>&2 echo "usage: $0 <ARCH>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl --fail -L -o "./lib/tor_${ARCH}.s9pk" "https://s9pks.nyc3.cdn.digitaloceanspaces.com/tor_${ARCH}.s9pk"
|
||||
@@ -131,6 +131,11 @@ ff02::1 ip6-allnodes
|
||||
ff02::2 ip6-allrouters
|
||||
EOT
|
||||
|
||||
if [[ "${IB_OS_ENV}" =~ (^|-)dev($|-) ]]; then
|
||||
mkdir -p config/includes.chroot/etc/ssh/sshd_config.d
|
||||
echo "PasswordAuthentication yes" > config/includes.chroot/etc/ssh/sshd_config.d/dev-password-auth.conf
|
||||
fi
|
||||
|
||||
# Installer marker file (used by installed GRUB to detect the live USB)
|
||||
mkdir -p config/includes.binary
|
||||
touch config/includes.binary/.startos-installer
|
||||
|
||||
@@ -58,6 +58,11 @@ check_variables () {
|
||||
main () {
|
||||
get_variables
|
||||
|
||||
# Fix GPT backup header first — the image was built with a tight root
|
||||
# partition, so the backup GPT is not at the end of the SD card. parted
|
||||
# will prompt interactively if this isn't fixed before we use it.
|
||||
sgdisk -e "$ROOT_DEV" 2>/dev/null || true
|
||||
|
||||
if ! check_variables; then
|
||||
return 1
|
||||
fi
|
||||
@@ -74,9 +79,6 @@ main () {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fix GPT backup header to reflect new partition layout
|
||||
sgdisk -e "$ROOT_DEV" 2>/dev/null || true
|
||||
|
||||
mount / -o remount,rw
|
||||
|
||||
btrfs filesystem resize max /media/startos/root
|
||||
|
||||
2
container-runtime/package-lock.json
generated
2
container-runtime/package-lock.json
generated
@@ -37,7 +37,7 @@
|
||||
},
|
||||
"../sdk/dist": {
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.59",
|
||||
"version": "0.4.0-beta.60",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^3.0.0",
|
||||
|
||||
22
core/Cargo.lock
generated
22
core/Cargo.lock
generated
@@ -3376,6 +3376,15 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keccak"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
|
||||
dependencies = [
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kv"
|
||||
version = "0.24.0"
|
||||
@@ -5985,6 +5994,16 @@ dependencies = [
|
||||
"digest 0.10.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha3"
|
||||
version = "0.10.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
"keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
@@ -6415,7 +6434,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "start-os"
|
||||
version = "0.4.0-alpha.20"
|
||||
version = "0.4.0-alpha.21"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"async-acme",
|
||||
@@ -6519,6 +6538,7 @@ dependencies = [
|
||||
"serde_yml",
|
||||
"sha-crypt",
|
||||
"sha2 0.10.9",
|
||||
"sha3",
|
||||
"signal-hook",
|
||||
"socket2 0.6.2",
|
||||
"socks5-impl",
|
||||
|
||||
@@ -15,7 +15,7 @@ license = "MIT"
|
||||
name = "start-os"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/Start9Labs/start-os"
|
||||
version = "0.4.0-alpha.20" # VERSION_BUMP
|
||||
version = "0.4.0-alpha.21" # VERSION_BUMP
|
||||
|
||||
[lib]
|
||||
name = "startos"
|
||||
@@ -200,6 +200,7 @@ serde_toml = { package = "toml", version = "0.9.9+spec-1.0.0" }
|
||||
serde_yaml = { package = "serde_yml", version = "0.0.12" }
|
||||
sha-crypt = "0.5.0"
|
||||
sha2 = "0.10.2"
|
||||
sha3 = "0.10"
|
||||
signal-hook = "0.3.17"
|
||||
socket2 = { version = "0.6.0", features = ["all"] }
|
||||
socks5-impl = { version = "0.7.2", features = ["client", "server"] }
|
||||
|
||||
@@ -67,6 +67,10 @@ if [[ "${ENVIRONMENT:-}" =~ (^|-)console($|-) ]]; then
|
||||
RUSTFLAGS="--cfg tokio_unstable"
|
||||
fi
|
||||
|
||||
if [[ "${ENVIRONMENT:-}" =~ (^|-)unstable($|-) ]]; then
|
||||
RUSTFLAGS="$RUSTFLAGS -C debuginfo=1"
|
||||
fi
|
||||
|
||||
echo "FEATURES=\"$FEATURES\""
|
||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||
rust-zig-builder cargo zigbuild --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features=$FEATURES --locked --bin start-cli --target=$TARGET
|
||||
|
||||
@@ -38,6 +38,10 @@ if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
|
||||
RUSTFLAGS="--cfg tokio_unstable"
|
||||
fi
|
||||
|
||||
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||
RUSTFLAGS="$RUSTFLAGS -C debuginfo=1"
|
||||
fi
|
||||
|
||||
echo "FEATURES=\"$FEATURES\""
|
||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||
rust-zig-builder cargo zigbuild --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features=$FEATURES --locked --bin registrybox --target=$RUST_ARCH-unknown-linux-musl
|
||||
|
||||
@@ -38,6 +38,10 @@ if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
|
||||
RUSTFLAGS="--cfg tokio_unstable"
|
||||
fi
|
||||
|
||||
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||
RUSTFLAGS="$RUSTFLAGS -C debuginfo=1"
|
||||
fi
|
||||
|
||||
echo "FEATURES=\"$FEATURES\""
|
||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||
rust-zig-builder cargo zigbuild --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features=$FEATURES --locked --bin start-container --target=$RUST_ARCH-unknown-linux-musl
|
||||
|
||||
@@ -38,6 +38,10 @@ if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
|
||||
RUSTFLAGS="--cfg tokio_unstable"
|
||||
fi
|
||||
|
||||
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||
RUSTFLAGS="$RUSTFLAGS -C debuginfo=1"
|
||||
fi
|
||||
|
||||
echo "FEATURES=\"$FEATURES\""
|
||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||
rust-zig-builder cargo zigbuild --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features=$FEATURES --locked --bin startbox --target=$RUST_ARCH-unknown-linux-musl
|
||||
|
||||
@@ -38,6 +38,10 @@ if [[ "${ENVIRONMENT}" =~ (^|-)console($|-) ]]; then
|
||||
RUSTFLAGS="--cfg tokio_unstable"
|
||||
fi
|
||||
|
||||
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||
RUSTFLAGS="$RUSTFLAGS -C debuginfo=1"
|
||||
fi
|
||||
|
||||
echo "FEATURES=\"$FEATURES\""
|
||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||
rust-zig-builder cargo zigbuild --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features=$FEATURES --locked --bin tunnelbox --target=$RUST_ARCH-unknown-linux-musl
|
||||
|
||||
@@ -1255,6 +1255,13 @@ backup.bulk.leaked-reference:
|
||||
fr_FR: "référence fuitée vers BackupMountGuard"
|
||||
pl_PL: "wyciekła referencja do BackupMountGuard"
|
||||
|
||||
backup.bulk.service-not-ready:
|
||||
en_US: "Cannot create a backup of a service that is still initializing or in an error state"
|
||||
de_DE: "Es kann keine Sicherung eines Dienstes erstellt werden, der noch initialisiert wird oder sich im Fehlerzustand befindet"
|
||||
es_ES: "No se puede crear una copia de seguridad de un servicio que aún se está inicializando o está en estado de error"
|
||||
fr_FR: "Impossible de créer une sauvegarde d'un service encore en cours d'initialisation ou en état d'erreur"
|
||||
pl_PL: "Nie można utworzyć kopii zapasowej usługi, która jest jeszcze inicjalizowana lub znajduje się w stanie błędu"
|
||||
|
||||
# backup/restore.rs
|
||||
backup.restore.package-error:
|
||||
en_US: "Error restoring package %{id}: %{error}"
|
||||
|
||||
@@ -300,6 +300,15 @@ async fn perform_backup(
|
||||
error: backup_result,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
backup_report.insert(
|
||||
id.clone(),
|
||||
PackageBackupReport {
|
||||
error: Some(
|
||||
t!("backup.bulk.service-not-ready").to_string(),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use tracing::instrument;
|
||||
use ts_rs::TS;
|
||||
|
||||
use super::target::BackupTargetId;
|
||||
use crate::PackageId;
|
||||
use crate::backup::os::OsBackup;
|
||||
use crate::context::setup::SetupResult;
|
||||
use crate::context::{RpcContext, SetupContext};
|
||||
@@ -26,7 +27,6 @@ use crate::service::service_map::DownloadInstallFuture;
|
||||
use crate::setup::SetupExecuteProgress;
|
||||
use crate::system::{save_language, sync_kiosk};
|
||||
use crate::util::serde::{IoFormat, Pem};
|
||||
use crate::{PLATFORM, PackageId};
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -90,7 +90,7 @@ pub async fn recover_full_server(
|
||||
recovery_source: TmpMountGuard,
|
||||
server_id: &str,
|
||||
recovery_password: &str,
|
||||
kiosk: Option<bool>,
|
||||
kiosk: bool,
|
||||
hostname: Option<ServerHostnameInfo>,
|
||||
SetupExecuteProgress {
|
||||
init_phases,
|
||||
@@ -123,7 +123,6 @@ pub async fn recover_full_server(
|
||||
os_backup.account.hostname = h;
|
||||
}
|
||||
|
||||
let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi");
|
||||
sync_kiosk(kiosk).await?;
|
||||
|
||||
let language = ctx.language.peek(|a| a.clone());
|
||||
|
||||
@@ -149,6 +149,11 @@ impl MultiExecutable {
|
||||
}
|
||||
|
||||
pub fn execute(&self) {
|
||||
#[cfg(feature = "backtrace-on-stack-overflow")]
|
||||
unsafe {
|
||||
backtrace_on_stack_overflow::enable()
|
||||
};
|
||||
|
||||
set_locale_from_env();
|
||||
|
||||
let mut popped = Vec::with_capacity(2);
|
||||
|
||||
@@ -190,7 +190,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
|
||||
}
|
||||
}
|
||||
});
|
||||
rt.shutdown_timeout(Duration::from_secs(60));
|
||||
rt.shutdown_timeout(Duration::from_millis(100));
|
||||
res
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ impl DiagnosticContext {
|
||||
shutdown,
|
||||
disk_guid,
|
||||
error: Arc::new(error.into()),
|
||||
rpc_continuations: RpcContinuations::new(),
|
||||
rpc_continuations: RpcContinuations::new(None),
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ impl InitContext {
|
||||
error: watch::channel(None).0,
|
||||
progress,
|
||||
shutdown,
|
||||
rpc_continuations: RpcContinuations::new(),
|
||||
rpc_continuations: RpcContinuations::new(None),
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ pub struct RpcContextSeed {
|
||||
pub db: TypedPatchDb<Database>,
|
||||
pub sync_db: watch::Sender<u64>,
|
||||
pub account: SyncRwLock<AccountInfo>,
|
||||
pub net_controller: Arc<NetController>,
|
||||
pub os_net_service: NetService,
|
||||
pub net_controller: Arc<NetController>,
|
||||
pub s9pk_arch: Option<&'static str>,
|
||||
pub services: ServiceMap,
|
||||
pub cancellable_installs: SyncMutex<BTreeMap<PackageId, oneshot::Sender<()>>>,
|
||||
@@ -346,10 +346,10 @@ impl RpcContext {
|
||||
services,
|
||||
cancellable_installs: SyncMutex::new(BTreeMap::new()),
|
||||
metrics_cache,
|
||||
rpc_continuations: RpcContinuations::new(Some(shutdown.clone())),
|
||||
shutdown,
|
||||
lxc_manager: Arc::new(LxcManager::new()),
|
||||
open_authed_continuations: OpenAuthedContinuations::new(),
|
||||
rpc_continuations: RpcContinuations::new(),
|
||||
wifi_manager: Arc::new(RwLock::new(wifi_interface.clone().map(|i| WpaCli::init(i)))),
|
||||
current_secret: Arc::new(
|
||||
Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).map_err(|e| {
|
||||
|
||||
@@ -85,7 +85,7 @@ impl SetupContext {
|
||||
result: OnceCell::new(),
|
||||
disk_guid: OnceCell::new(),
|
||||
shutdown,
|
||||
rpc_continuations: RpcContinuations::new(),
|
||||
rpc_continuations: RpcContinuations::new(None),
|
||||
install_rootfs: SyncMutex::new(None),
|
||||
language: SyncMutex::new(None),
|
||||
keyboard: SyncMutex::new(None),
|
||||
|
||||
@@ -31,7 +31,7 @@ pub struct Database {
|
||||
impl Database {
|
||||
pub fn init(
|
||||
account: &AccountInfo,
|
||||
kiosk: Option<bool>,
|
||||
kiosk: bool,
|
||||
language: Option<InternedString>,
|
||||
keyboard: Option<KeyboardOptions>,
|
||||
) -> Result<Self, Error> {
|
||||
|
||||
@@ -49,7 +49,7 @@ pub struct Public {
|
||||
impl Public {
|
||||
pub fn init(
|
||||
account: &AccountInfo,
|
||||
kiosk: Option<bool>,
|
||||
kiosk: bool,
|
||||
language: Option<InternedString>,
|
||||
keyboard: Option<KeyboardOptions>,
|
||||
) -> Result<Self, Error> {
|
||||
@@ -149,7 +149,7 @@ impl Public {
|
||||
echoip_urls: default_echoip_urls(),
|
||||
ram: 0,
|
||||
devices: Vec::new(),
|
||||
kiosk,
|
||||
kiosk: Some(kiosk).filter(|_| &*PLATFORM != "raspberrypi"),
|
||||
language,
|
||||
keyboard,
|
||||
},
|
||||
|
||||
@@ -174,7 +174,9 @@ pub async fn init(
|
||||
local_auth.complete();
|
||||
|
||||
// Re-enroll MOK on every boot if Secure Boot key exists but isn't enrolled yet
|
||||
if let Err(e) = crate::util::mok::enroll_mok(std::path::Path::new(crate::util::mok::DKMS_MOK_PUB)).await {
|
||||
if let Err(e) =
|
||||
crate::util::mok::enroll_mok(std::path::Path::new(crate::util::mok::DKMS_MOK_PUB)).await
|
||||
{
|
||||
tracing::warn!("MOK enrollment failed: {e}");
|
||||
}
|
||||
|
||||
@@ -369,7 +371,7 @@ pub async fn init(
|
||||
enable_zram.complete();
|
||||
|
||||
update_server_info.start();
|
||||
sync_kiosk(server_info.as_kiosk().de()?).await?;
|
||||
sync_kiosk(server_info.as_kiosk().de()?.unwrap_or(false)).await?;
|
||||
let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
|
||||
let devices = lshw().await?;
|
||||
let status_info = ServerStatus {
|
||||
|
||||
@@ -820,7 +820,6 @@ impl NetService {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.shutdown = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -832,6 +831,7 @@ impl NetService {
|
||||
impl Drop for NetService {
|
||||
fn drop(&mut self) {
|
||||
if !self.shutdown {
|
||||
self.shutdown = true;
|
||||
let svc = std::mem::replace(self, Self::dummy());
|
||||
tokio::spawn(async move { svc.remove_all().await.log_err() });
|
||||
}
|
||||
|
||||
@@ -509,7 +509,7 @@ where
|
||||
drop(queue_cell.replace(None));
|
||||
|
||||
if !runner.is_empty() {
|
||||
tokio::time::timeout(Duration::from_secs(60), runner)
|
||||
tokio::time::timeout(Duration::from_millis(100), runner)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ impl RegistryContext {
|
||||
listen: config.registry_listen.unwrap_or(DEFAULT_REGISTRY_LISTEN),
|
||||
db,
|
||||
datadir,
|
||||
rpc_continuations: RpcContinuations::new(),
|
||||
rpc_continuations: RpcContinuations::new(None),
|
||||
client: Client::builder()
|
||||
.proxy(Proxy::custom(move |url| {
|
||||
if url.host_str().map_or(false, |h| h.ends_with(".onion")) {
|
||||
|
||||
@@ -17,6 +17,7 @@ use ts_rs::TS;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::prelude::*;
|
||||
use crate::shutdown::Shutdown;
|
||||
use crate::util::future::TimedResource;
|
||||
use crate::util::net::WebSocket;
|
||||
use crate::util::{FromStrParser, new_guid};
|
||||
@@ -98,12 +99,15 @@ pub type RestHandler = Box<dyn FnOnce(Request) -> RestFuture + Send>;
|
||||
|
||||
pub struct WebSocketFuture {
|
||||
kill: Option<broadcast::Receiver<()>>,
|
||||
shutdown: Option<broadcast::Receiver<Option<Shutdown>>>,
|
||||
fut: BoxFuture<'static, ()>,
|
||||
}
|
||||
impl Future for WebSocketFuture {
|
||||
type Output = ();
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
if self.kill.as_ref().map_or(false, |k| !k.is_empty()) {
|
||||
if self.kill.as_ref().map_or(false, |k| !k.is_empty())
|
||||
|| self.shutdown.as_ref().map_or(false, |s| !s.is_empty())
|
||||
{
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
self.fut.poll_unpin(cx)
|
||||
@@ -138,6 +142,7 @@ impl RpcContinuation {
|
||||
RpcContinuation::WebSocket(TimedResource::new(
|
||||
Box::new(|ws| WebSocketFuture {
|
||||
kill: None,
|
||||
shutdown: None,
|
||||
fut: handler(ws.into()).boxed(),
|
||||
}),
|
||||
timeout,
|
||||
@@ -170,6 +175,7 @@ impl RpcContinuation {
|
||||
RpcContinuation::WebSocket(TimedResource::new(
|
||||
Box::new(|ws| WebSocketFuture {
|
||||
kill,
|
||||
shutdown: None,
|
||||
fut: handler(ws.into()).boxed(),
|
||||
}),
|
||||
timeout,
|
||||
@@ -183,15 +189,21 @@ impl RpcContinuation {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RpcContinuations(AsyncMutex<BTreeMap<Guid, RpcContinuation>>);
|
||||
pub struct RpcContinuations {
|
||||
continuations: AsyncMutex<BTreeMap<Guid, RpcContinuation>>,
|
||||
shutdown: Option<broadcast::Sender<Option<Shutdown>>>,
|
||||
}
|
||||
impl RpcContinuations {
|
||||
pub fn new() -> Self {
|
||||
RpcContinuations(AsyncMutex::new(BTreeMap::new()))
|
||||
pub fn new(shutdown: Option<broadcast::Sender<Option<Shutdown>>>) -> Self {
|
||||
RpcContinuations {
|
||||
continuations: AsyncMutex::new(BTreeMap::new()),
|
||||
shutdown,
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn clean(&self) {
|
||||
let mut continuations = self.0.lock().await;
|
||||
let mut continuations = self.continuations.lock().await;
|
||||
let mut to_remove = Vec::new();
|
||||
for (guid, cont) in &*continuations {
|
||||
if cont.is_timed_out() {
|
||||
@@ -206,23 +218,28 @@ impl RpcContinuations {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn add(&self, guid: Guid, handler: RpcContinuation) {
|
||||
self.clean().await;
|
||||
self.0.lock().await.insert(guid, handler);
|
||||
self.continuations.lock().await.insert(guid, handler);
|
||||
}
|
||||
|
||||
pub async fn get_ws_handler(&self, guid: &Guid) -> Option<WebSocketHandler> {
|
||||
let mut continuations = self.0.lock().await;
|
||||
let mut continuations = self.continuations.lock().await;
|
||||
if !matches!(continuations.get(guid), Some(RpcContinuation::WebSocket(_))) {
|
||||
return None;
|
||||
}
|
||||
let Some(RpcContinuation::WebSocket(x)) = continuations.remove(guid) else {
|
||||
return None;
|
||||
};
|
||||
x.get().await
|
||||
let handler = x.get().await?;
|
||||
let shutdown = self.shutdown.as_ref().map(|s| s.subscribe());
|
||||
Some(Box::new(move |ws| {
|
||||
let mut fut = handler(ws);
|
||||
fut.shutdown = shutdown;
|
||||
fut
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_rest_handler(&self, guid: &Guid) -> Option<RestHandler> {
|
||||
let mut continuations: tokio::sync::MutexGuard<'_, BTreeMap<Guid, RpcContinuation>> =
|
||||
self.0.lock().await;
|
||||
let mut continuations = self.continuations.lock().await;
|
||||
if !matches!(continuations.get(guid), Some(RpcContinuation::Rest(_))) {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ pub async fn list_disks(ctx: SetupContext) -> Result<Vec<DiskInfo>, Error> {
|
||||
async fn setup_init(
|
||||
ctx: &SetupContext,
|
||||
password: Option<String>,
|
||||
kiosk: Option<bool>,
|
||||
kiosk: bool,
|
||||
hostname: Option<ServerHostnameInfo>,
|
||||
init_phases: InitPhases,
|
||||
) -> Result<(AccountInfo, InitResult), Error> {
|
||||
@@ -137,9 +137,8 @@ async fn setup_init(
|
||||
account.save(m)?;
|
||||
let info = m.as_public_mut().as_server_info_mut();
|
||||
info.as_password_hash_mut().ser(&account.password)?;
|
||||
if let Some(kiosk) = kiosk {
|
||||
info.as_kiosk_mut().ser(&Some(kiosk))?;
|
||||
}
|
||||
info.as_kiosk_mut()
|
||||
.ser(&Some(kiosk).filter(|_| &*PLATFORM != "raspberrypi"))?;
|
||||
if let Some(language) = language.clone() {
|
||||
info.as_language_mut().ser(&Some(language))?;
|
||||
}
|
||||
@@ -174,8 +173,7 @@ async fn setup_init(
|
||||
pub struct AttachParams {
|
||||
pub password: Option<EncryptedWire>,
|
||||
pub guid: InternedString,
|
||||
#[ts(optional)]
|
||||
pub kiosk: Option<bool>,
|
||||
pub kiosk: bool,
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
@@ -411,8 +409,7 @@ pub struct SetupExecuteParams {
|
||||
guid: InternedString,
|
||||
password: Option<EncryptedWire>,
|
||||
recovery_source: Option<RecoverySource<EncryptedWire>>,
|
||||
#[ts(optional)]
|
||||
kiosk: Option<bool>,
|
||||
kiosk: bool,
|
||||
name: Option<InternedString>,
|
||||
hostname: Option<InternedString>,
|
||||
}
|
||||
@@ -549,7 +546,7 @@ pub async fn execute_inner(
|
||||
guid: InternedString,
|
||||
password: Option<String>,
|
||||
recovery_source: Option<RecoverySource<String>>,
|
||||
kiosk: Option<bool>,
|
||||
kiosk: bool,
|
||||
hostname: Option<ServerHostnameInfo>,
|
||||
) -> Result<(SetupResult, RpcContext), Error> {
|
||||
let progress = &ctx.progress;
|
||||
@@ -622,7 +619,7 @@ async fn fresh_setup(
|
||||
ctx: &SetupContext,
|
||||
guid: InternedString,
|
||||
password: &str,
|
||||
kiosk: Option<bool>,
|
||||
kiosk: bool,
|
||||
hostname: Option<ServerHostnameInfo>,
|
||||
SetupExecuteProgress {
|
||||
init_phases,
|
||||
@@ -633,7 +630,6 @@ async fn fresh_setup(
|
||||
let account = AccountInfo::new(password, root_ca_start_time().await, hostname)?;
|
||||
|
||||
let db = ctx.db().await?;
|
||||
let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi");
|
||||
sync_kiosk(kiosk).await?;
|
||||
|
||||
let language = ctx.language.peek(|a| a.clone());
|
||||
@@ -684,7 +680,7 @@ async fn recover(
|
||||
recovery_source: BackupTargetFS,
|
||||
server_id: String,
|
||||
recovery_password: String,
|
||||
kiosk: Option<bool>,
|
||||
kiosk: bool,
|
||||
hostname: Option<ServerHostnameInfo>,
|
||||
progress: SetupExecuteProgress,
|
||||
) -> Result<(SetupResult, RpcContext), Error> {
|
||||
@@ -709,7 +705,7 @@ async fn migrate(
|
||||
guid: InternedString,
|
||||
old_guid: &str,
|
||||
password: Option<String>,
|
||||
kiosk: Option<bool>,
|
||||
kiosk: bool,
|
||||
hostname: Option<ServerHostnameInfo>,
|
||||
SetupExecuteProgress {
|
||||
init_phases,
|
||||
|
||||
@@ -319,14 +319,12 @@ pub fn kernel_logs<C: Context + AsRef<RpcContinuations>>() -> ParentHandler<C, L
|
||||
const DISABLE_KIOSK_PATH: &str =
|
||||
"/media/startos/config/overlay/etc/systemd/system/getty@tty1.service.d/autologin.conf";
|
||||
|
||||
pub async fn sync_kiosk(kiosk: Option<bool>) -> Result<(), Error> {
|
||||
if let Some(kiosk) = kiosk {
|
||||
pub async fn sync_kiosk(kiosk: bool) -> Result<(), Error> {
|
||||
if kiosk {
|
||||
enable_kiosk().await?;
|
||||
} else {
|
||||
disable_kiosk().await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ impl TunnelContext {
|
||||
listen,
|
||||
db,
|
||||
datadir,
|
||||
rpc_continuations: RpcContinuations::new(),
|
||||
rpc_continuations: RpcContinuations::new(None),
|
||||
open_authed_continuations: OpenAuthedContinuations::new(),
|
||||
ephemeral_sessions: SyncMutex::new(Sessions::new()),
|
||||
net_iface,
|
||||
|
||||
@@ -13,7 +13,6 @@ use futures::{FutureExt, Stream, StreamExt, ready};
|
||||
use http::header::CONTENT_LENGTH;
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use imbl_value::InternedString;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::sync::watch;
|
||||
|
||||
@@ -23,6 +22,7 @@ use crate::progress::{PhaseProgressTrackerHandle, ProgressUnits};
|
||||
use crate::rpc_continuations::{Guid, RpcContinuation};
|
||||
use crate::s9pk::merkle_archive::source::ArchiveSource;
|
||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::{FileCursor, MultiCursorFile};
|
||||
use crate::util::direct_io::DirectIoFile;
|
||||
use crate::util::io::{TmpDir, create_file};
|
||||
|
||||
pub async fn upload(
|
||||
@@ -69,16 +69,6 @@ impl Progress {
|
||||
false
|
||||
}
|
||||
}
|
||||
fn handle_write(&mut self, res: &std::io::Result<usize>) -> bool {
|
||||
match res {
|
||||
Ok(a) => {
|
||||
self.written += *a as u64;
|
||||
self.tracker += *a as u64;
|
||||
true
|
||||
}
|
||||
Err(e) => self.handle_error(e),
|
||||
}
|
||||
}
|
||||
async fn expected_size(watch: &mut watch::Receiver<Self>) -> Option<u64> {
|
||||
watch
|
||||
.wait_for(|progress| progress.error.is_some() || progress.expected_size.is_some())
|
||||
@@ -192,16 +182,19 @@ impl UploadingFile {
|
||||
complete: false,
|
||||
});
|
||||
let file = create_file(path).await?;
|
||||
let multi_cursor = MultiCursorFile::open(&file).await?;
|
||||
let direct_file = DirectIoFile::from_tokio_file(file).await?;
|
||||
let uploading = Self {
|
||||
tmp_dir: None,
|
||||
file: MultiCursorFile::open(&file).await?,
|
||||
file: multi_cursor,
|
||||
progress: progress.1,
|
||||
};
|
||||
Ok((
|
||||
UploadHandle {
|
||||
tmp_dir: None,
|
||||
file,
|
||||
file: direct_file,
|
||||
progress: progress.0,
|
||||
last_synced: 0,
|
||||
},
|
||||
uploading,
|
||||
))
|
||||
@@ -346,8 +339,9 @@ impl AsyncSeek for UploadingFileReader {
|
||||
pub struct UploadHandle {
|
||||
tmp_dir: Option<Arc<TmpDir>>,
|
||||
#[pin]
|
||||
file: File,
|
||||
file: DirectIoFile,
|
||||
progress: watch::Sender<Progress>,
|
||||
last_synced: u64,
|
||||
}
|
||||
impl UploadHandle {
|
||||
pub async fn upload(&mut self, request: Request) {
|
||||
@@ -394,6 +388,19 @@ impl UploadHandle {
|
||||
if let Err(e) = self.file.sync_all().await {
|
||||
self.progress.send_if_modified(|p| p.handle_error(&e));
|
||||
}
|
||||
// Update progress with final synced bytes
|
||||
self.update_sync_progress();
|
||||
}
|
||||
fn update_sync_progress(&mut self) {
|
||||
let synced = self.file.bytes_synced();
|
||||
let delta = synced - self.last_synced;
|
||||
if delta > 0 {
|
||||
self.last_synced = synced;
|
||||
self.progress.send_modify(|p| {
|
||||
p.written += delta;
|
||||
p.tracker += delta;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
#[pin_project::pinned_drop]
|
||||
@@ -410,13 +417,23 @@ impl AsyncWrite for UploadHandle {
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, std::io::Error>> {
|
||||
let this = self.project();
|
||||
match this.file.poll_write(cx, buf) {
|
||||
Poll::Ready(res) => {
|
||||
this.progress
|
||||
.send_if_modified(|progress| progress.handle_write(&res));
|
||||
Poll::Ready(res)
|
||||
// Update progress based on bytes actually flushed to disk
|
||||
let synced = this.file.bytes_synced();
|
||||
let delta = synced - *this.last_synced;
|
||||
if delta > 0 {
|
||||
*this.last_synced = synced;
|
||||
this.progress.send_modify(|p| {
|
||||
p.written += delta;
|
||||
p.tracker += delta;
|
||||
});
|
||||
}
|
||||
Poll::Pending => Poll::Pending,
|
||||
match this.file.poll_write(cx, buf) {
|
||||
Poll::Ready(Err(e)) => {
|
||||
this.progress
|
||||
.send_if_modified(|progress| progress.handle_error(&e));
|
||||
Poll::Ready(Err(e))
|
||||
}
|
||||
a => a,
|
||||
}
|
||||
}
|
||||
fn poll_flush(
|
||||
|
||||
298
core/src/util/direct_io.rs
Normal file
298
core/src/util/direct_io.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
use std::alloc::Layout;
|
||||
use std::io::Write;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use tokio::io::AsyncWrite;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
const BLOCK_SIZE: usize = 4096;
|
||||
const BUF_CAP: usize = 256 * 1024; // 256KB
|
||||
|
||||
/// Aligned buffer for O_DIRECT I/O.
|
||||
struct AlignedBuf {
|
||||
ptr: *mut u8,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
// SAFETY: We have exclusive ownership of the allocation.
|
||||
unsafe impl Send for AlignedBuf {}
|
||||
|
||||
impl AlignedBuf {
|
||||
fn new() -> Self {
|
||||
let layout = Layout::from_size_align(BUF_CAP, BLOCK_SIZE).unwrap();
|
||||
// SAFETY: layout has non-zero size
|
||||
let ptr = unsafe { std::alloc::alloc(layout) };
|
||||
if ptr.is_null() {
|
||||
std::alloc::handle_alloc_error(layout);
|
||||
}
|
||||
Self { ptr, len: 0 }
|
||||
}
|
||||
|
||||
fn as_slice(&self) -> &[u8] {
|
||||
// SAFETY: ptr is valid for len bytes, properly aligned, exclusively owned
|
||||
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
|
||||
}
|
||||
|
||||
fn push(&mut self, data: &[u8]) -> usize {
|
||||
let n = data.len().min(BUF_CAP - self.len);
|
||||
// SAFETY: src and dst don't overlap, both valid for n bytes
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(data.as_ptr(), self.ptr.add(self.len), n);
|
||||
}
|
||||
self.len += n;
|
||||
n
|
||||
}
|
||||
|
||||
fn aligned_len(&self) -> usize {
|
||||
self.len & !(BLOCK_SIZE - 1)
|
||||
}
|
||||
|
||||
fn drain_front(&mut self, n: usize) {
|
||||
debug_assert!(n <= self.len);
|
||||
let remaining = self.len - n;
|
||||
if remaining > 0 {
|
||||
// SAFETY: regions may overlap, so we use copy (memmove)
|
||||
unsafe {
|
||||
std::ptr::copy(self.ptr.add(n), self.ptr, remaining);
|
||||
}
|
||||
}
|
||||
self.len = remaining;
|
||||
}
|
||||
|
||||
/// Extract aligned data into a new buffer for flushing, leaving remainder.
|
||||
fn take_aligned(&mut self) -> Option<(AlignedBuf, u64)> {
|
||||
let aligned = self.aligned_len();
|
||||
if aligned == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut flush_buf = AlignedBuf::new();
|
||||
flush_buf.push(&self.as_slice()[..aligned]);
|
||||
self.drain_front(aligned);
|
||||
Some((flush_buf, aligned as u64))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AlignedBuf {
|
||||
fn drop(&mut self) {
|
||||
let layout = Layout::from_size_align(BUF_CAP, BLOCK_SIZE).unwrap();
|
||||
// SAFETY: ptr was allocated with this layout in new()
|
||||
unsafe { std::alloc::dealloc(self.ptr, layout) };
|
||||
}
|
||||
}
|
||||
|
||||
enum FileState {
|
||||
Idle(std::fs::File),
|
||||
Flushing(JoinHandle<std::io::Result<(std::fs::File, u64)>>),
|
||||
Done,
|
||||
}
|
||||
|
||||
/// A file writer that uses O_DIRECT to bypass the kernel page cache.
|
||||
///
|
||||
/// Buffers writes in an aligned buffer and flushes to disk in the background.
|
||||
/// New writes can proceed while a flush is in progress (double-buffering).
|
||||
/// Progress is tracked via [`bytes_synced`](Self::bytes_synced), which reflects
|
||||
/// bytes actually written to disk.
|
||||
pub struct DirectIoFile {
|
||||
file_state: FileState,
|
||||
buf: AlignedBuf,
|
||||
synced: u64,
|
||||
}
|
||||
|
||||
impl DirectIoFile {
|
||||
fn new(file: std::fs::File) -> Self {
|
||||
Self {
|
||||
file_state: FileState::Idle(file),
|
||||
buf: AlignedBuf::new(),
|
||||
synced: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an existing tokio File into a DirectIoFile by adding O_DIRECT.
|
||||
pub async fn from_tokio_file(file: tokio::fs::File) -> std::io::Result<Self> {
|
||||
let std_file = file.into_std().await;
|
||||
let fd = std_file.as_raw_fd();
|
||||
// SAFETY: fd is valid, F_GETFL/F_SETFL are standard fcntl ops
|
||||
unsafe {
|
||||
let flags = libc::fcntl(fd, libc::F_GETFL);
|
||||
if flags == -1 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
if libc::fcntl(fd, libc::F_SETFL, flags | libc::O_DIRECT) == -1 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
Ok(Self::new(std_file))
|
||||
}
|
||||
|
||||
/// Number of bytes confirmed written to disk.
|
||||
pub fn bytes_synced(&self) -> u64 {
|
||||
self.synced
|
||||
}
|
||||
|
||||
/// Flush any remaining buffered data and sync to disk.
|
||||
///
|
||||
/// Removes the O_DIRECT flag for the final partial-block write, then
|
||||
/// calls fsync. Updates `bytes_synced` to the final total.
|
||||
pub async fn sync_all(&mut self) -> std::io::Result<()> {
|
||||
// Wait for any in-flight flush
|
||||
self.await_flush().await?;
|
||||
|
||||
let FileState::Idle(file) = std::mem::replace(&mut self.file_state, FileState::Done)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut buf = std::mem::replace(&mut self.buf, AlignedBuf::new());
|
||||
let remaining = buf.len as u64;
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut file = file;
|
||||
|
||||
// Write any aligned portion
|
||||
let aligned = buf.aligned_len();
|
||||
if aligned > 0 {
|
||||
let slice = unsafe { std::slice::from_raw_parts(buf.ptr, aligned) };
|
||||
file.write_all(slice)?;
|
||||
buf.drain_front(aligned);
|
||||
}
|
||||
|
||||
// Write remainder with O_DIRECT disabled
|
||||
if buf.len > 0 {
|
||||
let fd = file.as_raw_fd();
|
||||
// SAFETY: fd is valid, F_GETFL/F_SETFL are standard fcntl ops
|
||||
unsafe {
|
||||
let flags = libc::fcntl(fd, libc::F_GETFL);
|
||||
libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_DIRECT);
|
||||
}
|
||||
file.write_all(buf.as_slice())?;
|
||||
}
|
||||
|
||||
file.sync_all()
|
||||
})
|
||||
.await
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))??;
|
||||
|
||||
self.synced += remaining;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn await_flush(&mut self) -> std::io::Result<()> {
|
||||
if let FileState::Flushing(handle) = &mut self.file_state {
|
||||
let (file, flushed) = handle
|
||||
.await
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))??;
|
||||
self.synced += flushed;
|
||||
self.file_state = FileState::Idle(file);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Non-blocking poll: try to complete a pending flush.
|
||||
/// Returns Ready(Ok(())) if idle (or just became idle), Pending if still flushing.
|
||||
fn poll_complete_flush(&mut self, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
|
||||
if let FileState::Flushing(handle) = &mut self.file_state {
|
||||
match Pin::new(handle).poll(cx) {
|
||||
Poll::Ready(Ok(Ok((file, flushed)))) => {
|
||||
self.synced += flushed;
|
||||
self.file_state = FileState::Idle(file);
|
||||
}
|
||||
Poll::Ready(Ok(Err(e))) => {
|
||||
self.file_state = FileState::Done;
|
||||
return Poll::Ready(Err(e));
|
||||
}
|
||||
Poll::Ready(Err(e)) => {
|
||||
self.file_state = FileState::Done;
|
||||
return Poll::Ready(Err(std::io::Error::new(std::io::ErrorKind::Other, e)));
|
||||
}
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
/// Start a background flush of aligned data if the file is idle.
|
||||
fn maybe_start_flush(&mut self) {
|
||||
if !matches!(self.file_state, FileState::Idle(_)) {
|
||||
return;
|
||||
}
|
||||
let Some((flush_buf, count)) = self.buf.take_aligned() else {
|
||||
return;
|
||||
};
|
||||
let FileState::Idle(file) = std::mem::replace(&mut self.file_state, FileState::Done)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let handle = tokio::task::spawn_blocking(move || {
|
||||
let mut file = file;
|
||||
file.write_all(flush_buf.as_slice())?;
|
||||
Ok((file, count))
|
||||
});
|
||||
self.file_state = FileState::Flushing(handle);
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for DirectIoFile {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<std::io::Result<usize>> {
|
||||
// Try to complete any pending flush (non-blocking, registers waker)
|
||||
match self.poll_complete_flush(cx) {
|
||||
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
|
||||
_ => {} // Pending is fine — we can still accept writes into the buffer
|
||||
}
|
||||
|
||||
// If file just became idle and buffer has aligned data, start a flush
|
||||
// to free buffer space before accepting new data
|
||||
self.maybe_start_flush();
|
||||
|
||||
// Accept data into the buffer
|
||||
let n = self.buf.push(buf);
|
||||
if n == 0 {
|
||||
// Buffer full, must wait for flush to complete and free space.
|
||||
// Waker was already registered by poll_complete_flush above.
|
||||
return Poll::Pending;
|
||||
}
|
||||
|
||||
// If file is idle and we now have aligned data, start flushing
|
||||
self.maybe_start_flush();
|
||||
|
||||
Poll::Ready(Ok(n))
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
match self.poll_complete_flush(cx) {
|
||||
Poll::Pending => return Poll::Pending,
|
||||
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
|
||||
Poll::Ready(Ok(())) => {}
|
||||
}
|
||||
|
||||
if self.buf.aligned_len() > 0 {
|
||||
self.maybe_start_flush();
|
||||
// Poll the just-started flush
|
||||
return self.poll_complete_flush(cx).map(|r| r.map(|_| ()));
|
||||
}
|
||||
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
match self.poll_complete_flush(cx) {
|
||||
Poll::Pending => return Poll::Pending,
|
||||
Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
|
||||
Poll::Ready(Ok(())) => {}
|
||||
}
|
||||
|
||||
self.file_state = FileState::Done;
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ pub mod collections;
|
||||
pub mod cpupower;
|
||||
pub mod crypto;
|
||||
pub mod data_url;
|
||||
pub mod direct_io;
|
||||
pub mod future;
|
||||
pub mod http_reader;
|
||||
pub mod io;
|
||||
|
||||
@@ -60,8 +60,9 @@ mod v0_4_0_alpha_17;
|
||||
mod v0_4_0_alpha_18;
|
||||
mod v0_4_0_alpha_19;
|
||||
mod v0_4_0_alpha_20;
|
||||
mod v0_4_0_alpha_21;
|
||||
|
||||
pub type Current = v0_4_0_alpha_20::Version; // VERSION_BUMP
|
||||
pub type Current = v0_4_0_alpha_21::Version; // VERSION_BUMP
|
||||
|
||||
impl Current {
|
||||
#[instrument(skip(self, db))]
|
||||
@@ -189,7 +190,8 @@ enum Version {
|
||||
V0_4_0_alpha_17(Wrapper<v0_4_0_alpha_17::Version>),
|
||||
V0_4_0_alpha_18(Wrapper<v0_4_0_alpha_18::Version>),
|
||||
V0_4_0_alpha_19(Wrapper<v0_4_0_alpha_19::Version>),
|
||||
V0_4_0_alpha_20(Wrapper<v0_4_0_alpha_20::Version>), // VERSION_BUMP
|
||||
V0_4_0_alpha_20(Wrapper<v0_4_0_alpha_20::Version>),
|
||||
V0_4_0_alpha_21(Wrapper<v0_4_0_alpha_21::Version>), // VERSION_BUMP
|
||||
Other(exver::Version),
|
||||
}
|
||||
|
||||
@@ -252,7 +254,8 @@ impl Version {
|
||||
Self::V0_4_0_alpha_17(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_4_0_alpha_18(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_4_0_alpha_19(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_4_0_alpha_20(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
|
||||
Self::V0_4_0_alpha_20(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_4_0_alpha_21(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
|
||||
Self::Other(v) => {
|
||||
return Err(Error::new(
|
||||
eyre!("unknown version {v}"),
|
||||
@@ -307,7 +310,8 @@ impl Version {
|
||||
Version::V0_4_0_alpha_17(Wrapper(x)) => x.semver(),
|
||||
Version::V0_4_0_alpha_18(Wrapper(x)) => x.semver(),
|
||||
Version::V0_4_0_alpha_19(Wrapper(x)) => x.semver(),
|
||||
Version::V0_4_0_alpha_20(Wrapper(x)) => x.semver(), // VERSION_BUMP
|
||||
Version::V0_4_0_alpha_20(Wrapper(x)) => x.semver(),
|
||||
Version::V0_4_0_alpha_21(Wrapper(x)) => x.semver(), // VERSION_BUMP
|
||||
Version::Other(x) => x.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,8 @@ pub struct Version;
|
||||
|
||||
impl VersionT for Version {
|
||||
type Previous = v0_3_5_2::Version;
|
||||
type PreUpRes = (AccountInfo, SshKeys, CifsTargets);
|
||||
/// (package_id, host_id, expanded_key)
|
||||
type PreUpRes = (AccountInfo, SshKeys, CifsTargets, Vec<(String, String, [u8; 64])>);
|
||||
fn semver(self) -> exver::Version {
|
||||
V0_3_6_alpha_0.clone()
|
||||
}
|
||||
@@ -158,15 +159,17 @@ impl VersionT for Version {
|
||||
|
||||
let cifs = previous_cifs(&pg).await?;
|
||||
|
||||
let tor_keys = previous_tor_keys(&pg).await?;
|
||||
|
||||
Command::new("systemctl")
|
||||
.arg("stop")
|
||||
.arg("postgresql@*.service")
|
||||
.invoke(crate::ErrorKind::Database)
|
||||
.await?;
|
||||
|
||||
Ok((account, ssh_keys, cifs))
|
||||
Ok((account, ssh_keys, cifs, tor_keys))
|
||||
}
|
||||
fn up(self, db: &mut Value, (account, ssh_keys, cifs): Self::PreUpRes) -> Result<Value, Error> {
|
||||
fn up(self, db: &mut Value, (account, ssh_keys, cifs, tor_keys): Self::PreUpRes) -> Result<Value, Error> {
|
||||
let prev_package_data = db["package-data"].clone();
|
||||
|
||||
let wifi = json!({
|
||||
@@ -183,6 +186,11 @@ impl VersionT for Version {
|
||||
"shuttingDown": db["server-info"]["status-info"]["shutting-down"],
|
||||
"restarting": db["server-info"]["status-info"]["restarting"],
|
||||
});
|
||||
let tor_address: String = from_value(db["server-info"]["tor-address"].clone())?;
|
||||
let onion_address = tor_address
|
||||
.replace("https://", "")
|
||||
.replace("http://", "")
|
||||
.replace(".onion/", "");
|
||||
let server_info = {
|
||||
let mut server_info = json!({
|
||||
"arch": db["server-info"]["arch"],
|
||||
@@ -196,15 +204,9 @@ impl VersionT for Version {
|
||||
});
|
||||
|
||||
server_info["postInitMigrationTodos"] = json!({});
|
||||
let tor_address: String = from_value(db["server-info"]["tor-address"].clone())?;
|
||||
// Maybe we do this like the Public::init does
|
||||
server_info["torAddress"] = json!(tor_address);
|
||||
server_info["onionAddress"] = json!(
|
||||
tor_address
|
||||
.replace("https://", "")
|
||||
.replace("http://", "")
|
||||
.replace(".onion/", "")
|
||||
);
|
||||
server_info["torAddress"] = json!(&tor_address);
|
||||
server_info["onionAddress"] = json!(&onion_address);
|
||||
server_info["networkInterfaces"] = json!({});
|
||||
server_info["statusInfo"] = status_info;
|
||||
server_info["wifi"] = wifi;
|
||||
@@ -233,6 +235,30 @@ impl VersionT for Version {
|
||||
let private = {
|
||||
let mut value = json!({});
|
||||
value["keyStore"] = crate::dbg!(to_value(&keystore)?);
|
||||
// Preserve tor onion keys so later migrations (v0_4_0_alpha_20) can
|
||||
// include them in onion-migration.json for the tor service.
|
||||
if !tor_keys.is_empty() {
|
||||
let mut onion_map: Value = json!({});
|
||||
let onion_obj = onion_map.as_object_mut().unwrap();
|
||||
let mut tor_migration = imbl::Vector::<Value>::new();
|
||||
for (package_id, host_id, key_bytes) in &tor_keys {
|
||||
let onion_addr = onion_address_from_key(key_bytes);
|
||||
let encoded_key =
|
||||
base64::Engine::encode(&crate::util::serde::BASE64, key_bytes);
|
||||
onion_obj.insert(
|
||||
onion_addr.as_str().into(),
|
||||
Value::String(encoded_key.clone().into()),
|
||||
);
|
||||
tor_migration.push_back(json!({
|
||||
"hostname": &onion_addr,
|
||||
"packageId": package_id,
|
||||
"hostId": host_id,
|
||||
"key": &encoded_key,
|
||||
}));
|
||||
}
|
||||
value["keyStore"]["onion"] = onion_map;
|
||||
value["torMigration"] = Value::Array(tor_migration);
|
||||
}
|
||||
value["password"] = to_value(&account.password)?;
|
||||
value["compatS9pkKey"] =
|
||||
to_value(&crate::db::model::private::generate_developer_key())?;
|
||||
@@ -498,3 +524,109 @@ async fn previous_ssh_keys(pg: &sqlx::Pool<sqlx::Postgres>) -> Result<SshKeys, E
|
||||
};
|
||||
Ok(ssh_keys)
|
||||
}
|
||||
|
||||
/// Returns `Vec<(package_id, host_id, expanded_key)>`.
|
||||
/// Server key uses `("STARTOS", "STARTOS")`.
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn previous_tor_keys(
|
||||
pg: &sqlx::Pool<sqlx::Postgres>,
|
||||
) -> Result<Vec<(String, String, [u8; 64])>, Error> {
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// Server tor key from the account table.
|
||||
// Older installs have tor_key (64 bytes). Newer installs (post-NetworkKeys migration)
|
||||
// made tor_key nullable and use network_key (32 bytes, needs expansion) instead.
|
||||
let row = sqlx::query(r#"SELECT tor_key, network_key FROM account"#)
|
||||
.fetch_one(pg)
|
||||
.await
|
||||
.with_kind(ErrorKind::Database)?;
|
||||
if let Ok(tor_key) = row.try_get::<Vec<u8>, _>("tor_key") {
|
||||
if let Ok(key) = <[u8; 64]>::try_from(tor_key) {
|
||||
keys.push(("STARTOS".to_owned(), "STARTOS".to_owned(), key));
|
||||
}
|
||||
} else if let Ok(net_key) = row.try_get::<Vec<u8>, _>("network_key") {
|
||||
if let Ok(seed) = <[u8; 32]>::try_from(net_key) {
|
||||
keys.push((
|
||||
"STARTOS".to_owned(),
|
||||
"STARTOS".to_owned(),
|
||||
crate::util::crypto::ed25519_expand_key(&seed),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Package tor keys from the network_keys table (32-byte keys that need expansion)
|
||||
if let Ok(rows) = sqlx::query(r#"SELECT package, interface, key FROM network_keys"#)
|
||||
.fetch_all(pg)
|
||||
.await
|
||||
{
|
||||
for row in rows {
|
||||
let Ok(package) = row.try_get::<String, _>("package") else {
|
||||
continue;
|
||||
};
|
||||
let Ok(interface) = row.try_get::<String, _>("interface") else {
|
||||
continue;
|
||||
};
|
||||
let Ok(key_bytes) = row.try_get::<Vec<u8>, _>("key") else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(seed) = <[u8; 32]>::try_from(key_bytes) {
|
||||
keys.push((
|
||||
package,
|
||||
interface,
|
||||
crate::util::crypto::ed25519_expand_key(&seed),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Package tor keys from the tor table (already 64-byte expanded keys)
|
||||
if let Ok(rows) = sqlx::query(r#"SELECT package, interface, key FROM tor"#)
|
||||
.fetch_all(pg)
|
||||
.await
|
||||
{
|
||||
for row in rows {
|
||||
let Ok(package) = row.try_get::<String, _>("package") else {
|
||||
continue;
|
||||
};
|
||||
let Ok(interface) = row.try_get::<String, _>("interface") else {
|
||||
continue;
|
||||
};
|
||||
let Ok(key_bytes) = row.try_get::<Vec<u8>, _>("key") else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(key) = <[u8; 64]>::try_from(key_bytes) {
|
||||
keys.push((package, interface, key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
/// Derive the tor v3 onion address (without .onion suffix) from a 64-byte
|
||||
/// expanded ed25519 secret key.
|
||||
fn onion_address_from_key(expanded_key: &[u8; 64]) -> String {
|
||||
use sha3::Digest;
|
||||
|
||||
// Derive public key from expanded secret key using ed25519-dalek v1
|
||||
let esk =
|
||||
ed25519_dalek_v1::ExpandedSecretKey::from_bytes(expanded_key).expect("invalid tor key");
|
||||
let pk = ed25519_dalek_v1::PublicKey::from(&esk);
|
||||
let pk_bytes = pk.to_bytes();
|
||||
|
||||
// Compute onion v3 address: base32(pubkey || checksum || version)
|
||||
// checksum = SHA3-256(".onion checksum" || pubkey || version)[0..2]
|
||||
let mut hasher = sha3::Sha3_256::new();
|
||||
hasher.update(b".onion checksum");
|
||||
hasher.update(&pk_bytes);
|
||||
hasher.update(b"\x03");
|
||||
let hash = hasher.finalize();
|
||||
|
||||
let mut raw = [0u8; 35];
|
||||
raw[..32].copy_from_slice(&pk_bytes);
|
||||
raw[32] = hash[0]; // checksum byte 0
|
||||
raw[33] = hash[1]; // checksum byte 1
|
||||
raw[34] = 0x03; // version
|
||||
|
||||
base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &raw).to_ascii_lowercase()
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ use std::path::Path;
|
||||
|
||||
use exver::{PreReleaseSegment, VersionRange};
|
||||
use imbl_value::json;
|
||||
use reqwest::Url;
|
||||
|
||||
use super::v0_3_5::V0_3_0_COMPAT;
|
||||
use super::{VersionT, v0_4_0_alpha_19};
|
||||
use crate::context::RpcContext;
|
||||
use crate::prelude::*;
|
||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref V0_4_0_alpha_20: exver::Version = exver::Version::new(
|
||||
@@ -33,7 +35,21 @@ impl VersionT for Version {
|
||||
}
|
||||
#[instrument(skip_all)]
|
||||
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
||||
// Extract onion migration data before removing it
|
||||
// Use the pre-built torMigration data from v0_3_6_alpha_0 if available.
|
||||
// This contains all (hostname, packageId, hostId, key) entries with keys
|
||||
// already resolved, avoiding the issue where packageData is empty during
|
||||
// migration (packages aren't reinstalled until post_up).
|
||||
let migration_data = if let Some(tor_migration) = db
|
||||
.get("private")
|
||||
.and_then(|p| p.get("torMigration"))
|
||||
.and_then(|t| t.as_array())
|
||||
{
|
||||
json!({
|
||||
"addresses": tor_migration.clone(),
|
||||
})
|
||||
} else {
|
||||
// Fallback for fresh installs or installs that didn't go through
|
||||
// v0_3_6_alpha_0 with the torMigration field.
|
||||
let onion_store = db
|
||||
.get("private")
|
||||
.and_then(|p| p.get("keyStore"))
|
||||
@@ -57,11 +73,16 @@ impl VersionT for Version {
|
||||
let key = onion_store
|
||||
.get(hostname)
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default();
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("missing tor key for onion address {hostname}"),
|
||||
ErrorKind::Database,
|
||||
)
|
||||
})?;
|
||||
addresses.push_back(json!({
|
||||
"hostname": hostname,
|
||||
"packageId": "STARTOS",
|
||||
"hostId": "STARTOS",
|
||||
"hostId": "startos-ui",
|
||||
"key": key,
|
||||
}));
|
||||
}
|
||||
@@ -83,7 +104,14 @@ impl VersionT for Version {
|
||||
let key = onion_store
|
||||
.get(hostname)
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default();
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!(
|
||||
"missing tor key for onion address {hostname}"
|
||||
),
|
||||
ErrorKind::Database,
|
||||
)
|
||||
})?;
|
||||
addresses.push_back(json!({
|
||||
"hostname": hostname,
|
||||
"packageId": &**package_id,
|
||||
@@ -98,9 +126,15 @@ impl VersionT for Version {
|
||||
}
|
||||
}
|
||||
|
||||
let migration_data = json!({
|
||||
json!({
|
||||
"addresses": addresses,
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
// Clean up torMigration from private
|
||||
if let Some(private) = db.get_mut("private").and_then(|p| p.as_object_mut()) {
|
||||
private.remove("torMigration");
|
||||
}
|
||||
|
||||
// Remove onions and tor-related fields from server host
|
||||
if let Some(host) = db
|
||||
@@ -200,7 +234,7 @@ impl VersionT for Version {
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn post_up(self, _ctx: &RpcContext, input: Value) -> Result<(), Error> {
|
||||
async fn post_up(self, ctx: &RpcContext, input: Value) -> Result<(), Error> {
|
||||
let path = Path::new(
|
||||
"/media/startos/data/package-data/volumes/tor/data/startos/onion-migration.json",
|
||||
);
|
||||
@@ -209,6 +243,53 @@ impl VersionT for Version {
|
||||
|
||||
crate::util::io::write_file_atomic(path, json).await?;
|
||||
|
||||
// Sideload the bundled tor s9pk
|
||||
let s9pk_path_str = format!("/usr/lib/startos/tor_{}.s9pk", crate::ARCH);
|
||||
let s9pk_path = Path::new(&s9pk_path_str);
|
||||
if tokio::fs::metadata(s9pk_path).await.is_ok() {
|
||||
if let Err(e) = async {
|
||||
let package_s9pk = tokio::fs::File::open(s9pk_path).await?;
|
||||
let file = MultiCursorFile::open(&package_s9pk).await?;
|
||||
|
||||
let key = ctx.db.peek().await.into_private().into_developer_key();
|
||||
let registry_url =
|
||||
Url::parse("https://registry.start9.com/").with_kind(ErrorKind::ParseUrl)?;
|
||||
|
||||
ctx.services
|
||||
.install(
|
||||
ctx.clone(),
|
||||
|| crate::s9pk::load(file.clone(), || Ok(key.de()?.0), None),
|
||||
None,
|
||||
None::<crate::util::Never>,
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
.await?
|
||||
.await?;
|
||||
|
||||
// Set the marketplace URL on the installed tor package
|
||||
let tor_id = "tor".parse::<crate::PackageId>()?;
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
if let Some(pkg) =
|
||||
db.as_public_mut().as_package_data_mut().as_idx_mut(&tor_id)
|
||||
{
|
||||
pkg.as_registry_mut().ser(&Some(registry_url))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error installing tor package: {e}");
|
||||
tracing::debug!("{e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
||||
|
||||
37
core/src/version/v0_4_0_alpha_21.rs
Normal file
37
core/src/version/v0_4_0_alpha_21.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use exver::{PreReleaseSegment, VersionRange};
|
||||
|
||||
use super::v0_3_5::V0_3_0_COMPAT;
|
||||
use super::{VersionT, v0_4_0_alpha_20};
|
||||
use crate::prelude::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref V0_4_0_alpha_21: exver::Version = exver::Version::new(
|
||||
[0, 4, 0],
|
||||
[PreReleaseSegment::String("alpha".into()), 21.into()]
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Version;
|
||||
|
||||
impl VersionT for Version {
|
||||
type Previous = v0_4_0_alpha_20::Version;
|
||||
type PreUpRes = ();
|
||||
|
||||
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn semver(self) -> exver::Version {
|
||||
V0_4_0_alpha_21.clone()
|
||||
}
|
||||
fn compat(self) -> &'static VersionRange {
|
||||
&V0_3_0_COMPAT
|
||||
}
|
||||
#[instrument(skip_all)]
|
||||
fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## 0.4.0-beta.60 — StartOS v0.4.0-alpha.20 (2026-03-16)
|
||||
|
||||
### Added
|
||||
|
||||
- Tunnel TS type exports and port forward labels
|
||||
- Secure Boot MOK key enrollment fields in `SetupInfo`
|
||||
|
||||
### Changed
|
||||
|
||||
- Consolidated `Watchable` base class with generic `map`/`eq` support; renamed `call` to `fetch`
|
||||
- Moved `GetServiceManifest` and `GetSslCertificate` from `package/` to `base/`
|
||||
- Simplified `getServiceInterface`, `getServiceInterfaces`, `GetOutboundGateway`, `GetSystemSmtp`, and `fileHelper` using `Watchable` base class
|
||||
- Simplified SDK Makefile with rsync
|
||||
|
||||
### Fixed
|
||||
|
||||
- Added `restart_again` flag to `DesiredStatus::Restarting`
|
||||
|
||||
## 0.4.0-beta.59 — StartOS v0.4.0-alpha.20 (2026-03-06)
|
||||
|
||||
### Added
|
||||
|
||||
@@ -4,5 +4,5 @@ import type { EncryptedWire } from './EncryptedWire'
|
||||
export type AttachParams = {
|
||||
password: EncryptedWire | null
|
||||
guid: string
|
||||
kiosk?: boolean
|
||||
kiosk: boolean
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export type SetupExecuteParams = {
|
||||
guid: string
|
||||
password: EncryptedWire | null
|
||||
recoverySource: RecoverySource<EncryptedWire> | null
|
||||
kiosk?: boolean
|
||||
kiosk: boolean
|
||||
name: string | null
|
||||
hostname: string | null
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ import { getOwnServiceInterfaces } from '../../base/lib/util/getServiceInterface
|
||||
import { Volumes, createVolumes } from './util/Volume'
|
||||
|
||||
/** The minimum StartOS version required by this SDK release */
|
||||
export const OSVersion = testTypeVersion('0.4.0-alpha.20')
|
||||
export const OSVersion = testTypeVersion('0.4.0-alpha.21')
|
||||
|
||||
// prettier-ignore
|
||||
type AnyNeverCond<T extends any[], Then, Else> =
|
||||
|
||||
4
sdk/package/package-lock.json
generated
4
sdk/package/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.59",
|
||||
"version": "0.4.0-beta.60",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.59",
|
||||
"version": "0.4.0-beta.60",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^3.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-beta.59",
|
||||
"version": "0.4.0-beta.60",
|
||||
"description": "Software development kit to facilitate packaging services for StartOS",
|
||||
"main": "./package/lib/index.js",
|
||||
"types": "./package/lib/index.d.ts",
|
||||
|
||||
24
web/package-lock.json
generated
24
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "startos-ui",
|
||||
"version": "0.4.0-alpha.20",
|
||||
"version": "0.4.0-alpha.21",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "startos-ui",
|
||||
"version": "0.4.0-alpha.20",
|
||||
"version": "0.4.0-alpha.21",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^21.2.1",
|
||||
@@ -836,6 +836,7 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -8322,6 +8323,7 @@
|
||||
"integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cli-truncate": "^5.0.0",
|
||||
"colorette": "^2.0.20",
|
||||
@@ -12524,24 +12526,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "18.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "startos-ui",
|
||||
"version": "0.4.0-alpha.20",
|
||||
"version": "0.4.0-alpha.21",
|
||||
"author": "Start9 Labs, Inc",
|
||||
"homepage": "https://start9.com/",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -72,6 +72,7 @@ export class StateService {
|
||||
await this.api.attach({
|
||||
guid: this.dataDriveGuid,
|
||||
password: password ? await this.api.encrypt(password) : null,
|
||||
kiosk: this.kiosk,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,6 +107,7 @@ export class StateService {
|
||||
name,
|
||||
hostname,
|
||||
recoverySource,
|
||||
kiosk: this.kiosk,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user