mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +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
|
tmp
|
||||||
web/.i18n-checked
|
web/.i18n-checked
|
||||||
docs/USER.md
|
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
|
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
|
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)
|
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/)
|
IMAGE_RECIPE_SRC := $(call ls-files, build/image-recipe/)
|
||||||
STARTD_SRC := core/startd.service $(BUILD_SRC)
|
STARTD_SRC := core/startd.service $(BUILD_SRC)
|
||||||
CORE_SRC := $(call ls-files, core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE)
|
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 mkdir,$(DESTDIR)/lib/systemd/system)
|
||||||
$(call cp,core/startd.service,$(DESTDIR)/lib/systemd/system/startd.service)
|
$(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 mkdir,$(DESTDIR)/usr/lib)
|
||||||
$(call rm,$(DESTDIR)/usr/lib/startos)
|
$(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)
|
$(FIRMWARE_ROMS): build/lib/firmware.json ./build/download-firmware.sh $(PLATFORM_FILE)
|
||||||
./build/download-firmware.sh $(PLATFORM)
|
./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)
|
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
|
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build/build-startbox.sh
|
||||||
touch core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox
|
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
|
ff02::2 ip6-allrouters
|
||||||
EOT
|
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)
|
# Installer marker file (used by installed GRUB to detect the live USB)
|
||||||
mkdir -p config/includes.binary
|
mkdir -p config/includes.binary
|
||||||
touch config/includes.binary/.startos-installer
|
touch config/includes.binary/.startos-installer
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ check_variables () {
|
|||||||
main () {
|
main () {
|
||||||
get_variables
|
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
|
if ! check_variables; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -74,9 +79,6 @@ main () {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fix GPT backup header to reflect new partition layout
|
|
||||||
sgdisk -e "$ROOT_DEV" 2>/dev/null || true
|
|
||||||
|
|
||||||
mount / -o remount,rw
|
mount / -o remount,rw
|
||||||
|
|
||||||
btrfs filesystem resize max /media/startos/root
|
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": {
|
"../sdk/dist": {
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "0.4.0-beta.59",
|
"version": "0.4.0-beta.60",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iarna/toml": "^3.0.0",
|
"@iarna/toml": "^3.0.0",
|
||||||
|
|||||||
22
core/Cargo.lock
generated
22
core/Cargo.lock
generated
@@ -3376,6 +3376,15 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "keccak"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
|
||||||
|
dependencies = [
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kv"
|
name = "kv"
|
||||||
version = "0.24.0"
|
version = "0.24.0"
|
||||||
@@ -5985,6 +5994,16 @@ dependencies = [
|
|||||||
"digest 0.10.7",
|
"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]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -6415,7 +6434,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "start-os"
|
name = "start-os"
|
||||||
version = "0.4.0-alpha.20"
|
version = "0.4.0-alpha.21"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"async-acme",
|
"async-acme",
|
||||||
@@ -6519,6 +6538,7 @@ dependencies = [
|
|||||||
"serde_yml",
|
"serde_yml",
|
||||||
"sha-crypt",
|
"sha-crypt",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
|
"sha3",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
"socket2 0.6.2",
|
"socket2 0.6.2",
|
||||||
"socks5-impl",
|
"socks5-impl",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ license = "MIT"
|
|||||||
name = "start-os"
|
name = "start-os"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/Start9Labs/start-os"
|
repository = "https://github.com/Start9Labs/start-os"
|
||||||
version = "0.4.0-alpha.20" # VERSION_BUMP
|
version = "0.4.0-alpha.21" # VERSION_BUMP
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "startos"
|
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" }
|
serde_yaml = { package = "serde_yml", version = "0.0.12" }
|
||||||
sha-crypt = "0.5.0"
|
sha-crypt = "0.5.0"
|
||||||
sha2 = "0.10.2"
|
sha2 = "0.10.2"
|
||||||
|
sha3 = "0.10"
|
||||||
signal-hook = "0.3.17"
|
signal-hook = "0.3.17"
|
||||||
socket2 = { version = "0.6.0", features = ["all"] }
|
socket2 = { version = "0.6.0", features = ["all"] }
|
||||||
socks5-impl = { version = "0.7.2", features = ["client", "server"] }
|
socks5-impl = { version = "0.7.2", features = ["client", "server"] }
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ if [[ "${ENVIRONMENT:-}" =~ (^|-)console($|-) ]]; then
|
|||||||
RUSTFLAGS="--cfg tokio_unstable"
|
RUSTFLAGS="--cfg tokio_unstable"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${ENVIRONMENT:-}" =~ (^|-)unstable($|-) ]]; then
|
||||||
|
RUSTFLAGS="$RUSTFLAGS -C debuginfo=1"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "FEATURES=\"$FEATURES\""
|
echo "FEATURES=\"$FEATURES\""
|
||||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
||||||
rust-zig-builder cargo zigbuild --manifest-path=./core/Cargo.toml $BUILD_FLAGS --features=$FEATURES --locked --bin start-cli --target=$TARGET
|
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"
|
RUSTFLAGS="--cfg tokio_unstable"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||||
|
RUSTFLAGS="$RUSTFLAGS -C debuginfo=1"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "FEATURES=\"$FEATURES\""
|
echo "FEATURES=\"$FEATURES\""
|
||||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
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
|
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"
|
RUSTFLAGS="--cfg tokio_unstable"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||||
|
RUSTFLAGS="$RUSTFLAGS -C debuginfo=1"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "FEATURES=\"$FEATURES\""
|
echo "FEATURES=\"$FEATURES\""
|
||||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
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
|
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"
|
RUSTFLAGS="--cfg tokio_unstable"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||||
|
RUSTFLAGS="$RUSTFLAGS -C debuginfo=1"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "FEATURES=\"$FEATURES\""
|
echo "FEATURES=\"$FEATURES\""
|
||||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
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
|
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"
|
RUSTFLAGS="--cfg tokio_unstable"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then
|
||||||
|
RUSTFLAGS="$RUSTFLAGS -C debuginfo=1"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "FEATURES=\"$FEATURES\""
|
echo "FEATURES=\"$FEATURES\""
|
||||||
echo "RUSTFLAGS=\"$RUSTFLAGS\""
|
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
|
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"
|
fr_FR: "référence fuitée vers BackupMountGuard"
|
||||||
pl_PL: "wyciekła referencja do 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.rs
|
||||||
backup.restore.package-error:
|
backup.restore.package-error:
|
||||||
en_US: "Error restoring package %{id}: %{error}"
|
en_US: "Error restoring package %{id}: %{error}"
|
||||||
|
|||||||
@@ -300,6 +300,15 @@ async fn perform_backup(
|
|||||||
error: backup_result,
|
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 ts_rs::TS;
|
||||||
|
|
||||||
use super::target::BackupTargetId;
|
use super::target::BackupTargetId;
|
||||||
|
use crate::PackageId;
|
||||||
use crate::backup::os::OsBackup;
|
use crate::backup::os::OsBackup;
|
||||||
use crate::context::setup::SetupResult;
|
use crate::context::setup::SetupResult;
|
||||||
use crate::context::{RpcContext, SetupContext};
|
use crate::context::{RpcContext, SetupContext};
|
||||||
@@ -26,7 +27,6 @@ use crate::service::service_map::DownloadInstallFuture;
|
|||||||
use crate::setup::SetupExecuteProgress;
|
use crate::setup::SetupExecuteProgress;
|
||||||
use crate::system::{save_language, sync_kiosk};
|
use crate::system::{save_language, sync_kiosk};
|
||||||
use crate::util::serde::{IoFormat, Pem};
|
use crate::util::serde::{IoFormat, Pem};
|
||||||
use crate::{PLATFORM, PackageId};
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -90,7 +90,7 @@ pub async fn recover_full_server(
|
|||||||
recovery_source: TmpMountGuard,
|
recovery_source: TmpMountGuard,
|
||||||
server_id: &str,
|
server_id: &str,
|
||||||
recovery_password: &str,
|
recovery_password: &str,
|
||||||
kiosk: Option<bool>,
|
kiosk: bool,
|
||||||
hostname: Option<ServerHostnameInfo>,
|
hostname: Option<ServerHostnameInfo>,
|
||||||
SetupExecuteProgress {
|
SetupExecuteProgress {
|
||||||
init_phases,
|
init_phases,
|
||||||
@@ -123,7 +123,6 @@ pub async fn recover_full_server(
|
|||||||
os_backup.account.hostname = h;
|
os_backup.account.hostname = h;
|
||||||
}
|
}
|
||||||
|
|
||||||
let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi");
|
|
||||||
sync_kiosk(kiosk).await?;
|
sync_kiosk(kiosk).await?;
|
||||||
|
|
||||||
let language = ctx.language.peek(|a| a.clone());
|
let language = ctx.language.peek(|a| a.clone());
|
||||||
|
|||||||
@@ -149,6 +149,11 @@ impl MultiExecutable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute(&self) {
|
pub fn execute(&self) {
|
||||||
|
#[cfg(feature = "backtrace-on-stack-overflow")]
|
||||||
|
unsafe {
|
||||||
|
backtrace_on_stack_overflow::enable()
|
||||||
|
};
|
||||||
|
|
||||||
set_locale_from_env();
|
set_locale_from_env();
|
||||||
|
|
||||||
let mut popped = Vec::with_capacity(2);
|
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
|
res
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ impl DiagnosticContext {
|
|||||||
shutdown,
|
shutdown,
|
||||||
disk_guid,
|
disk_guid,
|
||||||
error: Arc::new(error.into()),
|
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,
|
error: watch::channel(None).0,
|
||||||
progress,
|
progress,
|
||||||
shutdown,
|
shutdown,
|
||||||
rpc_continuations: RpcContinuations::new(),
|
rpc_continuations: RpcContinuations::new(None),
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ pub struct RpcContextSeed {
|
|||||||
pub db: TypedPatchDb<Database>,
|
pub db: TypedPatchDb<Database>,
|
||||||
pub sync_db: watch::Sender<u64>,
|
pub sync_db: watch::Sender<u64>,
|
||||||
pub account: SyncRwLock<AccountInfo>,
|
pub account: SyncRwLock<AccountInfo>,
|
||||||
pub net_controller: Arc<NetController>,
|
|
||||||
pub os_net_service: NetService,
|
pub os_net_service: NetService,
|
||||||
|
pub net_controller: Arc<NetController>,
|
||||||
pub s9pk_arch: Option<&'static str>,
|
pub s9pk_arch: Option<&'static str>,
|
||||||
pub services: ServiceMap,
|
pub services: ServiceMap,
|
||||||
pub cancellable_installs: SyncMutex<BTreeMap<PackageId, oneshot::Sender<()>>>,
|
pub cancellable_installs: SyncMutex<BTreeMap<PackageId, oneshot::Sender<()>>>,
|
||||||
@@ -346,10 +346,10 @@ impl RpcContext {
|
|||||||
services,
|
services,
|
||||||
cancellable_installs: SyncMutex::new(BTreeMap::new()),
|
cancellable_installs: SyncMutex::new(BTreeMap::new()),
|
||||||
metrics_cache,
|
metrics_cache,
|
||||||
|
rpc_continuations: RpcContinuations::new(Some(shutdown.clone())),
|
||||||
shutdown,
|
shutdown,
|
||||||
lxc_manager: Arc::new(LxcManager::new()),
|
lxc_manager: Arc::new(LxcManager::new()),
|
||||||
open_authed_continuations: OpenAuthedContinuations::new(),
|
open_authed_continuations: OpenAuthedContinuations::new(),
|
||||||
rpc_continuations: RpcContinuations::new(),
|
|
||||||
wifi_manager: Arc::new(RwLock::new(wifi_interface.clone().map(|i| WpaCli::init(i)))),
|
wifi_manager: Arc::new(RwLock::new(wifi_interface.clone().map(|i| WpaCli::init(i)))),
|
||||||
current_secret: Arc::new(
|
current_secret: Arc::new(
|
||||||
Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).map_err(|e| {
|
Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).map_err(|e| {
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ impl SetupContext {
|
|||||||
result: OnceCell::new(),
|
result: OnceCell::new(),
|
||||||
disk_guid: OnceCell::new(),
|
disk_guid: OnceCell::new(),
|
||||||
shutdown,
|
shutdown,
|
||||||
rpc_continuations: RpcContinuations::new(),
|
rpc_continuations: RpcContinuations::new(None),
|
||||||
install_rootfs: SyncMutex::new(None),
|
install_rootfs: SyncMutex::new(None),
|
||||||
language: SyncMutex::new(None),
|
language: SyncMutex::new(None),
|
||||||
keyboard: SyncMutex::new(None),
|
keyboard: SyncMutex::new(None),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ pub struct Database {
|
|||||||
impl Database {
|
impl Database {
|
||||||
pub fn init(
|
pub fn init(
|
||||||
account: &AccountInfo,
|
account: &AccountInfo,
|
||||||
kiosk: Option<bool>,
|
kiosk: bool,
|
||||||
language: Option<InternedString>,
|
language: Option<InternedString>,
|
||||||
keyboard: Option<KeyboardOptions>,
|
keyboard: Option<KeyboardOptions>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ pub struct Public {
|
|||||||
impl Public {
|
impl Public {
|
||||||
pub fn init(
|
pub fn init(
|
||||||
account: &AccountInfo,
|
account: &AccountInfo,
|
||||||
kiosk: Option<bool>,
|
kiosk: bool,
|
||||||
language: Option<InternedString>,
|
language: Option<InternedString>,
|
||||||
keyboard: Option<KeyboardOptions>,
|
keyboard: Option<KeyboardOptions>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
@@ -149,7 +149,7 @@ impl Public {
|
|||||||
echoip_urls: default_echoip_urls(),
|
echoip_urls: default_echoip_urls(),
|
||||||
ram: 0,
|
ram: 0,
|
||||||
devices: Vec::new(),
|
devices: Vec::new(),
|
||||||
kiosk,
|
kiosk: Some(kiosk).filter(|_| &*PLATFORM != "raspberrypi"),
|
||||||
language,
|
language,
|
||||||
keyboard,
|
keyboard,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -174,7 +174,9 @@ pub async fn init(
|
|||||||
local_auth.complete();
|
local_auth.complete();
|
||||||
|
|
||||||
// Re-enroll MOK on every boot if Secure Boot key exists but isn't enrolled yet
|
// 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}");
|
tracing::warn!("MOK enrollment failed: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +371,7 @@ pub async fn init(
|
|||||||
enable_zram.complete();
|
enable_zram.complete();
|
||||||
|
|
||||||
update_server_info.start();
|
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 ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024;
|
||||||
let devices = lshw().await?;
|
let devices = lshw().await?;
|
||||||
let status_info = ServerStatus {
|
let status_info = ServerStatus {
|
||||||
|
|||||||
@@ -820,7 +820,6 @@ impl NetService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.shutdown = true;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -832,6 +831,7 @@ impl NetService {
|
|||||||
impl Drop for NetService {
|
impl Drop for NetService {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if !self.shutdown {
|
if !self.shutdown {
|
||||||
|
self.shutdown = true;
|
||||||
let svc = std::mem::replace(self, Self::dummy());
|
let svc = std::mem::replace(self, Self::dummy());
|
||||||
tokio::spawn(async move { svc.remove_all().await.log_err() });
|
tokio::spawn(async move { svc.remove_all().await.log_err() });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -509,7 +509,7 @@ where
|
|||||||
drop(queue_cell.replace(None));
|
drop(queue_cell.replace(None));
|
||||||
|
|
||||||
if !runner.is_empty() {
|
if !runner.is_empty() {
|
||||||
tokio::time::timeout(Duration::from_secs(60), runner)
|
tokio::time::timeout(Duration::from_millis(100), runner)
|
||||||
.await
|
.await
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ impl RegistryContext {
|
|||||||
listen: config.registry_listen.unwrap_or(DEFAULT_REGISTRY_LISTEN),
|
listen: config.registry_listen.unwrap_or(DEFAULT_REGISTRY_LISTEN),
|
||||||
db,
|
db,
|
||||||
datadir,
|
datadir,
|
||||||
rpc_continuations: RpcContinuations::new(),
|
rpc_continuations: RpcContinuations::new(None),
|
||||||
client: Client::builder()
|
client: Client::builder()
|
||||||
.proxy(Proxy::custom(move |url| {
|
.proxy(Proxy::custom(move |url| {
|
||||||
if url.host_str().map_or(false, |h| h.ends_with(".onion")) {
|
if url.host_str().map_or(false, |h| h.ends_with(".onion")) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use ts_rs::TS;
|
|||||||
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::shutdown::Shutdown;
|
||||||
use crate::util::future::TimedResource;
|
use crate::util::future::TimedResource;
|
||||||
use crate::util::net::WebSocket;
|
use crate::util::net::WebSocket;
|
||||||
use crate::util::{FromStrParser, new_guid};
|
use crate::util::{FromStrParser, new_guid};
|
||||||
@@ -98,12 +99,15 @@ pub type RestHandler = Box<dyn FnOnce(Request) -> RestFuture + Send>;
|
|||||||
|
|
||||||
pub struct WebSocketFuture {
|
pub struct WebSocketFuture {
|
||||||
kill: Option<broadcast::Receiver<()>>,
|
kill: Option<broadcast::Receiver<()>>,
|
||||||
|
shutdown: Option<broadcast::Receiver<Option<Shutdown>>>,
|
||||||
fut: BoxFuture<'static, ()>,
|
fut: BoxFuture<'static, ()>,
|
||||||
}
|
}
|
||||||
impl Future for WebSocketFuture {
|
impl Future for WebSocketFuture {
|
||||||
type Output = ();
|
type Output = ();
|
||||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::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(())
|
Poll::Ready(())
|
||||||
} else {
|
} else {
|
||||||
self.fut.poll_unpin(cx)
|
self.fut.poll_unpin(cx)
|
||||||
@@ -138,6 +142,7 @@ impl RpcContinuation {
|
|||||||
RpcContinuation::WebSocket(TimedResource::new(
|
RpcContinuation::WebSocket(TimedResource::new(
|
||||||
Box::new(|ws| WebSocketFuture {
|
Box::new(|ws| WebSocketFuture {
|
||||||
kill: None,
|
kill: None,
|
||||||
|
shutdown: None,
|
||||||
fut: handler(ws.into()).boxed(),
|
fut: handler(ws.into()).boxed(),
|
||||||
}),
|
}),
|
||||||
timeout,
|
timeout,
|
||||||
@@ -170,6 +175,7 @@ impl RpcContinuation {
|
|||||||
RpcContinuation::WebSocket(TimedResource::new(
|
RpcContinuation::WebSocket(TimedResource::new(
|
||||||
Box::new(|ws| WebSocketFuture {
|
Box::new(|ws| WebSocketFuture {
|
||||||
kill,
|
kill,
|
||||||
|
shutdown: None,
|
||||||
fut: handler(ws.into()).boxed(),
|
fut: handler(ws.into()).boxed(),
|
||||||
}),
|
}),
|
||||||
timeout,
|
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 {
|
impl RpcContinuations {
|
||||||
pub fn new() -> Self {
|
pub fn new(shutdown: Option<broadcast::Sender<Option<Shutdown>>>) -> Self {
|
||||||
RpcContinuations(AsyncMutex::new(BTreeMap::new()))
|
RpcContinuations {
|
||||||
|
continuations: AsyncMutex::new(BTreeMap::new()),
|
||||||
|
shutdown,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn clean(&self) {
|
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();
|
let mut to_remove = Vec::new();
|
||||||
for (guid, cont) in &*continuations {
|
for (guid, cont) in &*continuations {
|
||||||
if cont.is_timed_out() {
|
if cont.is_timed_out() {
|
||||||
@@ -206,23 +218,28 @@ impl RpcContinuations {
|
|||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn add(&self, guid: Guid, handler: RpcContinuation) {
|
pub async fn add(&self, guid: Guid, handler: RpcContinuation) {
|
||||||
self.clean().await;
|
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> {
|
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(_))) {
|
if !matches!(continuations.get(guid), Some(RpcContinuation::WebSocket(_))) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let Some(RpcContinuation::WebSocket(x)) = continuations.remove(guid) else {
|
let Some(RpcContinuation::WebSocket(x)) = continuations.remove(guid) else {
|
||||||
return None;
|
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> {
|
pub async fn get_rest_handler(&self, guid: &Guid) -> Option<RestHandler> {
|
||||||
let mut continuations: tokio::sync::MutexGuard<'_, BTreeMap<Guid, RpcContinuation>> =
|
let mut continuations = self.continuations.lock().await;
|
||||||
self.0.lock().await;
|
|
||||||
if !matches!(continuations.get(guid), Some(RpcContinuation::Rest(_))) {
|
if !matches!(continuations.get(guid), Some(RpcContinuation::Rest(_))) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ pub async fn list_disks(ctx: SetupContext) -> Result<Vec<DiskInfo>, Error> {
|
|||||||
async fn setup_init(
|
async fn setup_init(
|
||||||
ctx: &SetupContext,
|
ctx: &SetupContext,
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
kiosk: Option<bool>,
|
kiosk: bool,
|
||||||
hostname: Option<ServerHostnameInfo>,
|
hostname: Option<ServerHostnameInfo>,
|
||||||
init_phases: InitPhases,
|
init_phases: InitPhases,
|
||||||
) -> Result<(AccountInfo, InitResult), Error> {
|
) -> Result<(AccountInfo, InitResult), Error> {
|
||||||
@@ -137,9 +137,8 @@ async fn setup_init(
|
|||||||
account.save(m)?;
|
account.save(m)?;
|
||||||
let info = m.as_public_mut().as_server_info_mut();
|
let info = m.as_public_mut().as_server_info_mut();
|
||||||
info.as_password_hash_mut().ser(&account.password)?;
|
info.as_password_hash_mut().ser(&account.password)?;
|
||||||
if let Some(kiosk) = kiosk {
|
info.as_kiosk_mut()
|
||||||
info.as_kiosk_mut().ser(&Some(kiosk))?;
|
.ser(&Some(kiosk).filter(|_| &*PLATFORM != "raspberrypi"))?;
|
||||||
}
|
|
||||||
if let Some(language) = language.clone() {
|
if let Some(language) = language.clone() {
|
||||||
info.as_language_mut().ser(&Some(language))?;
|
info.as_language_mut().ser(&Some(language))?;
|
||||||
}
|
}
|
||||||
@@ -174,8 +173,7 @@ async fn setup_init(
|
|||||||
pub struct AttachParams {
|
pub struct AttachParams {
|
||||||
pub password: Option<EncryptedWire>,
|
pub password: Option<EncryptedWire>,
|
||||||
pub guid: InternedString,
|
pub guid: InternedString,
|
||||||
#[ts(optional)]
|
pub kiosk: bool,
|
||||||
pub kiosk: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
@@ -411,8 +409,7 @@ pub struct SetupExecuteParams {
|
|||||||
guid: InternedString,
|
guid: InternedString,
|
||||||
password: Option<EncryptedWire>,
|
password: Option<EncryptedWire>,
|
||||||
recovery_source: Option<RecoverySource<EncryptedWire>>,
|
recovery_source: Option<RecoverySource<EncryptedWire>>,
|
||||||
#[ts(optional)]
|
kiosk: bool,
|
||||||
kiosk: Option<bool>,
|
|
||||||
name: Option<InternedString>,
|
name: Option<InternedString>,
|
||||||
hostname: Option<InternedString>,
|
hostname: Option<InternedString>,
|
||||||
}
|
}
|
||||||
@@ -549,7 +546,7 @@ pub async fn execute_inner(
|
|||||||
guid: InternedString,
|
guid: InternedString,
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
recovery_source: Option<RecoverySource<String>>,
|
recovery_source: Option<RecoverySource<String>>,
|
||||||
kiosk: Option<bool>,
|
kiosk: bool,
|
||||||
hostname: Option<ServerHostnameInfo>,
|
hostname: Option<ServerHostnameInfo>,
|
||||||
) -> Result<(SetupResult, RpcContext), Error> {
|
) -> Result<(SetupResult, RpcContext), Error> {
|
||||||
let progress = &ctx.progress;
|
let progress = &ctx.progress;
|
||||||
@@ -622,7 +619,7 @@ async fn fresh_setup(
|
|||||||
ctx: &SetupContext,
|
ctx: &SetupContext,
|
||||||
guid: InternedString,
|
guid: InternedString,
|
||||||
password: &str,
|
password: &str,
|
||||||
kiosk: Option<bool>,
|
kiosk: bool,
|
||||||
hostname: Option<ServerHostnameInfo>,
|
hostname: Option<ServerHostnameInfo>,
|
||||||
SetupExecuteProgress {
|
SetupExecuteProgress {
|
||||||
init_phases,
|
init_phases,
|
||||||
@@ -633,7 +630,6 @@ async fn fresh_setup(
|
|||||||
let account = AccountInfo::new(password, root_ca_start_time().await, hostname)?;
|
let account = AccountInfo::new(password, root_ca_start_time().await, hostname)?;
|
||||||
|
|
||||||
let db = ctx.db().await?;
|
let db = ctx.db().await?;
|
||||||
let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi");
|
|
||||||
sync_kiosk(kiosk).await?;
|
sync_kiosk(kiosk).await?;
|
||||||
|
|
||||||
let language = ctx.language.peek(|a| a.clone());
|
let language = ctx.language.peek(|a| a.clone());
|
||||||
@@ -684,7 +680,7 @@ async fn recover(
|
|||||||
recovery_source: BackupTargetFS,
|
recovery_source: BackupTargetFS,
|
||||||
server_id: String,
|
server_id: String,
|
||||||
recovery_password: String,
|
recovery_password: String,
|
||||||
kiosk: Option<bool>,
|
kiosk: bool,
|
||||||
hostname: Option<ServerHostnameInfo>,
|
hostname: Option<ServerHostnameInfo>,
|
||||||
progress: SetupExecuteProgress,
|
progress: SetupExecuteProgress,
|
||||||
) -> Result<(SetupResult, RpcContext), Error> {
|
) -> Result<(SetupResult, RpcContext), Error> {
|
||||||
@@ -709,7 +705,7 @@ async fn migrate(
|
|||||||
guid: InternedString,
|
guid: InternedString,
|
||||||
old_guid: &str,
|
old_guid: &str,
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
kiosk: Option<bool>,
|
kiosk: bool,
|
||||||
hostname: Option<ServerHostnameInfo>,
|
hostname: Option<ServerHostnameInfo>,
|
||||||
SetupExecuteProgress {
|
SetupExecuteProgress {
|
||||||
init_phases,
|
init_phases,
|
||||||
|
|||||||
@@ -319,13 +319,11 @@ pub fn kernel_logs<C: Context + AsRef<RpcContinuations>>() -> ParentHandler<C, L
|
|||||||
const DISABLE_KIOSK_PATH: &str =
|
const DISABLE_KIOSK_PATH: &str =
|
||||||
"/media/startos/config/overlay/etc/systemd/system/getty@tty1.service.d/autologin.conf";
|
"/media/startos/config/overlay/etc/systemd/system/getty@tty1.service.d/autologin.conf";
|
||||||
|
|
||||||
pub async fn sync_kiosk(kiosk: Option<bool>) -> Result<(), Error> {
|
pub async fn sync_kiosk(kiosk: bool) -> Result<(), Error> {
|
||||||
if let Some(kiosk) = kiosk {
|
if kiosk {
|
||||||
if kiosk {
|
enable_kiosk().await?;
|
||||||
enable_kiosk().await?;
|
} else {
|
||||||
} else {
|
disable_kiosk().await?;
|
||||||
disable_kiosk().await?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ impl TunnelContext {
|
|||||||
listen,
|
listen,
|
||||||
db,
|
db,
|
||||||
datadir,
|
datadir,
|
||||||
rpc_continuations: RpcContinuations::new(),
|
rpc_continuations: RpcContinuations::new(None),
|
||||||
open_authed_continuations: OpenAuthedContinuations::new(),
|
open_authed_continuations: OpenAuthedContinuations::new(),
|
||||||
ephemeral_sessions: SyncMutex::new(Sessions::new()),
|
ephemeral_sessions: SyncMutex::new(Sessions::new()),
|
||||||
net_iface,
|
net_iface,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ use futures::{FutureExt, Stream, StreamExt, ready};
|
|||||||
use http::header::CONTENT_LENGTH;
|
use http::header::CONTENT_LENGTH;
|
||||||
use http::{HeaderMap, StatusCode};
|
use http::{HeaderMap, StatusCode};
|
||||||
use imbl_value::InternedString;
|
use imbl_value::InternedString;
|
||||||
use tokio::fs::File;
|
|
||||||
use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt};
|
use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt};
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
|
|
||||||
@@ -23,6 +22,7 @@ use crate::progress::{PhaseProgressTrackerHandle, ProgressUnits};
|
|||||||
use crate::rpc_continuations::{Guid, RpcContinuation};
|
use crate::rpc_continuations::{Guid, RpcContinuation};
|
||||||
use crate::s9pk::merkle_archive::source::ArchiveSource;
|
use crate::s9pk::merkle_archive::source::ArchiveSource;
|
||||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::{FileCursor, MultiCursorFile};
|
use crate::s9pk::merkle_archive::source::multi_cursor_file::{FileCursor, MultiCursorFile};
|
||||||
|
use crate::util::direct_io::DirectIoFile;
|
||||||
use crate::util::io::{TmpDir, create_file};
|
use crate::util::io::{TmpDir, create_file};
|
||||||
|
|
||||||
pub async fn upload(
|
pub async fn upload(
|
||||||
@@ -69,16 +69,6 @@ impl Progress {
|
|||||||
false
|
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> {
|
async fn expected_size(watch: &mut watch::Receiver<Self>) -> Option<u64> {
|
||||||
watch
|
watch
|
||||||
.wait_for(|progress| progress.error.is_some() || progress.expected_size.is_some())
|
.wait_for(|progress| progress.error.is_some() || progress.expected_size.is_some())
|
||||||
@@ -192,16 +182,19 @@ impl UploadingFile {
|
|||||||
complete: false,
|
complete: false,
|
||||||
});
|
});
|
||||||
let file = create_file(path).await?;
|
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 {
|
let uploading = Self {
|
||||||
tmp_dir: None,
|
tmp_dir: None,
|
||||||
file: MultiCursorFile::open(&file).await?,
|
file: multi_cursor,
|
||||||
progress: progress.1,
|
progress: progress.1,
|
||||||
};
|
};
|
||||||
Ok((
|
Ok((
|
||||||
UploadHandle {
|
UploadHandle {
|
||||||
tmp_dir: None,
|
tmp_dir: None,
|
||||||
file,
|
file: direct_file,
|
||||||
progress: progress.0,
|
progress: progress.0,
|
||||||
|
last_synced: 0,
|
||||||
},
|
},
|
||||||
uploading,
|
uploading,
|
||||||
))
|
))
|
||||||
@@ -346,8 +339,9 @@ impl AsyncSeek for UploadingFileReader {
|
|||||||
pub struct UploadHandle {
|
pub struct UploadHandle {
|
||||||
tmp_dir: Option<Arc<TmpDir>>,
|
tmp_dir: Option<Arc<TmpDir>>,
|
||||||
#[pin]
|
#[pin]
|
||||||
file: File,
|
file: DirectIoFile,
|
||||||
progress: watch::Sender<Progress>,
|
progress: watch::Sender<Progress>,
|
||||||
|
last_synced: u64,
|
||||||
}
|
}
|
||||||
impl UploadHandle {
|
impl UploadHandle {
|
||||||
pub async fn upload(&mut self, request: Request) {
|
pub async fn upload(&mut self, request: Request) {
|
||||||
@@ -394,6 +388,19 @@ impl UploadHandle {
|
|||||||
if let Err(e) = self.file.sync_all().await {
|
if let Err(e) = self.file.sync_all().await {
|
||||||
self.progress.send_if_modified(|p| p.handle_error(&e));
|
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]
|
#[pin_project::pinned_drop]
|
||||||
@@ -410,13 +417,23 @@ impl AsyncWrite for UploadHandle {
|
|||||||
buf: &[u8],
|
buf: &[u8],
|
||||||
) -> Poll<Result<usize, std::io::Error>> {
|
) -> Poll<Result<usize, std::io::Error>> {
|
||||||
let this = self.project();
|
let this = self.project();
|
||||||
|
// 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
match this.file.poll_write(cx, buf) {
|
match this.file.poll_write(cx, buf) {
|
||||||
Poll::Ready(res) => {
|
Poll::Ready(Err(e)) => {
|
||||||
this.progress
|
this.progress
|
||||||
.send_if_modified(|progress| progress.handle_write(&res));
|
.send_if_modified(|progress| progress.handle_error(&e));
|
||||||
Poll::Ready(res)
|
Poll::Ready(Err(e))
|
||||||
}
|
}
|
||||||
Poll::Pending => Poll::Pending,
|
a => a,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn poll_flush(
|
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 cpupower;
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod data_url;
|
pub mod data_url;
|
||||||
|
pub mod direct_io;
|
||||||
pub mod future;
|
pub mod future;
|
||||||
pub mod http_reader;
|
pub mod http_reader;
|
||||||
pub mod io;
|
pub mod io;
|
||||||
|
|||||||
@@ -60,8 +60,9 @@ mod v0_4_0_alpha_17;
|
|||||||
mod v0_4_0_alpha_18;
|
mod v0_4_0_alpha_18;
|
||||||
mod v0_4_0_alpha_19;
|
mod v0_4_0_alpha_19;
|
||||||
mod v0_4_0_alpha_20;
|
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 {
|
impl Current {
|
||||||
#[instrument(skip(self, db))]
|
#[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_17(Wrapper<v0_4_0_alpha_17::Version>),
|
||||||
V0_4_0_alpha_18(Wrapper<v0_4_0_alpha_18::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_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),
|
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_17(v) => DynVersion(Box::new(v.0)),
|
||||||
Self::V0_4_0_alpha_18(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_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) => {
|
Self::Other(v) => {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
eyre!("unknown version {v}"),
|
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_17(Wrapper(x)) => x.semver(),
|
||||||
Version::V0_4_0_alpha_18(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_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(),
|
Version::Other(x) => x.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,8 @@ pub struct Version;
|
|||||||
|
|
||||||
impl VersionT for Version {
|
impl VersionT for Version {
|
||||||
type Previous = v0_3_5_2::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 {
|
fn semver(self) -> exver::Version {
|
||||||
V0_3_6_alpha_0.clone()
|
V0_3_6_alpha_0.clone()
|
||||||
}
|
}
|
||||||
@@ -158,15 +159,17 @@ impl VersionT for Version {
|
|||||||
|
|
||||||
let cifs = previous_cifs(&pg).await?;
|
let cifs = previous_cifs(&pg).await?;
|
||||||
|
|
||||||
|
let tor_keys = previous_tor_keys(&pg).await?;
|
||||||
|
|
||||||
Command::new("systemctl")
|
Command::new("systemctl")
|
||||||
.arg("stop")
|
.arg("stop")
|
||||||
.arg("postgresql@*.service")
|
.arg("postgresql@*.service")
|
||||||
.invoke(crate::ErrorKind::Database)
|
.invoke(crate::ErrorKind::Database)
|
||||||
.await?;
|
.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 prev_package_data = db["package-data"].clone();
|
||||||
|
|
||||||
let wifi = json!({
|
let wifi = json!({
|
||||||
@@ -183,6 +186,11 @@ impl VersionT for Version {
|
|||||||
"shuttingDown": db["server-info"]["status-info"]["shutting-down"],
|
"shuttingDown": db["server-info"]["status-info"]["shutting-down"],
|
||||||
"restarting": db["server-info"]["status-info"]["restarting"],
|
"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 server_info = {
|
||||||
let mut server_info = json!({
|
let mut server_info = json!({
|
||||||
"arch": db["server-info"]["arch"],
|
"arch": db["server-info"]["arch"],
|
||||||
@@ -196,15 +204,9 @@ impl VersionT for Version {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server_info["postInitMigrationTodos"] = json!({});
|
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
|
// Maybe we do this like the Public::init does
|
||||||
server_info["torAddress"] = json!(tor_address);
|
server_info["torAddress"] = json!(&tor_address);
|
||||||
server_info["onionAddress"] = json!(
|
server_info["onionAddress"] = json!(&onion_address);
|
||||||
tor_address
|
|
||||||
.replace("https://", "")
|
|
||||||
.replace("http://", "")
|
|
||||||
.replace(".onion/", "")
|
|
||||||
);
|
|
||||||
server_info["networkInterfaces"] = json!({});
|
server_info["networkInterfaces"] = json!({});
|
||||||
server_info["statusInfo"] = status_info;
|
server_info["statusInfo"] = status_info;
|
||||||
server_info["wifi"] = wifi;
|
server_info["wifi"] = wifi;
|
||||||
@@ -233,6 +235,30 @@ impl VersionT for Version {
|
|||||||
let private = {
|
let private = {
|
||||||
let mut value = json!({});
|
let mut value = json!({});
|
||||||
value["keyStore"] = crate::dbg!(to_value(&keystore)?);
|
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["password"] = to_value(&account.password)?;
|
||||||
value["compatS9pkKey"] =
|
value["compatS9pkKey"] =
|
||||||
to_value(&crate::db::model::private::generate_developer_key())?;
|
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)
|
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 exver::{PreReleaseSegment, VersionRange};
|
||||||
use imbl_value::json;
|
use imbl_value::json;
|
||||||
|
use reqwest::Url;
|
||||||
|
|
||||||
use super::v0_3_5::V0_3_0_COMPAT;
|
use super::v0_3_5::V0_3_0_COMPAT;
|
||||||
use super::{VersionT, v0_4_0_alpha_19};
|
use super::{VersionT, v0_4_0_alpha_19};
|
||||||
use crate::context::RpcContext;
|
use crate::context::RpcContext;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref V0_4_0_alpha_20: exver::Version = exver::Version::new(
|
static ref V0_4_0_alpha_20: exver::Version = exver::Version::new(
|
||||||
@@ -33,74 +35,106 @@ impl VersionT for Version {
|
|||||||
}
|
}
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
fn up(self, db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
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.
|
||||||
let onion_store = db
|
// 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")
|
.get("private")
|
||||||
.and_then(|p| p.get("keyStore"))
|
.and_then(|p| p.get("torMigration"))
|
||||||
.and_then(|k| k.get("onion"))
|
.and_then(|t| t.as_array())
|
||||||
.cloned()
|
|
||||||
.unwrap_or(Value::Object(Default::default()));
|
|
||||||
|
|
||||||
let mut addresses = imbl::Vector::<Value>::new();
|
|
||||||
|
|
||||||
// Extract OS host onion addresses
|
|
||||||
if let Some(onions) = db
|
|
||||||
.get("public")
|
|
||||||
.and_then(|p| p.get("serverInfo"))
|
|
||||||
.and_then(|s| s.get("network"))
|
|
||||||
.and_then(|n| n.get("host"))
|
|
||||||
.and_then(|h| h.get("onions"))
|
|
||||||
.and_then(|o| o.as_array())
|
|
||||||
{
|
{
|
||||||
for onion in onions {
|
json!({
|
||||||
if let Some(hostname) = onion.as_str() {
|
"addresses": tor_migration.clone(),
|
||||||
let key = onion_store
|
})
|
||||||
.get(hostname)
|
} else {
|
||||||
.and_then(|v| v.as_str())
|
// Fallback for fresh installs or installs that didn't go through
|
||||||
.unwrap_or_default();
|
// v0_3_6_alpha_0 with the torMigration field.
|
||||||
addresses.push_back(json!({
|
let onion_store = db
|
||||||
"hostname": hostname,
|
.get("private")
|
||||||
"packageId": "STARTOS",
|
.and_then(|p| p.get("keyStore"))
|
||||||
"hostId": "STARTOS",
|
.and_then(|k| k.get("onion"))
|
||||||
"key": key,
|
.cloned()
|
||||||
}));
|
.unwrap_or(Value::Object(Default::default()));
|
||||||
|
|
||||||
|
let mut addresses = imbl::Vector::<Value>::new();
|
||||||
|
|
||||||
|
// Extract OS host onion addresses
|
||||||
|
if let Some(onions) = db
|
||||||
|
.get("public")
|
||||||
|
.and_then(|p| p.get("serverInfo"))
|
||||||
|
.and_then(|s| s.get("network"))
|
||||||
|
.and_then(|n| n.get("host"))
|
||||||
|
.and_then(|h| h.get("onions"))
|
||||||
|
.and_then(|o| o.as_array())
|
||||||
|
{
|
||||||
|
for onion in onions {
|
||||||
|
if let Some(hostname) = onion.as_str() {
|
||||||
|
let key = onion_store
|
||||||
|
.get(hostname)
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.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-ui",
|
||||||
|
"key": key,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Extract package host onion addresses
|
// Extract package host onion addresses
|
||||||
if let Some(packages) = db
|
if let Some(packages) = db
|
||||||
.get("public")
|
.get("public")
|
||||||
.and_then(|p| p.get("packageData"))
|
.and_then(|p| p.get("packageData"))
|
||||||
.and_then(|p| p.as_object())
|
.and_then(|p| p.as_object())
|
||||||
{
|
{
|
||||||
for (package_id, package) in packages.iter() {
|
for (package_id, package) in packages.iter() {
|
||||||
if let Some(hosts) = package.get("hosts").and_then(|h| h.as_object()) {
|
if let Some(hosts) = package.get("hosts").and_then(|h| h.as_object()) {
|
||||||
for (host_id, host) in hosts.iter() {
|
for (host_id, host) in hosts.iter() {
|
||||||
if let Some(onions) = host.get("onions").and_then(|o| o.as_array()) {
|
if let Some(onions) = host.get("onions").and_then(|o| o.as_array()) {
|
||||||
for onion in onions {
|
for onion in onions {
|
||||||
if let Some(hostname) = onion.as_str() {
|
if let Some(hostname) = onion.as_str() {
|
||||||
let key = onion_store
|
let key = onion_store
|
||||||
.get(hostname)
|
.get(hostname)
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or_default();
|
.ok_or_else(|| {
|
||||||
addresses.push_back(json!({
|
Error::new(
|
||||||
"hostname": hostname,
|
eyre!(
|
||||||
"packageId": &**package_id,
|
"missing tor key for onion address {hostname}"
|
||||||
"hostId": &**host_id,
|
),
|
||||||
"key": key,
|
ErrorKind::Database,
|
||||||
}));
|
)
|
||||||
|
})?;
|
||||||
|
addresses.push_back(json!({
|
||||||
|
"hostname": hostname,
|
||||||
|
"packageId": &**package_id,
|
||||||
|
"hostId": &**host_id,
|
||||||
|
"key": key,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let migration_data = json!({
|
json!({
|
||||||
"addresses": addresses,
|
"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
|
// Remove onions and tor-related fields from server host
|
||||||
if let Some(host) = db
|
if let Some(host) = db
|
||||||
@@ -200,7 +234,7 @@ impl VersionT for Version {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[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(
|
let path = Path::new(
|
||||||
"/media/startos/data/package-data/volumes/tor/data/startos/onion-migration.json",
|
"/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?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
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
|
# 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)
|
## 0.4.0-beta.59 — StartOS v0.4.0-alpha.20 (2026-03-06)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ import type { EncryptedWire } from './EncryptedWire'
|
|||||||
export type AttachParams = {
|
export type AttachParams = {
|
||||||
password: EncryptedWire | null
|
password: EncryptedWire | null
|
||||||
guid: string
|
guid: string
|
||||||
kiosk?: boolean
|
kiosk: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export type SetupExecuteParams = {
|
|||||||
guid: string
|
guid: string
|
||||||
password: EncryptedWire | null
|
password: EncryptedWire | null
|
||||||
recoverySource: RecoverySource<EncryptedWire> | null
|
recoverySource: RecoverySource<EncryptedWire> | null
|
||||||
kiosk?: boolean
|
kiosk: boolean
|
||||||
name: string | null
|
name: string | null
|
||||||
hostname: string | null
|
hostname: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ import { getOwnServiceInterfaces } from '../../base/lib/util/getServiceInterface
|
|||||||
import { Volumes, createVolumes } from './util/Volume'
|
import { Volumes, createVolumes } from './util/Volume'
|
||||||
|
|
||||||
/** The minimum StartOS version required by this SDK release */
|
/** 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
|
// prettier-ignore
|
||||||
type AnyNeverCond<T extends any[], Then, Else> =
|
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",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "0.4.0-beta.59",
|
"version": "0.4.0-beta.60",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@start9labs/start-sdk",
|
"name": "@start9labs/start-sdk",
|
||||||
"version": "0.4.0-beta.59",
|
"version": "0.4.0-beta.60",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iarna/toml": "^3.0.0",
|
"@iarna/toml": "^3.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@start9labs/start-sdk",
|
"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",
|
"description": "Software development kit to facilitate packaging services for StartOS",
|
||||||
"main": "./package/lib/index.js",
|
"main": "./package/lib/index.js",
|
||||||
"types": "./package/lib/index.d.ts",
|
"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",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.20",
|
"version": "0.4.0-alpha.21",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.20",
|
"version": "0.4.0-alpha.21",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^21.2.1",
|
"@angular/cdk": "^21.2.1",
|
||||||
@@ -836,6 +836,7 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -8322,6 +8323,7 @@
|
|||||||
"integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
|
"integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cli-truncate": "^5.0.0",
|
"cli-truncate": "^5.0.0",
|
||||||
"colorette": "^2.0.20",
|
"colorette": "^2.0.20",
|
||||||
@@ -12524,24 +12526,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/yargs": {
|
||||||
"version": "18.0.0",
|
"version": "18.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "startos-ui",
|
"name": "startos-ui",
|
||||||
"version": "0.4.0-alpha.20",
|
"version": "0.4.0-alpha.21",
|
||||||
"author": "Start9 Labs, Inc",
|
"author": "Start9 Labs, Inc",
|
||||||
"homepage": "https://start9.com/",
|
"homepage": "https://start9.com/",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export class StateService {
|
|||||||
await this.api.attach({
|
await this.api.attach({
|
||||||
guid: this.dataDriveGuid,
|
guid: this.dataDriveGuid,
|
||||||
password: password ? await this.api.encrypt(password) : null,
|
password: password ? await this.api.encrypt(password) : null,
|
||||||
|
kiosk: this.kiosk,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +107,7 @@ export class StateService {
|
|||||||
name,
|
name,
|
||||||
hostname,
|
hostname,
|
||||||
recoverySource,
|
recoverySource,
|
||||||
|
kiosk: this.kiosk,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user