working setup flow + manifest localization

This commit is contained in:
Aiden McClelland
2026-01-20 18:28:28 -07:00
parent 6a1c1fde06
commit 3828b03790
51 changed files with 799 additions and 324 deletions

View File

@@ -319,6 +319,7 @@ export function makeEffects(context: EffectContext): Effects {
}
if (context.callbacks?.onLeaveContext)
self.onLeaveContext(() => {
self.constRetry = undefined
self.isInContext = false
self.onLeaveContext = () => {
console.warn(

View File

@@ -163,11 +163,11 @@ setup.no-backup-found:
pl_PL: "Nie znaleziono kopii zapasowej"
setup.couldnt-decode-startos-password:
en_US: "Couldn't decode startOsPassword"
de_DE: "startOsPassword konnte nicht dekodiert werden"
es_ES: "No se pudo decodificar startOsPassword"
fr_FR: "Impossible de décoder startOsPassword"
pl_PL: "Nie można zdekodować startOsPassword"
en_US: "Couldn't decode password"
de_DE: "Passwort konnte nicht dekodiert werden"
es_ES: "No se pudo decodificar la contraseña"
fr_FR: "Impossible de décoder le mot de passe"
pl_PL: "Nie można zdekodować hasła"
setup.couldnt-decode-recovery-password:
en_US: "Couldn't decode recoveryPassword"
@@ -183,13 +183,6 @@ setup.execute-not-completed:
fr_FR: "setup.execute ne s'est pas terminé avec succès"
pl_PL: "setup.execute nie zakończyło się pomyślnie"
setup.formatting-data-drive:
en_US: "Formatting data drive"
de_DE: "Datenlaufwerk wird formatiert"
es_ES: "Formateando unidad de datos"
fr_FR: "Formatage du disque de données"
pl_PL: "Formatowanie dysku danych"
setup.restoring-backup:
en_US: "Restoring backup"
de_DE: "Sicherung wird wiederhergestellt"
@@ -3472,6 +3465,13 @@ help.arg.keyboard-variant:
fr_FR: "Variante de disposition du clavier"
pl_PL: "Wariant układu klawiatury"
help.arg.keyboard-keymap:
en_US: "Console keymap for vconsole"
de_DE: "Konsolen-Tastaturbelegung für vconsole"
es_ES: "Mapa de teclado de consola para vconsole"
fr_FR: "Disposition clavier de console pour vconsole"
pl_PL: "Mapa klawiszy konsoli dla vconsole"
help.arg.language-code:
en_US: "Language code"
de_DE: "Sprachcode"
@@ -5125,6 +5125,20 @@ about.set-gateway-enabled-for-binding:
fr_FR: "Définir la passerelle activée pour la liaison"
pl_PL: "Ustaw bramę jako włączoną dla powiązania"
about.set-keyboard:
en_US: "Set the keyboard layout"
de_DE: "Das Tastaturlayout festlegen"
es_ES: "Establecer la distribución del teclado"
fr_FR: "Définir la disposition du clavier"
pl_PL: "Ustaw układ klawiatury"
about.set-language:
en_US: "Set the system language"
de_DE: "Die Systemsprache festlegen"
es_ES: "Establecer el idioma del sistema"
fr_FR: "Définir la langue du système"
pl_PL: "Ustaw język systemu"
about.set-listen-address-for-webserver:
en_US: "Set the listen address for the webserver"
de_DE: "Adresse festlegen, auf der der Webserver lauscht"

View File

@@ -23,8 +23,8 @@ use crate::progress::ProgressUnits;
use crate::s9pk::S9pk;
use crate::service::service_map::DownloadInstallFuture;
use crate::setup::SetupExecuteProgress;
use crate::system::sync_kiosk;
use crate::util::serde::IoFormat;
use crate::system::{save_language, sync_kiosk};
use crate::util::serde::{IoFormat, Pem};
use crate::{PLATFORM, PackageId};
#[derive(Deserialize, Serialize, Parser, TS)]
@@ -66,7 +66,10 @@ pub async fn restore_packages_rpc(
match async { res.await?.await }.await {
Ok(_) => (),
Err(err) => {
tracing::error!("{}", t!("backup.restore.package-error", id = id, error = err));
tracing::error!(
"{}",
t!("backup.restore.package-error", id = id, error = err)
);
tracing::debug!("{:?}", err);
}
}
@@ -81,7 +84,7 @@ pub async fn restore_packages_rpc(
pub async fn recover_full_server(
ctx: &SetupContext,
disk_guid: InternedString,
start_os_password: String,
password: String,
recovery_source: TmpMountGuard,
server_id: &str,
recovery_password: &str,
@@ -105,7 +108,7 @@ pub async fn recover_full_server(
)?;
os_backup.account.password = argon2::hash_encoded(
start_os_password.as_bytes(),
password.as_bytes(),
&rand::random::<[u8; 16]>()[..],
&argon2::Config::rfc9106_low_mem(),
)
@@ -114,9 +117,23 @@ pub async fn recover_full_server(
let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi");
sync_kiosk(kiosk).await?;
let language = ctx.language.peek(|a| a.clone());
let keyboard = ctx.keyboard.peek(|a| a.clone());
if let Some(language) = &language {
save_language(&**language).await?;
}
if let Some(keyboard) = &keyboard {
keyboard.save().await?;
}
let db = ctx.db().await?;
db.put(&ROOT, &Database::init(&os_backup.account, kiosk)?)
.await?;
db.put(
&ROOT,
&Database::init(&os_backup.account, kiosk, language, keyboard)?,
)
.await?;
drop(db);
let config = ctx.config.peek(|c| c.clone());
@@ -150,7 +167,10 @@ pub async fn recover_full_server(
match async { res.await?.await }.await {
Ok(_) => (),
Err(err) => {
tracing::error!("{}", t!("backup.restore.package-error", id = id, error = err));
tracing::error!(
"{}",
t!("backup.restore.package-error", id = id, error = err)
);
tracing::debug!("{:?}", err);
}
}
@@ -160,7 +180,14 @@ pub async fn recover_full_server(
.await;
restore_phase.lock().await.complete();
Ok(((&os_backup.account).try_into()?, rpc_ctx))
Ok((
SetupResult {
hostname: os_backup.account.hostname,
root_ca: Pem(os_backup.account.root_ca_cert),
needs_restart: ctx.install_rootfs.peek(|a| a.is_some()),
},
rpc_ctx,
))
}
#[instrument(skip(ctx, backup_guard))]

View File

@@ -12,22 +12,27 @@ pub mod start_init;
pub mod startd;
pub mod tunnel;
pub fn set_locale() {
pub fn set_locale_from_env() {
let lang = std::env::var("LANG").ok();
let lang = lang
.as_deref()
.map_or("C", |l| l.strip_suffix(".UTF-8").unwrap_or(l));
let mut set_lang = lang;
set_locale(lang)
}
pub fn set_locale(lang: &str) {
let mut best = None;
let prefix = lang.split_inclusive("_").next().unwrap();
for l in rust_i18n::available_locales!() {
if l == lang {
set_lang = l;
best = Some(l);
break;
}
if l.split("_").next().unwrap() == lang.split("_").next().unwrap() {
set_lang = l;
if best.is_none() && l.starts_with(prefix) {
best = Some(l);
}
}
rust_i18n::set_locale(set_lang);
rust_i18n::set_locale(best.unwrap_or(lang));
}
pub fn translate_cli(mut cmd: clap::Command) -> clap::Command {
@@ -144,7 +149,7 @@ impl MultiExecutable {
}
pub fn execute(&self) {
set_locale();
set_locale_from_env();
let mut popped = Vec::with_capacity(2);
let mut args = std::env::args_os().collect::<VecDeque<_>>();

View File

@@ -6,6 +6,7 @@ use std::time::Duration;
use futures::{Future, StreamExt};
use imbl_value::InternedString;
use josekit::jwk::Jwk;
use openssl::x509::X509;
use patch_db::PatchDb;
use rpc_toolkit::Context;
use serde::{Deserialize, Serialize};
@@ -15,10 +16,9 @@ use tracing::instrument;
use ts_rs::TS;
use crate::MAIN_DATA;
use crate::account::AccountInfo;
use crate::context::RpcContext;
use crate::context::config::ServerConfig;
use crate::disk::mount::guard::TmpMountGuard;
use crate::disk::mount::guard::{MountGuard, TmpMountGuard};
use crate::hostname::Hostname;
use crate::net::gateway::UpgradableListener;
use crate::net::web_server::{WebServer, WebServerAcceptorSetter};
@@ -27,7 +27,9 @@ use crate::progress::FullProgressTracker;
use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations};
use crate::setup::SetupProgress;
use crate::shutdown::Shutdown;
use crate::system::KeyboardOptions;
use crate::util::future::NonDetachingJoinHandle;
use crate::util::serde::Pem;
use crate::util::sync::SyncMutex;
lazy_static::lazy_static! {
@@ -42,27 +44,10 @@ lazy_static::lazy_static! {
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct SetupResult {
pub tor_addresses: Vec<String>,
#[ts(type = "string")]
pub hostname: Hostname,
#[ts(type = "string")]
pub lan_address: InternedString,
pub root_ca: String,
}
impl TryFrom<&AccountInfo> for SetupResult {
type Error = Error;
fn try_from(value: &AccountInfo) -> Result<Self, Self::Error> {
Ok(Self {
tor_addresses: value
.tor_keys
.iter()
.map(|tor_key| format!("https://{}", tor_key.onion_address()))
.collect(),
hostname: value.hostname.clone(),
lan_address: value.hostname.lan_address(),
root_ca: String::from_utf8(value.root_ca_cert.to_pem()?)?,
})
}
pub root_ca: Pem<X509>,
pub needs_restart: bool,
}
pub struct SetupContextSeed {
@@ -75,7 +60,9 @@ pub struct SetupContextSeed {
pub disk_guid: OnceCell<InternedString>,
pub shutdown: Sender<Option<Shutdown>>,
pub rpc_continuations: RpcContinuations,
pub install_rootfs: SyncMutex<Option<TmpMountGuard>>,
pub install_rootfs: SyncMutex<Option<(TmpMountGuard, MountGuard)>>,
pub keyboard: SyncMutex<Option<KeyboardOptions>>,
pub language: SyncMutex<Option<InternedString>>,
}
#[derive(Clone)]
@@ -100,6 +87,8 @@ impl SetupContext {
shutdown,
rpc_continuations: RpcContinuations::new(),
install_rootfs: SyncMutex::new(None),
language: SyncMutex::new(None),
keyboard: SyncMutex::new(None),
})))
}
#[instrument(skip_all)]
@@ -129,7 +118,10 @@ impl SetupContext {
Ok(res)
}
Err(e) => {
tracing::error!("{}", t!("context.setup.setup-failed", error = e));
tracing::error!(
"{}",
t!("context.setup.setup-failed", error = e)
);
tracing::debug!("{e:?}");
Err(e)
}
@@ -142,7 +134,10 @@ impl SetupContext {
)
.map_err(|_| {
if self.result.initialized() {
Error::new(eyre!("{}", t!("context.setup.setup-already-complete")), ErrorKind::InvalidRequest)
Error::new(
eyre!("{}", t!("context.setup.setup-already-complete")),
ErrorKind::InvalidRequest,
)
} else {
Error::new(
eyre!("{}", t!("context.setup.setup-already-in-progress")),

View File

@@ -14,6 +14,7 @@ use crate::notifications::Notifications;
use crate::prelude::*;
use crate::sign::AnyVerifyingKey;
use crate::ssh::SshKeys;
use crate::system::KeyboardOptions;
use crate::util::serde::Pem;
pub mod package;
@@ -28,9 +29,14 @@ pub struct Database {
pub private: Private,
}
impl Database {
pub fn init(account: &AccountInfo, kiosk: Option<bool>) -> Result<Self, Error> {
pub fn init(
account: &AccountInfo,
kiosk: Option<bool>,
language: Option<InternedString>,
keyboard: Option<KeyboardOptions>,
) -> Result<Self, Error> {
Ok(Self {
public: Public::init(account, kiosk)?,
public: Public::init(account, kiosk, language, keyboard)?,
private: Private {
key_store: KeyStore::new(account)?,
password: account.password.clone(),

View File

@@ -417,8 +417,7 @@ impl Map for CurrentDependencies {
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
pub struct CurrentDependencyInfo {
#[ts(type = "string | null")]
pub title: Option<InternedString>,
pub title: Option<String>,
pub icon: Option<DataUrl<'static>>,
#[serde(flatten)]
pub kind: CurrentDependencyKind,

View File

@@ -45,7 +45,12 @@ pub struct Public {
pub ui: Value,
}
impl Public {
pub fn init(account: &AccountInfo, kiosk: Option<bool>) -> Result<Self, Error> {
pub fn init(
account: &AccountInfo,
kiosk: Option<bool>,
language: Option<InternedString>,
keyboard: Option<KeyboardOptions>,
) -> Result<Self, Error> {
Ok(Self {
server_info: ServerInfo {
arch: get_arch(),
@@ -139,8 +144,8 @@ impl Public {
ram: 0,
devices: Vec::new(),
kiosk,
language: None,
keyboard: None,
language,
keyboard,
},
package_data: AllPackageData::default(),
ui: serde_json::from_str(*DB_UI_SEED_CELL.get().unwrap_or(&"null"))

View File

@@ -1,11 +1,11 @@
use std::collections::BTreeMap;
use std::path::Path;
use imbl_value::InternedString;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::prelude::*;
use crate::s9pk::manifest::LocaleString;
use crate::util::PathOrUrl;
use crate::{Error, PackageId};
@@ -28,7 +28,7 @@ impl Map for Dependencies {
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
pub struct DepInfo {
pub description: Option<String>,
pub description: Option<LocaleString>,
pub optional: bool,
#[serde(flatten)]
pub metadata: Option<MetadataSrc>,
@@ -73,7 +73,7 @@ pub enum MetadataSrc {
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct Metadata {
pub title: InternedString,
pub title: LocaleString,
pub icon: PathOrUrl,
}
@@ -82,5 +82,5 @@ pub struct Metadata {
#[model = "Model<Self>"]
pub struct DependencyMetadata {
#[ts(type = "string")]
pub title: InternedString,
pub title: LocaleString,
}

View File

@@ -377,6 +377,20 @@ pub fn server<C: Context>() -> ParentHandler<C> {
"host",
net::host::server_host_api::<C>().with_about("about.commands-host-system-ui"),
)
.subcommand(
"set-keyboard",
from_fn_async(system::set_keyboard)
.no_display()
.with_about("about.set-keyboard")
.with_call_remote::<CliContext>(),
)
.subcommand(
"set-language",
from_fn_async(system::set_language)
.no_display()
.with_about("about.set-language")
.with_call_remote::<CliContext>(),
)
}
pub fn package<C: Context>() -> ParentHandler<C> {

View File

@@ -78,37 +78,6 @@ pub async fn partition(
gpt.update_partitions(Default::default())?;
// Calculate where the OS partitions will end
// EFI/BIOS: 100MB or 8MB, Boot: 1GB, Root: 15GB
let efi_size_sectors = if use_efi {
100 * 1024 * 1024 / 512
} else {
8 * 1024 * 1024 / 512
};
let boot_size_sectors = 1024 * 1024 * 1024 / 512;
let root_size_sectors = 15 * 1024 * 1024 * 1024 / 512;
// GPT typically starts partitions at sector 2048
let os_partitions_end_sector =
2048 + efi_size_sectors + boot_size_sectors + root_size_sectors;
// Check if protected partition would be overwritten
if let Some((first_lba, _, ref path)) = protected_partition_info {
if first_lba < os_partitions_end_sector {
return Err(Error::new(
eyre!(
concat!(
"Protected partition {} starts at sector {}",
" which would be overwritten by OS partitions ending at sector {}"
),
path.display(),
first_lba,
os_partitions_end_sector
),
crate::ErrorKind::DiskManagement,
));
}
}
let efi = if use_efi {
gpt.add_partition("efi", 100 * 1024 * 1024, gpt::partition_types::EFI, 0, None)?;
true
@@ -141,6 +110,30 @@ pub async fn partition(
None,
)?;
// Check if protected partition would be overwritten by OS partitions
if let Some((first_lba, _, ref path)) = protected_partition_info {
// Get the actual end sector of the last OS partition (root = partition 3)
let os_partitions_end_sector = gpt
.partitions()
.get(&3)
.map(|p| p.last_lba)
.unwrap_or(0);
if first_lba <= os_partitions_end_sector {
return Err(Error::new(
eyre!(
concat!(
"Protected partition {} starts at sector {}",
" which would be overwritten by OS partitions ending at sector {}"
),
path.display(),
first_lba,
os_partitions_end_sector
),
crate::ErrorKind::DiskManagement,
));
}
}
let data_part = if let Some((first_lba, last_lba, path)) = protected_partition_info {
// Re-create the data partition entry at the same location
let length_lba = last_lba - first_lba + 1;

View File

@@ -454,20 +454,25 @@ pub async fn install_os(
)
})?;
}
if let Some(guid) = disks.iter().find_map(|d| {
d.guid
.as_ref()
.filter(|_| &d.logicalname == logicalname)
.cloned()
.or_else(|| {
d.partitions.iter().find_map(|p| {
p.guid
.as_ref()
.filter(|_| &p.logicalname == logicalname)
.cloned()
if let Some(guid) = (!data_drive.wipe)
.then(|| disks.iter())
.into_iter()
.flatten()
.find_map(|d| {
d.guid
.as_ref()
.filter(|_| &d.logicalname == logicalname)
.cloned()
.or_else(|| {
d.partitions.iter().find_map(|p| {
p.guid
.as_ref()
.filter(|_| &p.logicalname == logicalname)
.cloned()
})
})
})
}) {
})
{
setup_info.guid = Some(guid);
setup_info.attach = true;
} else {
@@ -476,13 +481,20 @@ pub async fn install_os(
}
}
let config = MountGuard::mount(
&Bind::new(rootfs.path().join("config")),
"/media/startos/config",
ReadWrite,
)
.await?;
write_file_atomic(
rootfs.path().join("config/setup.json"),
"/media/startos/config/setup.json",
IoFormat::JsonPretty.to_vec(&setup_info)?,
)
.await?;
ctx.install_rootfs.replace(Some(rootfs));
ctx.install_rootfs.replace(Some((rootfs, config)));
Ok(setup_info)
}

View File

@@ -44,28 +44,44 @@ impl DeviceInfo {
impl DeviceInfo {
pub fn to_header_value(&self) -> HeaderValue {
let mut url: Url = "http://localhost".parse().unwrap();
url.query_pairs_mut()
.append_pair("os.version", &self.os.version.to_string())
let mut qp = url.query_pairs_mut();
qp.append_pair("os.version", &self.os.version.to_string())
.append_pair("os.compat", &self.os.compat.to_string())
.append_pair("os.platform", &*self.os.platform);
if let Some(lang) = self.os.language.as_deref() {
qp.append_pair("os.language", lang);
}
drop(qp);
HeaderValue::from_str(url.query().unwrap_or_default()).unwrap()
}
pub fn from_header_value(header: &HeaderValue) -> Result<Self, Error> {
let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect();
let has_hw_info = query.keys().any(|k| k.starts_with("hardware."));
let version = query
.get("os.version")
.or_not_found("os.version")?
.parse()?;
Ok(Self {
os: OsInfo {
version: query
.get("os.version")
.or_not_found("os.version")?
.parse()?,
compat: query.get("os.compat").or_not_found("os.compat")?.parse()?,
platform: query
.get("os.platform")
.or_not_found("os.platform")?
.deref()
.into(),
language: query
.get("os.language")
.map(|v| v.deref())
.map(InternedString::intern)
.or_else(|| {
if version < "0.4.0-alpha.18".parse().ok()? {
Some(rust_i18n::locale().deref().into())
} else {
None
}
}),
version,
},
hardware: has_hw_info
.then(|| {
@@ -190,8 +206,8 @@ pub struct OsInfo {
pub version: Version,
#[ts(type = "string")]
pub compat: VersionRange,
#[ts(type = "string")]
pub platform: InternedString,
pub language: Option<InternedString>,
}
impl From<&RpcContext> for OsInfo {
fn from(_: &RpcContext) -> Self {
@@ -199,6 +215,7 @@ impl From<&RpcContext> for OsInfo {
version: crate::version::Current::default().semver(),
compat: crate::version::Current::default().compat().clone(),
platform: InternedString::intern(&*crate::PLATFORM),
language: Some(InternedString::intern(&*rust_i18n::locale())),
}
}
}

View File

@@ -11,6 +11,7 @@ use crate::context::CliContext;
use crate::prelude::*;
use crate::registry::context::RegistryContext;
use crate::registry::package::index::Category;
use crate::s9pk::manifest::LocaleString;
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
pub fn category_api<C: Context>() -> ParentHandler<C> {
@@ -66,7 +67,7 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
pub struct AddCategoryParams {
#[ts(type = "string")]
pub id: InternedString,
pub name: String,
pub name: LocaleString,
}
pub async fn add_category(
@@ -196,7 +197,7 @@ pub fn display_categories<T>(
"NAME",
]);
for (id, info) in categories {
table.add_row(row![&*id, &info.name]);
table.add_row(row![&*id, &info.name.localized()]);
}
table.print_tty(false)?;
Ok(())

View File

@@ -79,20 +79,20 @@ pub struct GetPackageResponse {
pub other_versions: Option<BTreeMap<VersionString, PackageInfoShort>>,
}
impl GetPackageResponse {
pub fn tables(&self) -> Vec<prettytable::Table> {
pub fn tables(self) -> Vec<prettytable::Table> {
use prettytable::*;
let mut res = Vec::with_capacity(self.best.len());
for (version, info) in &self.best {
let mut table = info.table(version);
for (version, info) in self.best {
let mut table = info.table(&version);
let lesser_versions: BTreeMap<_, _> = self
.other_versions
.as_ref()
.into_iter()
.flatten()
.filter(|(v, _)| ***v < **version)
.filter(|(v, _)| ***v < *version)
.collect();
if !lesser_versions.is_empty() {
@@ -121,13 +121,17 @@ pub struct GetPackageResponseFull {
pub other_versions: BTreeMap<VersionString, PackageVersionInfo>,
}
impl GetPackageResponseFull {
pub fn tables(&self) -> Vec<prettytable::Table> {
pub fn tables(self) -> Vec<prettytable::Table> {
let mut res = Vec::with_capacity(self.best.len());
let all: BTreeMap<_, _> = self.best.iter().chain(self.other_versions.iter()).collect();
let all: BTreeMap<_, _> = self
.best
.into_iter()
.chain(self.other_versions.into_iter())
.collect();
for (version, info) in all {
res.push(info.table(version));
res.push(info.table(&version));
}
res
@@ -444,7 +448,11 @@ pub async fn cli_download(
return Err(Error::new(
eyre!(
"{}",
t!("registry.package.get.version-not-found", id = id, version = target_version.unwrap_or(VersionRange::Any))
t!(
"registry.package.get.version-not-found",
id = id,
version = target_version.unwrap_or(VersionRange::Any)
)
),
ErrorKind::NotFound,
));
@@ -465,7 +473,11 @@ pub async fn cli_download(
return Err(Error::new(
eyre!(
"{}",
t!("registry.package.get.version-not-found", id = id, version = target_version.unwrap_or(VersionRange::Any))
t!(
"registry.package.get.version-not-found",
id = id,
version = target_version.unwrap_or(VersionRange::Any)
)
),
ErrorKind::NotFound,
));

View File

@@ -17,7 +17,7 @@ use crate::registry::device_info::DeviceInfo;
use crate::rpc_continuations::Guid;
use crate::s9pk::S9pk;
use crate::s9pk::git_hash::GitHash;
use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements};
use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements, LocaleString};
use crate::s9pk::merkle_archive::source::FileSource;
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
use crate::sign::{AnySignature, AnyVerifyingKey};
@@ -49,22 +49,27 @@ pub struct PackageInfo {
#[model = "Model<Self>"]
#[ts(export)]
pub struct Category {
pub name: String,
pub name: LocaleString,
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq, Eq)]
#[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
#[ts(export)]
pub struct DependencyMetadata {
#[ts(type = "string | null")]
pub title: Option<InternedString>,
pub title: Option<LocaleString>,
pub icon: Option<DataUrl<'static>>,
pub description: Option<String>,
pub description: Option<LocaleString>,
pub optional: bool,
}
impl DependencyMetadata {
pub fn localize_for(&mut self, locale: &str) {
self.title.as_mut().map(|t| t.localize_for(locale));
self.description.as_mut().map(|d| d.localize_for(locale));
}
}
#[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq, Eq)]
#[derive(Debug, Deserialize, Serialize, HasModel, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[model = "Model<Self>"]
pub struct PackageMetadata {
@@ -72,7 +77,7 @@ pub struct PackageMetadata {
pub title: InternedString,
pub icon: DataUrl<'static>,
pub description: Description,
pub release_notes: String,
pub release_notes: LocaleString,
pub git_hash: Option<GitHash>,
#[ts(type = "string")]
pub license: InternedString,
@@ -199,20 +204,20 @@ impl PackageVersionInfo {
self.s9pks.sort_by_key(|(h, _)| h.specificity_desc());
Ok(())
}
pub fn table(&self, version: &VersionString) -> prettytable::Table {
pub fn table(self, version: &VersionString) -> prettytable::Table {
use prettytable::*;
let mut table = Table::new();
table.add_row(row![bc => &self.metadata.title]);
table.add_row(row![br -> "VERSION", AsRef::<str>::as_ref(version)]);
table.add_row(row![br -> "RELEASE NOTES", &self.metadata.release_notes]);
table.add_row(row![br -> "RELEASE NOTES", &self.metadata.release_notes.localized()]);
table.add_row(
row![br -> "ABOUT", &textwrap::wrap(&self.metadata.description.short, 80).join("\n")],
row![br -> "ABOUT", &textwrap::wrap(&self.metadata.description.short.localized(), 80).join("\n")],
);
table.add_row(row![
br -> "DESCRIPTION",
&textwrap::wrap(&self.metadata.description.long, 80).join("\n")
&textwrap::wrap(&self.metadata.description.long.localized(), 80).join("\n")
]);
table.add_row(row![br -> "GIT HASH", self.metadata.git_hash.as_deref().unwrap_or("N/A")]);
table.add_row(row![br -> "LICENSE", &self.metadata.license]);
@@ -280,6 +285,24 @@ impl Model<PackageVersionInfo> {
{
return Ok(false);
}
if let Some(locale) = device_info.os.language.as_deref() {
let metadata = self.as_metadata_mut();
metadata
.as_alerts_mut()
.mutate(|a| Ok(a.localize_for(locale)));
metadata
.as_dependency_metadata_mut()
.as_entries_mut()?
.into_iter()
.try_for_each(|(_, d)| d.mutate(|d| Ok(d.localize_for(locale))));
metadata
.as_description_mut()
.mutate(|d| Ok(d.localize_for(locale)))?;
metadata
.as_release_notes_mut()
.mutate(|r| Ok(r.localize_for(locale)))?;
}
}
Ok(true)

View File

@@ -9,7 +9,7 @@ use tokio::process::Command;
use crate::dependencies::{DepInfo, Dependencies};
use crate::prelude::*;
use crate::s9pk::manifest::{DeviceFilter, Manifest};
use crate::s9pk::manifest::{DeviceFilter, LocaleString, Manifest};
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
use crate::s9pk::merkle_archive::source::TmpSource;
use crate::s9pk::merkle_archive::{Entry, MerkleArchive};
@@ -198,7 +198,7 @@ impl TryFrom<ManifestV1> for Manifest {
title: format!("{} (Legacy)", value.title).into(),
version: version.into(),
satisfies: BTreeSet::new(),
release_notes: value.release_notes,
release_notes: LocaleString::Translated(value.release_notes),
can_migrate_from: VersionRange::any(),
can_migrate_to: VersionRange::none(),
license: value.license.into(),
@@ -226,7 +226,7 @@ impl TryFrom<ManifestV1> for Manifest {
(
id,
DepInfo {
description: value.description,
description: value.description.map(LocaleString::Translated),
optional: !value.requirement.required(),
metadata: None,
},

View File

@@ -1,9 +1,10 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use clap::builder::ValueParserFactory;
use color_eyre::eyre::eyre;
use exver::{Version, VersionRange};
use imbl_value::InternedString;
use imbl_value::{InOMap, InternedString};
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use url::Url;
@@ -17,7 +18,7 @@ use crate::s9pk::merkle_archive::expected::{Expected, Filter};
use crate::s9pk::v2::pack::ImageConfig;
use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor};
use crate::util::serde::Regex;
use crate::util::{VersionString, mime};
use crate::util::{FromStrParser, VersionString, mime};
use crate::version::{Current, VersionT};
use crate::{ImageId, VolumeId};
@@ -35,7 +36,7 @@ pub struct Manifest {
pub title: InternedString,
pub version: VersionString,
pub satisfies: BTreeSet<VersionString>,
pub release_notes: String,
pub release_notes: LocaleString,
#[ts(type = "string")]
pub can_migrate_to: VersionRange,
#[ts(type = "string")]
@@ -190,6 +191,118 @@ impl HardwareRequirements {
}
}
#[derive(Clone, Debug, PartialEq, TS)]
#[ts(type = "string | Record<string, string>")]
pub enum LocaleString {
Translated(String),
LanguageMap(InOMap<InternedString, String>),
}
impl std::str::FromStr for LocaleString {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Try JSON parse first (for maps or quoted strings)
if let Ok(parsed) = serde_json::from_str::<LocaleString>(s) {
return Ok(parsed);
}
// Fall back to plain string
Ok(LocaleString::Translated(s.to_owned()))
}
}
impl LocaleString {
pub fn localize_for(&mut self, locale: &str) {
if let Self::LanguageMap(map) = self {
if let Some(translated) = map.remove(locale) {
*self = Self::Translated(translated);
return;
}
let prefix = locale.split_inclusive("_").next().unwrap();
let mut first = None;
for (lang, translated) in std::mem::take(map) {
if lang.starts_with(prefix) {
*self = Self::Translated(translated);
return;
}
if first.is_none() {
first = Some(translated);
}
}
*self = Self::Translated(first.unwrap_or_default())
}
}
pub fn localized_for(mut self, locale: &str) -> String {
self.localize_for(locale);
if let Self::Translated(s) = self {
s
} else {
unreachable!()
}
}
pub fn localize(&mut self) {
self.localize_for(&*rust_i18n::locale());
}
pub fn localized(mut self) -> String {
self.localized_for(&*rust_i18n::locale())
}
}
impl<'de> Deserialize<'de> for LocaleString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct LocaleStringVisitor;
impl<'de> serde::de::Visitor<'de> for LocaleStringVisitor {
type Value = LocaleString;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or a map of language codes to strings")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(LocaleString::Translated(value.to_owned()))
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(LocaleString::Translated(value))
}
fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
let language_map =
InOMap::deserialize(serde::de::value::MapAccessDeserializer::new(map))?;
Ok(LocaleString::LanguageMap(language_map))
}
}
deserializer.deserialize_any(LocaleStringVisitor)
}
}
impl Serialize for LocaleString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
LocaleString::Translated(s) => serializer.serialize_str(s),
LocaleString::LanguageMap(map) => map.serialize(serializer),
}
}
}
impl ValueParserFactory for LocaleString {
type Parser = FromStrParser<Self>;
fn value_parser() -> Self::Parser {
FromStrParser::new()
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
@@ -294,21 +407,32 @@ impl DeviceFilter {
}
}
#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq)]
#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq)]
#[ts(export)]
pub struct Description {
pub short: String,
pub long: String,
pub short: LocaleString,
pub long: LocaleString,
}
impl Description {
pub fn localize_for(&mut self, locale: &str) {
self.short.localize_for(locale);
self.long.localize_for(locale);
}
pub fn validate(&self) -> Result<(), Error> {
if self.short.chars().skip(160).next().is_some() {
if match &self.short {
LocaleString::Translated(s) => s.len() > 160,
LocaleString::LanguageMap(map) => map.values().any(|s| s.len() > 160),
} {
return Err(Error::new(
eyre!("Short description must be 160 characters or less."),
crate::ErrorKind::ValidateS9pk,
));
}
if self.long.chars().skip(5000).next().is_some() {
if match &self.short {
LocaleString::Translated(s) => s.len() > 5000,
LocaleString::LanguageMap(map) => map.values().any(|s| s.len() > 5000),
} {
return Err(Error::new(
eyre!("Long description must be 5000 characters or less."),
crate::ErrorKind::ValidateS9pk,
@@ -318,13 +442,22 @@ impl Description {
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq, Eq)]
#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct Alerts {
pub install: Option<String>,
pub uninstall: Option<String>,
pub restore: Option<String>,
pub start: Option<String>,
pub stop: Option<String>,
pub install: Option<LocaleString>,
pub uninstall: Option<LocaleString>,
pub restore: Option<LocaleString>,
pub start: Option<LocaleString>,
pub stop: Option<LocaleString>,
}
impl Alerts {
pub fn localize_for(&mut self, locale: &str) {
self.install.as_mut().map(|s| s.localize_for(locale));
self.uninstall.as_mut().map(|s| s.localize_for(locale));
self.restore.as_mut().map(|s| s.localize_for(locale));
self.start.as_mut().map(|s| s.localize_for(locale));
self.stop.as_mut().map(|s| s.localize_for(locale));
}
}

View File

@@ -20,7 +20,7 @@ use crate::prelude::*;
use crate::rpc_continuations::Guid;
use crate::s9pk::S9pk;
use crate::s9pk::git_hash::GitHash;
use crate::s9pk::manifest::Manifest;
use crate::s9pk::manifest::{LocaleString, Manifest};
use crate::s9pk::merkle_archive::directory_contents::DirectoryContents;
use crate::s9pk::merkle_archive::source::http::HttpSource;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
@@ -756,7 +756,7 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
}
};
Some((
s9pk.as_manifest().title.clone(),
LocaleString::Translated(s9pk.as_manifest().title.to_string()),
s9pk.icon_data_url().await?,
))
}

View File

@@ -4,7 +4,6 @@ use std::str::FromStr;
use clap::builder::ValueParserFactory;
use exver::VersionRange;
use imbl_value::InternedString;
use rust_i18n::t;
use crate::db::model::package::{
@@ -149,13 +148,25 @@ impl FromStr for DependencyRequirement {
.map(|id| id.parse().map_err(Error::from))
.collect(),
Some((kind, _)) => Err(Error::new(
eyre!("{}", t!("service.effects.dependency.unknown-dependency-kind", kind = kind)),
eyre!(
"{}",
t!(
"service.effects.dependency.unknown-dependency-kind",
kind = kind
)
),
ErrorKind::InvalidRequest,
)),
None => match rest {
"r" | "running" => Ok(BTreeSet::new()),
kind => Err(Error::new(
eyre!("{}", t!("service.effects.dependency.unknown-dependency-kind", kind = kind)),
eyre!(
"{}",
t!(
"service.effects.dependency.unknown-dependency-kind",
kind = kind
)
),
ErrorKind::InvalidRequest,
)),
},
@@ -217,7 +228,7 @@ pub async fn set_dependencies(
.s9pk
.dependency_metadata(&dep_id)
.await?
.map(|m| m.title),
.map(|m| m.title.localized()),
icon: context
.seed
.persistent_container
@@ -294,8 +305,7 @@ pub struct CheckDependenciesParam {
#[ts(export)]
pub struct CheckDependenciesResult {
package_id: PackageId,
#[ts(type = "string | null")]
title: Option<InternedString>,
title: Option<String>,
installed_version: Option<VersionString>,
satisfies: BTreeSet<VersionString>,
is_running: bool,

View File

@@ -19,6 +19,7 @@ use crate::account::AccountInfo;
use crate::auth::write_shadow;
use crate::backup::restore::recover_full_server;
use crate::backup::target::BackupTargetFS;
use crate::bins::set_locale;
use crate::context::rpc::InitRpcContextPhases;
use crate::context::setup::SetupResult;
use crate::context::{RpcContext, SetupContext};
@@ -36,11 +37,11 @@ use crate::prelude::*;
use crate::progress::{FullProgress, PhaseProgressTrackerHandle, ProgressUnits};
use crate::rpc_continuations::Guid;
use crate::shutdown::Shutdown;
use crate::system::sync_kiosk;
use crate::system::{KeyboardOptions, SetLanguageParams, save_language, sync_kiosk};
use crate::util::Invoke;
use crate::util::crypto::EncryptedWire;
use crate::util::io::{Counter, create_file, dir_copy, dir_size, read_file_to_string};
use crate::util::serde::IoFormat;
use crate::util::serde::{IoFormat, Pem};
use crate::{DATA_DIR, Error, ErrorKind, MAIN_DATA, PACKAGE_DATA, PLATFORM, ResultExt};
pub fn setup<C: Context>() -> ParentHandler<C> {
@@ -75,6 +76,9 @@ pub fn setup<C: Context>() -> ParentHandler<C> {
.with_about("about.display-os-logs"),
)
.subcommand("restart", from_fn_async(restart).no_cli())
.subcommand("shutdown", from_fn_async(shutdown).no_cli())
.subcommand("set-language", from_fn_async(set_language).no_cli())
.subcommand("set-keyboard", from_fn_async(set_keyboard).no_cli())
}
pub fn disk<C: Context>() -> ParentHandler<C> {
@@ -103,6 +107,8 @@ async fn setup_init(
init_phases: InitPhases,
) -> Result<(AccountInfo, InitResult), Error> {
let init_result = init(&ctx.webserver, &ctx.config.peek(|c| c.clone()), init_phases).await?;
let language = ctx.language.peek(|a| a.clone());
let keyboard = ctx.keyboard.peek(|a| a.clone());
let account = init_result
.net_ctrl
@@ -118,6 +124,12 @@ async fn setup_init(
if let Some(kiosk) = kiosk {
info.as_kiosk_mut().ser(&Some(kiosk))?;
}
if let Some(language) = language.clone() {
info.as_language_mut().ser(&Some(language))?;
}
if let Some(keyboard) = keyboard.clone() {
info.as_keyboard_mut().ser(&Some(keyboard))?;
}
Ok(account)
})
@@ -126,6 +138,13 @@ async fn setup_init(
sync_kiosk(kiosk).await?;
if let Some(language) = language {
save_language(&*language).await?;
}
if let Some(keyboard) = keyboard {
keyboard.save().await?;
}
if let Some(password) = &password {
write_shadow(&password).await?;
}
@@ -137,7 +156,6 @@ async fn setup_init(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct AttachParams {
#[serde(rename = "startOsPassword")]
pub password: Option<EncryptedWire>,
pub guid: InternedString,
#[ts(optional)]
@@ -155,63 +173,81 @@ pub async fn attach(
) -> Result<SetupProgress, Error> {
let setup_ctx = ctx.clone();
ctx.run_setup(move || async move {
let progress = &setup_ctx.progress;
let mut disk_phase = progress.add_phase(t!("setup.opening-data-drive").into(), Some(10));
let init_phases = InitPhases::new(&progress);
let rpc_ctx_phases = InitRpcContextPhases::new(&progress);
let progress = &setup_ctx.progress;
let mut disk_phase = progress.add_phase(t!("setup.opening-data-drive").into(), Some(10));
let init_phases = InitPhases::new(&progress);
let rpc_ctx_phases = InitRpcContextPhases::new(&progress);
let password: Option<String> = match password {
Some(a) => match a.decrypt(&setup_ctx) {
a @ Some(_) => a,
None => {
return Err(Error::new(
color_eyre::eyre::eyre!("{}", t!("setup.couldnt-decode-password")),
crate::ErrorKind::Unknown,
));
}
},
None => None,
};
let password: Option<String> = match password {
Some(a) => match a.decrypt(&setup_ctx) {
a @ Some(_) => a,
None => {
return Err(Error::new(
color_eyre::eyre::eyre!("{}", t!("setup.couldnt-decode-password")),
crate::ErrorKind::Unknown,
));
}
},
None => None,
};
disk_phase.start();
let requires_reboot = crate::disk::main::import(
&*disk_guid,
DATA_DIR,
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
RepairStrategy::Aggressive
} else {
RepairStrategy::Preen
},
if disk_guid.ends_with("_UNENC") { None } else { Some(DEFAULT_PASSWORD) },
)
.await?;
let _ = setup_ctx.disk_guid.set(disk_guid.clone());
disk_phase.start();
let requires_reboot = crate::disk::main::import(
&*disk_guid,
DATA_DIR,
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
tokio::fs::remove_file(REPAIR_DISK_PATH)
.await
.with_ctx(|_| (ErrorKind::Filesystem, REPAIR_DISK_PATH))?;
}
if requires_reboot.0 {
crate::disk::main::export(&*disk_guid, DATA_DIR).await?;
return Err(Error::new(
eyre!("{}", t!("setup.disk-errors-corrected-restart-required")),
ErrorKind::DiskManagement,
));
}
disk_phase.complete();
RepairStrategy::Aggressive
} else {
RepairStrategy::Preen
},
if disk_guid.ends_with("_UNENC") {
None
} else {
Some(DEFAULT_PASSWORD)
},
)
.await?;
let _ = setup_ctx.disk_guid.set(disk_guid.clone());
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
tokio::fs::remove_file(REPAIR_DISK_PATH)
.await
.with_ctx(|_| (ErrorKind::Filesystem, REPAIR_DISK_PATH))?;
}
if requires_reboot.0 {
crate::disk::main::export(&*disk_guid, DATA_DIR).await?;
return Err(Error::new(
eyre!("{}", t!("setup.disk-errors-corrected-restart-required")),
ErrorKind::DiskManagement,
));
}
disk_phase.complete();
let (account, net_ctrl) = setup_init(&setup_ctx, password, kiosk, init_phases).await?;
let (account, net_ctrl) = setup_init(&setup_ctx, password, kiosk, init_phases).await?;
let rpc_ctx = RpcContext::init(&setup_ctx.webserver, &setup_ctx.config.peek(|c| c.clone()), disk_guid, Some(net_ctrl), rpc_ctx_phases).await?;
let rpc_ctx = RpcContext::init(
&setup_ctx.webserver,
&setup_ctx.config.peek(|c| c.clone()),
disk_guid,
Some(net_ctrl),
rpc_ctx_phases,
)
.await?;
Ok(((&account).try_into()?, rpc_ctx))
})?;
Ok((
SetupResult {
hostname: account.hostname,
root_ca: Pem(account.root_ca_cert),
needs_restart: setup_ctx.install_rootfs.peek(|a| a.is_some()),
},
rpc_ctx,
))
})?;
Ok(ctx.progress().await)
}
#[derive(Debug, Deserialize, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "kebab-case")]
#[ts(export)]
#[serde(tag = "status")]
pub enum SetupStatusRes {
@@ -246,17 +282,10 @@ pub async fn status(ctx: SetupContext) -> Result<SetupStatusRes, Error> {
if ctx.task.initialized() {
Ok(SetupStatusRes::Running(ctx.progress().await))
} else {
let path = if tokio::fs::metadata("/run/live/medium").await.is_ok() {
let Some(path) = ctx
.install_rootfs
.peek(|fs| fs.as_ref().map(|fs| fs.path().join("config/setup.json")))
else {
return Ok(SetupStatusRes::NeedsInstall);
};
path
} else {
Path::new("/media/startos/config/setup.json").to_path_buf()
};
let path = Path::new("/media/startos/config/setup.json");
if tokio::fs::metadata(path).await.is_err() {
return Ok(SetupStatusRes::NeedsInstall);
}
IoFormat::Json
.from_slice(read_file_to_string(path).await?.as_bytes())
.map(SetupStatusRes::Incomplete)
@@ -361,8 +390,8 @@ pub async fn setup_data_drive(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct SetupExecuteParams {
start_os_logicalname: PathBuf,
start_os_password: EncryptedWire,
guid: InternedString,
password: EncryptedWire,
recovery_source: Option<RecoverySource<EncryptedWire>>,
#[ts(optional)]
kiosk: Option<bool>,
@@ -372,13 +401,13 @@ pub struct SetupExecuteParams {
pub async fn execute(
ctx: SetupContext,
SetupExecuteParams {
start_os_logicalname,
start_os_password,
guid,
password,
recovery_source,
kiosk,
}: SetupExecuteParams,
) -> Result<SetupProgress, Error> {
let start_os_password = match start_os_password.decrypt(&ctx) {
let password = match password.decrypt(&ctx) {
Some(a) => a,
None => {
return Err(Error::new(
@@ -407,15 +436,7 @@ pub async fn execute(
};
let setup_ctx = ctx.clone();
ctx.run_setup(move || {
execute_inner(
setup_ctx,
start_os_logicalname,
start_os_password,
recovery,
kiosk,
)
})?;
ctx.run_setup(move || execute_inner(setup_ctx, guid, password, recovery, kiosk))?;
Ok(ctx.progress().await)
}
@@ -447,12 +468,27 @@ pub async fn complete(ctx: SetupContext) -> Result<SetupResult, Error> {
#[instrument(skip_all)]
pub async fn exit(ctx: SetupContext) -> Result<(), Error> {
ctx.shutdown.send(None).expect("failed to shutdown");
let shutdown = if let Some((rootfs, config)) = ctx.install_rootfs.replace(None) {
config.unmount(false).await?;
rootfs.unmount().await?;
Some(Shutdown {
disk_guid: ctx.disk_guid.get().cloned(),
restart: true,
})
} else {
None
};
ctx.shutdown.send(shutdown).expect("failed to shutdown");
Ok(())
}
#[instrument(skip_all)]
pub async fn restart(ctx: SetupContext) -> Result<(), Error> {
if let Some((rootfs, config)) = ctx.install_rootfs.replace(None) {
config.unmount(false).await?;
rootfs.unmount().await?;
}
ctx.shutdown
.send(Some(Shutdown {
disk_guid: ctx.disk_guid.get().cloned(),
@@ -462,16 +498,30 @@ pub async fn restart(ctx: SetupContext) -> Result<(), Error> {
Ok(())
}
#[instrument(skip_all)]
pub async fn shutdown(ctx: SetupContext) -> Result<(), Error> {
if let Some((rootfs, config)) = ctx.install_rootfs.replace(None) {
config.unmount(false).await?;
rootfs.unmount().await?;
}
ctx.shutdown
.send(Some(Shutdown {
disk_guid: ctx.disk_guid.get().cloned(),
restart: false,
}))
.expect("failed to shutdown");
Ok(())
}
#[instrument(skip_all)]
pub async fn execute_inner(
ctx: SetupContext,
start_os_logicalname: PathBuf,
start_os_password: String,
guid: InternedString,
password: String,
recovery_source: Option<RecoverySource<String>>,
kiosk: Option<bool>,
) -> Result<(SetupResult, RpcContext), Error> {
let progress = &ctx.progress;
let mut disk_phase = progress.add_phase(t!("setup.formatting-data-drive").into(), Some(10));
let restore_phase = match recovery_source.as_ref() {
Some(RecoverySource::Backup { .. }) => {
Some(progress.add_phase(t!("setup.restoring-backup").into(), Some(100)))
@@ -484,10 +534,6 @@ pub async fn execute_inner(
let init_phases = InitPhases::new(&progress);
let rpc_ctx_phases = InitRpcContextPhases::new(&progress);
disk_phase.start();
let guid = setup_data_drive(&ctx, &start_os_logicalname).await?;
disk_phase.complete();
let progress = SetupExecuteProgress {
init_phases,
restore_phase,
@@ -497,25 +543,25 @@ pub async fn execute_inner(
match recovery_source {
Some(RecoverySource::Backup {
target,
password,
password: recovery_password,
server_id,
}) => {
recover(
&ctx,
guid,
start_os_password,
password,
target,
server_id,
password,
recovery_password,
kiosk,
progress,
)
.await
}
Some(RecoverySource::Migrate { guid: old_guid }) => {
migrate(&ctx, guid, &old_guid, start_os_password, kiosk, progress).await
migrate(&ctx, guid, &old_guid, password, kiosk, progress).await
}
None => fresh_setup(&ctx, guid, &start_os_password, kiosk, progress).await,
None => fresh_setup(&ctx, guid, &password, kiosk, progress).await,
}
}
@@ -528,7 +574,7 @@ pub struct SetupExecuteProgress {
async fn fresh_setup(
ctx: &SetupContext,
guid: InternedString,
start_os_password: &str,
password: &str,
kiosk: Option<bool>,
SetupExecuteProgress {
init_phases,
@@ -536,11 +582,24 @@ async fn fresh_setup(
..
}: SetupExecuteProgress,
) -> Result<(SetupResult, RpcContext), Error> {
let account = AccountInfo::new(start_os_password, root_ca_start_time().await)?;
let account = AccountInfo::new(password, root_ca_start_time().await)?;
let db = ctx.db().await?;
let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi");
sync_kiosk(kiosk).await?;
db.put(&ROOT, &Database::init(&account, kiosk)?).await?;
let language = ctx.language.peek(|a| a.clone());
let keyboard = ctx.keyboard.peek(|a| a.clone());
if let Some(language) = &language {
save_language(&**language).await?;
}
if let Some(keyboard) = &keyboard {
keyboard.save().await?;
}
db.put(&ROOT, &Database::init(&account, kiosk, language, keyboard)?)
.await?;
drop(db);
let config = ctx.config.peek(|c| c.clone());
@@ -556,16 +615,23 @@ async fn fresh_setup(
)
.await?;
write_shadow(start_os_password).await?;
write_shadow(password).await?;
Ok(((&account).try_into()?, rpc_ctx))
Ok((
SetupResult {
hostname: account.hostname,
root_ca: Pem(account.root_ca_cert),
needs_restart: ctx.install_rootfs.peek(|a| a.is_some()),
},
rpc_ctx,
))
}
#[instrument(skip_all)]
async fn recover(
ctx: &SetupContext,
guid: InternedString,
start_os_password: String,
password: String,
recovery_source: BackupTargetFS,
server_id: String,
recovery_password: String,
@@ -576,7 +642,7 @@ async fn recover(
recover_full_server(
ctx,
guid.clone(),
start_os_password,
password,
recovery_source,
&server_id,
&recovery_password,
@@ -591,7 +657,7 @@ async fn migrate(
ctx: &SetupContext,
guid: InternedString,
old_guid: &str,
start_os_password: String,
password: String,
kiosk: Option<bool>,
SetupExecuteProgress {
init_phases,
@@ -671,7 +737,7 @@ async fn migrate(
crate::disk::main::export(&old_guid, "/media/startos/migrate").await?;
restore_phase.complete();
let (account, net_ctrl) = setup_init(&ctx, Some(start_os_password), kiosk, init_phases).await?;
let (account, net_ctrl) = setup_init(&ctx, Some(password), kiosk, init_phases).await?;
let rpc_ctx = RpcContext::init(
&ctx.webserver,
@@ -682,5 +748,27 @@ async fn migrate(
)
.await?;
Ok(((&account).try_into()?, rpc_ctx))
Ok((
SetupResult {
hostname: account.hostname,
root_ca: Pem(account.root_ca_cert),
needs_restart: ctx.install_rootfs.peek(|a| a.is_some()),
},
rpc_ctx,
))
}
pub async fn set_language(
ctx: SetupContext,
SetLanguageParams { language }: SetLanguageParams,
) -> Result<(), Error> {
set_locale(&*language);
ctx.language.replace(Some(language));
Ok(())
}
pub async fn set_keyboard(ctx: SetupContext, options: KeyboardOptions) -> Result<(), Error> {
options.apply_to_session().await?;
ctx.keyboard.replace(Some(options));
Ok(())
}

View File

@@ -15,6 +15,7 @@ use tokio::sync::broadcast::Receiver;
use tracing::instrument;
use ts_rs::TS;
use crate::bins::set_locale;
use crate::context::{CliContext, RpcContext};
use crate::disk::util::{get_available, get_used};
use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT};
@@ -1140,9 +1141,11 @@ pub async fn test_smtp(
pub struct KeyboardOptions {
#[arg(help = "help.arg.keyboard-layout")]
pub layout: InternedString,
#[arg(long, help = "help.arg.keyboard-model")]
#[arg(short, long, help = "help.arg.keyboard-keymap")]
pub keymap: Option<InternedString>,
#[arg(short, long, help = "help.arg.keyboard-model")]
pub model: Option<InternedString>,
#[arg(long, help = "help.arg.keyboard-variant")]
#[arg(short, long, help = "help.arg.keyboard-variant")]
pub variant: Option<InternedString>,
#[arg(short, long = "option", help = "help.arg.keyboard-option")]
#[serde(default)]
@@ -1166,7 +1169,18 @@ impl KeyboardOptions {
}
pub async fn save(&self) -> Result<(), Error> {
// TODO: set console keyboard
write_file_atomic(
"/media/startos/config/overlay/etc/vconsole.conf",
format!(
include_str!("./vconsole.conf.template"),
model = self.model.as_deref().unwrap_or_default(),
layout = &*self.layout,
variant = self.variant.as_deref().unwrap_or_default(),
options = self.options.join(","),
keymap = self.keymap.as_deref().unwrap_or(&*self.layout),
),
)
.await?;
write_file_atomic(
"/media/startos/config/overlay/etc/X11/xorg.conf.d/00-keyboard.conf",
format!(
@@ -1203,10 +1217,7 @@ pub struct SetLanguageParams {
pub language: InternedString,
}
pub async fn set_language(
ctx: RpcContext,
SetLanguageParams { language }: SetLanguageParams,
) -> Result<(), Error> {
pub async fn save_language(language: &str) -> Result<(), Error> {
write_file_atomic(
"/etc/locale.gen",
format!("{language}.UTF-8 UTF-8\n").as_bytes(),
@@ -1225,6 +1236,15 @@ pub async fn set_language(
format!("LANG={language}.UTF-8\n").as_bytes(),
)
.await?;
Ok(())
}
pub async fn set_language(
ctx: RpcContext,
SetLanguageParams { language }: SetLanguageParams,
) -> Result<(), Error> {
set_locale(&*language);
save_language(&*language).await?;
ctx.db
.mutate(|db| {
db.as_public_mut()
@@ -1234,7 +1254,6 @@ pub async fn set_language(
})
.await
.result?;
rust_i18n::set_locale(&*language);
Ok(())
}

View File

@@ -0,0 +1,7 @@
XKBMODEL="{model}"
XKBLAYOUT="{layout}"
XKBVARIANT="{variant}"
XKBOPTIONS="{options}"
KEYMAP="{keymap}"

View File

@@ -25,6 +25,7 @@ impl LshwDevice {
Self::Display(_) => "display",
}
}
#[instrument(skip_all)]
pub fn from_value(value: &Value) -> Option<Self> {
match value["class"].as_str() {
Some("processor") => Some(LshwDevice::Processor(LshwProcessor::from_value(value))),
@@ -41,6 +42,7 @@ pub struct LshwProcessor {
pub capabilities: BTreeSet<InternedString>,
}
impl LshwProcessor {
#[instrument(skip_all)]
fn from_value(value: &Value) -> Self {
Self {
product: value["product"].as_str().map(From::from),
@@ -63,6 +65,7 @@ pub struct LshwDisplay {
pub driver: Option<InternedString>,
}
impl LshwDisplay {
#[instrument(skip_all)]
fn from_value(value: &Value) -> Self {
Self {
product: value["product"].as_str().map(From::from),

View File

@@ -1,3 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { LocaleString } from "./LocaleString"
export type AddCategoryParams = { id: string; name: string }
export type AddCategoryParams = { id: string; name: LocaleString }

View File

@@ -1,9 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { LocaleString } from "./LocaleString"
export type Alerts = {
install: string | null
uninstall: string | null
restore: string | null
start: string | null
stop: string | null
install: LocaleString | null
uninstall: LocaleString | null
restore: LocaleString | null
start: LocaleString | null
stop: LocaleString | null
}

View File

@@ -2,7 +2,7 @@
import type { EncryptedWire } from "./EncryptedWire"
export type AttachParams = {
startOsPassword: EncryptedWire | null
password: EncryptedWire | null
guid: string
kiosk?: boolean
}

View File

@@ -1,3 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { LocaleString } from "./LocaleString"
export type Category = { name: string }
export type Category = { name: LocaleString }

View File

@@ -1,9 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DataUrl } from "./DataUrl"
import type { LocaleString } from "./LocaleString"
export type DependencyMetadata = {
title: string | null
title: LocaleString | null
icon: DataUrl | null
description: string | null
description: LocaleString | null
optional: boolean
}

View File

@@ -1,3 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { LocaleString } from "./LocaleString"
export type Description = { short: string; long: string }
export type Description = { short: LocaleString; long: LocaleString }

View File

@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type KeyboardOptions = {
layout: string
keymap: string | null
model: string | null
variant: string | null
options: Array<string>
}

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LocaleString = string | Record<string, string>

View File

@@ -6,6 +6,7 @@ import type { GitHash } from "./GitHash"
import type { HardwareRequirements } from "./HardwareRequirements"
import type { ImageConfig } from "./ImageConfig"
import type { ImageId } from "./ImageId"
import type { LocaleString } from "./LocaleString"
import type { PackageId } from "./PackageId"
import type { Version } from "./Version"
import type { VolumeId } from "./VolumeId"
@@ -15,7 +16,7 @@ export type Manifest = {
title: string
version: Version
satisfies: Array<Version>
releaseNotes: string
releaseNotes: LocaleString
canMigrateTo: string
canMigrateFrom: string
license: string

View File

@@ -1,4 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { LocaleString } from "./LocaleString"
import type { PathOrUrl } from "./PathOrUrl"
export type Metadata = { title: string; icon: PathOrUrl }
export type Metadata = { title: LocaleString; icon: PathOrUrl }

View File

@@ -5,6 +5,7 @@ import type { DependencyMetadata } from "./DependencyMetadata"
import type { Description } from "./Description"
import type { GitHash } from "./GitHash"
import type { HardwareRequirements } from "./HardwareRequirements"
import type { LocaleString } from "./LocaleString"
import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment"
import type { PackageId } from "./PackageId"
import type { RegistryAsset } from "./RegistryAsset"
@@ -15,7 +16,7 @@ export type PackageVersionInfo = {
title: string
icon: DataUrl
description: Description
releaseNotes: string
releaseNotes: LocaleString
gitHash: GitHash | null
license: string
wrapperRepo: string

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Pem = string

View File

@@ -1,5 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Governor } from "./Governor"
import type { KeyboardOptions } from "./KeyboardOptions"
import type { LshwDevice } from "./LshwDevice"
import type { NetworkInfo } from "./NetworkInfo"
import type { ServerStatus } from "./ServerStatus"
@@ -27,4 +28,6 @@ export type ServerInfo = {
ram: number
devices: Array<LshwDevice>
kiosk: boolean | null
language: string | null
keyboard: KeyboardOptions | null
}

View File

@@ -3,8 +3,8 @@ import type { EncryptedWire } from "./EncryptedWire"
import type { RecoverySource } from "./RecoverySource"
export type SetupExecuteParams = {
startOsLogicalname: string
startOsPassword: EncryptedWire
guid: string
password: EncryptedWire
recoverySource: RecoverySource<EncryptedWire> | null
kiosk?: boolean
}

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SetupInfo = { guid: string | null; attach: boolean }

View File

@@ -1,8 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Pem } from "./Pem"
export type SetupResult = {
torAddresses: Array<string>
hostname: string
lanAddress: string
rootCa: string
rootCa: Pem
needsRestart: boolean
}

View File

@@ -1,7 +1,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SetupInfo } from "./SetupInfo"
import type { SetupProgress } from "./SetupProgress"
import type { SetupResult } from "./SetupResult"
export type SetupStatusRes =
| ({ status: "complete" } & SetupResult)
| { status: "needs-install" }
| ({ status: "incomplete" } & SetupInfo)
| ({ status: "running" } & SetupProgress)
| ({ status: "complete" } & SetupResult)

View File

@@ -121,9 +121,11 @@ export { InstallingState } from "./InstallingState"
export { InstallParams } from "./InstallParams"
export { IpHostname } from "./IpHostname"
export { IpInfo } from "./IpInfo"
export { KeyboardOptions } from "./KeyboardOptions"
export { ListPackageSignersParams } from "./ListPackageSignersParams"
export { ListServiceInterfacesParams } from "./ListServiceInterfacesParams"
export { ListVersionSignersParams } from "./ListVersionSignersParams"
export { LocaleString } from "./LocaleString"
export { LoginParams } from "./LoginParams"
export { LshwDevice } from "./LshwDevice"
export { LshwDisplay } from "./LshwDisplay"
@@ -161,6 +163,7 @@ export { PackageState } from "./PackageState"
export { PackageVersionInfo } from "./PackageVersionInfo"
export { PasswordType } from "./PasswordType"
export { PathOrUrl } from "./PathOrUrl"
export { Pem } from "./Pem"
export { Percentage } from "./Percentage"
export { Progress } from "./Progress"
export { ProgressUnits } from "./ProgressUnits"
@@ -199,6 +202,7 @@ export { SetMainStatusStatus } from "./SetMainStatusStatus"
export { SetMainStatus } from "./SetMainStatus"
export { SetNameParams } from "./SetNameParams"
export { SetupExecuteParams } from "./SetupExecuteParams"
export { SetupInfo } from "./SetupInfo"
export { SetupProgress } from "./SetupProgress"
export { SetupResult } from "./SetupResult"
export { SetupStatusRes } from "./SetupStatusRes"

View File

@@ -48,9 +48,9 @@ export type SDKManifest = {
readonly docsUrl: string
readonly description: {
/** Short description to display on the marketplace list page. Max length 80 chars. */
readonly short: string
readonly short: T.LocaleString
/** Long description to display on the marketplace details page for this service. Max length 500 chars. */
readonly long: string
readonly long: T.LocaleString
}
/**
* override the StartOS version this package was made for
@@ -96,17 +96,17 @@ export type SDKManifest = {
readonly alerts?: {
/** An warning alert requiring user confirmation before proceeding with initial installation of this service. */
readonly install?: string | null
readonly install?: T.LocaleString | null
/** An warning alert requiring user confirmation before updating this service. */
readonly update?: string | null
readonly update?: T.LocaleString | null
/** An warning alert requiring user confirmation before uninstalling this service. */
readonly uninstall?: string | null
readonly uninstall?: T.LocaleString | null
/** An warning alert requiring user confirmation before restoring this service from backup. */
readonly restore?: string | null
readonly restore?: T.LocaleString | null
/** An warning alert requiring user confirmation before starting this service. */
readonly start?: string | null
readonly start?: T.LocaleString | null
/** An warning alert requiring user confirmation before stopping this service. */
readonly stop?: string | null
readonly stop?: T.LocaleString | null
}
/**
* @description A mapping of service dependencies to be displayed to users when viewing the Marketplace

View File

@@ -355,10 +355,13 @@ export class GetServiceInterface<Mapped = ServiceInterfaceFilled | null> {
const watch = this.watch(abort.signal)
const res = await watch.next()
if (this.effects.constRetry) {
watch.next().then(() => {
abort.abort()
this.effects.constRetry && this.effects.constRetry()
})
watch
.next()
.then(() => {
abort.abort()
this.effects.constRetry && this.effects.constRetry()
})
.catch()
}
return res.value
}

View File

@@ -55,10 +55,13 @@ export class GetServiceInterfaces<Mapped = ServiceInterfaceFilled[]> {
const watch = this.watch(abort.signal)
const res = await watch.next()
if (this.effects.constRetry) {
watch.next().then(() => {
abort.abort()
this.effects.constRetry && this.effects.constRetry()
})
watch
.next()
.then(() => {
abort.abort()
this.effects.constRetry && this.effects.constRetry()
})
.catch()
}
return res.value
}

View File

@@ -19,10 +19,13 @@ export class GetServiceManifest<Mapped = Manifest> {
const watch = this.watch(abort.signal)
const res = await watch.next()
if (this.effects.constRetry) {
watch.next().then(() => {
abort.abort()
this.effects.constRetry && this.effects.constRetry()
})
watch
.next()
.then(() => {
abort.abort()
this.effects.constRetry && this.effects.constRetry()
})
.catch()
}
return res.value
}

View File

@@ -230,10 +230,13 @@ export class FileHelper<A> {
eq,
]
this.consts.push(record)
watch.next().then(() => {
this.consts = this.consts.filter((r) => r !== record)
effects.constRetry && effects.constRetry()
})
watch
.next()
.then(() => {
this.consts = this.consts.filter((r) => r !== record)
effects.constRetry && effects.constRetry()
})
.catch()
}
return res.value
}
@@ -263,6 +266,7 @@ export class FileHelper<A> {
})
.catch((e) => console.error(asError(e)))
if (!prev || !eq(prev.value, newRes)) {
console.error("yielding", JSON.stringify({ prev: prev, newRes }))
yield newRes
}
prev = { value: newRes }

52
web/package-lock.json generated
View File

@@ -386,6 +386,7 @@
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.13.tgz",
"integrity": "sha512-/D84T1Caxll3I2sRihPDR9UaWBhF50M+tAX15PdP6uSh/TxwAlLl9p7Rm1bD0mPjPercqaEKA+h9a9qLP16hug==",
"license": "MIT",
"peer": true,
"dependencies": {
"ajv": "8.17.1",
"ajv-formats": "3.0.1",
@@ -413,6 +414,7 @@
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.13.tgz",
"integrity": "sha512-hdMKY4rUTko8xqeWYGnwwDYDomkeOoLsYsP6SdaHWK7hpGvzWsT6Q/aIv8J8NrCYkLu+M+5nLiKOooweUZu3GQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@angular-devkit/core": "20.3.13",
"jsonc-parser": "3.3.1",
@@ -449,6 +451,7 @@
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.3.15.tgz",
"integrity": "sha512-ikyKfhkxoqQA6JcBN0B9RaN6369sM1XYX81Id0lI58dmWCe7gYfrTp8ejqxxKftl514psQO3pkW8Gn1nJ131Gw==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -563,6 +566,7 @@
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.14.tgz",
"integrity": "sha512-7bZxc01URbiPiIBWThQ69XwOxVduqEKN4PhpbF2AAyfMc/W8Hcr4VoIJOwL0O1Nkq5beS8pCAqoOeIgFyXd/kg==",
"license": "MIT",
"peer": true,
"dependencies": {
"parse5": "^8.0.0",
"tslib": "^2.3.0"
@@ -579,6 +583,7 @@
"integrity": "sha512-G78I/HDJULloS2LSqfUfbmBlhDCbcWujIRWfuMnGsRf82TyGA2OEPe3IA/F8MrJfeOzPQim2fMyn24MqHL40Vg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@angular-devkit/architect": "0.2003.13",
"@angular-devkit/core": "20.3.13",
@@ -613,6 +618,7 @@
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.15.tgz",
"integrity": "sha512-k4mCXWRFiOHK3bUKfWkRQQ8KBPxW8TAJuKLYCsSHPCpMz6u0eA1F0VlrnOkZVKWPI792fOaEAWH2Y4PTaXlUHw==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -629,6 +635,7 @@
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.15.tgz",
"integrity": "sha512-lMicIAFAKZXa+BCZWs3soTjNQPZZXrF/WMVDinm8dQcggNarnDj4UmXgKSyXkkyqK5SLfnLsXVzrX6ndVT6z7A==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -642,6 +649,7 @@
"integrity": "sha512-8sJoxodxsfyZ8eJ5r6Bx7BCbazXYgsZ1+dE8t5u5rTQ6jNggwNtYEzkyReoD5xvP+MMtRkos3xpwq4rtFnpI6A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "7.28.3",
"@jridgewell/sourcemap-codec": "^1.4.14",
@@ -674,6 +682,7 @@
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.15.tgz",
"integrity": "sha512-NMbX71SlTZIY9+rh/SPhRYFJU0pMJYW7z/TBD4lqiO+b0DTOIg1k7Pg9ydJGqSjFO1Z4dQaA6TteNuF99TJCNw==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -699,6 +708,7 @@
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.15.tgz",
"integrity": "sha512-gS5hQkinq52pm/7mxz4yHPCzEcmRWjtUkOVddPH0V1BW/HMni/p4Y6k2KqKBeGb9p8S5EAp6PDxDVLOPukp3mg==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -727,6 +737,7 @@
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.15.tgz",
"integrity": "sha512-TxRM/wTW/oGXv/3/Iohn58yWoiYXOaeEnxSasiGNS1qhbkcKtR70xzxW6NjChBUYAixz2ERkLURkpx3pI8Q6Dw==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -791,6 +802,7 @@
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.15.tgz",
"integrity": "sha512-6+qgk8swGSoAu7ISSY//GatAyCP36hEvvUgvjbZgkXLLH9yUQxdo77ij05aJ5s0OyB25q/JkqS8VTY0z1yE9NQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -809,6 +821,7 @@
"resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-20.3.15.tgz",
"integrity": "sha512-HCptODPVWg30XJwSueOz2zqsJjQ1chSscTs7FyIQcfuCTTthO35Lvz2Gtct8/GNHel9QNvvVwA5jrLjsU4dt1A==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -854,6 +867,7 @@
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -1818,6 +1832,7 @@
"integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@inquirer/checkbox": "^4.2.1",
"@inquirer/confirm": "^5.1.14",
@@ -3436,6 +3451,7 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
@@ -3955,6 +3971,7 @@
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.66.0.tgz",
"integrity": "sha512-tRWyuqK5j5nEjlk0x5HaeLArgVpAIJZNeMiPy//95v4/8tlHdQLM4gh3qcvwS70GN5fnlFXINWhnblvxSDv2dw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": ">=2.8.1"
},
@@ -4020,6 +4037,7 @@
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.66.0.tgz",
"integrity": "sha512-5DFbwHo7JHKBjgizbGTaIRJsai20+ZknhOQ1SRYwRTc9+6C1HbY/gGC+cjJTLmEQvk14rOoz8qbeWzJx88BU2Q==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "2.8.1"
},
@@ -4051,6 +4069,7 @@
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.66.0.tgz",
"integrity": "sha512-AjjH+xhgonjf9Xnx3SHNrP5VbsS9jdtGB3BCTQbicYd6QuujQBKldK0fnYMjCY3L0+lboI2OPCVg9PTliOdJ8A==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": ">=2.8.1"
},
@@ -4090,6 +4109,7 @@
"resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.7.0.tgz",
"integrity": "sha512-j3HPRPR7XxKxgMeytb+r/CNUoLBMVrfdfL8KJr1XiFO9jyEvoC4chFXDXWlkGyUHJIC6wy5VIXlIlI/kpqOiGg==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -4148,6 +4168,7 @@
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.66.0.tgz",
"integrity": "sha512-uqY3wslMs7KiBceaHPwCyWVrP8IPqb3OgAy1zd5DHosoUj/ciUl4JWVdx+QdsDypV/Cs4EZrqcIUtMDKQ/Zk0g==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": ">=2.8.1"
},
@@ -4176,6 +4197,7 @@
"resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.66.0.tgz",
"integrity": "sha512-D6REwySoaPGZlkdqTfrWahMqziXOY7GGTm1pXWVYDi5kEcSP9+F8ojo6saHDlwhN+V4/2jlMrkseSPlfXbmngQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": ">=2.8.1"
},
@@ -4194,6 +4216,7 @@
"resolved": "https://registry.npmjs.org/@taiga-ui/polymorpheus/-/polymorpheus-4.9.0.tgz",
"integrity": "sha512-TbIIwslbEnxunKuL9OyPZdmefrvJEK6HYiADEKQHUMUs4Pk2UbhMckUieURo83yPDamk/Mww+Nu/g60J/4uh2w==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "^2.8.1"
},
@@ -4320,6 +4343,7 @@
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/trusted-types": "*"
}
@@ -4369,8 +4393,9 @@
"version": "22.19.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
"integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -4839,6 +4864,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -5116,6 +5142,7 @@
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"readdirp": "^4.0.1"
},
@@ -5761,7 +5788,8 @@
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz",
"integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==",
"license": "(MPL-2.0 OR Apache-2.0)"
"license": "(MPL-2.0 OR Apache-2.0)",
"peer": true
},
"node_modules/domutils": {
"version": "3.2.2",
@@ -5833,6 +5861,7 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -5843,6 +5872,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -6125,6 +6155,7 @@
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -7668,6 +7699,7 @@
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"copy-anything": "^2.0.1",
"parse-node-version": "^1.0.1",
@@ -8025,6 +8057,7 @@
"integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"cli-truncate": "^4.0.0",
"colorette": "^2.0.20",
@@ -8864,6 +8897,7 @@
"integrity": "sha512-yW5ME0hqTz38r/th/7zVwX5oSIw1FviSA2PUlGZdVjghDme/KX6iiwmOBmlt9E9whNmwijEC6Gn3KKbrsBx8ig==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@rollup/plugin-json": "^6.1.0",
@@ -10670,6 +10704,7 @@
"integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -10815,6 +10850,7 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
@@ -10838,6 +10874,7 @@
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -11754,7 +11791,8 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
"license": "0BSD",
"peer": true
},
"node_modules/tslint": {
"version": "6.1.3",
@@ -12046,6 +12084,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -12058,7 +12097,7 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/union": {
@@ -12206,6 +12245,7 @@
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -12673,6 +12713,7 @@
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
"devOptional": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -12691,7 +12732,8 @@
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
"license": "MIT"
"license": "MIT",
"peer": true
}
}
}

View File

@@ -51,7 +51,7 @@ export class StateService {
async attachDrive(password: string | null): Promise<void> {
await this.api.attach({
guid: this.dataDriveGuid,
startOsPassword: password ? await this.api.encrypt(password) : null,
password: password ? await this.api.encrypt(password) : null,
})
}
@@ -77,8 +77,8 @@ export class StateService {
}
await this.api.execute({
startOsLogicalname: this.dataDriveGuid,
startOsPassword: password ? await this.api.encrypt(password) : null,
guid: this.dataDriveGuid,
password: password ? await this.api.encrypt(password) : null,
recoverySource,
})
}

View File

@@ -43,15 +43,15 @@ export interface InstallOsRes {
// === Attach ===
export interface AttachParams {
startOsPassword: T.EncryptedWire | null
password: T.EncryptedWire | null
guid: string // data drive
}
// === Execute ===
export interface SetupExecuteParams {
startOsLogicalname: string
startOsPassword: T.EncryptedWire | null // null = keep existing password (for restore/transfer)
guid: string
password: T.EncryptedWire | null // null = keep existing password (for restore/transfer)
recoverySource:
| {
type: 'migrate'