diff --git a/container-runtime/src/Adapters/EffectCreator.ts b/container-runtime/src/Adapters/EffectCreator.ts index f38746c52..44c5d40b2 100644 --- a/container-runtime/src/Adapters/EffectCreator.ts +++ b/container-runtime/src/Adapters/EffectCreator.ts @@ -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( diff --git a/core/locales/i18n.yaml b/core/locales/i18n.yaml index 32c49d203..f0ac17ca5 100644 --- a/core/locales/i18n.yaml +++ b/core/locales/i18n.yaml @@ -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" diff --git a/core/src/backup/restore.rs b/core/src/backup/restore.rs index d1d48092e..6f5d78eac 100644 --- a/core/src/backup/restore.rs +++ b/core/src/backup/restore.rs @@ -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))] diff --git a/core/src/bins/mod.rs b/core/src/bins/mod.rs index 234818d35..2b1959db7 100644 --- a/core/src/bins/mod.rs +++ b/core/src/bins/mod.rs @@ -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::>(); diff --git a/core/src/context/setup.rs b/core/src/context/setup.rs index fda7545e8..bbfee9862 100644 --- a/core/src/context/setup.rs +++ b/core/src/context/setup.rs @@ -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, #[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 { - 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, + pub needs_restart: bool, } pub struct SetupContextSeed { @@ -75,7 +60,9 @@ pub struct SetupContextSeed { pub disk_guid: OnceCell, pub shutdown: Sender>, pub rpc_continuations: RpcContinuations, - pub install_rootfs: SyncMutex>, + pub install_rootfs: SyncMutex>, + pub keyboard: SyncMutex>, + pub language: SyncMutex>, } #[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")), diff --git a/core/src/db/model/mod.rs b/core/src/db/model/mod.rs index b153ea44b..64a9ae4c6 100644 --- a/core/src/db/model/mod.rs +++ b/core/src/db/model/mod.rs @@ -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) -> Result { + pub fn init( + account: &AccountInfo, + kiosk: Option, + language: Option, + keyboard: Option, + ) -> Result { 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(), diff --git a/core/src/db/model/package.rs b/core/src/db/model/package.rs index b93904dfa..76869511a 100644 --- a/core/src/db/model/package.rs +++ b/core/src/db/model/package.rs @@ -417,8 +417,7 @@ impl Map for CurrentDependencies { #[serde(rename_all = "camelCase")] #[model = "Model"] pub struct CurrentDependencyInfo { - #[ts(type = "string | null")] - pub title: Option, + pub title: Option, pub icon: Option>, #[serde(flatten)] pub kind: CurrentDependencyKind, diff --git a/core/src/db/model/public.rs b/core/src/db/model/public.rs index b3cfc4401..20c5bc390 100644 --- a/core/src/db/model/public.rs +++ b/core/src/db/model/public.rs @@ -45,7 +45,12 @@ pub struct Public { pub ui: Value, } impl Public { - pub fn init(account: &AccountInfo, kiosk: Option) -> Result { + pub fn init( + account: &AccountInfo, + kiosk: Option, + language: Option, + keyboard: Option, + ) -> Result { 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")) diff --git a/core/src/dependencies.rs b/core/src/dependencies.rs index 3b6f7bd75..73627075b 100644 --- a/core/src/dependencies.rs +++ b/core/src/dependencies.rs @@ -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"] pub struct DepInfo { - pub description: Option, + pub description: Option, pub optional: bool, #[serde(flatten)] pub metadata: Option, @@ -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"] pub struct DependencyMetadata { #[ts(type = "string")] - pub title: InternedString, + pub title: LocaleString, } diff --git a/core/src/lib.rs b/core/src/lib.rs index 6f3304ed6..756f880ac 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -377,6 +377,20 @@ pub fn server() -> ParentHandler { "host", net::host::server_host_api::().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::(), + ) + .subcommand( + "set-language", + from_fn_async(system::set_language) + .no_display() + .with_about("about.set-language") + .with_call_remote::(), + ) } pub fn package() -> ParentHandler { diff --git a/core/src/os_install/gpt.rs b/core/src/os_install/gpt.rs index 3a5e9fb5b..ff17bb620 100644 --- a/core/src/os_install/gpt.rs +++ b/core/src/os_install/gpt.rs @@ -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; diff --git a/core/src/os_install/mod.rs b/core/src/os_install/mod.rs index 79f02e6e2..1e823c754 100644 --- a/core/src/os_install/mod.rs +++ b/core/src/os_install/mod.rs @@ -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) } diff --git a/core/src/registry/device_info.rs b/core/src/registry/device_info.rs index 08f233936..d2deff41a 100644 --- a/core/src/registry/device_info.rs +++ b/core/src/registry/device_info.rs @@ -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 { 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, } 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())), } } } diff --git a/core/src/registry/package/category.rs b/core/src/registry/package/category.rs index 369084c69..f41ae9363 100644 --- a/core/src/registry/package/category.rs +++ b/core/src/registry/package/category.rs @@ -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() -> ParentHandler { @@ -66,7 +67,7 @@ pub fn category_api() -> ParentHandler { 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( "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(()) diff --git a/core/src/registry/package/get.rs b/core/src/registry/package/get.rs index 3b09588c8..7525ce54d 100644 --- a/core/src/registry/package/get.rs +++ b/core/src/registry/package/get.rs @@ -79,20 +79,20 @@ pub struct GetPackageResponse { pub other_versions: Option>, } impl GetPackageResponse { - pub fn tables(&self) -> Vec { + pub fn tables(self) -> Vec { 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, } impl GetPackageResponseFull { - pub fn tables(&self) -> Vec { + pub fn tables(self) -> Vec { 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, )); diff --git a/core/src/registry/package/index.rs b/core/src/registry/package/index.rs index 61240f025..4a53c1c13 100644 --- a/core/src/registry/package/index.rs +++ b/core/src/registry/package/index.rs @@ -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"] #[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"] #[ts(export)] pub struct DependencyMetadata { - #[ts(type = "string | null")] - pub title: Option, + pub title: Option, pub icon: Option>, - pub description: Option, + pub description: Option, 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"] 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, #[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::::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 { { 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) diff --git a/core/src/s9pk/v2/compat.rs b/core/src/s9pk/v2/compat.rs index c15baef42..837632fff 100644 --- a/core/src/s9pk/v2/compat.rs +++ b/core/src/s9pk/v2/compat.rs @@ -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 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 for Manifest { ( id, DepInfo { - description: value.description, + description: value.description.map(LocaleString::Translated), optional: !value.requirement.required(), metadata: None, }, diff --git a/core/src/s9pk/v2/manifest.rs b/core/src/s9pk/v2/manifest.rs index 4f18453c4..00e57f18c 100644 --- a/core/src/s9pk/v2/manifest.rs +++ b/core/src/s9pk/v2/manifest.rs @@ -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, - 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")] +pub enum LocaleString { + Translated(String), + LanguageMap(InOMap), +} +impl std::str::FromStr for LocaleString { + type Err = std::convert::Infallible; + fn from_str(s: &str) -> Result { + // Try JSON parse first (for maps or quoted strings) + if let Ok(parsed) = serde_json::from_str::(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(deserializer: D) -> Result + 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(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(LocaleString::Translated(value.to_owned())) + } + + fn visit_string(self, value: String) -> Result + where + E: serde::de::Error, + { + Ok(LocaleString::Translated(value)) + } + + fn visit_map(self, map: M) -> Result + 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(&self, serializer: S) -> Result + 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; + 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, - pub uninstall: Option, - pub restore: Option, - pub start: Option, - pub stop: Option, + pub install: Option, + pub uninstall: Option, + pub restore: Option, + pub start: Option, + pub stop: Option, +} +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)); + } } diff --git a/core/src/s9pk/v2/pack.rs b/core/src/s9pk/v2/pack.rs index 473924ce3..d60e05f5b 100644 --- a/core/src/s9pk/v2/pack.rs +++ b/core/src/s9pk/v2/pack.rs @@ -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?, )) } diff --git a/core/src/service/effects/dependency.rs b/core/src/service/effects/dependency.rs index e725cd27c..ffd38ddac 100644 --- a/core/src/service/effects/dependency.rs +++ b/core/src/service/effects/dependency.rs @@ -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, + title: Option, installed_version: Option, satisfies: BTreeSet, is_running: bool, diff --git a/core/src/setup.rs b/core/src/setup.rs index 51e945a00..5b9770df8 100644 --- a/core/src/setup.rs +++ b/core/src/setup.rs @@ -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() -> ParentHandler { @@ -75,6 +76,9 @@ pub fn setup() -> ParentHandler { .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() -> ParentHandler { @@ -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, pub guid: InternedString, #[ts(optional)] @@ -155,63 +173,81 @@ pub async fn attach( ) -> Result { 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 = 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 = 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 { 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>, #[ts(optional)] kiosk: Option, @@ -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 { - 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 { #[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>, kiosk: Option, ) -> 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, 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, 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(()) } diff --git a/core/src/keyboard.conf.template b/core/src/system/keyboard.conf.template similarity index 100% rename from core/src/keyboard.conf.template rename to core/src/system/keyboard.conf.template diff --git a/core/src/system.rs b/core/src/system/mod.rs similarity index 97% rename from core/src/system.rs rename to core/src/system/mod.rs index 6097a6668..3fe43797a 100644 --- a/core/src/system.rs +++ b/core/src/system/mod.rs @@ -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, + #[arg(short, long, help = "help.arg.keyboard-model")] pub model: Option, - #[arg(long, help = "help.arg.keyboard-variant")] + #[arg(short, long, help = "help.arg.keyboard-variant")] pub variant: Option, #[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(()) } diff --git a/core/src/system/vconsole.conf.template b/core/src/system/vconsole.conf.template new file mode 100644 index 000000000..8bf585b37 --- /dev/null +++ b/core/src/system/vconsole.conf.template @@ -0,0 +1,7 @@ +XKBMODEL="{model}" +XKBLAYOUT="{layout}" +XKBVARIANT="{variant}" +XKBOPTIONS="{options}" + +KEYMAP="{keymap}" + diff --git a/core/src/util/lshw.rs b/core/src/util/lshw.rs index 2f61741b9..8b8a8654c 100644 --- a/core/src/util/lshw.rs +++ b/core/src/util/lshw.rs @@ -25,6 +25,7 @@ impl LshwDevice { Self::Display(_) => "display", } } + #[instrument(skip_all)] pub fn from_value(value: &Value) -> Option { match value["class"].as_str() { Some("processor") => Some(LshwDevice::Processor(LshwProcessor::from_value(value))), @@ -41,6 +42,7 @@ pub struct LshwProcessor { pub capabilities: BTreeSet, } 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, } impl LshwDisplay { + #[instrument(skip_all)] fn from_value(value: &Value) -> Self { Self { product: value["product"].as_str().map(From::from), diff --git a/sdk/base/lib/osBindings/AddCategoryParams.ts b/sdk/base/lib/osBindings/AddCategoryParams.ts index 62b04e6e2..b6dfa3fac 100644 --- a/sdk/base/lib/osBindings/AddCategoryParams.ts +++ b/sdk/base/lib/osBindings/AddCategoryParams.ts @@ -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 } diff --git a/sdk/base/lib/osBindings/Alerts.ts b/sdk/base/lib/osBindings/Alerts.ts index 819d1c407..9103c1582 100644 --- a/sdk/base/lib/osBindings/Alerts.ts +++ b/sdk/base/lib/osBindings/Alerts.ts @@ -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 } diff --git a/sdk/base/lib/osBindings/AttachParams.ts b/sdk/base/lib/osBindings/AttachParams.ts index e1de943a5..36ba7f958 100644 --- a/sdk/base/lib/osBindings/AttachParams.ts +++ b/sdk/base/lib/osBindings/AttachParams.ts @@ -2,7 +2,7 @@ import type { EncryptedWire } from "./EncryptedWire" export type AttachParams = { - startOsPassword: EncryptedWire | null + password: EncryptedWire | null guid: string kiosk?: boolean } diff --git a/sdk/base/lib/osBindings/Category.ts b/sdk/base/lib/osBindings/Category.ts index 615094527..cc2bc35ac 100644 --- a/sdk/base/lib/osBindings/Category.ts +++ b/sdk/base/lib/osBindings/Category.ts @@ -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 } diff --git a/sdk/base/lib/osBindings/DependencyMetadata.ts b/sdk/base/lib/osBindings/DependencyMetadata.ts index 3d56ef052..1af1c2ba8 100644 --- a/sdk/base/lib/osBindings/DependencyMetadata.ts +++ b/sdk/base/lib/osBindings/DependencyMetadata.ts @@ -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 } diff --git a/sdk/base/lib/osBindings/Description.ts b/sdk/base/lib/osBindings/Description.ts index bcb92071f..e69b2e90c 100644 --- a/sdk/base/lib/osBindings/Description.ts +++ b/sdk/base/lib/osBindings/Description.ts @@ -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 } diff --git a/sdk/base/lib/osBindings/KeyboardOptions.ts b/sdk/base/lib/osBindings/KeyboardOptions.ts new file mode 100644 index 000000000..b54144de9 --- /dev/null +++ b/sdk/base/lib/osBindings/KeyboardOptions.ts @@ -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 +} diff --git a/sdk/base/lib/osBindings/LocaleString.ts b/sdk/base/lib/osBindings/LocaleString.ts new file mode 100644 index 000000000..60a66a331 --- /dev/null +++ b/sdk/base/lib/osBindings/LocaleString.ts @@ -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 diff --git a/sdk/base/lib/osBindings/Manifest.ts b/sdk/base/lib/osBindings/Manifest.ts index 1d0dbfe0a..30b7068f2 100644 --- a/sdk/base/lib/osBindings/Manifest.ts +++ b/sdk/base/lib/osBindings/Manifest.ts @@ -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 - releaseNotes: string + releaseNotes: LocaleString canMigrateTo: string canMigrateFrom: string license: string diff --git a/sdk/base/lib/osBindings/Metadata.ts b/sdk/base/lib/osBindings/Metadata.ts index 0ea43923e..842fa17b6 100644 --- a/sdk/base/lib/osBindings/Metadata.ts +++ b/sdk/base/lib/osBindings/Metadata.ts @@ -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 } diff --git a/sdk/base/lib/osBindings/PackageVersionInfo.ts b/sdk/base/lib/osBindings/PackageVersionInfo.ts index e19cf7d01..f11249acc 100644 --- a/sdk/base/lib/osBindings/PackageVersionInfo.ts +++ b/sdk/base/lib/osBindings/PackageVersionInfo.ts @@ -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 diff --git a/sdk/base/lib/osBindings/Pem.ts b/sdk/base/lib/osBindings/Pem.ts new file mode 100644 index 000000000..1ec1cd375 --- /dev/null +++ b/sdk/base/lib/osBindings/Pem.ts @@ -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 diff --git a/sdk/base/lib/osBindings/ServerInfo.ts b/sdk/base/lib/osBindings/ServerInfo.ts index 7fc8718ff..1beef03de 100644 --- a/sdk/base/lib/osBindings/ServerInfo.ts +++ b/sdk/base/lib/osBindings/ServerInfo.ts @@ -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 kiosk: boolean | null + language: string | null + keyboard: KeyboardOptions | null } diff --git a/sdk/base/lib/osBindings/SetupExecuteParams.ts b/sdk/base/lib/osBindings/SetupExecuteParams.ts index 289836d78..a8e1e4ae1 100644 --- a/sdk/base/lib/osBindings/SetupExecuteParams.ts +++ b/sdk/base/lib/osBindings/SetupExecuteParams.ts @@ -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 | null kiosk?: boolean } diff --git a/sdk/base/lib/osBindings/SetupInfo.ts b/sdk/base/lib/osBindings/SetupInfo.ts new file mode 100644 index 000000000..06b6447e6 --- /dev/null +++ b/sdk/base/lib/osBindings/SetupInfo.ts @@ -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 } diff --git a/sdk/base/lib/osBindings/SetupResult.ts b/sdk/base/lib/osBindings/SetupResult.ts index 3147187c1..4d7f51bce 100644 --- a/sdk/base/lib/osBindings/SetupResult.ts +++ b/sdk/base/lib/osBindings/SetupResult.ts @@ -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 hostname: string - lanAddress: string - rootCa: string + rootCa: Pem + needsRestart: boolean } diff --git a/sdk/base/lib/osBindings/SetupStatusRes.ts b/sdk/base/lib/osBindings/SetupStatusRes.ts index 93d10c59b..a7f0342ec 100644 --- a/sdk/base/lib/osBindings/SetupStatusRes.ts +++ b/sdk/base/lib/osBindings/SetupStatusRes.ts @@ -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) diff --git a/sdk/base/lib/osBindings/index.ts b/sdk/base/lib/osBindings/index.ts index f0da142fe..32d67a358 100644 --- a/sdk/base/lib/osBindings/index.ts +++ b/sdk/base/lib/osBindings/index.ts @@ -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" diff --git a/sdk/base/lib/types/ManifestTypes.ts b/sdk/base/lib/types/ManifestTypes.ts index fb9c74425..3ca2ae0cb 100644 --- a/sdk/base/lib/types/ManifestTypes.ts +++ b/sdk/base/lib/types/ManifestTypes.ts @@ -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 diff --git a/sdk/base/lib/util/getServiceInterface.ts b/sdk/base/lib/util/getServiceInterface.ts index 2fab5c7cf..48d883edb 100644 --- a/sdk/base/lib/util/getServiceInterface.ts +++ b/sdk/base/lib/util/getServiceInterface.ts @@ -355,10 +355,13 @@ export class GetServiceInterface { 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 } diff --git a/sdk/base/lib/util/getServiceInterfaces.ts b/sdk/base/lib/util/getServiceInterfaces.ts index 0155b3114..e2eb6131a 100644 --- a/sdk/base/lib/util/getServiceInterfaces.ts +++ b/sdk/base/lib/util/getServiceInterfaces.ts @@ -55,10 +55,13 @@ export class GetServiceInterfaces { 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 } diff --git a/sdk/package/lib/util/GetServiceManifest.ts b/sdk/package/lib/util/GetServiceManifest.ts index 62c661c82..5965721bf 100644 --- a/sdk/package/lib/util/GetServiceManifest.ts +++ b/sdk/package/lib/util/GetServiceManifest.ts @@ -19,10 +19,13 @@ export class GetServiceManifest { 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 } diff --git a/sdk/package/lib/util/fileHelper.ts b/sdk/package/lib/util/fileHelper.ts index e48dd6be9..e7a4c0341 100644 --- a/sdk/package/lib/util/fileHelper.ts +++ b/sdk/package/lib/util/fileHelper.ts @@ -230,10 +230,13 @@ export class FileHelper { 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 { }) .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 } diff --git a/web/package-lock.json b/web/package-lock.json index d61efeaf1..a23c11ed2 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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 } } } diff --git a/web/projects/setup-wizard/src/app/services/state.service.ts b/web/projects/setup-wizard/src/app/services/state.service.ts index f0fe03a77..e1c07ff12 100644 --- a/web/projects/setup-wizard/src/app/services/state.service.ts +++ b/web/projects/setup-wizard/src/app/services/state.service.ts @@ -51,7 +51,7 @@ export class StateService { async attachDrive(password: string | null): Promise { 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, }) } diff --git a/web/projects/setup-wizard/src/app/types.ts b/web/projects/setup-wizard/src/app/types.ts index be5dcb3f5..854ca3d48 100644 --- a/web/projects/setup-wizard/src/app/types.ts +++ b/web/projects/setup-wizard/src/app/types.ts @@ -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'