Feature/consolidate setup (#3092)

* start consolidating

* add start-cli flash-os

* combine install and setup and refactor all

* use http

* undo mock

* fix translation

* translations

* use dialogservice wrapper

* better ST messaging on setup

* only warn on update if breakages (#3097)

* finish setup wizard and ui language-keyboard feature

* fix typo

* wip: localization

* remove start-tunnel readme

* switch to posix strings for language internal

* revert mock

* translate backend strings

* fix missing about text

* help text for args

* feat: add "Add new gateway" option (#3098)

* feat: add "Add new gateway" option

* Update web/projects/ui/src/app/routes/portal/components/form/controls/select.component.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add translation

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Matt Hill <mattnine@protonmail.com>

* fix dns selection

* keyboard keymap also

* ability to shutdown after install

* revert mock

* working setup flow + manifest localization

* (mostly) redundant localization on frontend

* version bump

* omit live medium from disk list and better space management

* ignore missing package archive on 035 migration

* fix device migration

* add i18n helper to sdk

* fix install over 0.3.5.1

* fix grub config

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
Co-authored-by: Alex Inkin <alexander@inkin.ru>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Aiden McClelland
2026-01-27 14:44:41 -08:00
committed by GitHub
parent 99871805bd
commit c65db31fd9
251 changed files with 12163 additions and 3966 deletions

View File

@@ -23,7 +23,7 @@ pub fn action_api<C: Context>() -> ParentHandler<C> {
"get-input",
from_fn_async(get_action_input)
.with_display_serializable()
.with_about("Get action input spec")
.with_about("about.get-action-input-spec")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -36,14 +36,14 @@ pub fn action_api<C: Context>() -> ParentHandler<C> {
}
Ok(())
})
.with_about("Run service action")
.with_about("about.run-service-action")
.with_call_remote::<CliContext>(),
)
.subcommand(
"clear-task",
from_fn_async(clear_task)
.no_display()
.with_about("Clear a service task")
.with_about("about.clear-service-task")
.with_call_remote::<CliContext>(),
)
}
@@ -63,7 +63,9 @@ pub struct ActionInput {
#[derive(Deserialize, Serialize, TS, Parser)]
#[serde(rename_all = "camelCase")]
pub struct GetActionInputParams {
#[arg(help = "help.arg.package-id")]
pub package_id: PackageId,
#[arg(help = "help.arg.action-id")]
pub action_id: ActionId,
}
@@ -280,8 +282,11 @@ pub struct RunActionParams {
#[derive(Parser)]
struct CliRunActionParams {
#[arg(help = "help.arg.package-id")]
pub package_id: PackageId,
#[arg(help = "help.arg.event-id")]
pub event_id: Option<Guid>,
#[arg(help = "help.arg.action-id")]
pub action_id: ActionId,
#[command(flatten)]
pub input: StdinDeserializable<Option<Value>>,
@@ -360,9 +365,11 @@ pub async fn run_action(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct ClearTaskParams {
#[arg(help = "help.arg.package-id")]
pub package_id: PackageId,
#[arg(help = "help.arg.replay-id")]
pub replay_id: ReplayId,
#[arg(long)]
#[arg(long, help = "help.arg.force-clear-task")]
#[serde(default)]
pub force: bool,
}

View File

@@ -51,7 +51,10 @@ pub async fn write_shadow(password: &str) -> Result<(), Error> {
match line.split_once(":") {
Some((user, rest)) if user == "start9" || user == "kiosk" => {
let (_, rest) = rest.split_once(":").ok_or_else(|| {
Error::new(eyre!("malformed /etc/shadow"), ErrorKind::ParseSysInfo)
Error::new(
eyre!("{}", t!("auth.malformed-etc-shadow")),
ErrorKind::ParseSysInfo,
)
})?;
shadow_file
.write_all(format!("{user}:{hash}:{rest}\n").as_bytes())
@@ -81,7 +84,7 @@ impl PasswordType {
PasswordType::String(x) => Ok(x),
PasswordType::EncryptedWire(x) => x.decrypt(current_secret).ok_or_else(|| {
Error::new(
color_eyre::eyre::eyre!("Couldn't decode password"),
color_eyre::eyre::eyre!("{}", t!("auth.couldnt-decode-password")),
crate::ErrorKind::Unknown,
)
}),
@@ -125,19 +128,19 @@ where
"login",
from_fn_async(cli_login::<AC>)
.no_display()
.with_about("Log in a new auth session"),
.with_about("about.login-new-auth-session"),
)
.subcommand(
"logout",
from_fn_async(logout::<AC>)
.with_metadata("get_session", Value::Bool(true))
.no_display()
.with_about("Log out of current auth session")
.with_about("about.logout-current-auth-session")
.with_call_remote::<CliContext>(),
)
.subcommand(
"session",
session::<C, AC>().with_about("List or kill auth sessions"),
session::<C, AC>().with_about("about.list-or-kill-auth-sessions"),
)
.subcommand(
"reset-password",
@@ -147,14 +150,14 @@ where
"reset-password",
from_fn_async(cli_reset_password)
.no_display()
.with_about("Reset password"),
.with_about("about.reset-password"),
)
.subcommand(
"get-pubkey",
from_fn_async(get_pubkey)
.with_metadata("authenticated", Value::Bool(false))
.no_display()
.with_about("Get public key derived from server private key")
.with_about("about.get-pubkey-from-server")
.with_call_remote::<CliContext>(),
)
}
@@ -208,12 +211,12 @@ pub fn check_password(hash: &str, password: &str) -> Result<(), Error> {
ensure_code!(
argon2::verify_encoded(&hash, password.as_bytes()).map_err(|_| {
Error::new(
eyre!("Password Incorrect"),
eyre!("{}", t!("auth.password-incorrect")),
crate::ErrorKind::IncorrectPassword,
)
})?,
crate::ErrorKind::IncorrectPassword,
"Password Incorrect"
t!("auth.password-incorrect")
);
Ok(())
}
@@ -327,14 +330,14 @@ where
.with_metadata("get_session", Value::Bool(true))
.with_display_serializable()
.with_custom_display_fn(|handle, result| display_sessions(handle.params, result))
.with_about("Display all auth sessions")
.with_about("about.display-all-auth-sessions")
.with_call_remote::<CliContext>(),
)
.subcommand(
"kill",
from_fn_async(kill::<AC>)
.no_display()
.with_about("Terminate existing auth session(s)")
.with_about("about.terminate-auth-sessions")
.with_call_remote::<CliContext>(),
)
}
@@ -418,6 +421,7 @@ impl AsLogoutSessionId for KillSessionId {
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct KillParams {
#[arg(help = "help.arg.session-ids")]
ids: Vec<String>,
}
@@ -434,7 +438,9 @@ pub async fn kill<C: SessionAuthContext>(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct ResetPasswordParams {
#[arg(help = "help.arg.old-password")]
old_password: Option<PasswordType>,
#[arg(help = "help.arg.new-password")]
new_password: Option<PasswordType>,
}
@@ -447,13 +453,13 @@ async fn cli_reset_password(
..
}: HandlerArgs<CliContext>,
) -> Result<(), RpcError> {
let old_password = rpassword::prompt_password("Current Password: ")?;
let old_password = rpassword::prompt_password(&t!("auth.prompt-current-password"))?;
let new_password = {
let new_password = rpassword::prompt_password("New Password: ")?;
if new_password != rpassword::prompt_password("Confirm: ")? {
let new_password = rpassword::prompt_password(&t!("auth.prompt-new-password"))?;
if new_password != rpassword::prompt_password(&t!("auth.prompt-confirm"))? {
return Err(Error::new(
eyre!("Passwords do not match"),
eyre!("{}", t!("auth.passwords-do-not-match")),
crate::ErrorKind::IncorrectPassword,
)
.into());
@@ -486,7 +492,7 @@ pub async fn reset_password_impl(
.with_kind(crate::ErrorKind::IncorrectPassword)?
{
return Err(Error::new(
eyre!("Incorrect Password"),
eyre!("{}", t!("auth.password-incorrect")),
crate::ErrorKind::IncorrectPassword,
));
}

View File

@@ -33,11 +33,13 @@ use crate::version::VersionT;
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct BackupParams {
#[arg(help = "help.arg.backup-target-id")]
target_id: BackupTargetId,
#[arg(long = "old-password")]
#[arg(long = "old-password", help = "help.arg.old-backup-password")]
old_password: Option<crate::auth::PasswordType>,
#[arg(long = "package-ids")]
#[arg(long = "package-ids", help = "help.arg.package-ids-to-backup")]
package_ids: Option<Vec<PackageId>>,
#[arg(help = "help.arg.backup-password")]
password: crate::auth::PasswordType,
}
@@ -69,8 +71,8 @@ impl BackupStatusGuard {
db,
None,
NotificationLevel::Success,
"Backup Complete".to_owned(),
"Your backup has completed".to_owned(),
t!("backup.bulk.complete-title").to_string(),
t!("backup.bulk.complete-message").to_string(),
BackupReport {
server: ServerBackupReport {
attempted: true,
@@ -88,9 +90,8 @@ impl BackupStatusGuard {
db,
None,
NotificationLevel::Warning,
"Backup Complete".to_owned(),
"Your backup has completed, but some package(s) failed to backup"
.to_owned(),
t!("backup.bulk.complete-title").to_string(),
t!("backup.bulk.complete-with-failures").to_string(),
BackupReport {
server: ServerBackupReport {
attempted: true,
@@ -103,7 +104,7 @@ impl BackupStatusGuard {
.await
}
Err(e) => {
tracing::error!("Backup Failed: {}", e);
tracing::error!("{}", t!("backup.bulk.failed-error", error = e));
tracing::debug!("{:?}", e);
let err_string = e.to_string();
db.mutate(|db| {
@@ -111,8 +112,8 @@ impl BackupStatusGuard {
db,
None,
NotificationLevel::Error,
"Backup Failed".to_owned(),
"Your backup failed to complete.".to_owned(),
t!("backup.bulk.failed-title").to_string(),
t!("backup.bulk.failed-message").to_string(),
BackupReport {
server: ServerBackupReport {
attempted: true,
@@ -224,7 +225,7 @@ fn assure_backing_up<'a>(
.as_backup_progress_mut();
if backing_up.transpose_ref().is_some() {
return Err(Error::new(
eyre!("Server is already backing up!"),
eyre!("{}", t!("backup.bulk.already-backing-up")),
ErrorKind::InvalidRequest,
));
}
@@ -303,7 +304,7 @@ async fn perform_backup(
let mut backup_guard = Arc::try_unwrap(backup_guard).map_err(|_| {
Error::new(
eyre!("leaked reference to BackupMountGuard"),
eyre!("{}", t!("backup.bulk.leaked-reference")),
ErrorKind::Incoherent,
)
})?;

View File

@@ -37,12 +37,12 @@ pub fn backup<C: Context>() -> ParentHandler<C> {
"create",
from_fn_async(backup_bulk::backup_all)
.no_display()
.with_about("Create backup for all packages")
.with_about("about.create-backup-all-packages")
.with_call_remote::<CliContext>(),
)
.subcommand(
"target",
target::target::<C>().with_about("Commands related to a backup target"),
target::target::<C>().with_about("about.commands-backup-target"),
)
}
@@ -51,7 +51,7 @@ pub fn package_backup<C: Context>() -> ParentHandler<C> {
"restore",
from_fn_async(restore::restore_packages_rpc)
.no_display()
.with_about("Restore package(s) from backup")
.with_about("about.restore-packages-from-backup")
.with_call_remote::<CliContext>(),
)
}

View File

@@ -23,16 +23,19 @@ 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)]
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct RestorePackageParams {
#[arg(help = "help.arg.package-ids")]
pub ids: Vec<PackageId>,
#[arg(help = "help.arg.backup-target-id")]
pub target_id: BackupTargetId,
#[arg(help = "help.arg.backup-password")]
pub password: String,
}
@@ -63,7 +66,10 @@ pub async fn restore_packages_rpc(
match async { res.await?.await }.await {
Ok(_) => (),
Err(err) => {
tracing::error!("Error restoring package {}: {}", id, err);
tracing::error!(
"{}",
t!("backup.restore.package-error", id = id, error = err)
);
tracing::debug!("{:?}", err);
}
}
@@ -75,10 +81,10 @@ pub async fn restore_packages_rpc(
}
#[instrument(skip_all)]
pub async fn recover_full_embassy(
pub async fn recover_full_server(
ctx: &SetupContext,
disk_guid: Arc<String>,
start_os_password: String,
disk_guid: InternedString,
password: String,
recovery_source: TmpMountGuard,
server_id: &str,
recovery_password: &str,
@@ -102,7 +108,7 @@ pub async fn recover_full_embassy(
)?;
os_backup.account.password = argon2::hash_encoded(
start_os_password.as_bytes(),
password.as_bytes(),
&rand::random::<[u8; 16]>()[..],
&argon2::Config::rfc9106_low_mem(),
)
@@ -111,16 +117,32 @@ pub async fn recover_full_embassy(
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 init_result = init(&ctx.webserver, &ctx.config, init_phases).await?;
let config = ctx.config.peek(|c| c.clone());
let init_result = init(&ctx.webserver, &config, init_phases).await?;
let rpc_ctx = RpcContext::init(
&ctx.webserver,
&ctx.config,
&config,
disk_guid.clone(),
Some(init_result),
rpc_ctx_phases,
@@ -145,7 +167,10 @@ pub async fn recover_full_embassy(
match async { res.await?.await }.await {
Ok(_) => (),
Err(err) => {
tracing::error!("Error restoring package {}: {}", id, err);
tracing::error!(
"{}",
t!("backup.restore.package-error", id = id, error = err)
);
tracing::debug!("{:?}", err);
}
}
@@ -155,7 +180,14 @@ pub async fn recover_full_embassy(
.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

@@ -52,21 +52,21 @@ pub fn cifs<C: Context>() -> ParentHandler<C> {
"add",
from_fn_async(add)
.no_display()
.with_about("Add a new backup target")
.with_about("about.add-new-backup-target")
.with_call_remote::<CliContext>(),
)
.subcommand(
"update",
from_fn_async(update)
.no_display()
.with_about("Update an existing backup target")
.with_about("about.update-existing-backup-target")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove)
.no_display()
.with_about("Remove an existing backup target")
.with_about("about.remove-existing-backup-target")
.with_call_remote::<CliContext>(),
)
}
@@ -75,9 +75,13 @@ pub fn cifs<C: Context>() -> ParentHandler<C> {
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct AddParams {
#[arg(help = "help.arg.cifs-hostname")]
pub hostname: String,
#[arg(help = "help.arg.cifs-path")]
pub path: PathBuf,
#[arg(help = "help.arg.cifs-username")]
pub username: String,
#[arg(help = "help.arg.cifs-password")]
pub password: Option<String>,
}
@@ -130,10 +134,15 @@ pub async fn add(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct UpdateParams {
#[arg(help = "help.arg.backup-target-id")]
pub id: BackupTargetId,
#[arg(help = "help.arg.cifs-hostname")]
pub hostname: String,
#[arg(help = "help.arg.cifs-path")]
pub path: PathBuf,
#[arg(help = "help.arg.cifs-username")]
pub username: String,
#[arg(help = "help.arg.cifs-password")]
pub password: Option<String>,
}
@@ -151,7 +160,7 @@ pub async fn update(
id
} else {
return Err(Error::new(
eyre!("Backup Target ID {} Not Found", id),
eyre!("{}", t!("backup.target.cifs.target-not-found", id = id)),
ErrorKind::NotFound,
));
};
@@ -171,7 +180,7 @@ pub async fn update(
.as_idx_mut(&id)
.ok_or_else(|| {
Error::new(
eyre!("Backup Target ID {} Not Found", BackupTargetId::Cifs { id }),
eyre!("{}", t!("backup.target.cifs.target-not-found", id = BackupTargetId::Cifs { id })),
ErrorKind::NotFound,
)
})?
@@ -195,6 +204,7 @@ pub async fn update(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct RemoveParams {
#[arg(help = "help.arg.backup-target-id")]
pub id: BackupTargetId,
}
@@ -203,7 +213,7 @@ pub async fn remove(ctx: RpcContext, RemoveParams { id }: RemoveParams) -> Resul
id
} else {
return Err(Error::new(
eyre!("Backup Target ID {} Not Found", id),
eyre!("{}", t!("backup.target.cifs.target-not-found", id = id)),
ErrorKind::NotFound,
));
};
@@ -220,7 +230,7 @@ pub fn load(db: &DatabaseModel, id: u32) -> Result<Cifs, Error> {
.as_idx(&id)
.ok_or_else(|| {
Error::new(
eyre!("Backup Target ID {} Not Found", id),
eyre!("{}", t!("backup.target.cifs.target-not-found-id", id = id)),
ErrorKind::NotFound,
)
})?

View File

@@ -143,13 +143,13 @@ pub fn target<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"cifs",
cifs::cifs::<C>().with_about("Add, remove, or update a backup target"),
cifs::cifs::<C>().with_about("about.add-remove-update-backup-target"),
)
.subcommand(
"list",
from_fn_async(list)
.with_display_serializable()
.with_about("List existing backup targets")
.with_about("about.list-existing-backup-targets")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -159,20 +159,20 @@ pub fn target<C: Context>() -> ParentHandler<C> {
.with_custom_display_fn::<CliContext, _>(|params, info| {
display_backup_info(params.params, info)
})
.with_about("Display package backup information")
.with_about("about.display-package-backup-information")
.with_call_remote::<CliContext>(),
)
.subcommand(
"mount",
from_fn_async(mount)
.with_about("Mount backup target")
.with_about("about.mount-backup-target")
.with_call_remote::<CliContext>(),
)
.subcommand(
"umount",
from_fn_async(umount)
.no_display()
.with_about("Unmount backup target")
.with_about("about.unmount-backup-target")
.with_call_remote::<CliContext>(),
)
}
@@ -268,8 +268,11 @@ fn display_backup_info(params: WithIoFormat<InfoParams>, info: BackupInfo) -> Re
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct InfoParams {
#[arg(help = "help.arg.backup-target-id")]
target_id: BackupTargetId,
#[arg(help = "help.arg.server-id")]
server_id: String,
#[arg(help = "help.arg.backup-password")]
password: String,
}
@@ -305,11 +308,13 @@ lazy_static::lazy_static! {
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct MountParams {
#[arg(help = "help.arg.backup-target-id")]
target_id: BackupTargetId,
#[arg(long)]
#[arg(long, help = "help.arg.server-id")]
server_id: Option<String>,
#[arg(help = "help.arg.backup-password")]
password: String, // TODO: rpassword
#[arg(long)]
#[arg(long, help = "help.arg.allow-partial-backup")]
allow_partial: bool,
}
@@ -385,6 +390,7 @@ pub async fn mount(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct UmountParams {
#[arg(help = "help.arg.backup-target-id")]
target_id: Option<BackupTargetId>,
}

View File

@@ -17,6 +17,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
|cfg: ContainerClientConfig| Ok(ContainerCliContext::init(cfg)),
crate::service::effects::handler(),
)
.mutate_command(super::translate_cli)
.run(args)
{
match e.data {

View File

@@ -1,9 +1,14 @@
use rust_i18n::t;
pub fn renamed(old: &str, new: &str) -> ! {
eprintln!("{old} has been renamed to {new}");
eprintln!(
"{}",
t!("bins.deprecated.renamed", old = old, new = new)
);
std::process::exit(1)
}
pub fn removed(name: &str) -> ! {
eprintln!("{name} has been removed");
eprintln!("{}", t!("bins.deprecated.removed", name = name));
std::process::exit(1)
}

View File

@@ -2,6 +2,8 @@ use std::collections::{BTreeMap, VecDeque};
use std::ffi::OsString;
use std::path::Path;
use rust_i18n::t;
pub mod container_cli;
pub mod deprecated;
pub mod registry;
@@ -10,6 +12,85 @@ pub mod start_init;
pub mod startd;
pub mod tunnel;
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));
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 {
best = Some(l);
break;
}
if best.is_none() && l.starts_with(prefix) {
best = Some(l);
}
}
rust_i18n::set_locale(best.unwrap_or(lang));
}
pub fn translate_cli(mut cmd: clap::Command) -> clap::Command {
fn translate(s: impl std::fmt::Display) -> String {
t!(s.to_string()).into_owned()
}
if let Some(s) = cmd.get_about() {
let s = translate(s);
cmd = cmd.about(s);
}
if let Some(s) = cmd.get_long_about() {
let s = translate(s);
cmd = cmd.long_about(s);
}
if let Some(s) = cmd.get_before_help() {
let s = translate(s);
cmd = cmd.before_help(s);
}
if let Some(s) = cmd.get_before_long_help() {
let s = translate(s);
cmd = cmd.before_long_help(s);
}
if let Some(s) = cmd.get_after_help() {
let s = translate(s);
cmd = cmd.after_help(s);
}
if let Some(s) = cmd.get_after_long_help() {
let s = translate(s);
cmd = cmd.after_long_help(s);
}
let arg_ids = cmd
.get_arguments()
.map(|a| a.get_id().clone())
.collect::<Vec<_>>();
for id in arg_ids {
cmd = cmd.mut_arg(id, |arg| {
let arg = if let Some(s) = arg.get_help() {
let s = translate(s);
arg.help(s)
} else {
arg
};
if let Some(s) = arg.get_long_help() {
let s = translate(s);
arg.long_help(s)
} else {
arg
}
});
}
for cmd in cmd.get_subcommands_mut() {
*cmd = translate_cli(cmd.clone());
}
cmd
}
#[derive(Default)]
pub struct MultiExecutable {
default: Option<&'static str>,
@@ -58,7 +139,7 @@ impl MultiExecutable {
if let Some((name, _)) = self.bins.get_key_value(name) {
self.default = Some(*name);
} else {
panic!("{name} does not exist in MultiExecutable");
panic!("{}", t!("bins.mod.does-not-exist", name = name));
}
self
}
@@ -68,6 +149,8 @@ impl MultiExecutable {
}
pub fn execute(&self) {
set_locale_from_env();
let mut popped = Vec::with_capacity(2);
let mut args = std::env::args_os().collect::<VecDeque<_>>();
@@ -96,11 +179,15 @@ impl MultiExecutable {
}
let args = std::env::args().collect::<VecDeque<_>>();
eprintln!(
"unknown executable: {}",
args.get(1)
.or_else(|| args.get(0))
.map(|s| s.as_str())
.unwrap_or("N/A")
"{}",
t!(
"bins.mod.unknown-executable",
name = args
.get(1)
.or_else(|| args.get(0))
.map(|s| s.as_str())
.unwrap_or("N/A")
)
);
std::process::exit(1);
}

View File

@@ -3,6 +3,7 @@ use std::ffi::OsString;
use clap::Parser;
use futures::FutureExt;
use rpc_toolkit::CliApp;
use rust_i18n::t;
use tokio::signal::unix::signal;
use tracing::instrument;
@@ -77,7 +78,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("failed to initialize runtime");
.expect(&t!("bins.registry.failed-to-initialize-runtime"));
rt.block_on(inner_main(&config))
};
@@ -99,6 +100,7 @@ pub fn cli(args: impl IntoIterator<Item = OsString>) {
|cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?),
crate::registry::registry_api(),
)
.mutate_command(super::translate_cli)
.run(args)
{
match e.data {

View File

@@ -19,6 +19,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
|cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?),
crate::main_api(),
)
.mutate_command(super::translate_cli)
.run(args)
{
match e.data {

View File

@@ -1,11 +1,9 @@
use std::sync::Arc;
use tokio::process::Command;
use tracing::instrument;
use crate::context::config::ServerConfig;
use crate::context::rpc::InitRpcContextPhases;
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::context::{DiagnosticContext, InitContext, RpcContext, SetupContext};
use crate::disk::REPAIR_DISK_PATH;
use crate::disk::fsck::RepairStrategy;
use crate::disk::main::DEFAULT_PASSWORD;
@@ -27,7 +25,13 @@ async fn setup_or_init(
if let Some(firmware) = check_for_firmware_update()
.await
.map_err(|e| {
tracing::warn!("Error checking for firmware update: {e}");
tracing::warn!(
"{}",
t!(
"bins.start-init.error-checking-firmware",
error = e.to_string()
)
);
tracing::debug!("{e:?}");
})
.ok()
@@ -35,14 +39,21 @@ async fn setup_or_init(
{
let init_ctx = InitContext::init(config).await?;
let handle = &init_ctx.progress;
let mut update_phase = handle.add_phase("Updating Firmware".into(), Some(10));
let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1));
let mut update_phase =
handle.add_phase(t!("bins.start-init.updating-firmware").into(), Some(10));
let mut reboot_phase = handle.add_phase(t!("bins.start-init.rebooting").into(), Some(1));
server.serve_ui_for(init_ctx);
update_phase.start();
if let Err(e) = update_firmware(firmware).await {
tracing::warn!("Error performing firmware update: {e}");
tracing::warn!(
"{}",
t!(
"bins.start-init.error-firmware-update",
error = e.to_string()
)
);
tracing::debug!("{e:?}");
} else {
update_phase.complete();
@@ -79,40 +90,11 @@ async fn setup_or_init(
.invoke(crate::ErrorKind::OpenSsl)
.await?;
if tokio::fs::metadata("/run/live/medium").await.is_ok() {
Command::new("sed")
.arg("-i")
.arg("s/PasswordAuthentication no/PasswordAuthentication yes/g")
.arg("/etc/ssh/sshd_config")
.invoke(crate::ErrorKind::Filesystem)
.await?;
Command::new("systemctl")
.arg("reload")
.arg("ssh")
.invoke(crate::ErrorKind::OpenSsh)
.await?;
let ctx = InstallContext::init().await?;
server.serve_ui_for(ctx.clone());
ctx.shutdown
.subscribe()
.recv()
.await
.expect("context dropped");
return Ok(Err(Shutdown {
disk_guid: None,
restart: true,
}));
}
if tokio::fs::metadata("/media/startos/config/disk.guid")
.await
.is_err()
{
let ctx = SetupContext::init(server, config)?;
let ctx = SetupContext::init(server, config.clone())?;
server.serve_ui_for(ctx.clone());
@@ -127,7 +109,13 @@ async fn setup_or_init(
.invoke(ErrorKind::NotFound)
.await
{
tracing::error!("Failed to kill kiosk: {}", e);
tracing::error!(
"{}",
t!(
"bins.start-init.failed-to-kill-kiosk",
error = e.to_string()
)
);
tracing::debug!("{:?}", e);
}
@@ -136,7 +124,7 @@ async fn setup_or_init(
Some(Err(e)) => return Err(e.clone_output()),
None => {
return Err(Error::new(
eyre!("Setup mode exited before setup completed"),
eyre!("{}", t!("bins.start-init.setup-mode-exited")),
ErrorKind::Unknown,
));
}
@@ -146,7 +134,8 @@ async fn setup_or_init(
let handle = init_ctx.progress.clone();
let err_channel = init_ctx.error.clone();
let mut disk_phase = handle.add_phase("Opening data drive".into(), Some(10));
let mut disk_phase =
handle.add_phase(t!("bins.start-init.opening-data-drive").into(), Some(10));
let init_phases = InitPhases::new(&handle);
let rpc_ctx_phases = InitRpcContextPhases::new(&handle);
@@ -156,9 +145,9 @@ async fn setup_or_init(
disk_phase.start();
let guid_string = tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
.await?;
let disk_guid = Arc::new(String::from(guid_string.trim()));
let disk_guid = InternedString::intern(guid_string.trim());
let requires_reboot = crate::disk::main::import(
&**disk_guid,
&*disk_guid,
DATA_DIR,
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
RepairStrategy::Aggressive
@@ -178,11 +167,12 @@ async fn setup_or_init(
.with_ctx(|_| (crate::ErrorKind::Filesystem, REPAIR_DISK_PATH))?;
}
disk_phase.complete();
tracing::info!("Loaded Disk");
tracing::info!("{}", t!("bins.start-init.loaded-disk"));
if requires_reboot.0 {
tracing::info!("Rebooting...");
let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1));
tracing::info!("{}", t!("bins.start-init.rebooting"));
let mut reboot_phase =
handle.add_phase(t!("bins.start-init.rebooting").into(), Some(1));
reboot_phase.start();
return Ok(Err(Shutdown {
disk_guid: Some(disk_guid),
@@ -236,11 +226,10 @@ pub async fn main(
.await
.is_ok()
{
Some(Arc::new(
Some(InternedString::intern(
tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
.await?
.trim()
.to_owned(),
.trim(),
))
} else {
None

View File

@@ -1,10 +1,10 @@
use std::cmp::max;
use std::ffi::OsString;
use std::sync::Arc;
use std::time::Duration;
use clap::Parser;
use color_eyre::eyre::eyre;
use rust_i18n::t;
use futures::{FutureExt, TryFutureExt};
use tokio::signal::unix::signal;
use tracing::instrument;
@@ -15,11 +15,11 @@ use crate::context::{DiagnosticContext, InitContext, RpcContext};
use crate::net::gateway::{BindTcp, SelfContainedNetworkInterfaceListener, UpgradableListener};
use crate::net::static_server::refresher;
use crate::net::web_server::{Acceptor, WebServer};
use crate::prelude::*;
use crate::shutdown::Shutdown;
use crate::system::launch_metrics_task;
use crate::util::io::append_file;
use crate::util::logger::LOGGER;
use crate::{Error, ErrorKind, ResultExt};
#[instrument(skip_all)]
async fn inner_main(
@@ -53,11 +53,10 @@ async fn inner_main(
let ctx = RpcContext::init(
&server.acceptor_setter(),
config,
Arc::new(
InternedString::intern(
tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
.await?
.trim()
.to_owned(),
.trim(),
),
None,
rpc_ctx_phases,
@@ -114,11 +113,11 @@ async fn inner_main(
metrics_task
.map_err(|e| {
Error::new(
eyre!("{}", e).wrap_err("Metrics daemon panicked!"),
eyre!("{}", e).wrap_err(t!("bins.startd.metrics-daemon-panicked").to_string()),
ErrorKind::Unknown,
)
})
.map_ok(|_| tracing::debug!("Metrics daemon Shutdown"))
.map_ok(|_| tracing::debug!("{}", t!("bins.startd.metrics-daemon-shutdown")))
.await?;
let shutdown = shutdown_recv
@@ -146,7 +145,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
.worker_threads(max(1, num_cpus::get()))
.enable_all()
.build()
.expect("failed to initialize runtime");
.expect(&t!("bins.startd.failed-to-initialize-runtime"));
let res = rt.block_on(async {
let mut server = WebServer::new(
Acceptor::bind_upgradable(SelfContainedNetworkInterfaceListener::bind(BindTcp, 80)),
@@ -167,11 +166,10 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
.await
.is_ok()
{
Some(Arc::new(
Some(InternedString::intern(
tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
.await?
.trim()
.to_owned(),
.trim(),
))
} else {
None

View File

@@ -6,6 +6,7 @@ use std::time::Duration;
use clap::Parser;
use futures::FutureExt;
use rpc_toolkit::CliApp;
use rust_i18n::t;
use tokio::signal::unix::signal;
use tracing::instrument;
use visit_rs::Visit;
@@ -70,7 +71,7 @@ async fn inner_main(config: &TunnelConfig) -> Result<(), Error> {
true
}
Err(e) => {
tracing::error!("error adding ssl listener: {e}");
tracing::error!("{}", t!("bins.tunnel.error-adding-ssl-listener", error = e.to_string()));
tracing::debug!("{e:?}");
false
@@ -92,7 +93,7 @@ async fn inner_main(config: &TunnelConfig) -> Result<(), Error> {
}
.await
{
tracing::error!("error updating webserver bind: {e}");
tracing::error!("{}", t!("bins.tunnel.error-updating-webserver-bind", error = e.to_string()));
tracing::debug!("{e:?}");
tokio::time::sleep(Duration::from_secs(5)).await;
}
@@ -157,7 +158,7 @@ pub fn main(args: impl IntoIterator<Item = OsString>) {
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("failed to initialize runtime");
.expect(&t!("bins.tunnel.failed-to-initialize-runtime"));
rt.block_on(inner_main(&config))
};
@@ -179,6 +180,7 @@ pub fn cli(args: impl IntoIterator<Item = OsString>) {
|cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?),
crate::tunnel::api::tunnel_api(),
)
.mutate_command(super::translate_cli)
.run(args)
{
match e.data {

View File

@@ -23,7 +23,7 @@ use tracing::instrument;
use super::setup::CURRENT_SECRET;
use crate::context::config::{ClientConfig, local_config_path};
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::context::{DiagnosticContext, InitContext, RpcContext, SetupContext};
use crate::developer::{OS_DEVELOPER_KEY_PATH, default_developer_key_path};
use crate::middleware::auth::local::LocalAuthContext;
use crate::prelude::*;
@@ -166,14 +166,14 @@ impl CliContext {
.with_kind(crate::ErrorKind::Pem)?;
let secret = ed25519_dalek::SecretKey::try_from(&pair.secret_key[..]).map_err(|_| {
Error::new(
eyre!("pkcs8 key is of incorrect length"),
eyre!("{}", t!("context.cli.pkcs8-key-incorrect-length")),
ErrorKind::OpenSsl,
)
})?;
return Ok(secret.into())
}
Err(Error::new(
eyre!("Developer Key does not exist! Please run `start-cli init-key` before running this command."),
eyre!("{}", t!("context.cli.developer-key-does-not-exist")),
crate::ErrorKind::Uninitialized
))
})
@@ -189,14 +189,14 @@ impl CliContext {
"http" => "ws",
_ => {
return Err(Error::new(
eyre!("Cannot parse scheme from base URL"),
eyre!("{}", t!("context.cli.cannot-parse-scheme-from-base-url")),
crate::ErrorKind::ParseUrl,
)
.into());
}
};
url.set_scheme(ws_scheme)
.map_err(|_| Error::new(eyre!("Cannot set URL scheme"), crate::ErrorKind::ParseUrl))?;
.map_err(|_| Error::new(eyre!("{}", t!("context.cli.cannot-set-url-scheme")), crate::ErrorKind::ParseUrl))?;
url.path_segments_mut()
.map_err(|_| eyre!("Url cannot be base"))
.with_kind(crate::ErrorKind::ParseUrl)?
@@ -394,22 +394,3 @@ impl CallRemote<SetupContext> for CliContext {
.await
}
}
impl CallRemote<InstallContext> for CliContext {
async fn call_remote(
&self,
method: &str,
_: OrdMap<&'static str, Value>,
params: Value,
_: Empty,
) -> Result<Value, RpcError> {
crate::middleware::auth::signature::call_remote(
self,
self.rpc_url.clone(),
HeaderMap::new(),
self.rpc_url.host_str(),
method,
params,
)
.await
}
}

View File

@@ -58,27 +58,27 @@ pub trait ContextConfig: DeserializeOwned + Default {
#[command(rename_all = "kebab-case")]
#[command(version = crate::version::Current::default().semver().to_string())]
pub struct ClientConfig {
#[arg(short = 'c', long)]
#[arg(short = 'c', long, help = "help.arg.config-file-path")]
pub config: Option<PathBuf>,
#[arg(short = 'H', long)]
#[arg(short = 'H', long, help = "help.arg.host-url")]
pub host: Option<Url>,
#[arg(short = 'r', long)]
#[arg(short = 'r', long, help = "help.arg.registry-url")]
pub registry: Option<Url>,
#[arg(long)]
#[arg(long, help = "help.arg.registry-hostname")]
pub registry_hostname: Option<Vec<InternedString>>,
#[arg(skip)]
pub registry_listen: Option<SocketAddr>,
#[arg(short = 't', long)]
#[arg(short = 't', long, help = "help.arg.tunnel-address")]
pub tunnel: Option<SocketAddr>,
#[arg(skip)]
pub tunnel_listen: Option<SocketAddr>,
#[arg(short = 'p', long)]
#[arg(short = 'p', long, help = "help.arg.proxy-url")]
pub proxy: Option<Url>,
#[arg(skip)]
pub socks_listen: Option<SocketAddr>,
#[arg(long)]
#[arg(long, help = "help.arg.cookie-path")]
pub cookie_path: Option<PathBuf>,
#[arg(long)]
#[arg(long, help = "help.arg.developer-key-path")]
pub developer_key_path: Option<PathBuf>,
}
impl ContextConfig for ClientConfig {
@@ -109,21 +109,19 @@ impl ClientConfig {
#[serde(rename_all = "kebab-case")]
#[command(rename_all = "kebab-case")]
pub struct ServerConfig {
#[arg(short, long)]
#[arg(short, long, help = "help.arg.config-file-path")]
pub config: Option<PathBuf>,
#[arg(long)]
pub ethernet_interface: Option<String>,
#[arg(skip)]
pub os_partitions: Option<OsPartitionInfo>,
#[arg(long)]
#[arg(long, help = "help.arg.socks-listen-address")]
pub socks_listen: Option<SocketAddr>,
#[arg(long)]
#[arg(long, help = "help.arg.revision-cache-size")]
pub revision_cache_size: Option<usize>,
#[arg(long)]
#[arg(long, help = "help.arg.disable-encryption")]
pub disable_encryption: Option<bool>,
#[arg(long)]
#[arg(long, help = "help.arg.multi-arch-s9pks")]
pub multi_arch_s9pks: Option<bool>,
#[arg(long)]
#[arg(long, help = "help.arg.developer-key-path")]
pub developer_key_path: Option<PathBuf>,
}
impl ContextConfig for ServerConfig {
@@ -131,7 +129,6 @@ impl ContextConfig for ServerConfig {
self.config.take()
}
fn merge_with(&mut self, other: Self) {
self.ethernet_interface = self.ethernet_interface.take().or(other.ethernet_interface);
self.os_partitions = self.os_partitions.take().or(other.os_partitions);
self.socks_listen = self.socks_listen.take().or(other.socks_listen);
self.revision_cache_size = self

View File

@@ -6,15 +6,15 @@ use rpc_toolkit::yajrc::RpcError;
use tokio::sync::broadcast::Sender;
use tracing::instrument;
use crate::Error;
use crate::context::config::ServerConfig;
use crate::prelude::*;
use crate::rpc_continuations::RpcContinuations;
use crate::shutdown::Shutdown;
pub struct DiagnosticContextSeed {
pub shutdown: Sender<Shutdown>,
pub error: Arc<RpcError>,
pub disk_guid: Option<Arc<String>>,
pub disk_guid: Option<InternedString>,
pub rpc_continuations: RpcContinuations,
}
@@ -24,10 +24,10 @@ impl DiagnosticContext {
#[instrument(skip_all)]
pub fn init(
_config: &ServerConfig,
disk_guid: Option<Arc<String>>,
disk_guid: Option<InternedString>,
error: Error,
) -> Result<Self, Error> {
tracing::error!("Error: {}: Starting diagnostic UI", error);
tracing::error!("{}", t!("context.diagnostic.starting-diagnostic-ui", error = error));
tracing::debug!("{:?}", error);
let (shutdown, _) = tokio::sync::broadcast::channel(1);

View File

@@ -1,44 +0,0 @@
use std::ops::Deref;
use std::sync::Arc;
use rpc_toolkit::Context;
use tokio::sync::broadcast::Sender;
use tracing::instrument;
use crate::Error;
use crate::net::utils::find_eth_iface;
use crate::rpc_continuations::RpcContinuations;
pub struct InstallContextSeed {
pub ethernet_interface: String,
pub shutdown: Sender<()>,
pub rpc_continuations: RpcContinuations,
}
#[derive(Clone)]
pub struct InstallContext(Arc<InstallContextSeed>);
impl InstallContext {
#[instrument(skip_all)]
pub async fn init() -> Result<Self, Error> {
let (shutdown, _) = tokio::sync::broadcast::channel(1);
Ok(Self(Arc::new(InstallContextSeed {
ethernet_interface: find_eth_iface().await?,
shutdown,
rpc_continuations: RpcContinuations::new(),
})))
}
}
impl AsRef<RpcContinuations> for InstallContext {
fn as_ref(&self) -> &RpcContinuations {
&self.rpc_continuations
}
}
impl Context for InstallContext {}
impl Deref for InstallContext {
type Target = InstallContextSeed;
fn deref(&self) -> &Self::Target {
&*self.0
}
}

View File

@@ -2,13 +2,11 @@ pub mod cli;
pub mod config;
pub mod diagnostic;
pub mod init;
pub mod install;
pub mod rpc;
pub mod setup;
pub use cli::CliContext;
pub use diagnostic::DiagnosticContext;
pub use init::InitContext;
pub use install::InstallContext;
pub use rpc::RpcContext;
pub use setup::SetupContext;

View File

@@ -60,7 +60,7 @@ pub struct RpcContextSeed {
pub os_partitions: OsPartitionInfo,
pub wifi_interface: Option<String>,
pub ethernet_interface: String,
pub disk_guid: Arc<String>,
pub disk_guid: InternedString,
pub ephemeral_sessions: SyncMutex<Sessions>,
pub db: TypedPatchDb<Database>,
pub sync_db: watch::Sender<u64>,
@@ -84,7 +84,7 @@ pub struct RpcContextSeed {
}
impl Drop for RpcContextSeed {
fn drop(&mut self) {
tracing::info!("RpcContext is dropped");
tracing::info!("{}", t!("context.rpc.rpc-context-dropped"));
}
}
@@ -134,7 +134,7 @@ impl RpcContext {
pub async fn init(
webserver: &WebServerAcceptorSetter<UpgradableListener>,
config: &ServerConfig,
disk_guid: Arc<String>,
disk_guid: InternedString,
init_result: Option<InitResult>,
InitRpcContextPhases {
mut load_db,
@@ -155,7 +155,7 @@ impl RpcContext {
let peek = db.peek().await;
let account = AccountInfo::load(&peek)?;
load_db.complete();
tracing::info!("Opened PatchDB");
tracing::info!("{}", t!("context.rpc.opened-patchdb"));
init_net_ctrl.start();
let (net_controller, os_net_service) = if let Some(InitResult {
@@ -172,15 +172,15 @@ impl RpcContext {
(net_ctrl, os_net_service)
};
init_net_ctrl.complete();
tracing::info!("Initialized Net Controller");
tracing::info!("{}", t!("context.rpc.initialized-net-controller"));
if PLATFORM.ends_with("-nonfree") {
if let Err(e) = Command::new("nvidia-smi")
.invoke(ErrorKind::ParseSysInfo)
.await
{
tracing::warn!("nvidia-smi: {e}");
tracing::info!("The above warning can be ignored if no NVIDIA card is present");
tracing::warn!("{}", t!("context.rpc.nvidia-smi-error", error = e));
tracing::info!("{}", t!("context.rpc.nvidia-warning-can-be-ignored"));
} else {
async {
let version: InternedString = String::from_utf8(
@@ -279,7 +279,7 @@ impl RpcContext {
.arg("100000")
.invoke(ErrorKind::Filesystem)
.await?;
tmp.unmount_and_delete().await?;
// tmp.unmount_and_delete().await?;
}
BlockDev::new(&sqfs)
.mount(NVIDIA_OVERLAY_PATH, ReadOnly)
@@ -335,16 +335,12 @@ impl RpcContext {
is_closed: AtomicBool::new(false),
os_partitions: config.os_partitions.clone().ok_or_else(|| {
Error::new(
eyre!("OS Partition Information Missing"),
eyre!("{}", t!("context.rpc.os-partition-info-missing")),
ErrorKind::Filesystem,
)
})?,
wifi_interface: wifi_interface.clone(),
ethernet_interface: if let Some(eth) = config.ethernet_interface.clone() {
eth
} else {
find_eth_iface().await?
},
ethernet_interface: find_eth_iface().await?,
disk_guid,
ephemeral_sessions: SyncMutex::new(Sessions::new()),
sync_db: watch::Sender::new(db.sequence().await),
@@ -369,9 +365,9 @@ impl RpcContext {
current_secret: Arc::new(
Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).map_err(|e| {
tracing::debug!("{:?}", e);
tracing::error!("Couldn't generate ec key");
tracing::error!("{}", t!("context.rpc.couldnt-generate-ec-key"));
Error::new(
color_eyre::eyre::eyre!("Couldn't generate ec key"),
color_eyre::eyre::eyre!("{}", t!("context.rpc.couldnt-generate-ec-key")),
crate::ErrorKind::Unknown,
)
})?,
@@ -386,10 +382,10 @@ impl RpcContext {
let res = Self(seed.clone());
res.cleanup_and_initialize(cleanup_init).await?;
tracing::info!("Cleaned up transient states");
tracing::info!("{}", t!("context.rpc.cleaned-up-transient-states"));
crate::version::post_init(&res, run_migrations).await?;
tracing::info!("Completed migrations");
tracing::info!("{}", t!("context.rpc.completed-migrations"));
Ok(res)
}
@@ -398,7 +394,7 @@ impl RpcContext {
self.crons.mutate(|c| std::mem::take(c));
self.services.shutdown_all().await?;
self.is_closed.store(true, Ordering::SeqCst);
tracing::info!("RpcContext is shutdown");
tracing::info!("{}", t!("context.rpc.rpc-context-shutdown"));
Ok(())
}
@@ -467,7 +463,7 @@ impl RpcContext {
.await
.result
{
tracing::error!("Error in session cleanup cron: {e}");
tracing::error!("{}", t!("context.rpc.error-in-session-cleanup-cron", error = e));
tracing::debug!("{e:?}");
}
}

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::OsPartitionInfo;
use crate::disk::mount::guard::{MountGuard, TmpMountGuard};
use crate::hostname::Hostname;
use crate::net::gateway::UpgradableListener;
use crate::net::web_server::{WebServer, WebServerAcceptorSetter};
@@ -27,12 +27,15 @@ 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! {
pub static ref CURRENT_SECRET: Jwk = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).unwrap_or_else(|e| {
tracing::debug!("{:?}", e);
tracing::error!("Couldn't generate ec key");
tracing::error!("{}", t!("context.setup.couldnt-generate-ec-key"));
panic!("Couldn't generate ec key")
});
}
@@ -41,40 +44,25 @@ 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 {
pub webserver: WebServerAcceptorSetter<UpgradableListener>,
pub config: ServerConfig,
pub os_partitions: OsPartitionInfo,
pub config: SyncMutex<ServerConfig>,
pub disable_encryption: bool,
pub progress: FullProgressTracker,
pub task: OnceCell<NonDetachingJoinHandle<()>>,
pub result: OnceCell<Result<(SetupResult, RpcContext), Error>>,
pub disk_guid: OnceCell<Arc<String>>,
pub disk_guid: OnceCell<InternedString>,
pub shutdown: Sender<Option<Shutdown>>,
pub rpc_continuations: RpcContinuations,
pub install_rootfs: SyncMutex<Option<(TmpMountGuard, MountGuard)>>,
pub keyboard: SyncMutex<Option<KeyboardOptions>>,
pub language: SyncMutex<Option<InternedString>>,
}
#[derive(Clone)]
@@ -83,27 +71,24 @@ impl SetupContext {
#[instrument(skip_all)]
pub fn init(
webserver: &WebServer<UpgradableListener>,
config: &ServerConfig,
config: ServerConfig,
) -> Result<Self, Error> {
let (shutdown, _) = tokio::sync::broadcast::channel(1);
let mut progress = FullProgressTracker::new();
progress.enable_logging(true);
Ok(Self(Arc::new(SetupContextSeed {
webserver: webserver.acceptor_setter(),
config: config.clone(),
os_partitions: config.os_partitions.clone().ok_or_else(|| {
Error::new(
eyre!("missing required configuration: `os-partitions`"),
ErrorKind::NotFound,
)
})?,
disable_encryption: config.disable_encryption.unwrap_or(false),
config: SyncMutex::new(config),
progress,
task: OnceCell::new(),
result: OnceCell::new(),
disk_guid: OnceCell::new(),
shutdown,
rpc_continuations: RpcContinuations::new(),
install_rootfs: SyncMutex::new(None),
language: SyncMutex::new(None),
keyboard: SyncMutex::new(None),
})))
}
#[instrument(skip_all)]
@@ -129,11 +114,14 @@ impl SetupContext {
.get_or_init(|| async {
match f().await {
Ok(res) => {
tracing::info!("Setup complete!");
tracing::info!("{}", t!("context.setup.setup-complete"));
Ok(res)
}
Err(e) => {
tracing::error!("Setup failed: {e}");
tracing::error!(
"{}",
t!("context.setup.setup-failed", error = e)
);
tracing::debug!("{e:?}");
Err(e)
}
@@ -146,10 +134,13 @@ impl SetupContext {
)
.map_err(|_| {
if self.result.initialized() {
Error::new(eyre!("Setup already complete"), ErrorKind::InvalidRequest)
Error::new(
eyre!("{}", t!("context.setup.setup-already-complete")),
ErrorKind::InvalidRequest,
)
} else {
Error::new(
eyre!("Setup already in progress"),
eyre!("{}", t!("context.setup.setup-already-in-progress")),
ErrorKind::InvalidRequest,
)
}
@@ -199,7 +190,7 @@ impl SetupContext {
}
.await
{
tracing::error!("Error in setup progress websocket: {e}");
tracing::error!("{}", t!("context.setup.error-in-setup-progress-websocket", error = e));
tracing::debug!("{e:?}");
}
},

View File

@@ -11,6 +11,7 @@ use crate::{Error, PackageId};
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct ControlParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
}

View File

@@ -54,7 +54,7 @@ pub fn db<C: Context>() -> ParentHandler<C> {
"dump",
from_fn_async(cli_dump)
.with_display_serializable()
.with_about("Filter/query db to display tables and records"),
.with_about("about.filter-query-db"),
)
.subcommand("dump", from_fn_async(dump).no_cli())
.subcommand(
@@ -65,13 +65,13 @@ pub fn db<C: Context>() -> ParentHandler<C> {
)
.subcommand(
"put",
put::<C>().with_about("Command for adding UI record to db"),
put::<C>().with_about("about.command-add-ui-record-db"),
)
.subcommand(
"apply",
from_fn_async(cli_apply)
.no_display()
.with_about("Update a db record"),
.with_about("about.update-db-record"),
)
.subcommand("apply", from_fn_async(apply).no_cli())
}
@@ -87,9 +87,10 @@ pub enum RevisionsRes {
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct CliDumpParams {
#[arg(long = "include-private", short = 'p')]
#[arg(long = "include-private", short = 'p', help = "help.arg.include-private-data")]
#[serde(default)]
include_private: bool,
#[arg(help = "help.arg.db-path")]
path: Option<PathBuf>,
}
@@ -258,9 +259,11 @@ pub async fn subscribe(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct CliApplyParams {
#[arg(long)]
#[arg(long, help = "help.arg.allow-model-mismatch")]
allow_model_mismatch: bool,
#[arg(help = "help.arg.db-apply-expr")]
expr: String,
#[arg(help = "help.arg.db-path")]
path: Option<PathBuf>,
}
@@ -327,6 +330,7 @@ async fn cli_apply(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct ApplyParams {
#[arg(help = "help.arg.db-apply-expr")]
expr: String,
}
@@ -358,7 +362,7 @@ pub fn put<C: Context>() -> ParentHandler<C> {
"ui",
from_fn_async(ui)
.with_display_serializable()
.with_about("Add path and value to db")
.with_about("about.add-path-value-db")
.with_call_remote::<CliContext>(),
)
}
@@ -366,8 +370,10 @@ pub fn put<C: Context>() -> ParentHandler<C> {
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct UiParams {
#[arg(help = "help.arg.json-pointer")]
#[ts(type = "string")]
pointer: JsonPointer,
#[arg(help = "help.arg.json-value")]
#[ts(type = "any")]
value: Value,
}

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

@@ -14,7 +14,7 @@ use crate::net::host::Hosts;
use crate::net::service_interface::ServiceInterface;
use crate::prelude::*;
use crate::progress::FullProgress;
use crate::s9pk::manifest::Manifest;
use crate::s9pk::manifest::{LocaleString, Manifest};
use crate::status::StatusInfo;
use crate::util::DataUrl;
use crate::util::serde::{Pem, is_partial_of};
@@ -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<LocaleString>,
pub icon: Option<DataUrl<'static>>,
#[serde(flatten)]
pub kind: CurrentDependencyKind,

View File

@@ -25,7 +25,7 @@ use crate::net::utils::ipv6_is_local;
use crate::net::vhost::AlpnInfo;
use crate::prelude::*;
use crate::progress::FullProgress;
use crate::system::SmtpValue;
use crate::system::{KeyboardOptions, SmtpValue};
use crate::util::cpupower::Governor;
use crate::util::lshw::LshwDevice;
use crate::util::serde::MaybeUtf8String;
@@ -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,6 +144,8 @@ impl Public {
ram: 0,
devices: Vec::new(),
kiosk,
language,
keyboard,
},
package_data: AllPackageData::default(),
ui: serde_json::from_str(*DB_UI_SEED_CELL.get().unwrap_or(&"null"))
@@ -195,6 +202,8 @@ pub struct ServerInfo {
pub ram: u64,
pub devices: Vec<LshwDevice>,
pub kiosk: Option<bool>,
pub language: Option<InternedString>,
pub keyboard: Option<KeyboardOptions>,
}
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]

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

@@ -17,45 +17,46 @@ pub fn diagnostic<C: Context>() -> ParentHandler<C> {
.subcommand(
"error",
from_fn(error)
.with_about("Display diagnostic error")
.with_about("about.display-diagnostic-error")
.with_call_remote::<CliContext>(),
)
.subcommand(
"logs",
crate::system::logs::<DiagnosticContext>().with_about("Display OS logs"),
crate::system::logs::<DiagnosticContext>().with_about("about.display-os-logs"),
)
.subcommand(
"logs",
from_fn_async(crate::logs::cli_logs::<DiagnosticContext, Empty>)
.no_display()
.with_about("Display OS logs"),
.with_about("about.display-os-logs"),
)
.subcommand(
"kernel-logs",
crate::system::kernel_logs::<DiagnosticContext>().with_about("Display kernel logs"),
crate::system::kernel_logs::<DiagnosticContext>()
.with_about("about.display-kernel-logs"),
)
.subcommand(
"kernel-logs",
from_fn_async(crate::logs::cli_logs::<DiagnosticContext, Empty>)
.no_display()
.with_about("Display kernal logs"),
.with_about("about.display-kernel-logs"),
)
.subcommand(
"restart",
from_fn(restart)
.no_display()
.with_about("Restart the server")
.with_about("about.restart-server")
.with_call_remote::<CliContext>(),
)
.subcommand(
"disk",
disk::<C>().with_about("Command to remove disk from filesystem"),
disk::<C>().with_about("about.command-remove-disk-filesystem"),
)
.subcommand(
"rebuild",
from_fn_async(rebuild)
.no_display()
.with_about("Teardown and rebuild service containers")
.with_about("about.teardown-rebuild-containers")
.with_call_remote::<CliContext>(),
)
}
@@ -89,16 +90,16 @@ pub fn disk<C: Context>() -> ParentHandler<C> {
from_fn_async(forget_disk::<RpcContext>).no_display(),
)
.no_display()
.with_about("Remove disk from filesystem"),
.with_about("about.remove-disk-filesystem"),
)
.subcommand("repair", from_fn_async(|_: C| repair()).no_cli())
.subcommand(
"repair",
CallRemoteHandler::<CliContext, _, _>::new(
from_fn_async(|_: RpcContext| repair())
.no_display()
.with_about("Repair disk in the event of corruption"),
),
from_fn_async(|_: RpcContext| repair()).no_display(),
)
.no_display()
.with_about("about.repair-disk-corruption"),
)
}

View File

@@ -4,6 +4,7 @@ use std::path::Path;
use color_eyre::eyre::eyre;
use futures::FutureExt;
use futures::future::BoxFuture;
use rust_i18n::t;
use tokio::process::Command;
use tracing::instrument;
@@ -62,33 +63,31 @@ async fn e2fsck_runner(
let e2fsck_stderr = String::from_utf8(e2fsck_out.stderr)?;
let code = e2fsck_out.status.code().ok_or_else(|| {
Error::new(
eyre!("e2fsck: process terminated by signal"),
eyre!("{}", t!("disk.fsck.process-terminated-by-signal")),
crate::ErrorKind::DiskManagement,
)
})?;
if code & 4 != 0 {
tracing::error!(
"some filesystem errors NOT corrected on {}:\n{}",
logicalname.as_ref().display(),
e2fsck_stderr,
"{}",
t!("disk.fsck.errors-not-corrected", device = logicalname.as_ref().display(), stderr = e2fsck_stderr),
);
} else if code & 1 != 0 {
tracing::warn!(
"filesystem errors corrected on {}:\n{}",
logicalname.as_ref().display(),
e2fsck_stderr,
"{}",
t!("disk.fsck.errors-corrected", device = logicalname.as_ref().display(), stderr = e2fsck_stderr),
);
}
if code < 8 {
if code & 2 != 0 {
tracing::warn!("reboot required");
tracing::warn!("{}", t!("disk.fsck.reboot-required"));
Ok(RequiresReboot(true))
} else {
Ok(RequiresReboot(false))
}
} else {
Err(Error::new(
eyre!("e2fsck: {}", e2fsck_stderr),
eyre!("{}", t!("disk.fsck.e2fsck-error", stderr = e2fsck_stderr)),
crate::ErrorKind::DiskManagement,
))
}

View File

@@ -2,6 +2,8 @@ use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use color_eyre::eyre::eyre;
use imbl_value::InternedString;
use rust_i18n::t;
use tokio::process::Command;
use tracing::instrument;
@@ -20,10 +22,10 @@ pub const MAIN_FS_SIZE: FsSize = FsSize::Gigabytes(8);
#[instrument(skip_all)]
pub async fn create<I, P>(
disks: &I,
pvscan: &BTreeMap<PathBuf, Option<String>>,
pvscan: &BTreeMap<PathBuf, Option<InternedString>>,
datadir: impl AsRef<Path>,
password: Option<&str>,
) -> Result<String, Error>
) -> Result<InternedString, Error>
where
for<'a> &'a I: IntoIterator<Item = &'a P>,
P: AsRef<Path>,
@@ -37,9 +39,9 @@ where
#[instrument(skip_all)]
pub async fn create_pool<I, P>(
disks: &I,
pvscan: &BTreeMap<PathBuf, Option<String>>,
pvscan: &BTreeMap<PathBuf, Option<InternedString>>,
encrypted: bool,
) -> Result<String, Error>
) -> Result<InternedString, Error>
where
for<'a> &'a I: IntoIterator<Item = &'a P>,
P: AsRef<Path>,
@@ -79,7 +81,7 @@ where
cmd.arg(disk.as_ref());
}
cmd.invoke(crate::ErrorKind::DiskManagement).await?;
Ok(guid)
Ok(guid.into())
}
#[derive(Debug, Clone, Copy)]
@@ -224,7 +226,7 @@ pub async fn import<P: AsRef<Path>>(
.is_none()
{
return Err(Error::new(
eyre!("StartOS disk not found."),
eyre!("{}", t!("disk.main.disk-not-found")),
crate::ErrorKind::DiskNotAvailable,
));
}
@@ -234,7 +236,7 @@ pub async fn import<P: AsRef<Path>>(
.any(|id| id == guid)
{
return Err(Error::new(
eyre!("A StartOS disk was found, but it is not the correct disk for this device."),
eyre!("{}", t!("disk.main.incorrect-disk")),
crate::ErrorKind::IncorrectDisk,
));
}

View File

@@ -25,6 +25,8 @@ pub struct OsPartitionInfo {
pub bios: Option<PathBuf>,
pub boot: PathBuf,
pub root: PathBuf,
#[serde(skip)] // internal use only
pub data: Option<PathBuf>,
}
impl OsPartitionInfo {
pub fn contains(&self, logicalname: impl AsRef<Path>) -> bool {
@@ -49,7 +51,7 @@ pub fn disk<C: Context>() -> ParentHandler<C> {
from_fn_async(list)
.with_display_serializable()
.with_custom_display_fn(|handle, result| display_disk_info(handle.params, result))
.with_about("List disk info")
.with_about("about.list-disk-info")
.with_call_remote::<CliContext>(),
)
.subcommand("repair", from_fn_async(|_: C| repair()).no_cli())
@@ -58,7 +60,7 @@ pub fn disk<C: Context>() -> ParentHandler<C> {
CallRemoteHandler::<CliContext, _, _>::new(
from_fn_async(|_: RpcContext| repair())
.no_display()
.with_about("Repair disk in the event of corruption"),
.with_about("about.repair-disk-corruption"),
),
)
}

View File

@@ -23,9 +23,8 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
read_only: bool,
) -> Result<(), Error> {
tracing::info!(
"Binding {} to {}",
src.as_ref().display(),
dst.as_ref().display()
"{}",
t!("disk.mount.binding", src = src.as_ref().display(), dst = dst.as_ref().display())
);
if is_mountpoint(&dst).await? {
unmount(dst.as_ref(), true).await?;

View File

@@ -20,9 +20,9 @@ use super::mount::guard::TmpMountGuard;
use crate::disk::OsPartitionInfo;
use crate::disk::mount::guard::GenericMountGuard;
use crate::hostname::Hostname;
use crate::prelude::*;
use crate::util::Invoke;
use crate::util::serde::IoFormat;
use crate::{Error, ResultExt as _};
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
@@ -40,7 +40,7 @@ pub struct DiskInfo {
pub model: Option<String>,
pub partitions: Vec<PartitionInfo>,
pub capacity: u64,
pub guid: Option<String>,
pub guid: Option<InternedString>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -51,7 +51,7 @@ pub struct PartitionInfo {
pub capacity: u64,
pub used: Option<u64>,
pub start_os: BTreeMap<String, StartOsRecoveryInfo>,
pub guid: Option<String>,
pub guid: Option<InternedString>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
@@ -95,7 +95,7 @@ pub async fn get_vendor<P: AsRef<Path>>(path: P) -> Result<Option<String>, Error
Path::new(SYS_BLOCK_PATH)
.join(path.as_ref().strip_prefix("/dev").map_err(|_| {
Error::new(
eyre!("not a canonical block device"),
eyre!("{}", t!("disk.util.not-canonical-block-device")),
crate::ErrorKind::BlockDevice,
)
})?)
@@ -118,7 +118,7 @@ pub async fn get_model<P: AsRef<Path>>(path: P) -> Result<Option<String>, Error>
Path::new(SYS_BLOCK_PATH)
.join(path.as_ref().strip_prefix("/dev").map_err(|_| {
Error::new(
eyre!("not a canonical block device"),
eyre!("{}", t!("disk.util.not-canonical-block-device")),
crate::ErrorKind::BlockDevice,
)
})?)
@@ -215,7 +215,7 @@ pub async fn get_percentage<P: AsRef<Path>>(path: P) -> Result<u64, Error> {
}
#[instrument(skip_all)]
pub async fn pvscan() -> Result<BTreeMap<PathBuf, Option<String>>, Error> {
pub async fn pvscan() -> Result<BTreeMap<PathBuf, Option<InternedString>>, Error> {
let pvscan_out = Command::new("pvscan")
.invoke(crate::ErrorKind::DiskManagement)
.await?;
@@ -259,6 +259,31 @@ pub async fn recovery_info(
Ok(res)
}
/// Returns the canonical path of the source device for a given mount point,
/// or None if the mount point doesn't exist or isn't mounted.
#[instrument(skip_all)]
pub async fn get_mount_source(mountpoint: impl AsRef<Path>) -> Result<Option<PathBuf>, Error> {
let mounts_content = tokio::fs::read_to_string("/proc/mounts")
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, "/proc/mounts"))?;
let mountpoint = mountpoint.as_ref();
for line in mounts_content.lines() {
let mut parts = line.split_whitespace();
let source = parts.next();
let mount = parts.next();
if let (Some(source), Some(mount)) = (source, mount) {
if Path::new(mount) == mountpoint {
// Try to canonicalize the source path
if let Ok(canonical) = tokio::fs::canonicalize(source).await {
return Ok(Some(canonical));
}
}
}
}
Ok(None)
}
#[instrument(skip_all)]
pub async fn list(os: &OsPartitionInfo) -> Result<Vec<DiskInfo>, Error> {
struct DiskIndex {
@@ -374,23 +399,53 @@ async fn disk_info(disk: PathBuf) -> DiskInfo {
.await
.map_err(|e| {
tracing::warn!(
"Could not get partition table of {}: {}",
disk.display(),
e.source
"{}",
t!(
"disk.util.could-not-get-partition-table",
disk = disk.display(),
error = e.source
)
)
})
.unwrap_or_default();
let vendor = get_vendor(&disk)
.await
.map_err(|e| tracing::warn!("Could not get vendor of {}: {}", disk.display(), e.source))
.map_err(|e| {
tracing::warn!(
"{}",
t!(
"disk.util.could-not-get-vendor",
disk = disk.display(),
error = e.source
)
)
})
.unwrap_or_default();
let model = get_model(&disk)
.await
.map_err(|e| tracing::warn!("Could not get model of {}: {}", disk.display(), e.source))
.map_err(|e| {
tracing::warn!(
"{}",
t!(
"disk.util.could-not-get-model",
disk = disk.display(),
error = e.source
)
)
})
.unwrap_or_default();
let capacity = get_capacity(&disk)
.await
.map_err(|e| tracing::warn!("Could not get capacity of {}: {}", disk.display(), e.source))
.map_err(|e| {
tracing::warn!(
"{}",
t!(
"disk.util.could-not-get-capacity",
disk = disk.display(),
error = e.source
)
)
})
.unwrap_or_default();
DiskInfo {
logicalname: disk,
@@ -407,21 +462,49 @@ async fn part_info(part: PathBuf) -> PartitionInfo {
let mut start_os = BTreeMap::new();
let label = get_label(&part)
.await
.map_err(|e| tracing::warn!("Could not get label of {}: {}", part.display(), e.source))
.map_err(|e| {
tracing::warn!(
"{}",
t!(
"disk.util.could-not-get-label",
part = part.display(),
error = e.source
)
)
})
.unwrap_or_default();
let capacity = get_capacity(&part)
.await
.map_err(|e| tracing::warn!("Could not get capacity of {}: {}", part.display(), e.source))
.map_err(|e| {
tracing::warn!(
"{}",
t!(
"disk.util.could-not-get-capacity-part",
part = part.display(),
error = e.source
)
)
})
.unwrap_or_default();
let mut used = None;
match TmpMountGuard::mount(&BlockDev::new(&part), ReadOnly).await {
Err(e) => tracing::warn!("Could not collect usage information: {}", e.source),
Err(e) => tracing::warn!(
"{}",
t!("disk.util.could-not-collect-usage-info", error = e.source)
),
Ok(mount_guard) => {
used = get_used(mount_guard.path())
.await
.map_err(|e| {
tracing::warn!("Could not get usage of {}: {}", part.display(), e.source)
tracing::warn!(
"{}",
t!(
"disk.util.could-not-get-usage",
part = part.display(),
error = e.source
)
)
})
.ok();
match recovery_info(mount_guard.path()).await {
@@ -429,11 +512,21 @@ async fn part_info(part: PathBuf) -> PartitionInfo {
start_os = a;
}
Err(e) => {
tracing::error!("Error fetching unencrypted backup metadata: {}", e);
tracing::error!(
"{}",
t!("disk.util.error-fetching-backup-metadata", error = e)
);
}
}
if let Err(e) = mount_guard.unmount().await {
tracing::error!("Error unmounting partition {}: {}", part.display(), e);
tracing::error!(
"{}",
t!(
"disk.util.error-unmounting-partition",
part = part.display(),
error = e
)
);
}
}
}
@@ -448,7 +541,7 @@ async fn part_info(part: PathBuf) -> PartitionInfo {
}
}
fn parse_pvscan_output(pvscan_output: &str) -> BTreeMap<PathBuf, Option<String>> {
fn parse_pvscan_output(pvscan_output: &str) -> BTreeMap<PathBuf, Option<InternedString>> {
fn parse_line(line: &str) -> IResult<&str, (&str, Option<&str>)> {
let pv_parse = preceded(
tag(" PV "),
@@ -471,10 +564,10 @@ fn parse_pvscan_output(pvscan_output: &str) -> BTreeMap<PathBuf, Option<String>>
for entry in entries {
match parse_line(entry) {
Ok((_, (pv, vg))) => {
ret.insert(PathBuf::from(pv), vg.map(|s| s.to_owned()));
ret.insert(PathBuf::from(pv), vg.map(InternedString::intern));
}
Err(_) => {
tracing::warn!("Failed to parse pvscan output line: {}", entry);
tracing::warn!("{}", t!("disk.util.failed-to-parse-pvscan", line = entry));
}
}
}

View File

@@ -9,6 +9,7 @@ use rpc_toolkit::reqwest;
use rpc_toolkit::yajrc::{
INVALID_PARAMS_ERROR, INVALID_REQUEST_ERROR, METHOD_NOT_FOUND_ERROR, PARSE_ERROR, RpcError,
};
use rust_i18n::t;
use serde::{Deserialize, Serialize};
use tokio::task::JoinHandle;
use tokio_rustls::rustls;
@@ -97,95 +98,97 @@ pub enum ErrorKind {
InstallFailed = 76,
UpdateFailed = 77,
Smtp = 78,
SetSysInfo = 79,
}
impl ErrorKind {
pub fn as_str(&self) -> &'static str {
pub fn as_str(&self) -> String {
use ErrorKind::*;
match self {
Unknown => "Unknown Error",
Filesystem => "Filesystem I/O Error",
Docker => "Docker Error",
ConfigSpecViolation => "Config Spec Violation",
ConfigRulesViolation => "Config Rules Violation",
NotFound => "Not Found",
IncorrectPassword => "Incorrect Password",
VersionIncompatible => "Version Incompatible",
Network => "Network Error",
Registry => "Registry Error",
Serialization => "Serialization Error",
Deserialization => "Deserialization Error",
Utf8 => "UTF-8 Parse Error",
ParseVersion => "Version Parsing Error",
IncorrectDisk => "Incorrect Disk",
// Nginx => "Nginx Error",
Dependency => "Dependency Error",
ParseS9pk => "S9PK Parsing Error",
ParseUrl => "URL Parsing Error",
DiskNotAvailable => "Disk Not Available",
BlockDevice => "Block Device Error",
InvalidOnionAddress => "Invalid Onion Address",
Pack => "Pack Error",
ValidateS9pk => "S9PK Validation Error",
DiskCorrupted => "Disk Corrupted", // Remove
Tor => "Tor Daemon Error",
ConfigGen => "Config Generation Error",
ParseNumber => "Number Parsing Error",
Database => "Database Error",
InvalidId => "Invalid ID",
InvalidSignature => "Invalid Signature",
Backup => "Backup Error",
Restore => "Restore Error",
Authorization => "Unauthorized",
AutoConfigure => "Auto-Configure Error",
Action => "Action Failed",
RateLimited => "Rate Limited",
InvalidRequest => "Invalid Request",
MigrationFailed => "Migration Failed",
Uninitialized => "Uninitialized",
ParseNetAddress => "Net Address Parsing Error",
ParseSshKey => "SSH Key Parsing Error",
SoundError => "Sound Interface Error",
ParseTimestamp => "Timestamp Parsing Error",
ParseSysInfo => "System Info Parsing Error",
Wifi => "WiFi Internal Error",
Journald => "Journald Error",
DiskManagement => "Disk Management Error",
OpenSsl => "OpenSSL Internal Error",
PasswordHashGeneration => "Password Hash Generation Error",
DiagnosticMode => "Server is in Diagnostic Mode",
ParseDbField => "Database Field Parse Error",
Duplicate => "Duplication Error",
MultipleErrors => "Multiple Errors",
Incoherent => "Incoherent",
InvalidBackupTargetId => "Invalid Backup Target ID",
ProductKeyMismatch => "Incompatible Product Keys",
LanPortConflict => "Incompatible LAN Port Configuration",
Javascript => "Javascript Engine Error",
Pem => "PEM Encoding Error",
TLSInit => "TLS Backend Initialization Error",
Ascii => "ASCII Parse Error",
MissingHeader => "Missing Header",
Grub => "Grub Error",
Systemd => "Systemd Error",
OpenSsh => "OpenSSH Error",
Zram => "Zram Error",
Lshw => "LSHW Error",
CpuSettings => "CPU Settings Error",
Firmware => "Firmware Error",
Timeout => "Timeout Error",
Lxc => "LXC Error",
Cancelled => "Cancelled",
Git => "Git Error",
DBus => "DBus Error",
InstallFailed => "Install Failed",
UpdateFailed => "Update Failed",
Smtp => "SMTP Error",
}
Unknown => t!("error.unknown"),
Filesystem => t!("error.filesystem"),
Docker => t!("error.docker"),
ConfigSpecViolation => t!("error.config-spec-violation"),
ConfigRulesViolation => t!("error.config-rules-violation"),
NotFound => t!("error.not-found"),
IncorrectPassword => t!("error.incorrect-password"),
VersionIncompatible => t!("error.version-incompatible"),
Network => t!("error.network"),
Registry => t!("error.registry"),
Serialization => t!("error.serialization"),
Deserialization => t!("error.deserialization"),
Utf8 => t!("error.utf8"),
ParseVersion => t!("error.parse-version"),
IncorrectDisk => t!("error.incorrect-disk"),
// Nginx => t!("error.nginx"),
Dependency => t!("error.dependency"),
ParseS9pk => t!("error.parse-s9pk"),
ParseUrl => t!("error.parse-url"),
DiskNotAvailable => t!("error.disk-not-available"),
BlockDevice => t!("error.block-device"),
InvalidOnionAddress => t!("error.invalid-onion-address"),
Pack => t!("error.pack"),
ValidateS9pk => t!("error.validate-s9pk"),
DiskCorrupted => t!("error.disk-corrupted"), // Remove
Tor => t!("error.tor"),
ConfigGen => t!("error.config-gen"),
ParseNumber => t!("error.parse-number"),
Database => t!("error.database"),
InvalidId => t!("error.invalid-id"),
InvalidSignature => t!("error.invalid-signature"),
Backup => t!("error.backup"),
Restore => t!("error.restore"),
Authorization => t!("error.authorization"),
AutoConfigure => t!("error.auto-configure"),
Action => t!("error.action"),
RateLimited => t!("error.rate-limited"),
InvalidRequest => t!("error.invalid-request"),
MigrationFailed => t!("error.migration-failed"),
Uninitialized => t!("error.uninitialized"),
ParseNetAddress => t!("error.parse-net-address"),
ParseSshKey => t!("error.parse-ssh-key"),
SoundError => t!("error.sound-error"),
ParseTimestamp => t!("error.parse-timestamp"),
ParseSysInfo => t!("error.parse-sys-info"),
Wifi => t!("error.wifi"),
Journald => t!("error.journald"),
DiskManagement => t!("error.disk-management"),
OpenSsl => t!("error.openssl"),
PasswordHashGeneration => t!("error.password-hash-generation"),
DiagnosticMode => t!("error.diagnostic-mode"),
ParseDbField => t!("error.parse-db-field"),
Duplicate => t!("error.duplicate"),
MultipleErrors => t!("error.multiple-errors"),
Incoherent => t!("error.incoherent"),
InvalidBackupTargetId => t!("error.invalid-backup-target-id"),
ProductKeyMismatch => t!("error.product-key-mismatch"),
LanPortConflict => t!("error.lan-port-conflict"),
Javascript => t!("error.javascript"),
Pem => t!("error.pem"),
TLSInit => t!("error.tls-init"),
Ascii => t!("error.ascii"),
MissingHeader => t!("error.missing-header"),
Grub => t!("error.grub"),
Systemd => t!("error.systemd"),
OpenSsh => t!("error.openssh"),
Zram => t!("error.zram"),
Lshw => t!("error.lshw"),
CpuSettings => t!("error.cpu-settings"),
Firmware => t!("error.firmware"),
Timeout => t!("error.timeout"),
Lxc => t!("error.lxc"),
Cancelled => t!("error.cancelled"),
Git => t!("error.git"),
DBus => t!("error.dbus"),
InstallFailed => t!("error.install-failed"),
UpdateFailed => t!("error.update-failed"),
Smtp => t!("error.smtp"),
SetSysInfo => t!("error.set-sys-info"),
}.to_string()
}
}
impl Display for ErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
write!(f, "{}", &self.as_str())
}
}
@@ -199,7 +202,7 @@ pub struct Error {
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {:#}", self.kind.as_str(), self.source)
write!(f, "{}: {:#}", &self.kind.as_str(), self.source)
}
}
impl Debug for Error {
@@ -207,7 +210,7 @@ impl Debug for Error {
write!(
f,
"{}: {:?}",
self.kind.as_str(),
&self.kind.as_str(),
self.debug.as_ref().unwrap_or(&self.source)
)
}

View File

@@ -81,26 +81,28 @@ impl InitPhases {
pub fn new(handle: &FullProgressTracker) -> Self {
Self {
preinit: if Path::new("/media/startos/config/preinit.sh").exists() {
Some(handle.add_phase("Running preinit.sh".into(), Some(5)))
Some(handle.add_phase(t!("init.running-preinit").into(), Some(5)))
} else {
None
},
local_auth: handle.add_phase("Enabling local authentication".into(), Some(1)),
load_database: handle.add_phase("Loading database".into(), Some(5)),
load_ssh_keys: handle.add_phase("Loading SSH Keys".into(), Some(1)),
start_net: handle.add_phase("Starting network controller".into(), Some(1)),
mount_logs: handle.add_phase("Switching logs to write to data drive".into(), Some(1)),
load_ca_cert: handle.add_phase("Loading CA certificate".into(), Some(1)),
load_wifi: handle.add_phase("Loading WiFi configuration".into(), Some(1)),
init_tmp: handle.add_phase("Initializing temporary files".into(), Some(1)),
set_governor: handle.add_phase("Setting CPU performance profile".into(), Some(1)),
sync_clock: handle.add_phase("Synchronizing system clock".into(), Some(10)),
enable_zram: handle.add_phase("Enabling ZRAM".into(), Some(1)),
update_server_info: handle.add_phase("Updating server info".into(), Some(1)),
launch_service_network: handle.add_phase("Launching service intranet".into(), Some(1)),
validate_db: handle.add_phase("Validating database".into(), Some(1)),
local_auth: handle.add_phase(t!("init.enabling-local-auth").into(), Some(1)),
load_database: handle.add_phase(t!("init.loading-database").into(), Some(5)),
load_ssh_keys: handle.add_phase(t!("init.loading-ssh-keys").into(), Some(1)),
start_net: handle.add_phase(t!("init.starting-network-controller").into(), Some(1)),
mount_logs: handle.add_phase(t!("init.switching-logs-to-data-drive").into(), Some(1)),
load_ca_cert: handle.add_phase(t!("init.loading-ca-certificate").into(), Some(1)),
load_wifi: handle.add_phase(t!("init.loading-wifi-configuration").into(), Some(1)),
init_tmp: handle.add_phase(t!("init.initializing-temporary-files").into(), Some(1)),
set_governor: handle
.add_phase(t!("init.setting-cpu-performance-profile").into(), Some(1)),
sync_clock: handle.add_phase(t!("init.synchronizing-system-clock").into(), Some(10)),
enable_zram: handle.add_phase(t!("init.enabling-zram").into(), Some(1)),
update_server_info: handle.add_phase(t!("init.updating-server-info").into(), Some(1)),
launch_service_network: handle
.add_phase(t!("init.launching-service-intranet").into(), Some(1)),
validate_db: handle.add_phase(t!("init.validating-database").into(), Some(1)),
postinit: if Path::new("/media/startos/config/postinit.sh").exists() {
Some(handle.add_phase("Running postinit.sh".into(), Some(5)))
Some(handle.add_phase(t!("init.running-postinit").into(), Some(5)))
} else {
None
},
@@ -127,7 +129,14 @@ pub async fn run_script<P: AsRef<Path>>(path: P, mut progress: PhaseProgressTrac
}
.await
{
tracing::error!("Error Running {}: {}", script.display(), e);
tracing::error!(
"{}",
t!(
"init.error-running-script",
script = script.display(),
error = e
)
);
tracing::debug!("{:?}", e);
}
progress.complete();
@@ -230,6 +239,7 @@ pub async fn init(
.arg("-R")
.arg("+C")
.arg("/var/log/journal")
.env("LANG", "C.UTF-8")
.invoke(ErrorKind::Filesystem)
.await
{
@@ -314,14 +324,17 @@ pub async fn init(
{
Some(governor)
} else {
tracing::warn!("CPU Governor \"{governor}\" Not Available");
tracing::warn!(
"{}",
t!("init.cpu-governor-not-available", governor = governor)
);
None
}
} else {
cpupower::get_preferred_governor().await?
};
if let Some(governor) = governor {
tracing::info!("Setting CPU Governor to \"{governor}\"");
tracing::info!("{}", t!("init.setting-cpu-governor", governor = governor));
cpupower::set_governor(governor).await?;
}
set_governor.complete();
@@ -349,14 +362,14 @@ pub async fn init(
}
}
if !ntp_synced {
tracing::warn!("Timed out waiting for system time to synchronize");
tracing::warn!("{}", t!("init.clock-sync-timeout"));
}
sync_clock.complete();
enable_zram.start();
if server_info.as_zram().de()? {
crate::system::enable_zram().await?;
tracing::info!("Enabled ZRAM");
tracing::info!("{}", t!("init.enabled-zram"));
}
enable_zram.complete();
@@ -404,7 +417,7 @@ pub async fn init(
run_script("/media/startos/config/postinit.sh", progress).await;
}
tracing::info!("System initialized.");
tracing::info!("{}", t!("init.system-initialized"));
Ok(InitResult {
net_ctrl,
@@ -416,30 +429,30 @@ pub fn init_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"logs",
crate::system::logs::<InitContext>().with_about("Disply OS logs"),
crate::system::logs::<InitContext>().with_about("about.display-os-logs"),
)
.subcommand(
"logs",
from_fn_async(crate::logs::cli_logs::<InitContext, Empty>)
.no_display()
.with_about("Display OS logs"),
.with_about("about.display-os-logs"),
)
.subcommand(
"kernel-logs",
crate::system::kernel_logs::<InitContext>().with_about("Display kernel logs"),
crate::system::kernel_logs::<InitContext>().with_about("about.display-kernel-logs"),
)
.subcommand(
"kernel-logs",
from_fn_async(crate::logs::cli_logs::<InitContext, Empty>)
.no_display()
.with_about("Display kernel logs"),
.with_about("about.display-kernel-logs"),
)
.subcommand("subscribe", from_fn_async(init_progress).no_cli())
.subcommand(
"subscribe",
from_fn_async(cli_init_progress)
.no_display()
.with_about("Get initialization progress"),
.with_about("about.get-initialization-progress"),
)
}
@@ -495,7 +508,7 @@ pub async fn init_progress(ctx: InitContext) -> Result<InitProgressRes, Error> {
);
if let Err(e) = ws.close_result(res.map(|_| "complete")).await {
tracing::error!("error closing init progress websocket: {e}");
tracing::error!("{}", t!("init.error-closing-websocket", error = e));
tracing::debug!("{e:?}");
}
},
@@ -526,7 +539,7 @@ pub async fn cli_init_progress(
.await?,
)?;
let mut ws = ctx.ws_continuation(res.guid).await?;
let mut bar = PhasedProgressBar::new("Initializing...");
let mut bar = PhasedProgressBar::new(&t!("init.initializing"));
while let Some(msg) = ws.try_next().await.with_kind(ErrorKind::Network)? {
if let tokio_tungstenite::tungstenite::Message::Text(msg) = msg {
bar.update(&serde_json::from_str(&msg).with_kind(ErrorKind::Deserialization)?);

View File

@@ -283,6 +283,7 @@ pub async fn sideload(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct CancelInstallParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
}
@@ -299,7 +300,9 @@ pub fn cancel_install(
#[derive(Deserialize, Serialize, Parser)]
pub struct QueryPackageParams {
#[arg(help = "help.arg.package-id")]
id: PackageId,
#[arg(help = "help.arg.version-range")]
version: Option<VersionRange>,
}
@@ -357,6 +360,7 @@ impl FromArgMatches for CliInstallParams {
#[derive(Deserialize, Serialize, Parser, TS)]
#[ts(export)]
pub struct InstalledVersionParams {
#[arg(help = "help.arg.package-id")]
id: PackageId,
}
@@ -516,11 +520,12 @@ pub async fn cli_install(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct UninstallParams {
#[arg(help = "help.arg.package-id")]
id: PackageId,
#[arg(long, help = "Do not delete the service data")]
#[arg(long, help = "help.arg.soft-uninstall")]
#[serde(default)]
soft: bool,
#[arg(long, help = "Ignore errors in service uninit script")]
#[arg(long, help = "help.arg.force-uninstall")]
#[serde(default)]
force: bool,
}

View File

@@ -1,5 +1,7 @@
use const_format::formatcp;
rust_i18n::i18n!("locales", fallback = ["en_US"]);
pub const DATA_DIR: &str = "/media/startos/data";
pub const MAIN_DATA: &str = formatcp!("{DATA_DIR}/main");
pub const PACKAGE_DATA: &str = formatcp!("{DATA_DIR}/package-data");
@@ -8,7 +10,7 @@ pub use std::env::consts::ARCH;
lazy_static::lazy_static! {
pub static ref PLATFORM: String = {
if let Ok(platform) = std::fs::read_to_string("/usr/lib/startos/PLATFORM.txt") {
platform
platform.trim().to_string()
} else {
ARCH.to_string()
}
@@ -18,6 +20,17 @@ lazy_static::lazy_static! {
};
}
/// Map a platform string to its architecture
pub fn platform_to_arch(platform: &str) -> &str {
if let Some(arch) = platform.strip_suffix("-nonfree") {
return arch;
}
match platform {
"raspberrypi" | "rockchip64" => "aarch64",
_ => platform,
}
}
mod cap {
#![allow(non_upper_case_globals)]
@@ -97,6 +110,7 @@ use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
#[command(rename_all = "kebab-case")]
#[ts(export)]
pub struct EchoParams {
#[arg(help = "help.arg.echo-message")]
message: String,
}
@@ -122,80 +136,63 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
let mut api = ParentHandler::new()
.subcommand(
"git-info",
from_fn(|_: C| version::git_info()).with_about("Display the githash of StartOS CLI"),
from_fn(|_: C| version::git_info()).with_about("about.display-githash"),
)
.subcommand(
"echo",
from_fn(echo::<RpcContext>)
.with_metadata("authenticated", Value::Bool(false))
.with_about("Echo a message")
.with_about("about.echo-message")
.with_call_remote::<CliContext>(),
)
.subcommand(
"state",
from_fn(|_: RpcContext| Ok::<_, Error>(ApiState::Running))
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the API that is currently serving")
.with_about("about.display-current-api")
.with_call_remote::<CliContext>(),
)
.subcommand(
"state",
from_fn(|_: InitContext| Ok::<_, Error>(ApiState::Initializing))
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the API that is currently serving")
.with_about("about.display-current-api")
.with_call_remote::<CliContext>(),
)
.subcommand(
"state",
from_fn(|_: DiagnosticContext| Ok::<_, Error>(ApiState::Error))
.with_metadata("authenticated", Value::Bool(false))
.with_about("Display the API that is currently serving")
.with_about("about.display-current-api")
.with_call_remote::<CliContext>(),
)
.subcommand(
"server",
server::<C>()
.with_about("Commands related to the server i.e. restart, update, and shutdown"),
)
.subcommand("server", server::<C>().with_about("about.commands-server"))
.subcommand(
"package",
package::<C>().with_about("Commands related to packages"),
package::<C>().with_about("about.commands-packages"),
)
.subcommand(
"net",
net::net_api::<C>().with_about("Network commands related to tor and dhcp"),
net::net_api::<C>().with_about("about.network-commands"),
)
.subcommand(
"auth",
auth::auth::<C, RpcContext>()
.with_about("Commands related to Authentication i.e. login, logout"),
)
.subcommand(
"db",
db::db::<C>().with_about("Commands to interact with the db i.e. dump, put, apply"),
)
.subcommand(
"ssh",
ssh::ssh::<C>()
.with_about("Commands for interacting with ssh keys i.e. add, delete, list"),
auth::auth::<C, RpcContext>().with_about("about.commands-authentication"),
)
.subcommand("db", db::db::<C>().with_about("about.commands-db"))
.subcommand("ssh", ssh::ssh::<C>().with_about("about.commands-ssh-keys"))
.subcommand(
"wifi",
net::wifi::wifi::<C>()
.with_about("Commands related to wifi networks i.e. add, connect, delete"),
)
.subcommand(
"disk",
disk::disk::<C>().with_about("Commands for listing disk info and repairing"),
net::wifi::wifi::<C>().with_about("about.commands-wifi"),
)
.subcommand("disk", disk::disk::<C>().with_about("about.commands-disk"))
.subcommand(
"notification",
notifications::notification::<C>().with_about("Create, delete, or list notifications"),
notifications::notification::<C>().with_about("about.commands-notifications"),
)
.subcommand(
"backup",
backup::backup::<C>()
.with_about("Commands related to backup creation and backup targets"),
backup::backup::<C>().with_about("about.commands-backup"),
)
.subcommand(
"registry",
@@ -206,7 +203,7 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
)
.subcommand(
"registry",
registry::registry_api::<CliContext>().with_about("Commands related to the registry"),
registry::registry_api::<CliContext>().with_about("about.commands-registry"),
)
.subcommand(
"tunnel",
@@ -215,41 +212,46 @@ pub fn main_api<C: Context>() -> ParentHandler<C> {
)
.subcommand(
"tunnel",
tunnel::api::tunnel_api::<CliContext>().with_about("Commands related to StartTunnel"),
)
.subcommand(
"s9pk",
s9pk::rpc::s9pk().with_about("Commands for interacting with s9pk files"),
tunnel::api::tunnel_api::<CliContext>().with_about("about.commands-tunnel"),
)
.subcommand("s9pk", s9pk::rpc::s9pk().with_about("about.commands-s9pk"))
.subcommand(
"util",
util::rpc::util::<C>().with_about("Command for calculating the blake3 hash of a file"),
util::rpc::util::<C>().with_about("about.command-blake3-hash"),
)
.subcommand(
"init-key",
from_fn_async(developer::init)
.no_display()
.with_about("Create developer key if it doesn't exist"),
.with_about("about.create-developer-key"),
)
.subcommand(
"pubkey",
from_fn_blocking(developer::pubkey)
.with_about("Get public key for developer private key"),
from_fn_blocking(developer::pubkey).with_about("about.get-developer-pubkey"),
)
.subcommand(
"diagnostic",
diagnostic::diagnostic::<C>()
.with_about("Commands to display logs, restart the server, etc"),
diagnostic::diagnostic::<C>().with_about("about.commands-diagnostic"),
)
.subcommand("init", init::init_api::<C>())
.subcommand("setup", setup::setup::<C>())
.subcommand(
"install",
os_install::install::<C>()
.with_about("Commands to list disk info, install StartOS, and reboot"),
"init",
init::init_api::<C>().with_about("about.commands-init"),
)
.subcommand(
"setup",
setup::setup::<C>().with_about("about.commands-setup"),
);
if &*PLATFORM != "raspberrypi" {
api = api.subcommand("kiosk", kiosk::<C>());
api = api.subcommand("kiosk", kiosk::<C>().with_about("about.commands-kiosk"));
}
#[cfg(target_os = "linux")]
{
api = api.subcommand(
"flash-os",
from_fn_async(os_install::cli_install_os)
.no_display()
.with_about("about.flash-startos"),
);
}
api
}
@@ -263,29 +265,32 @@ pub fn server<C: Context>() -> ParentHandler<C> {
.with_custom_display_fn(|handle, result| {
system::display_time(handle.params, result)
})
.with_about("Display current time and server uptime")
.with_call_remote::<CliContext>()
.with_about("about.display-time-uptime")
.with_call_remote::<CliContext>(),
)
.subcommand(
"experimental",
system::experimental::<C>()
.with_about("Commands related to configuring experimental options such as zram and cpu governor"),
system::experimental::<C>().with_about("about.commands-experimental"),
)
.subcommand(
"logs",
system::logs::<RpcContext>().with_about("Display OS logs"),
system::logs::<RpcContext>().with_about("about.display-os-logs"),
)
.subcommand(
"logs",
from_fn_async(logs::cli_logs::<RpcContext, Empty>).no_display().with_about("Display OS logs"),
from_fn_async(logs::cli_logs::<RpcContext, Empty>)
.no_display()
.with_about("about.display-os-logs"),
)
.subcommand(
"kernel-logs",
system::kernel_logs::<RpcContext>().with_about("Display Kernel logs"),
system::kernel_logs::<RpcContext>().with_about("about.display-kernel-logs"),
)
.subcommand(
"kernel-logs",
from_fn_async(logs::cli_logs::<RpcContext, Empty>).no_display().with_about("Display Kernel logs"),
from_fn_async(logs::cli_logs::<RpcContext, Empty>)
.no_display()
.with_about("about.display-kernel-logs"),
)
.subcommand(
"metrics",
@@ -293,35 +298,31 @@ pub fn server<C: Context>() -> ParentHandler<C> {
.root_handler(
from_fn_async(system::metrics)
.with_display_serializable()
.with_about("Display information about the server i.e. temperature, RAM, CPU, and disk usage")
.with_call_remote::<CliContext>()
)
.subcommand(
"follow",
from_fn_async(system::metrics_follow)
.no_cli()
.with_about("about.display-server-metrics")
.with_call_remote::<CliContext>(),
)
.subcommand("follow", from_fn_async(system::metrics_follow).no_cli()),
)
.subcommand(
"shutdown",
from_fn_async(shutdown::shutdown)
.no_display()
.with_about("Shutdown the server")
.with_call_remote::<CliContext>()
.with_about("about.shutdown-server")
.with_call_remote::<CliContext>(),
)
.subcommand(
"restart",
from_fn_async(shutdown::restart)
.no_display()
.with_about("Restart the server")
.with_call_remote::<CliContext>()
.with_about("about.restart-server")
.with_call_remote::<CliContext>(),
)
.subcommand(
"rebuild",
from_fn_async(shutdown::rebuild)
.no_display()
.with_about("Teardown and rebuild service containers")
.with_call_remote::<CliContext>()
.with_about("about.teardown-rebuild-containers")
.with_call_remote::<CliContext>(),
)
.subcommand(
"update",
@@ -331,7 +332,9 @@ pub fn server<C: Context>() -> ParentHandler<C> {
)
.subcommand(
"update",
from_fn_async(update::cli_update_system).no_display().with_about("Check a given registry for StartOS updates and update if available"),
from_fn_async(update::cli_update_system)
.no_display()
.with_about("about.check-update-startos"),
)
.subcommand(
"update-firmware",
@@ -346,37 +349,55 @@ pub fn server<C: Context>() -> ParentHandler<C> {
.with_custom_display_fn(|_handle, result| {
Ok(firmware::display_firmware_update_result(result))
})
.with_about("Update the mainboard's firmware to the latest firmware available in this version of StartOS if available. Note: This command does not reach out to the Internet")
.with_call_remote::<CliContext>()
.with_about("about.update-firmware")
.with_call_remote::<CliContext>(),
)
.subcommand(
"set-smtp",
from_fn_async(system::set_system_smtp)
.no_display()
.with_about("Set system smtp server and credentials")
.with_call_remote::<CliContext>()
.with_about("about.set-smtp")
.with_call_remote::<CliContext>(),
)
.subcommand(
"test-smtp",
"test-smtp",
from_fn_async(system::test_smtp)
.no_display()
.with_about("Send test email using provided smtp server and credentials")
.with_call_remote::<CliContext>()
.with_about("about.test-smtp")
.with_call_remote::<CliContext>(),
)
.subcommand(
"clear-smtp",
from_fn_async(system::clear_system_smtp)
.no_display()
.with_about("Remove system smtp server and credentials")
.with_call_remote::<CliContext>()
).subcommand("host", net::host::server_host_api::<C>().with_about("Commands for modifying the host for the system ui"))
.with_about("about.clear-smtp")
.with_call_remote::<CliContext>(),
)
.subcommand(
"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> {
ParentHandler::new()
.subcommand(
"action",
action::action_api::<C>().with_about("Commands to get action input or run an action"),
action::action_api::<C>().with_about("about.commands-action"),
)
.subcommand(
"install",
@@ -394,13 +415,13 @@ pub fn package<C: Context>() -> ParentHandler<C> {
"install",
from_fn_async_local(install::cli_install)
.no_display()
.with_about("Install a package from a marketplace or via sideloading"),
.with_about("about.install-package"),
)
.subcommand(
"cancel-install",
from_fn(install::cancel_install)
.no_display()
.with_about("Cancel an install of a package")
.with_about("about.cancel-install-package")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -408,21 +429,21 @@ pub fn package<C: Context>() -> ParentHandler<C> {
from_fn_async(install::uninstall)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Remove a package")
.with_about("about.remove-package")
.with_call_remote::<CliContext>(),
)
.subcommand(
"list",
from_fn_async(install::list)
.with_display_serializable()
.with_about("List installed packages")
.with_about("about.list-installed-packages")
.with_call_remote::<CliContext>(),
)
.subcommand(
"installed-version",
from_fn_async(install::installed_version)
.with_display_serializable()
.with_about("Display installed version for a PackageId")
.with_about("about.display-installed-version")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -430,7 +451,7 @@ pub fn package<C: Context>() -> ParentHandler<C> {
from_fn_async(control::start)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Start a service")
.with_about("about.start-service")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -438,7 +459,7 @@ pub fn package<C: Context>() -> ParentHandler<C> {
from_fn_async(control::stop)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Stop a service")
.with_about("about.stop-service")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -446,7 +467,7 @@ pub fn package<C: Context>() -> ParentHandler<C> {
from_fn_async(control::restart)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Restart a service")
.with_about("about.restart-service")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -454,7 +475,7 @@ pub fn package<C: Context>() -> ParentHandler<C> {
from_fn_async(service::rebuild)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Rebuild service container")
.with_about("about.rebuild-service-container")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -494,35 +515,34 @@ pub fn package<C: Context>() -> ParentHandler<C> {
table.print_tty(false)?;
Ok(())
})
.with_about("List information related to the lxc containers i.e. CPU, Memory, Disk")
.with_about("about.list-lxc-container-info")
.with_call_remote::<CliContext>(),
)
.subcommand("logs", logs::package_logs())
.subcommand(
"logs",
logs::package_logs().with_about("Display package logs"),
logs::package_logs().with_about("about.display-package-logs"),
)
.subcommand(
"logs",
from_fn_async(logs::cli_logs::<RpcContext, logs::PackageIdParams>)
.no_display()
.with_about("Display package logs"),
.with_about("about.display-package-logs"),
)
.subcommand(
"backup",
backup::package_backup::<C>()
.with_about("Commands for restoring package(s) from backup"),
backup::package_backup::<C>().with_about("about.commands-restore-backup"),
)
.subcommand(
"attach",
from_fn_async(service::attach)
.with_metadata("get_session", Value::Bool(true))
.with_about("Execute commands within a service container")
.with_about("about.execute-commands-container")
.no_cli(),
)
.subcommand("attach", from_fn_async(service::cli_attach).no_display())
.subcommand(
"host",
net::host::host_api::<C>().with_about("Manage network hosts for a package"),
net::host::host_api::<C>().with_about("about.manage-network-hosts-package"),
)
}

View File

@@ -232,6 +232,7 @@ pub const SYSTEM_UNIT: &str = "startd";
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct PackageIdParams {
#[arg(help = "help.arg.package-id")]
id: PackageId,
}
@@ -327,14 +328,14 @@ pub struct LogsParams<Extra: FromArgMatches + Args = Empty> {
#[command(flatten)]
#[serde(flatten)]
extra: Extra,
#[arg(short = 'l', long = "limit")]
#[arg(short = 'l', long = "limit", help = "help.arg.log-limit")]
limit: Option<usize>,
#[arg(short = 'c', long = "cursor", conflicts_with = "follow")]
#[arg(short = 'c', long = "cursor", conflicts_with = "follow", help = "help.arg.log-cursor")]
cursor: Option<String>,
#[arg(short = 'b', long = "boot")]
#[arg(short = 'b', long = "boot", help = "help.arg.log-boot")]
#[serde(default)]
boot: Option<BootIdentifier>,
#[arg(short = 'B', long = "before", conflicts_with = "follow")]
#[arg(short = 'B', long = "before", conflicts_with = "follow", help = "help.arg.log-before")]
#[serde(default)]
before: bool,
}
@@ -346,7 +347,7 @@ pub struct CliLogsParams<Extra: FromArgMatches + Args = Empty> {
#[command(flatten)]
#[serde(flatten)]
rpc_params: LogsParams<Extra>,
#[arg(short = 'f', long = "follow")]
#[arg(short = 'f', long = "follow", help = "help.arg.log-follow")]
#[serde(default)]
follow: bool,
}
@@ -554,7 +555,7 @@ pub async fn journalctl(
let mut child = follow_cmd.stdout(Stdio::piped()).spawn()?;
let out =
BufReader::new(child.stdout.take().ok_or_else(|| {
Error::new(eyre!("No stdout available"), crate::ErrorKind::Journald)
Error::new(eyre!("{}", t!("logs.no-stdout-available")), crate::ErrorKind::Journald)
})?);
let journalctl_entries = LinesStream::new(out.lines());
@@ -700,7 +701,7 @@ pub async fn follow_logs<Context: AsRef<RpcContinuations>>(
RpcContinuation::ws(
move |socket| async move {
if let Err(e) = ws_handler(first_entry, stream, socket).await {
tracing::error!("Error in log stream: {}", e);
tracing::error!("{}", t!("logs.error-in-log-stream", error = e.to_string()));
}
},
Duration::from_secs(30),

View File

@@ -141,7 +141,7 @@ impl LxcManager {
> 0
{
return Err(Error::new(
eyre!("rootfs is not empty, refusing to delete"),
eyre!("{}", t!("lxc.mod.rootfs-not-empty")),
ErrorKind::InvalidRequest,
));
}
@@ -249,6 +249,7 @@ impl LxcContainer {
.arg("-R")
.arg("+C")
.arg(&log_mount_point)
.env("LANG", "C.UTF-8")
.invoke(ErrorKind::Filesystem)
.await
{
@@ -381,7 +382,7 @@ impl LxcContainer {
}
if start.elapsed() > CONTAINER_DHCP_TIMEOUT {
return Err(Error::new(
eyre!("Timed out waiting for container to acquire DHCP lease"),
eyre!("{}", t!("lxc.mod.dhcp-timeout")),
ErrorKind::Timeout,
));
}
@@ -407,9 +408,12 @@ impl LxcContainer {
if !output.status.success() {
return Err(Error::new(
eyre!(
"Command failed with exit code: {:?} \n Message: {:?}",
output.status.code(),
String::from_utf8(output.stderr)
"{}",
t!(
"lxc.mod.command-failed",
code = format!("{:?}", output.status.code()),
message = format!("{:?}", String::from_utf8(output.stderr))
)
),
ErrorKind::Docker,
));
@@ -437,7 +441,7 @@ impl LxcContainer {
> 0
{
return Err(Error::new(
eyre!("rootfs is not empty, refusing to delete"),
eyre!("{}", t!("lxc.mod.rootfs-not-empty")),
ErrorKind::InvalidRequest,
));
}
@@ -473,13 +477,19 @@ impl LxcContainer {
.await
);
return Err(Error::new(
eyre!("timed out waiting for socket"),
eyre!("{}", t!("lxc.mod.socket-timeout")),
ErrorKind::Timeout,
));
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
tracing::info!("Connected to socket in {:?}", started.elapsed());
tracing::info!(
"{}",
t!(
"lxc.mod.connected-to-socket",
elapsed = format!("{:?}", started.elapsed())
)
);
Ok(UnixRpcClient::new(sock_path))
}
@@ -569,8 +579,11 @@ impl Drop for LxcContainer {
fn drop(&mut self) {
if !self.exited {
tracing::warn!(
"Container {} was ungracefully dropped. Cleaning up dangling containers...",
&**self.guid
"{}",
t!(
"lxc.mod.container-ungracefully-dropped",
container = &**self.guid
)
);
let rootfs = self.rootfs.take();
let guid = std::mem::take(&mut self.guid);
@@ -589,16 +602,25 @@ impl Drop for LxcContainer {
}
.await
{
tracing::error!("Error reading logs from crashed container: {e}");
tracing::error!(
"{}",
t!("lxc.mod.error-reading-crashed-logs", error = e.to_string())
);
tracing::debug!("{e:?}")
}
rootfs.unmount(true).await.log_err();
drop(guid);
if let Err(e) = manager.gc().await {
tracing::error!("Error cleaning up dangling LXC containers: {e}");
tracing::error!(
"{}",
t!(
"lxc.mod.error-cleaning-up-containers",
error = e.to_string()
)
);
tracing::debug!("{e:?}")
} else {
tracing::info!("Successfully cleaned up dangling LXC containers");
tracing::info!("{}", t!("lxc.mod.cleaned-up-containers"));
}
});
}

View File

@@ -11,11 +11,6 @@ fn main() {
"$CARGO_MANIFEST_DIR/../web/dist/static/setup-wizard"
))
.ok();
startos::net::static_server::INSTALL_WIZARD_CELL
.set(include_dir::include_dir!(
"$CARGO_MANIFEST_DIR/../web/dist/static/install-wizard"
))
.ok();
#[cfg(not(feature = "beta"))]
startos::db::model::public::DB_UI_SEED_CELL
.set(include_str!(concat!(

View File

@@ -40,7 +40,7 @@ impl LocalAuthContext for RpcContext {
}
fn unauthorized() -> Error {
Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization)
Error::new(eyre!("{}", t!("middleware.auth.unauthorized")), crate::ErrorKind::Authorization)
}
async fn check_from_header<C: LocalAuthContext>(header: Option<&HeaderValue>) -> Result<(), Error> {

View File

@@ -146,7 +146,7 @@ impl HashSessionToken {
}
}
Err(Error::new(
eyre!("UNAUTHORIZED"),
eyre!("{}", t!("middleware.auth.unauthorized")),
crate::ErrorKind::Authorization,
))
}
@@ -221,7 +221,7 @@ impl ValidSessionToken {
}
}
Err(Error::new(
eyre!("UNAUTHORIZED"),
eyre!("{}", t!("middleware.auth.unauthorized")),
crate::ErrorKind::Authorization,
))
}
@@ -244,7 +244,7 @@ impl ValidSessionToken {
C::access_sessions(db)
.as_idx_mut(session_hash)
.ok_or_else(|| {
Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization)
Error::new(eyre!("{}", t!("middleware.auth.unauthorized")), crate::ErrorKind::Authorization)
})?
.mutate(|s| {
s.last_active = Utc::now();
@@ -305,7 +305,7 @@ impl<C: SessionAuthContext> Middleware<C> for SessionAuth {
self.rate_limiter.mutate(|(count, time)| {
if time.elapsed() < Duration::from_secs(20) && *count >= 3 {
Err(Error::new(
eyre!("Please limit login attempts to 3 per 20 seconds."),
eyre!("{}", t!("middleware.auth.rate-limited-login")),
crate::ErrorKind::RateLimited,
))
} else {

View File

@@ -90,7 +90,7 @@ impl SignatureAuthContext for RpcContext {
}
Err(Error::new(
eyre!("Key is not authorized"),
eyre!("{}", t!("middleware.auth.key-not-authorized")),
ErrorKind::IncorrectPassword,
))
}
@@ -141,7 +141,7 @@ impl SignatureAuth {
let mut cache = self.nonce_cache.lock().await;
if cache.values().any(|n| *n == nonce) {
return Err(Error::new(
eyre!("replay attack detected"),
eyre!("{}", t!("middleware.auth.replay-attack-detected")),
ErrorKind::Authorization,
));
}
@@ -226,7 +226,7 @@ impl<C: SignatureAuthContext> Middleware<C> for SignatureAuth {
context.sig_context().await.into_iter().fold(
Err(Error::new(
eyre!("no valid signature context available to verify"),
eyre!("{}", t!("middleware.auth.no-valid-sig-context")),
ErrorKind::Authorization,
)),
|acc, x| {
@@ -249,7 +249,7 @@ impl<C: SignatureAuthContext> Middleware<C> for SignatureAuth {
.unwrap_or_else(|e| e.duration().as_secs() as i64 * -1);
if (now - commitment.timestamp).abs() > 30 {
return Err(Error::new(
eyre!("timestamp not within 30s of now"),
eyre!("{}", t!("middleware.auth.timestamp-not-within-30s")),
ErrorKind::InvalidSignature,
));
}
@@ -347,6 +347,6 @@ pub async fn call_remote<Ctx: SigningContext + AsRef<Client>>(
.with_kind(ErrorKind::Deserialization)?
.result
}
_ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()),
_ => Err(Error::new(eyre!("{}", t!("middleware.auth.unknown-content-type")), ErrorKind::Network).into()),
}
}

View File

@@ -2,6 +2,7 @@ use axum::response::Response;
use http::HeaderValue;
use http::header::InvalidHeaderValue;
use rpc_toolkit::{Middleware, RpcRequest, RpcResponse};
use rust_i18n::t;
use serde::Deserialize;
use crate::context::RpcContext;
@@ -46,7 +47,7 @@ impl Middleware<RpcContext> for SyncDb {
}
.await
{
tracing::error!("error writing X-Patch-Sequence header: {e}");
tracing::error!("{}", t!("middleware.db.error-writing-patch-sequence-header", error = e));
tracing::debug!("{e:?}");
}
}

View File

@@ -395,7 +395,7 @@ pub fn acme_api<C: Context>() -> ParentHandler<C> {
from_fn_async(init)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Setup ACME certificate acquisition")
.with_about("about.setup-acme-certificate-acquisition")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -403,7 +403,7 @@ pub fn acme_api<C: Context>() -> ParentHandler<C> {
from_fn_async(remove)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Remove ACME certificate acquisition configuration")
.with_about("about.remove-acme-certificate-acquisition-configuration")
.with_call_remote::<CliContext>(),
)
}
@@ -463,9 +463,9 @@ impl ValueParserFactory for AcmeProvider {
#[derive(Deserialize, Serialize, Parser)]
pub struct InitAcmeParams {
#[arg(long)]
#[arg(long, help = "help.arg.acme-provider")]
pub provider: AcmeProvider,
#[arg(long)]
#[arg(long, help = "help.arg.acme-contact")]
pub contact: Vec<String>,
}
@@ -488,7 +488,7 @@ pub async fn init(
#[derive(Deserialize, Serialize, Parser)]
pub struct RemoveAcmeParams {
#[arg(long)]
#[arg(long, help = "help.arg.acme-provider")]
pub provider: AcmeProvider,
}

View File

@@ -54,13 +54,13 @@ pub fn dns_api<C: Context>() -> ParentHandler<C> {
Ok(())
})
.with_about("Test the DNS configuration for a domain"),
.with_about("about.test-dns-configuration-for-domain"),
)
.subcommand(
"set-static",
from_fn_async(set_static_dns)
.no_display()
.with_about("Set static DNS servers")
.with_about("about.set-static-dns-servers")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -88,13 +88,14 @@ pub fn dns_api<C: Context>() -> ParentHandler<C> {
Ok(())
})
.with_about("Dump address resolution table")
.with_about("about.dump-address-resolution-table")
.with_call_remote::<CliContext>(),
)
}
#[derive(Deserialize, Serialize, Parser)]
pub struct QueryDnsParams {
#[arg(help = "help.arg.fqdn")]
pub fqdn: InternedString,
}
@@ -134,6 +135,7 @@ pub fn query_dns<C: Context>(
#[derive(Deserialize, Serialize, Parser)]
pub struct SetStaticDnsParams {
#[arg(help = "help.arg.dns-servers")]
pub servers: Option<Vec<String>>,
}
@@ -292,7 +294,7 @@ impl Resolver {
.await
.map_err(|_| {
Error::new(
eyre!("timed out waiting to update dns catalog"),
eyre!("{}", t!("net.dns.timeout-updating-catalog")),
ErrorKind::Timeout,
)
})?;
@@ -348,7 +350,13 @@ impl Resolver {
}) {
return Some(res);
} else {
tracing::warn!("Could not determine source interface of {src}");
tracing::warn!(
"{}",
t!(
"net.dns.could-not-determine-source-interface",
src = src.to_string()
)
);
}
}
if STARTOS.zone_of(name) || EMBASSY.zone_of(name) {
@@ -473,7 +481,10 @@ impl RequestHandler for Resolver {
Ok(Some(a)) => return a,
Ok(None) => (),
Err(e) => {
tracing::error!("Error resolving internal DNS: {e}");
tracing::error!(
"{}",
t!("net.dns.error-resolving-internal", error = e.to_string())
);
tracing::debug!("{e:?}");
let mut header = Header::response_from_request(request.header());
header.set_recursion_available(true);
@@ -557,7 +568,7 @@ impl DnsController {
})
} else {
Err(Error::new(
eyre!("DNS Server Thread has exited"),
eyre!("{}", t!("net.dns.server-thread-exited")),
crate::ErrorKind::Network,
))
}
@@ -577,7 +588,7 @@ impl DnsController {
})
} else {
Err(Error::new(
eyre!("DNS Server Thread has exited"),
eyre!("{}", t!("net.dns.server-thread-exited")),
crate::ErrorKind::Network,
))
}
@@ -598,7 +609,7 @@ impl DnsController {
})
} else {
Err(Error::new(
eyre!("DNS Server Thread has exited"),
eyre!("{}", t!("net.dns.server-thread-exited")),
crate::ErrorKind::Network,
))
}
@@ -624,7 +635,7 @@ impl DnsController {
})
} else {
Err(Error::new(
eyre!("DNS Server Thread has exited"),
eyre!("{}", t!("net.dns.server-thread-exited")),
crate::ErrorKind::Network,
))
}

View File

@@ -34,7 +34,7 @@ impl AvailablePorts {
pub fn alloc(&mut self) -> Result<u16, Error> {
self.0.request_id().ok_or_else(|| {
Error::new(
eyre!("No more dynamic ports available!"),
eyre!("{}", t!("net.forward.no-dynamic-ports-available")),
ErrorKind::Network,
)
})
@@ -240,7 +240,7 @@ impl PortForwardController {
}
.await
{
tracing::error!("error initializing PortForwardController: {e:#}");
tracing::error!("{}", t!("net.forward.error-initializing-controller", error = format!("{e:#}")));
tracing::debug!("{e:?}");
tokio::time::sleep(Duration::from_secs(5)).await;
}
@@ -400,7 +400,7 @@ impl InterfaceForwardEntry {
) -> Result<Arc<()>, Error> {
if external != self.external {
return Err(Error::new(
eyre!("Mismatched external port in InterfaceForwardEntry"),
eyre!("{}", t!("net.forward.mismatched-external-port")),
ErrorKind::InvalidRequest,
));
}
@@ -477,7 +477,7 @@ impl InterfaceForwardState {
fn err_has_exited<T>(_: T) -> Error {
Error::new(
eyre!("PortForwardController thread has exited"),
eyre!("{}", t!("net.forward.controller-thread-exited")),
ErrorKind::Unknown,
)
}

View File

@@ -95,7 +95,7 @@ pub fn gateway_api<C: Context>() -> ParentHandler<C> {
Ok(())
})
.with_about("Show gateways StartOS can listen on")
.with_about("about.show-gateways-startos-can-listen-on")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -103,7 +103,7 @@ pub fn gateway_api<C: Context>() -> ParentHandler<C> {
from_fn_async(set_public)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Indicate whether this gateway has inbound access from the WAN")
.with_about("about.indicate-gateway-inbound-access-from-wan")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -111,10 +111,7 @@ pub fn gateway_api<C: Context>() -> ParentHandler<C> {
from_fn_async(unset_public)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about(concat!(
"Allow this gateway to infer whether it has",
" inbound access from the WAN based on its IPv4 address"
))
.with_about("about.allow-gateway-infer-inbound-access-from-wan")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -122,7 +119,7 @@ pub fn gateway_api<C: Context>() -> ParentHandler<C> {
from_fn_async(forget_iface)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Forget a disconnected gateway")
.with_about("about.forget-disconnected-gateway")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -130,7 +127,7 @@ pub fn gateway_api<C: Context>() -> ParentHandler<C> {
from_fn_async(set_name)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("Rename a gateway")
.with_about("about.rename-gateway")
.with_call_remote::<CliContext>(),
)
}
@@ -143,7 +140,9 @@ async fn list_interfaces(
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
struct NetworkInterfaceSetPublicParams {
#[arg(help = "help.arg.gateway-id")]
gateway: GatewayId,
#[arg(help = "help.arg.is-public")]
public: Option<bool>,
}
@@ -159,6 +158,7 @@ async fn set_public(
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
struct UnsetPublicParams {
#[arg(help = "help.arg.gateway-id")]
gateway: GatewayId,
}
@@ -174,6 +174,7 @@ async fn unset_public(
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
struct ForgetGatewayParams {
#[arg(help = "help.arg.gateway-id")]
gateway: GatewayId,
}
@@ -186,7 +187,9 @@ async fn forget_iface(
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
struct RenameGatewayParams {
#[arg(help = "help.arg.gateway-id")]
id: GatewayId,
#[arg(help = "help.arg.gateway-name")]
name: InternedString,
}
@@ -464,7 +467,8 @@ async fn watcher(
ensure_code!(
!devices.is_empty(),
ErrorKind::Network,
"NetworkManager returned no devices. Trying again..."
"{}",
t!("net.gateway.no-devices-returned")
);
let mut ifaces = BTreeSet::new();
let mut jobs = Vec::new();
@@ -731,7 +735,8 @@ async fn watch_ip(
Ok(a) => a,
Err(e) => {
tracing::error!(
"Failed to determine WAN IP for {iface}: {e}"
"{}",
t!("net.gateway.failed-to-determine-wan-ip", iface = iface.to_string(), error = e.to_string())
);
tracing::debug!("{e:?}");
None
@@ -1021,7 +1026,13 @@ impl NetworkInterfaceController {
info
}
Err(e) => {
tracing::error!("Error loading network interface info: {e}");
tracing::error!(
"{}",
t!(
"net.gateway.error-loading-interface-info",
error = e.to_string()
)
);
tracing::debug!("{e:?}");
OrdMap::new()
}
@@ -1050,7 +1061,10 @@ impl NetworkInterfaceController {
}
.await
{
tracing::error!("Error syncing ip info to db: {e}");
tracing::error!(
"{}",
t!("net.gateway.error-syncing-ip-info", error = e.to_string())
);
tracing::debug!("{e:?}");
}
@@ -1060,7 +1074,10 @@ impl NetworkInterfaceController {
}
.await;
if let Err(e) = res {
tracing::error!("Error syncing ip info to db: {e}");
tracing::error!(
"{}",
t!("net.gateway.error-syncing-ip-info", error = e.to_string())
);
tracing::debug!("{e:?}");
}
})
@@ -1121,7 +1138,7 @@ impl NetworkInterfaceController {
.map_or(false, |i| i.ip_info.is_some())
{
err = Some(Error::new(
eyre!("Cannot forget currently connected interface"),
eyre!("{}", t!("net.gateway.cannot-forget-connected-interface")),
ErrorKind::InvalidRequest,
));
return false;
@@ -1167,7 +1184,7 @@ impl NetworkInterfaceController {
if &*ac == "/" {
return Err(Error::new(
eyre!("Cannot delete device without active connection"),
eyre!("{}", t!("net.gateway.cannot-delete-without-connection")),
ErrorKind::InvalidRequest,
));
}

View File

@@ -120,7 +120,7 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
.with_metadata("sync_db", Value::Bool(true))
.with_inherited(|_, a| a)
.no_display()
.with_about("Add a public domain to this host")
.with_about("about.add-public-domain-to-host")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -129,7 +129,7 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
.with_metadata("sync_db", Value::Bool(true))
.with_inherited(|_, a| a)
.no_display()
.with_about("Remove a public domain from this host")
.with_about("about.remove-public-domain-from-host")
.with_call_remote::<CliContext>(),
)
.with_inherited(|_, a| a),
@@ -143,7 +143,7 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
.with_metadata("sync_db", Value::Bool(true))
.with_inherited(|_, a| a)
.no_display()
.with_about("Add a private domain to this host")
.with_about("about.add-private-domain-to-host")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -152,7 +152,7 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
.with_metadata("sync_db", Value::Bool(true))
.with_inherited(|_, a| a)
.no_display()
.with_about("Remove a private domain from this host")
.with_about("about.remove-private-domain-from-host")
.with_call_remote::<CliContext>(),
)
.with_inherited(|_, a| a),
@@ -168,7 +168,7 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
.with_metadata("sync_db", Value::Bool(true))
.with_inherited(|_, a| a)
.no_display()
.with_about("Add an address to this host")
.with_about("about.add-address-to-host")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -177,7 +177,7 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
.with_metadata("sync_db", Value::Bool(true))
.with_inherited(|_, a| a)
.no_display()
.with_about("Remove an address from this host")
.with_about("about.remove-address-from-host")
.with_call_remote::<CliContext>(),
)
.with_inherited(Kind::inheritance),
@@ -230,16 +230,18 @@ pub fn address_api<C: Context, Kind: HostApiKind>()
Ok(())
})
.with_about("List addresses for this host")
.with_about("about.list-addresses-for-host")
.with_call_remote::<CliContext>(),
)
}
#[derive(Deserialize, Serialize, Parser)]
pub struct AddPublicDomainParams {
#[arg(help = "help.arg.fqdn")]
pub fqdn: InternedString,
#[arg(long)]
#[arg(long, help = "help.arg.acme-provider")]
pub acme: Option<AcmeProvider>,
#[arg(help = "help.arg.gateway-id")]
pub gateway: GatewayId,
}
@@ -284,6 +286,7 @@ pub async fn add_public_domain<Kind: HostApiKind>(
#[derive(Deserialize, Serialize, Parser)]
pub struct RemoveDomainParams {
#[arg(help = "help.arg.fqdn")]
pub fqdn: InternedString,
}
@@ -307,6 +310,7 @@ pub async fn remove_public_domain<Kind: HostApiKind>(
#[derive(Deserialize, Serialize, Parser)]
pub struct AddPrivateDomainParams {
#[arg(help = "help.arg.fqdn")]
pub fqdn: InternedString,
}
@@ -349,6 +353,7 @@ pub async fn remove_private_domain<Kind: HostApiKind>(
#[derive(Deserialize, Serialize, Parser)]
pub struct OnionParams {
#[arg(help = "help.arg.onion-address")]
pub onion: String,
}

View File

@@ -209,7 +209,7 @@ pub fn binding<C: Context, Kind: HostApiKind>()
Ok(())
})
.with_about("List bindinges for this host")
.with_about("about.list-bindings-for-host")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -218,7 +218,7 @@ pub fn binding<C: Context, Kind: HostApiKind>()
.with_metadata("sync_db", Value::Bool(true))
.with_inherited(Kind::inheritance)
.no_display()
.with_about("Set whether this gateway should be enabled for this binding")
.with_about("about.set-gateway-enabled-for-binding")
.with_call_remote::<CliContext>(),
)
}
@@ -237,9 +237,11 @@ pub async fn list_bindings<Kind: HostApiKind>(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct BindingGatewaySetEnabledParams {
#[arg(help = "help.arg.internal-port")]
internal_port: u16,
#[arg(help = "help.arg.gateway-id")]
gateway: GatewayId,
#[arg(long)]
#[arg(long, help = "help.arg.binding-enabled")]
enabled: Option<bool>,
}

View File

@@ -166,11 +166,13 @@ impl Model<Host> {
#[derive(Deserialize, Serialize, Parser)]
pub struct RequiresPackageId {
#[arg(help = "help.arg.package-id")]
package: PackageId,
}
#[derive(Deserialize, Serialize, Parser)]
pub struct RequiresHostId {
#[arg(help = "help.arg.host-id")]
host: HostId,
}
@@ -243,7 +245,7 @@ pub fn host_api<C: Context>() -> ParentHandler<C, RequiresPackageId> {
}
Ok(())
})
.with_about("List host IDs available for this service"),
.with_about("about.list-host-ids-for-service"),
)
.subcommand(
"address",

View File

@@ -23,32 +23,29 @@ pub mod wifi;
pub fn net_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"tor",
tor::tor_api::<C>().with_about("Tor commands such as list-services, logs, and reset"),
)
.subcommand("tor", tor::tor_api::<C>().with_about("about.tor-commands"))
.subcommand(
"acme",
acme::acme_api::<C>().with_about("Setup automatic clearnet certificate acquisition"),
acme::acme_api::<C>().with_about("about.setup-acme-certificate"),
)
.subcommand(
"dns",
dns::dns_api::<C>().with_about("Manage and query DNS"),
dns::dns_api::<C>().with_about("about.manage-query-dns"),
)
.subcommand(
"forward",
forward::forward_api::<C>().with_about("Manage port forwards"),
forward::forward_api::<C>().with_about("about.manage-port-forwards"),
)
.subcommand(
"gateway",
gateway::gateway_api::<C>().with_about("View and edit gateway configurations"),
gateway::gateway_api::<C>().with_about("about.view-edit-gateway-configs"),
)
.subcommand(
"tunnel",
tunnel::tunnel_api::<C>().with_about("Manage tunnels"),
tunnel::tunnel_api::<C>().with_about("about.manage-tunnels"),
)
.subcommand(
"vhost",
vhost::vhost_api::<C>().with_about("Manage ssl virtual host proxy"),
vhost::vhost_api::<C>().with_about("about.manage-ssl-vhost-proxy"),
)
}

View File

@@ -170,7 +170,7 @@ impl FullchainCertData {
]
.into_iter()
.min()
.ok_or_else(|| Error::new(eyre!("unreachable"), ErrorKind::Unknown))
.ok_or_else(|| Error::new(eyre!("{}", t!("net.ssl.unreachable")), ErrorKind::Unknown))
}
}

View File

@@ -30,7 +30,7 @@ use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, BufReader};
use tokio_util::io::ReaderStream;
use url::Url;
use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext};
use crate::context::{DiagnosticContext, InitContext, RpcContext, SetupContext};
use crate::hostname::Hostname;
use crate::middleware::auth::Auth;
use crate::middleware::auth::session::ValidSessionToken;
@@ -178,20 +178,6 @@ impl UiContext for SetupContext {
}
}
pub static INSTALL_WIZARD_CELL: OnceLock<Dir<'static>> = OnceLock::new();
impl UiContext for InstallContext {
fn ui_dir() -> &'static Dir<'static> {
INSTALL_WIZARD_CELL.get().unwrap_or(&EMPTY_DIR)
}
fn api() -> ParentHandler<Self> {
main_api()
}
fn middleware(server: Server<Self>) -> HttpServer<Self> {
server.middleware(Cors::new())
}
}
pub fn rpc_router<C: Context + Clone + AsRef<RpcContinuations>>(
ctx: C,
server: HttpServer<C>,

View File

@@ -107,7 +107,10 @@ impl TorSecretKey {
Ok(Self(
tor_llcrypto::pk::ed25519::ExpandedKeypair::from_secret_key_bytes(bytes)
.ok_or_else(|| {
Error::new(eyre!("invalid ed25519 expanded secret key"), ErrorKind::Tor)
Error::new(
eyre!("{}", t!("net.tor.invalid-ed25519-key")),
ErrorKind::Tor,
)
})?
.into(),
))
@@ -226,19 +229,19 @@ pub fn tor_api<C: Context>() -> ParentHandler<C> {
from_fn_async(list_services)
.with_display_serializable()
.with_custom_display_fn(|handle, result| display_services(handle.params, result))
.with_about("Display Tor V3 Onion Addresses")
.with_about("about.display-tor-v3-onion-addresses")
.with_call_remote::<CliContext>(),
)
.subcommand(
"reset",
from_fn_async(reset)
.no_display()
.with_about("Reset Tor daemon")
.with_about("about.reset-tor-daemon")
.with_call_remote::<CliContext>(),
)
.subcommand(
"key",
key::<C>().with_about("Manage the onion service key store"),
key::<C>().with_about("about.manage-onion-service-key-store"),
)
}
@@ -247,13 +250,13 @@ pub fn key<C: Context>() -> ParentHandler<C> {
.subcommand(
"generate",
from_fn_async(generate_key)
.with_about("Generate an onion service key and add it to the key store")
.with_about("about.generate-onion-service-key-add-to-store")
.with_call_remote::<CliContext>(),
)
.subcommand(
"add",
from_fn_async(add_key)
.with_about("Add an onion service key to the key store")
.with_about("about.add-onion-service-key-to-store")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -265,7 +268,7 @@ pub fn key<C: Context>() -> ParentHandler<C> {
}
Ok(())
})
.with_about("List onion services with keys in the key store")
.with_about("about.list-onion-services-with-keys-in-store")
.with_call_remote::<CliContext>(),
)
}
@@ -286,6 +289,7 @@ pub async fn generate_key(ctx: RpcContext) -> Result<OnionAddress, Error> {
#[derive(Deserialize, Serialize, Parser)]
pub struct AddKeyParams {
#[arg(help = "help.arg.onion-secret-key")]
pub key: Base64<[u8; 64]>,
}
@@ -320,7 +324,7 @@ pub async fn list_keys(ctx: RpcContext) -> Result<BTreeSet<OnionAddress>, Error>
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct ResetParams {
#[arg(name = "wipe-state", short = 'w', long = "wipe-state")]
#[arg(name = "wipe-state", short = 'w', long = "wipe-state", help = "help.arg.wipe-tor-state")]
wipe_state: bool,
}
@@ -447,9 +451,13 @@ impl TorController {
if prev_inst.elapsed() > BOOTSTRAP_PROGRESS_TIMEOUT {
return Err(Error::new(
eyre!(
"Bootstrap has not made progress for {}",
crate::util::serde::Duration::from(
BOOTSTRAP_PROGRESS_TIMEOUT
"{}",
t!(
"net.tor.bootstrap-no-progress",
duration = crate::util::serde::Duration::from(
BOOTSTRAP_PROGRESS_TIMEOUT
)
.to_string()
)
),
ErrorKind::Tor,
@@ -466,7 +474,10 @@ impl TorController {
res = bootstrap_fut => res,
res = failure_fut => res,
} {
tracing::error!("Tor Bootstrap Error: {e}");
tracing::error!(
"{}",
t!("net.tor.bootstrap-error", error = e.to_string())
);
tracing::debug!("{e:?}");
} else {
bootstrapper_client.send_modify(|_| ());
@@ -516,7 +527,13 @@ impl TorController {
}
.await
{
tracing::error!("Tor Health Error: {e}");
tracing::error!(
"{}",
t!(
"net.tor.health-error",
error = e.to_string()
)
);
tracing::debug!("{e:?}");
}
});
@@ -529,7 +546,10 @@ impl TorController {
}
}
Err(Error::new(
eyre!("status event stream ended"),
eyre!(
"{}",
t!("net.tor.status-stream-ended")
),
ErrorKind::Tor,
))
})
@@ -560,13 +580,19 @@ impl TorController {
}
.await
{
tracing::error!("Tor Client Health Error: {e}");
tracing::error!(
"{}",
t!("net.tor.client-health-error", error = e.to_string())
);
tracing::debug!("{e:?}");
}
}
tracing::error!(
"Client failed health check {} times, recycling",
HEALTH_CHECK_FAILURE_ALLOWANCE
"{}",
t!(
"net.tor.health-check-failed-recycling",
count = HEALTH_CHECK_FAILURE_ALLOWANCE
)
);
}
@@ -574,7 +600,10 @@ impl TorController {
})
.await
{
tracing::error!("Tor Bootstrapper Error: {e}");
tracing::error!(
"{}",
t!("net.tor.bootstrapper-error", error = e.to_string())
);
tracing::debug!("{e:?}");
}
if let Err::<(), Error>(e) = async {
@@ -592,7 +621,10 @@ impl TorController {
}
.await
{
tracing::error!("Tor Client Creation Error: {e}");
tracing::error!(
"{}",
t!("net.tor.client-creation-error", error = e.to_string())
);
tracing::debug!("{e:?}");
}
}
@@ -684,7 +716,10 @@ impl TorController {
.await
.with_kind(ErrorKind::Network)?;
if let Err(e) = socket2::SockRef::from(&tcp_stream).set_keepalive(true) {
tracing::error!("Failed to set tcp keepalive: {e}");
tracing::error!(
"{}",
t!("net.tor.failed-to-set-tcp-keepalive", error = e.to_string())
);
tracing::debug!("{e:?}");
}
Ok(Box::new(tcp_stream))
@@ -812,7 +847,7 @@ impl OnionService {
.await
.with_kind(ErrorKind::Network)?;
if let Err(e) = socket2::SockRef::from(&outgoing).set_keepalive(true) {
tracing::error!("Failed to set tcp keepalive: {e}");
tracing::error!("{}", t!("net.tor.failed-to-set-tcp-keepalive", error = e.to_string()));
tracing::debug!("{e:?}");
}
let mut incoming = req
@@ -852,7 +887,7 @@ impl OnionService {
}
.await
{
tracing::error!("Tor Client Error: {e}");
tracing::error!("{}", t!("net.tor.client-error", error = e.to_string()));
tracing::debug!("{e:?}");
}
}

View File

@@ -249,26 +249,26 @@ pub fn tor_api<C: Context>() -> ParentHandler<C> {
from_fn_async(list_services)
.with_display_serializable()
.with_custom_display_fn(|handle, result| display_services(handle.params, result))
.with_about("Display Tor V3 Onion Addresses")
.with_about("about.display-tor-v3-onion-addresses")
.with_call_remote::<CliContext>(),
)
.subcommand("logs", logs().with_about("Display Tor logs"))
.subcommand("logs", logs().with_about("about.display-tor-logs"))
.subcommand(
"logs",
from_fn_async(crate::logs::cli_logs::<RpcContext, Empty>)
.no_display()
.with_about("Display Tor logs"),
.with_about("about.display-tor-logs"),
)
.subcommand(
"reset",
from_fn_async(reset)
.no_display()
.with_about("Reset Tor daemon")
.with_about("about.reset-tor-daemon")
.with_call_remote::<CliContext>(),
)
.subcommand(
"key",
key::<C>().with_about("Manage the onion service key store"),
key::<C>().with_about("about.manage-onion-service-key-store"),
)
}
@@ -277,13 +277,13 @@ pub fn key<C: Context>() -> ParentHandler<C> {
.subcommand(
"generate",
from_fn_async(generate_key)
.with_about("Generate an onion service key and add it to the key store")
.with_about("about.generate-onion-service-key-add-to-store")
.with_call_remote::<CliContext>(),
)
.subcommand(
"add",
from_fn_async(add_key)
.with_about("Add an onion service key to the key store")
.with_about("about.add-onion-service-key-to-store")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -295,7 +295,7 @@ pub fn key<C: Context>() -> ParentHandler<C> {
}
Ok(())
})
.with_about("List onion services with keys in the key store")
.with_about("about.list-onion-services-with-keys-in-store")
.with_call_remote::<CliContext>(),
)
}
@@ -316,6 +316,7 @@ pub async fn generate_key(ctx: RpcContext) -> Result<OnionAddress, Error> {
#[derive(Deserialize, Serialize, Parser)]
pub struct AddKeyParams {
#[arg(help = "help.arg.onion-secret-key")]
pub key: Base64<[u8; 64]>,
}
@@ -350,8 +351,9 @@ pub async fn list_keys(ctx: RpcContext) -> Result<BTreeSet<OnionAddress>, Error>
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct ResetParams {
#[arg(name = "wipe-state", short = 'w', long = "wipe-state")]
#[arg(name = "wipe-state", short = 'w', long = "wipe-state", help = "help.arg.wipe-tor-state")]
wipe_state: bool,
#[arg(help = "help.arg.reset-reason")]
reason: String,
}

View File

@@ -19,14 +19,14 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
.subcommand(
"add",
from_fn_async(add_tunnel)
.with_about("Add a new tunnel")
.with_about("about.add-new-tunnel")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove_tunnel)
.no_display()
.with_about("Remove a tunnel")
.with_about("about.remove-tunnel")
.with_call_remote::<CliContext>(),
)
}
@@ -34,8 +34,11 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)]
pub struct AddTunnelParams {
#[arg(help = "help.arg.tunnel-name")]
name: InternedString,
#[arg(help = "help.arg.wireguard-config")]
config: String,
#[arg(help = "help.arg.is-public")]
public: bool,
}
@@ -123,6 +126,7 @@ pub async fn add_tunnel(
#[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)]
#[ts(export)]
pub struct RemoveTunnelParams {
#[arg(help = "help.arg.gateway-id")]
id: GatewayId,
}
pub async fn remove_tunnel(

View File

@@ -30,7 +30,7 @@ type WifiManager = Arc<RwLock<Option<WpaCli>>>;
// Ok(wifi_manager)
// } else {
// Err(Error::new(
// color_eyre::eyre::eyre!("No WiFi interface available"),
// color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")),
// ErrorKind::Wifi,
// ))
// }
@@ -42,28 +42,28 @@ pub fn wifi<C: Context>() -> ParentHandler<C> {
"set-enabled",
from_fn_async(set_enabled)
.no_display()
.with_about("Enable or disable wifi")
.with_about("about.enable-disable-wifi")
.with_call_remote::<CliContext>(),
)
.subcommand(
"add",
from_fn_async(add)
.no_display()
.with_about("Add wifi ssid and password")
.with_about("about.add-wifi-ssid-password")
.with_call_remote::<CliContext>(),
)
.subcommand(
"connect",
from_fn_async(connect)
.no_display()
.with_about("Connect to wifi network")
.with_about("about.connect-wifi-network")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove)
.no_display()
.with_about("Remove a wifi network")
.with_about("about.remove-wifi-network")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -71,16 +71,16 @@ pub fn wifi<C: Context>() -> ParentHandler<C> {
from_fn_async(get)
.with_display_serializable()
.with_custom_display_fn(|handle, result| display_wifi_info(handle.params, result))
.with_about("List wifi info")
.with_about("about.list-wifi-info")
.with_call_remote::<CliContext>(),
)
.subcommand(
"country",
country::<C>().with_about("Command to set country"),
country::<C>().with_about("about.command-set-country"),
)
.subcommand(
"available",
available::<C>().with_about("Command to list available wifi networks"),
available::<C>().with_about("about.command-list-available-wifi"),
)
}
@@ -88,6 +88,7 @@ pub fn wifi<C: Context>() -> ParentHandler<C> {
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct SetWifiEnabledParams {
#[arg(help = "help.arg.wifi-enabled")]
pub enabled: bool,
}
@@ -133,7 +134,7 @@ pub fn available<C: Context>() -> ParentHandler<C> {
from_fn_async(get_available)
.with_display_serializable()
.with_custom_display_fn(|handle, result| display_wifi_list(handle.params, result))
.with_about("List available wifi networks")
.with_about("about.list-available-wifi-networks")
.with_call_remote::<CliContext>(),
)
}
@@ -143,7 +144,7 @@ pub fn country<C: Context>() -> ParentHandler<C> {
"set",
from_fn_async(set_country)
.no_display()
.with_about("Set Country")
.with_about("about.set-country")
.with_call_remote::<CliContext>(),
)
}
@@ -152,7 +153,9 @@ pub fn country<C: Context>() -> ParentHandler<C> {
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct AddParams {
#[arg(help = "help.arg.wifi-ssid")]
ssid: String,
#[arg(help = "help.arg.wifi-password")]
password: String,
}
#[instrument(skip_all)]
@@ -160,13 +163,13 @@ pub async fn add(ctx: RpcContext, AddParams { ssid, password }: AddParams) -> Re
let wifi_manager = ctx.wifi_manager.clone();
if !ssid.is_ascii() {
return Err(Error::new(
color_eyre::eyre::eyre!("SSID may not have special characters"),
color_eyre::eyre::eyre!("{}", t!("net.wifi.ssid-no-special-characters")),
ErrorKind::Wifi,
));
}
if !password.is_ascii() {
return Err(Error::new(
color_eyre::eyre::eyre!("WiFi Password may not have special characters"),
color_eyre::eyre::eyre!("{}", t!("net.wifi.password-no-special-characters")),
ErrorKind::Wifi,
));
}
@@ -176,11 +179,11 @@ pub async fn add(ctx: RpcContext, AddParams { ssid, password }: AddParams) -> Re
ssid: &Ssid,
password: &Psk,
) -> Result<(), Error> {
tracing::info!("Adding new WiFi network: '{}'", ssid.0);
tracing::info!("{}", t!("net.wifi.adding-network", ssid = &ssid.0));
let mut wpa_supplicant = wifi_manager.write_owned().await;
let wpa_supplicant = wpa_supplicant.as_mut().ok_or_else(|| {
Error::new(
color_eyre::eyre::eyre!("No WiFi interface available"),
color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")),
ErrorKind::Wifi,
)
})?;
@@ -195,10 +198,17 @@ pub async fn add(ctx: RpcContext, AddParams { ssid, password }: AddParams) -> Re
)
.await
{
tracing::error!("Failed to add new WiFi network '{}': {}", ssid, err);
tracing::error!(
"{}",
t!(
"net.wifi.failed-to-add-network",
ssid = &ssid,
error = err.to_string()
)
);
tracing::debug!("{:?}", err);
return Err(Error::new(
color_eyre::eyre::eyre!("Failed adding {}", ssid),
color_eyre::eyre::eyre!("{}", t!("net.wifi.failed-adding", ssid = &ssid)),
ErrorKind::Wifi,
));
}
@@ -222,6 +232,7 @@ pub async fn add(ctx: RpcContext, AddParams { ssid, password }: AddParams) -> Re
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct SsidParams {
#[arg(help = "help.arg.wifi-ssid")]
ssid: String,
}
@@ -230,7 +241,7 @@ pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result
let wifi_manager = ctx.wifi_manager.clone();
if !ssid.is_ascii() {
return Err(Error::new(
color_eyre::eyre::eyre!("SSID may not have special characters"),
color_eyre::eyre::eyre!("{}", t!("net.wifi.ssid-no-special-characters")),
ErrorKind::Wifi,
));
}
@@ -242,19 +253,19 @@ pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result
let mut wpa_supplicant = wifi_manager.write_owned().await;
let wpa_supplicant = wpa_supplicant.as_mut().ok_or_else(|| {
Error::new(
color_eyre::eyre::eyre!("No WiFi interface available"),
color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")),
ErrorKind::Wifi,
)
})?;
let current = wpa_supplicant.get_current_network().await?;
let connected = wpa_supplicant.select_network(db.clone(), ssid).await?;
if connected {
tracing::info!("Successfully connected to WiFi: '{}'", ssid.0);
tracing::info!("{}", t!("net.wifi.connected-successfully", ssid = &ssid.0));
} else {
tracing::info!("Failed to connect to WiFi: '{}'", ssid.0);
tracing::info!("{}", t!("net.wifi.connection-failed", ssid = &ssid.0));
match current {
None => {
tracing::info!("No WiFi to revert to!");
tracing::info!("{}", t!("net.wifi.no-wifi-to-revert"));
}
Some(current) => {
wpa_supplicant.select_network(db, &current).await?;
@@ -267,9 +278,16 @@ pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result
if let Err(err) =
connect_procedure(ctx.db.clone(), wifi_manager.clone(), &Ssid(ssid.clone())).await
{
tracing::error!("Failed to connect to WiFi network '{}': {}", &ssid, err);
tracing::error!(
"{}",
t!(
"net.wifi.failed-to-connect",
ssid = &ssid,
error = err.to_string()
)
);
return Err(Error::new(
color_eyre::eyre::eyre!("Can't connect to {}", ssid),
color_eyre::eyre::eyre!("{}", t!("net.wifi.cant-connect", ssid = &ssid)),
ErrorKind::Wifi,
));
}
@@ -297,7 +315,7 @@ pub async fn remove(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result<
let wifi_manager = ctx.wifi_manager.clone();
if !ssid.is_ascii() {
return Err(Error::new(
color_eyre::eyre::eyre!("SSID may not have special characters"),
color_eyre::eyre::eyre!("{}", t!("net.wifi.ssid-no-special-characters")),
ErrorKind::Wifi,
));
}
@@ -305,7 +323,7 @@ pub async fn remove(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result<
let mut wpa_supplicant = wifi_manager.write_owned().await;
let wpa_supplicant = wpa_supplicant.as_mut().ok_or_else(|| {
Error::new(
color_eyre::eyre::eyre!("No WiFi interface available"),
color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")),
ErrorKind::Wifi,
)
})?;
@@ -316,9 +334,7 @@ pub async fn remove(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result<
is_current_being_removed && !interface_connected(&ctx.ethernet_interface).await?;
if is_current_removed_and_no_hardwire {
return Err(Error::new(
color_eyre::eyre::eyre!(
"Forbidden: Deleting this network would make your server unreachable. Either connect to ethernet or connect to a different WiFi network to remedy this."
),
color_eyre::eyre::eyre!("{}", t!("net.wifi.forbidden-delete-would-disconnect")),
ErrorKind::Wifi,
));
}
@@ -463,7 +479,7 @@ pub async fn get(ctx: RpcContext, _: Empty) -> Result<WifiListInfo, Error> {
let wpa_supplicant = wifi_manager.read_owned().await;
let wpa_supplicant = wpa_supplicant.as_ref().ok_or_else(|| {
Error::new(
color_eyre::eyre::eyre!("No WiFi interface available"),
color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")),
ErrorKind::Wifi,
)
})?;
@@ -517,7 +533,7 @@ pub async fn get_available(ctx: RpcContext, _: Empty) -> Result<Vec<WifiListOut>
let wpa_supplicant = wifi_manager.read_owned().await;
let wpa_supplicant = wpa_supplicant.as_ref().ok_or_else(|| {
Error::new(
color_eyre::eyre::eyre!("No WiFi interface available"),
color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")),
ErrorKind::Wifi,
)
})?;
@@ -547,7 +563,7 @@ pub async fn get_available(ctx: RpcContext, _: Empty) -> Result<Vec<WifiListOut>
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct SetCountryParams {
#[arg(value_parser = CountryCodeParser)]
#[arg(value_parser = CountryCodeParser, help = "help.arg.wifi-country-code")]
#[ts(type = "string")]
country: CountryCode,
}
@@ -558,14 +574,14 @@ pub async fn set_country(
let wifi_manager = ctx.wifi_manager.clone();
if !interface_connected(&ctx.ethernet_interface).await? {
return Err(Error::new(
color_eyre::eyre::eyre!("Won't change country without hardwire connection"),
color_eyre::eyre::eyre!("{}", t!("net.wifi.wont-change-country-without-ethernet")),
crate::ErrorKind::Wifi,
));
}
let mut wpa_supplicant = wifi_manager.write_owned().await;
let wpa_supplicant = wpa_supplicant.as_mut().ok_or_else(|| {
Error::new(
color_eyre::eyre::eyre!("No WiFi interface available"),
color_eyre::eyre::eyre!("{}", t!("net.wifi.no-interface-available")),
ErrorKind::Wifi,
)
})?;
@@ -684,7 +700,14 @@ impl WpaCli {
.await
.map(|_| ())
.unwrap_or_else(|e| {
tracing::warn!("Failed to set interface {} for {}", self.interface, ssid.0);
tracing::warn!(
"{}",
t!(
"net.wifi.failed-to-set-interface",
interface = &self.interface,
ssid = &ssid.0
)
);
tracing::debug!("{:?}", e);
});
Command::new("nmcli")
@@ -719,13 +742,13 @@ impl WpaCli {
}
let first_country = r.lines().find(|s| s.contains("country")).ok_or_else(|| {
Error::new(
color_eyre::eyre::eyre!("Could not find a country config lines"),
color_eyre::eyre::eyre!("{}", t!("net.wifi.could-not-find-country-config")),
ErrorKind::Wifi,
)
})?;
let country = &RE.captures(first_country).ok_or_else(|| {
Error::new(
color_eyre::eyre::eyre!("Could not find a country config with regex"),
color_eyre::eyre::eyre!("{}", t!("net.wifi.could-not-parse-country-config")),
ErrorKind::Wifi,
)
})?[1];
@@ -734,7 +757,10 @@ impl WpaCli {
} else {
Ok(Some(CountryCode::for_alpha2(country).map_err(|_| {
Error::new(
color_eyre::eyre::eyre!("Invalid Country Code: {}", country),
color_eyre::eyre::eyre!(
"{}",
t!("net.wifi.invalid-country-code", country = country)
),
ErrorKind::Wifi,
)
})?))
@@ -877,7 +903,7 @@ impl WpaCli {
let m_id = self.check_active_network(ssid).await?;
match m_id {
None => Err(Error::new(
color_eyre::eyre::eyre!("SSID Not Found"),
color_eyre::eyre::eyre!("{}", t!("net.wifi.ssid-not-found")),
ErrorKind::Wifi,
)),
Some(x) => {
@@ -1058,7 +1084,7 @@ pub async fn synchronize_network_manager<P: AsRef<Path>>(
.invoke(ErrorKind::Wifi)
.await?;
if let Some(last_country_code) = wifi.last_region {
tracing::info!("Setting the region");
tracing::info!("{}", t!("net.wifi.setting-region"));
let _ = Command::new("iw")
.arg("reg")
.arg("set")
@@ -1066,7 +1092,7 @@ pub async fn synchronize_network_manager<P: AsRef<Path>>(
.invoke(ErrorKind::Wifi)
.await?;
} else {
tracing::info!("Setting the region fallback");
tracing::info!("{}", t!("net.wifi.setting-region-fallback"));
let _ = Command::new("iw")
.arg("reg")
.arg("set")

View File

@@ -27,49 +27,49 @@ pub fn notification<C: Context>() -> ParentHandler<C> {
"list",
from_fn_async(list)
.with_display_serializable()
.with_about("List notifications")
.with_about("about.list-notifications")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove",
from_fn_async(remove)
.no_display()
.with_about("Remove notification for given ids")
.with_about("about.remove-notification-for-ids")
.with_call_remote::<CliContext>(),
)
.subcommand(
"remove-before",
from_fn_async(remove_before)
.no_display()
.with_about("Remove notifications preceding a given id")
.with_about("about.remove-notifications-before-id")
.with_call_remote::<CliContext>(),
)
.subcommand(
"mark-seen",
from_fn_async(mark_seen)
.no_display()
.with_about("Mark given notifications as seen")
.with_about("about.mark-notifications-seen")
.with_call_remote::<CliContext>(),
)
.subcommand(
"mark-seen-before",
from_fn_async(mark_seen_before)
.no_display()
.with_about("Mark notifications preceding a given id as seen")
.with_about("about.mark-notifications-seen-before-id")
.with_call_remote::<CliContext>(),
)
.subcommand(
"mark-unseen",
from_fn_async(mark_unseen)
.no_display()
.with_about("Mark given notifications as unseen")
.with_about("about.mark-notifications-unseen")
.with_call_remote::<CliContext>(),
)
.subcommand(
"create",
from_fn_async(create)
.no_display()
.with_about("Persist a newly created notification")
.with_about("about.persist-new-notification")
.with_call_remote::<CliContext>(),
)
}
@@ -78,8 +78,10 @@ pub fn notification<C: Context>() -> ParentHandler<C> {
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct ListNotificationParams {
#[arg(help = "help.arg.notification-before-id")]
#[ts(type = "number | null")]
before: Option<u32>,
#[arg(help = "help.arg.notification-limit")]
#[ts(type = "number | null")]
limit: Option<usize>,
}
@@ -141,6 +143,7 @@ pub async fn list(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct ModifyNotificationParams {
#[arg(help = "help.arg.notification-ids")]
#[ts(type = "number[]")]
ids: Vec<u32>,
}
@@ -175,6 +178,7 @@ pub async fn remove(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct ModifyNotificationBeforeParams {
#[arg(help = "help.arg.notification-before-id")]
#[ts(type = "number")]
before: u32,
}
@@ -296,9 +300,13 @@ pub async fn mark_unseen(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct CreateParams {
#[arg(help = "help.arg.package-id")]
package: Option<PackageId>,
#[arg(help = "help.arg.notification-level")]
level: NotificationLevel,
#[arg(help = "help.arg.notification-title")]
title: String,
#[arg(help = "help.arg.notification-message")]
message: String,
}
@@ -346,7 +354,7 @@ pub struct InvalidNotificationLevel(String);
impl From<InvalidNotificationLevel> for crate::Error {
fn from(val: InvalidNotificationLevel) -> Self {
Error::new(
eyre!("Invalid Notification Level: {}", val.0),
eyre!("{}", t!("notifications.invalid-level", level = val.0)),
ErrorKind::ParseDbField,
)
}

View File

@@ -1,138 +1,207 @@
use std::path::Path;
use std::path::{Path, PathBuf};
use gpt::GptConfig;
use gpt::disk::LogicalBlockSize;
use tokio::process::Command;
use crate::disk::OsPartitionInfo;
use crate::disk::util::DiskInfo;
use crate::os_install::partition_for;
use crate::prelude::*;
use crate::util::Invoke;
pub async fn partition(disk: &DiskInfo, overwrite: bool) -> Result<OsPartitionInfo, Error> {
let efi = {
let disk = disk.clone();
tokio::task::spawn_blocking(move || {
let use_efi = Path::new("/sys/firmware/efi").exists();
let mut device = Box::new(
std::fs::File::options()
.read(true)
.write(true)
.open(&disk.logicalname)?,
);
let (mut gpt, guid_part) = if overwrite {
let mbr = gpt::mbr::ProtectiveMBR::with_lb_size(
u32::try_from((disk.capacity / 512) - 1).unwrap_or(0xFF_FF_FF_FF),
);
mbr.overwrite_lba0(&mut device)?;
(
GptConfig::new()
.writable(true)
.logical_block_size(LogicalBlockSize::Lb512)
.create_from_device(device, None)?,
None,
)
} else {
let gpt = GptConfig::new()
.writable(true)
pub async fn partition(
disk_path: &Path,
capacity: u64,
protect: Option<&Path>,
use_efi: bool,
) -> Result<OsPartitionInfo, Error> {
// Guard: cannot protect the whole disk
if let Some(p) = protect {
if p == disk_path {
return Err(Error::new(
eyre!(
"Cannot protect the entire disk {}; must specify a partition",
disk_path.display()
),
crate::ErrorKind::InvalidRequest,
));
}
}
let disk_path = disk_path.to_owned();
let disk_path_clone = disk_path.clone();
let protect = protect.map(|p| p.to_owned());
let (efi, data_part) = tokio::task::spawn_blocking(move || {
let disk_path = disk_path_clone;
let protected_partition_info: Option<(u64, u64, PathBuf)> =
if let Some(ref protect_path) = protect {
let existing_gpt = GptConfig::new()
.writable(false)
.logical_block_size(LogicalBlockSize::Lb512)
.open_from_device(device)?;
let mut guid_part = None;
for (idx, part_info) in disk
.partitions
.open_from_device(Box::new(
std::fs::File::options().read(true).open(&disk_path)?,
))?;
let info = existing_gpt
.partitions()
.iter()
.enumerate()
.map(|(idx, x)| (idx + 1, x))
{
if let Some(entry) = gpt.partitions().get(&(idx as u32)) {
if part_info.guid.is_some() {
if entry.first_lba < if use_efi { 33759266 } else { 33570850 } {
return Err(Error::new(
eyre!("Not enough space before StartOS data"),
crate::ErrorKind::InvalidRequest,
));
}
guid_part = Some(entry.clone());
break;
}
}
.find(|(num, _)| partition_for(&disk_path, **num) == *protect_path)
.map(|(_, p)| (p.first_lba, p.last_lba, protect_path.clone()));
if info.is_none() {
return Err(Error::new(
eyre!(
"Protected partition {} not found in GPT on {}",
protect_path.display(),
disk_path.display()
),
crate::ErrorKind::NotFound,
));
}
(gpt, guid_part)
};
gpt.update_partitions(Default::default())?;
let efi = if use_efi {
gpt.add_partition("efi", 100 * 1024 * 1024, gpt::partition_types::EFI, 0, None)?;
true
info
} else {
gpt.add_partition(
"bios-grub",
8 * 1024 * 1024,
gpt::partition_types::BIOS,
0,
None,
)?;
false
None
};
gpt.add_partition(
"boot",
1024 * 1024 * 1024,
gpt::partition_types::LINUX_FS,
0,
None,
)?;
gpt.add_partition(
"root",
15 * 1024 * 1024 * 1024,
match crate::ARCH {
"x86_64" => gpt::partition_types::LINUX_ROOT_X64,
"aarch64" => gpt::partition_types::LINUX_ROOT_ARM_64,
_ => gpt::partition_types::LINUX_FS,
},
0,
None,
)?;
if overwrite {
gpt.add_partition(
"data",
gpt.find_free_sectors()
.iter()
.map(|(_, size)| *size * u64::from(*gpt.logical_block_size()))
.max()
.ok_or_else(|| {
Error::new(
eyre!("No free space left on device"),
crate::ErrorKind::BlockDevice,
)
})?,
gpt::partition_types::LINUX_LVM,
0,
None,
)?;
} else if let Some(guid_part) = guid_part {
let mut parts = gpt.partitions().clone();
parts.insert(
gpt.find_next_partition_id().ok_or_else(|| {
Error::new(eyre!("Partition table is full"), ErrorKind::DiskManagement)
})?,
guid_part,
);
gpt.update_partitions(parts)?;
let mut device = Box::new(
std::fs::File::options()
.read(true)
.write(true)
.open(&disk_path)?,
);
let mbr = gpt::mbr::ProtectiveMBR::with_lb_size(
u32::try_from((capacity / 512) - 1).unwrap_or(0xFF_FF_FF_FF),
);
mbr.overwrite_lba0(&mut device)?;
let mut gpt = GptConfig::new()
.writable(true)
.logical_block_size(LogicalBlockSize::Lb512)
.create_from_device(device, None)?;
gpt.update_partitions(Default::default())?;
let efi = if use_efi {
gpt.add_partition("efi", 100 * 1024 * 1024, gpt::partition_types::EFI, 0, None)?;
true
} else {
gpt.add_partition(
"bios-grub",
8 * 1024 * 1024,
gpt::partition_types::BIOS,
0,
None,
)?;
false
};
gpt.add_partition(
"boot",
2 * 1024 * 1024 * 1024,
gpt::partition_types::LINUX_FS,
0,
None,
)?;
gpt.add_partition(
"root",
14 * 1024 * 1024 * 1024,
match crate::ARCH {
"x86_64" => gpt::partition_types::LINUX_ROOT_X64,
"aarch64" => gpt::partition_types::LINUX_ROOT_ARM_64,
_ => gpt::partition_types::LINUX_FS,
},
0,
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,
));
}
}
gpt.write()?;
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;
let next_id = gpt.partitions().keys().max().map(|k| k + 1).unwrap_or(1);
gpt.add_partition_at(
"data",
next_id,
first_lba,
length_lba,
gpt::partition_types::LINUX_LVM,
0,
)?;
Some(path)
} else {
gpt.add_partition(
"data",
gpt.find_free_sectors()
.iter()
.map(|(_, size)| *size * u64::from(*gpt.logical_block_size()))
.max()
.ok_or_else(|| {
Error::new(
eyre!("No free space left on device"),
crate::ErrorKind::BlockDevice,
)
})?,
gpt::partition_types::LINUX_LVM,
0,
None,
)?;
gpt.partitions()
.last_key_value()
.map(|(num, _)| partition_for(&disk_path, *num))
};
Ok(efi)
})
gpt.write()?;
Ok::<_, Error>((efi, data_part))
})
.await
.unwrap()?;
// Re-read partition table and wait for udev to create device nodes
Command::new("vgchange")
.arg("-an")
.invoke(crate::ErrorKind::DiskManagement)
.await
.unwrap()?
};
.ok();
Command::new("dmsetup")
.arg("remove_all")
.arg("--force")
.invoke(crate::ErrorKind::DiskManagement)
.await
.ok();
Command::new("blockdev")
.arg("--rereadpt")
.arg(&disk_path)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
Command::new("udevadm")
.arg("settle")
.invoke(crate::ErrorKind::DiskManagement)
.await?;
Ok(OsPartitionInfo {
efi: efi.then(|| partition_for(&disk.logicalname, 1)),
bios: (!efi).then(|| partition_for(&disk.logicalname, 1)),
boot: partition_for(&disk.logicalname, 2),
root: partition_for(&disk.logicalname, 3),
efi: efi.then(|| partition_for(&disk_path, 1)),
bios: (!efi).then(|| partition_for(&disk_path, 1)),
boot: partition_for(&disk_path, 2),
root: partition_for(&disk_path, 3),
data: data_part,
})
}

View File

@@ -1,88 +1,173 @@
use std::path::{Path, PathBuf};
use color_eyre::eyre::eyre;
use mbrman::{CHS, MBR, MBRPartitionEntry};
use tokio::process::Command;
use crate::Error;
use crate::disk::OsPartitionInfo;
use crate::disk::util::DiskInfo;
use crate::os_install::partition_for;
use crate::prelude::*;
use crate::util::Invoke;
pub async fn partition(disk: &DiskInfo, overwrite: bool) -> Result<OsPartitionInfo, Error> {
{
let sectors = (disk.capacity / 512) as u32;
let disk = disk.clone();
tokio::task::spawn_blocking(move || {
let mut file = std::fs::File::options()
.read(true)
.write(true)
.open(&disk.logicalname)?;
let (mut mbr, guid_part) = if overwrite {
(MBR::new_from(&mut file, 512, rand::random())?, None)
} else {
let mut mbr = MBR::read_from(&mut file, 512)?;
let mut guid_part = None;
for (idx, part_info) in disk
.partitions
.iter()
.enumerate()
.map(|(idx, x)| (idx + 1, x))
{
if let Some(entry) = mbr.get_mut(idx) {
if part_info.guid.is_some() {
if entry.starting_lba < 33556480 {
return Err(Error::new(
eyre!("Not enough space before embassy data"),
crate::ErrorKind::InvalidRequest,
));
}
guid_part = Some(std::mem::replace(entry, MBRPartitionEntry::empty()));
pub async fn partition(
disk_path: &Path,
capacity: u64,
protect: Option<&Path>,
) -> Result<OsPartitionInfo, Error> {
// Guard: cannot protect the whole disk
if let Some(p) = protect {
if p == disk_path {
return Err(Error::new(
eyre!(
"Cannot protect the entire disk {}; must specify a partition",
disk_path.display()
),
crate::ErrorKind::InvalidRequest,
));
}
}
let disk_path = disk_path.to_owned();
let disk_path_clone = disk_path.clone();
let protect = protect.map(|p| p.to_owned());
let sectors = (capacity / 512) as u32;
let data_part = tokio::task::spawn_blocking(move || {
let disk_path = disk_path_clone;
// If protecting a partition, read its location from the existing MBR
let protected_partition_info: Option<(u32, u32, PathBuf)> =
if let Some(ref protect_path) = protect {
let mut file = std::fs::File::options().read(true).open(&disk_path)?;
let existing_mbr = MBR::read_from(&mut file, 512)?;
// Find the partition matching the protected path (check partitions 1-4)
let info = (1..=4u32)
.find(|&idx| partition_for(&disk_path, idx) == *protect_path)
.and_then(|idx| {
let entry = &existing_mbr[idx as usize];
if entry.sectors > 0 {
Some((entry.starting_lba, entry.sectors, protect_path.clone()))
} else {
None
}
*entry = MBRPartitionEntry::empty();
}
});
if info.is_none() {
return Err(Error::new(
eyre!(
"Protected partition {} not found in MBR on {}",
protect_path.display(),
disk_path.display()
),
crate::ErrorKind::NotFound,
));
}
(mbr, guid_part)
info
} else {
None
};
mbr[1] = MBRPartitionEntry {
boot: 0x80,
first_chs: CHS::empty(),
sys: 0x0b,
last_chs: CHS::empty(),
starting_lba: 2048,
sectors: 2099200 - 2048,
};
mbr[2] = MBRPartitionEntry {
// MBR partition layout:
// Partition 1 (boot): starts at 2048, ends at 4196352 (sectors: 4194304 = 2GB)
// Partition 2 (root): starts at 4196352, ends at 33556480 (sectors: 29360128 = 14GB)
// OS partitions end at sector 33556480
let os_partitions_end_sector: u32 = 33556480;
// Check if protected partition would be overwritten
if let Some((starting_lba, _, ref path)) = protected_partition_info {
if starting_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(),
starting_lba,
os_partitions_end_sector
),
crate::ErrorKind::DiskManagement,
));
}
}
let mut file = std::fs::File::options()
.read(true)
.write(true)
.open(&disk_path)?;
let mut mbr = MBR::new_from(&mut file, 512, rand::random())?;
mbr[1] = MBRPartitionEntry {
boot: 0x80,
first_chs: CHS::empty(),
sys: 0x0b,
last_chs: CHS::empty(),
starting_lba: 2048,
sectors: 4196352 - 2048,
};
mbr[2] = MBRPartitionEntry {
boot: 0,
first_chs: CHS::empty(),
sys: 0x83,
last_chs: CHS::empty(),
starting_lba: 4196352,
sectors: 33556480 - 4196352,
};
let data_part = if let Some((starting_lba, part_sectors, path)) = protected_partition_info {
// Re-create the data partition entry at the same location
mbr[3] = MBRPartitionEntry {
boot: 0,
first_chs: CHS::empty(),
sys: 0x83,
sys: 0x8e,
last_chs: CHS::empty(),
starting_lba: 2099200,
sectors: 33556480 - 2099200,
starting_lba,
sectors: part_sectors,
};
Some(path)
} else {
mbr[3] = MBRPartitionEntry {
boot: 0,
first_chs: CHS::empty(),
sys: 0x8e,
last_chs: CHS::empty(),
starting_lba: 33556480,
sectors: sectors - 33556480,
};
Some(partition_for(&disk_path, 3))
};
mbr.write_into(&mut file)?;
if overwrite {
mbr[3] = MBRPartitionEntry {
boot: 0,
first_chs: CHS::empty(),
sys: 0x8e,
last_chs: CHS::empty(),
starting_lba: 33556480,
sectors: sectors - 33556480,
}
} else if let Some(guid_part) = guid_part {
mbr[3] = guid_part;
}
mbr.write_into(&mut file)?;
Ok::<_, Error>(data_part)
})
.await
.unwrap()?;
Ok(())
})
// Re-read partition table and wait for udev to create device nodes
Command::new("vgchange")
.arg("-an")
.invoke(crate::ErrorKind::DiskManagement)
.await
.unwrap()?;
}
.ok();
Command::new("dmsetup")
.arg("remove_all")
.arg("--force")
.invoke(crate::ErrorKind::DiskManagement)
.await
.ok();
Command::new("blockdev")
.arg("--rereadpt")
.arg(&disk_path)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
Command::new("udevadm")
.arg("settle")
.invoke(crate::ErrorKind::DiskManagement)
.await?;
Ok(OsPartitionInfo {
efi: None,
bios: None,
boot: partition_for(&disk.logicalname, 1),
root: partition_for(&disk.logicalname, 2),
boot: partition_for(&disk_path, 1),
root: partition_for(&disk_path, 2),
data: data_part,
})
}

View File

@@ -2,13 +2,13 @@ use std::path::{Path, PathBuf};
use clap::Parser;
use color_eyre::eyre::eyre;
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use ts_rs::TS;
use crate::Error;
use crate::context::config::ServerConfig;
use crate::context::{CliContext, InstallContext};
use crate::context::{CliContext, SetupContext};
use crate::disk::OsPartitionInfo;
use crate::disk::mount::filesystem::bind::Bind;
use crate::disk::mount::filesystem::block_dev::BlockDev;
@@ -16,81 +16,31 @@ use crate::disk::mount::filesystem::efivarfs::EfiVarFs;
use crate::disk::mount::filesystem::overlayfs::OverlayFs;
use crate::disk::mount::filesystem::{MountType, ReadWrite};
use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard};
use crate::disk::util::{DiskInfo, PartitionTable};
use crate::net::utils::find_eth_iface;
use crate::disk::util::PartitionTable;
use crate::prelude::*;
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
use crate::setup::SetupInfo;
use crate::util::Invoke;
use crate::util::io::{TmpDir, delete_file, open_file};
use crate::util::io::{TmpDir, delete_file, open_file, write_file_atomic};
use crate::util::serde::IoFormat;
use crate::{ARCH, Error};
mod gpt;
mod mbr;
pub fn install<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand("disk", disk::<C>().with_about("Command to list disk info"))
.subcommand(
"execute",
from_fn_async(execute::<InstallContext>)
.no_display()
.with_about("Install StartOS over existing version")
.with_call_remote::<CliContext>(),
)
.subcommand(
"reboot",
from_fn_async(reboot)
.no_display()
.with_about("Restart the server")
.with_call_remote::<CliContext>(),
)
/// Probe a squashfs image to determine its target architecture
async fn probe_squashfs_arch(squashfs_path: &Path) -> Result<InternedString, Error> {
let output = String::from_utf8(
Command::new("unsquashfs")
.arg("-cat")
.arg(squashfs_path)
.arg("usr/lib/startos/PLATFORM.txt")
.invoke(ErrorKind::ParseSysInfo)
.await?,
)?;
Ok(crate::platform_to_arch(&output.trim()).into())
}
pub fn disk<C: Context>() -> ParentHandler<C> {
ParentHandler::new().subcommand(
"list",
from_fn_async(list)
.no_display()
.with_about("List disk info")
.with_call_remote::<CliContext>(),
)
}
pub async fn list(_: InstallContext) -> Result<Vec<DiskInfo>, Error> {
let skip = match async {
Ok::<_, Error>(
Path::new(
&String::from_utf8(
Command::new("grub-probe-default")
.arg("-t")
.arg("disk")
.arg("/run/live/medium")
.invoke(crate::ErrorKind::Grub)
.await?,
)?
.trim(),
)
.to_owned(),
)
}
.await
{
Ok(a) => Some(a),
Err(e) => {
tracing::error!("Could not determine live usb device: {}", e);
tracing::debug!("{:?}", e);
None
}
};
Ok(crate::disk::util::list(&Default::default())
.await?
.into_iter()
.filter(|i| Some(&*i.logicalname) != skip.as_deref())
.collect())
}
pub fn partition_for(disk: impl AsRef<Path>, idx: usize) -> PathBuf {
pub fn partition_for(disk: impl AsRef<Path>, idx: u32) -> PathBuf {
let disk_path = disk.as_ref();
let (root, leaf) = if let (Some(root), Some(leaf)) = (
disk_path.parent(),
@@ -107,49 +57,90 @@ pub fn partition_for(disk: impl AsRef<Path>, idx: usize) -> PathBuf {
}
}
async fn partition(disk: &mut DiskInfo, overwrite: bool) -> Result<OsPartitionInfo, Error> {
let partition_type = match (overwrite, disk.partition_table) {
async fn partition(
disk_path: &Path,
capacity: u64,
partition_table: Option<PartitionTable>,
protect: Option<&Path>,
use_efi: bool,
) -> Result<OsPartitionInfo, Error> {
let partition_type = match (protect.is_none(), partition_table) {
(true, _) | (_, None) => PartitionTable::Gpt,
(_, Some(t)) => t,
};
disk.partition_table = Some(partition_type);
match partition_type {
PartitionTable::Gpt => gpt::partition(disk, overwrite).await,
PartitionTable::Mbr => mbr::partition(disk, overwrite).await,
PartitionTable::Gpt => gpt::partition(disk_path, capacity, protect, use_efi).await,
PartitionTable::Mbr => mbr::partition(disk_path, capacity, protect).await,
}
}
async fn get_block_device_size(path: impl AsRef<Path>) -> Result<u64, Error> {
let path = path.as_ref();
let device_name = path.file_name().and_then(|s| s.to_str()).ok_or_else(|| {
Error::new(
eyre!("Invalid block device path: {}", path.display()),
ErrorKind::BlockDevice,
)
})?;
let size_path = Path::new("/sys/block").join(device_name).join("size");
let sectors: u64 = tokio::fs::read_to_string(&size_path)
.await
.with_ctx(|_| {
(
ErrorKind::BlockDevice,
format!("reading {}", size_path.display()),
)
})?
.trim()
.parse()
.map_err(|e| {
Error::new(
eyre!("Failed to parse block device size: {}", e),
ErrorKind::BlockDevice,
)
})?;
Ok(sectors * 512)
}
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct ExecuteParams {
logicalname: PathBuf,
#[arg(short = 'o')]
overwrite: bool,
pub struct InstallOsParams {
#[arg(help = "help.arg.os-drive-path")]
os_drive: PathBuf,
#[command(flatten)]
data_drive: Option<DataDrive>,
}
pub async fn execute<C: Context>(
_: C,
ExecuteParams {
logicalname,
mut overwrite,
}: ExecuteParams,
) -> Result<(), Error> {
let mut disk = crate::disk::util::list(&Default::default())
.await?
.into_iter()
.find(|d| &d.logicalname == &logicalname)
.ok_or_else(|| {
Error::new(
eyre!("Unknown disk {}", logicalname.display()),
crate::ErrorKind::DiskManagement,
)
})?;
let eth_iface = find_eth_iface().await?;
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
struct DataDrive {
#[arg(long = "data-drive", help = "help.arg.data-drive-path")]
logicalname: PathBuf,
#[arg(long, help = "help.arg.wipe-drive")]
wipe: bool,
}
overwrite |= disk.guid.is_none() && disk.partitions.iter().all(|p| p.guid.is_none());
pub struct InstallOsResult {
pub part_info: OsPartitionInfo,
pub rootfs: TmpMountGuard,
}
let part_info = partition(&mut disk, overwrite).await?;
pub async fn install_os_to(
squashfs_path: impl AsRef<Path>,
disk_path: impl AsRef<Path>,
capacity: u64,
partition_table: Option<PartitionTable>,
protect: Option<impl AsRef<Path>>,
arch: &str,
use_efi: bool,
) -> Result<InstallOsResult, Error> {
let squashfs_path = squashfs_path.as_ref();
let disk_path = disk_path.as_ref();
let protect = protect.as_ref().map(|p| p.as_ref());
let part_info = partition(disk_path, capacity, partition_table, protect, use_efi).await?;
if let Some(efi) = &part_info.efi {
Command::new("mkfs.vfat")
@@ -173,7 +164,7 @@ pub async fn execute<C: Context>(
.invoke(crate::ErrorKind::DiskManagement)
.await?;
if !overwrite {
if protect.is_some() {
if let Ok(guard) =
TmpMountGuard::mount(&BlockDev::new(part_info.root.clone()), MountType::ReadWrite).await
{
@@ -234,13 +225,13 @@ pub async fn execute<C: Context>(
tokio::fs::create_dir_all(&images_path).await?;
let image_path = images_path
.join(hex::encode(
&MultiCursorFile::from(open_file("/run/live/medium/live/filesystem.squashfs").await?)
&MultiCursorFile::from(open_file(squashfs_path).await?)
.blake3_mmap()
.await?
.as_bytes()[..16],
))
.with_extension("rootfs");
tokio::fs::copy("/run/live/medium/live/filesystem.squashfs", &image_path).await?;
tokio::fs::copy(squashfs_path, &image_path).await?;
// TODO: check hash of fs
let unsquash_target = TmpDir::new().await?;
let bootfs = MountGuard::mount(
@@ -254,7 +245,7 @@ pub async fn execute<C: Context>(
.arg("-f")
.arg("-d")
.arg(&*unsquash_target)
.arg("/run/live/medium/live/filesystem.squashfs")
.arg(squashfs_path)
.arg("boot")
.invoke(crate::ErrorKind::Filesystem)
.await?;
@@ -271,7 +262,6 @@ pub async fn execute<C: Context>(
rootfs.path().join("config/config.yaml"),
IoFormat::Yaml.to_vec(&ServerConfig {
os_partitions: Some(part_info.clone()),
ethernet_interface: Some(eth_iface),
..Default::default()
})?,
)
@@ -357,13 +347,13 @@ pub async fn execute<C: Context>(
let mut install = Command::new("chroot");
install.arg(overlay.path()).arg("grub-install");
if tokio::fs::metadata("/sys/firmware/efi").await.is_err() {
match ARCH {
if !use_efi {
match arch {
"x86_64" => install.arg("--target=i386-pc"),
_ => &mut install,
};
} else {
match ARCH {
match arch {
"x86_64" => install.arg("--target=x86_64-efi"),
"aarch64" => install.arg("--target=arm64-efi"),
"riscv64" => install.arg("--target=riscv64-efi"),
@@ -371,7 +361,7 @@ pub async fn execute<C: Context>(
};
}
install
.arg(&disk.logicalname)
.arg(disk_path)
.invoke(crate::ErrorKind::Grub)
.await?;
@@ -396,15 +386,158 @@ pub async fn execute<C: Context>(
tokio::fs::remove_dir_all(&work).await?;
lower.unmount().await?;
Ok(InstallOsResult { part_info, rootfs })
}
pub async fn install_os(
ctx: SetupContext,
InstallOsParams {
os_drive,
data_drive,
}: InstallOsParams,
) -> Result<SetupInfo, Error> {
let mut disks = crate::disk::util::list(&Default::default()).await?;
let disk = disks
.iter_mut()
.find(|d| &d.logicalname == &os_drive)
.ok_or_else(|| {
Error::new(
eyre!("Unknown disk {}", os_drive.display()),
crate::ErrorKind::DiskManagement,
)
})?;
let protect: Option<PathBuf> = data_drive.as_ref().and_then(|dd| {
if dd.wipe {
return None;
}
if disk.guid.as_ref().map_or(false, |g| {
g.starts_with("EMBASSY_") || g.starts_with("STARTOS_")
}) && disk.logicalname == dd.logicalname
{
return Some(disk.logicalname.clone());
}
disk.partitions
.iter()
.find(|p| {
p.guid.as_ref().map_or(false, |g| {
g.starts_with("EMBASSY_") || g.starts_with("STARTOS_")
})
})
.map(|p| p.logicalname.clone())
});
let use_efi = tokio::fs::metadata("/sys/firmware/efi").await.is_ok();
let InstallOsResult { part_info, rootfs } = install_os_to(
"/run/live/medium/live/filesystem.squashfs",
&disk.logicalname,
disk.capacity,
disk.partition_table,
protect.as_ref(),
crate::ARCH,
use_efi,
)
.await?;
ctx.config
.mutate(|c| c.os_partitions = Some(part_info.clone()));
let mut setup_info = SetupInfo::default();
if let Some(data_drive) = data_drive {
let mut logicalname = &*data_drive.logicalname;
if logicalname == &os_drive {
logicalname = part_info.data.as_deref().ok_or_else(|| {
Error::new(
eyre!("not enough room on OS drive for data"),
ErrorKind::InvalidRequest,
)
})?;
}
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 {
let guid = crate::setup::setup_data_drive(&ctx, logicalname).await?;
setup_info.guid = Some(guid);
}
}
let config = MountGuard::mount(
&Bind::new(rootfs.path().join("config")),
"/media/startos/config",
ReadWrite,
)
.await?;
write_file_atomic(
"/media/startos/config/setup.json",
IoFormat::JsonPretty.to_vec(&setup_info)?,
)
.await?;
ctx.install_rootfs.replace(Some((rootfs, config)));
Ok(setup_info)
}
#[derive(Deserialize, Serialize, Parser)]
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct CliInstallOsParams {
#[arg(help = "help.arg.squashfs-image-path")]
squashfs: PathBuf,
#[arg(help = "help.arg.target-disk")]
disk: PathBuf,
#[arg(long, help = "help.arg.use-efi-boot")]
efi: Option<bool>,
}
pub async fn cli_install_os(
_ctx: CliContext,
CliInstallOsParams {
squashfs,
disk,
efi,
}: CliInstallOsParams,
) -> Result<OsPartitionInfo, Error> {
let capacity = get_block_device_size(&disk).await?;
let partition_table = crate::disk::util::get_partition_table(&disk).await?;
let arch = probe_squashfs_arch(&squashfs).await?;
let use_efi = efi.unwrap_or_else(|| !matches!(partition_table, Some(PartitionTable::Mbr)));
let InstallOsResult { part_info, rootfs } = install_os_to(
&squashfs,
&disk,
capacity,
partition_table,
None::<&str>,
&*arch,
use_efi,
)
.await?;
rootfs.unmount().await?;
Ok(())
}
pub async fn reboot(ctx: InstallContext) -> Result<(), Error> {
Command::new("sync")
.invoke(crate::ErrorKind::Filesystem)
.await?;
ctx.shutdown.send(()).unwrap();
Ok(())
Ok(part_info)
}

View File

@@ -1,6 +1,7 @@
pub use color_eyre::eyre::eyre;
pub use imbl_value::InternedString;
pub use lazy_format::lazy_format;
pub use rust_i18n::t;
pub use tracing::instrument;
pub use crate::db::prelude::*;

View File

@@ -21,7 +21,7 @@ pub fn admin_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand(
"signer",
signers_api::<C>().with_about("Commands to add or list signers"),
signers_api::<C>().with_about("about.commands-add-list-signers"),
)
.subcommand(
"add",
@@ -33,14 +33,14 @@ pub fn admin_api<C: Context>() -> ParentHandler<C> {
"add",
from_fn_async(cli_add_admin)
.no_display()
.with_about("Add admin signer"),
.with_about("about.add-admin-signer"),
)
.subcommand(
"remove",
from_fn_async(remove_admin)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Remove an admin signer")
.with_about("about.remove-admin-signer")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -49,7 +49,7 @@ pub fn admin_api<C: Context>() -> ParentHandler<C> {
.with_metadata("admin", Value::Bool(true))
.with_display_serializable()
.with_custom_display_fn(|handle, result| display_signers(handle.params, result))
.with_about("List admin signers")
.with_about("about.list-admin-signers")
.with_call_remote::<CliContext>(),
)
}
@@ -62,7 +62,7 @@ fn signers_api<C: Context>() -> ParentHandler<C> {
.with_metadata("admin", Value::Bool(true))
.with_display_serializable()
.with_custom_display_fn(|handle, result| display_signers(handle.params, result))
.with_about("List signers")
.with_about("about.list-signers")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -73,13 +73,14 @@ fn signers_api<C: Context>() -> ParentHandler<C> {
)
.subcommand(
"add",
from_fn_async(cli_add_signer).with_about("Add signer"),
from_fn_async(cli_add_signer).with_about("about.add-signer"),
)
.subcommand(
"edit",
from_fn_async(edit_signer)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("about.edit-signer")
.with_call_remote::<CliContext>(),
)
}
@@ -93,7 +94,7 @@ impl Model<BTreeMap<Guid, SignerInfo>> {
.next()
.transpose()?
.map(|(a, _)| a)
.ok_or_else(|| Error::new(eyre!("unknown signer"), ErrorKind::Authorization))
.ok_or_else(|| Error::new(eyre!("{}", t!("registry.admin.unknown-signer")), ErrorKind::Authorization))
}
pub fn get_signer_info(&self, key: &AnyVerifyingKey) -> Result<(Guid, SignerInfo), Error> {
@@ -103,7 +104,7 @@ impl Model<BTreeMap<Guid, SignerInfo>> {
.filter_ok(|(_, s)| s.keys.contains(key))
.next()
.transpose()?
.ok_or_else(|| Error::new(eyre!("unknown signer"), ErrorKind::Authorization))
.ok_or_else(|| Error::new(eyre!("{}", t!("registry.admin.unknown-signer")), ErrorKind::Authorization))
}
pub fn add_signer(&mut self, signer: &SignerInfo) -> Result<Guid, Error> {
@@ -117,9 +118,8 @@ impl Model<BTreeMap<Guid, SignerInfo>> {
{
return Err(Error::new(
eyre!(
"A signer {} ({}) already exists with a matching key",
guid,
s.name
"{}",
t!("registry.admin.signer-already-exists", guid = guid, name = s.name)
),
ErrorKind::InvalidRequest,
));
@@ -206,16 +206,17 @@ pub async fn add_signer(ctx: RegistryContext, signer: SignerInfo) -> Result<Guid
#[command(rename_all = "kebab-case")]
#[ts(export)]
pub struct EditSignerParams {
#[arg(help = "help.arg.signer-id")]
pub id: Guid,
#[arg(short = 'n', long)]
#[arg(short = 'n', long, help = "help.arg.set-signer-name")]
pub set_name: Option<String>,
#[arg(short = 'c', long)]
#[arg(short = 'c', long, help = "help.arg.add-signer-contact")]
pub add_contact: Vec<ContactInfo>,
#[arg(short = 'k', long)]
#[arg(short = 'k', long, help = "help.arg.add-signer-key")]
pub add_key: Vec<AnyVerifyingKey>,
#[arg(short = 'C', long)]
#[arg(short = 'C', long, help = "help.arg.remove-signer-contact")]
pub remove_contact: Vec<ContactInfo>,
#[arg(short = 'K', long)]
#[arg(short = 'K', long, help = "help.arg.remove-signer-key")]
pub remove_key: Vec<AnyVerifyingKey>,
}
@@ -264,12 +265,13 @@ pub async fn edit_signer(
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliAddSignerParams {
#[arg(long = "name", short = 'n')]
#[arg(long = "name", short = 'n', help = "help.arg.signer-name")]
pub name: String,
#[arg(long = "contact", short = 'c')]
#[arg(long = "contact", short = 'c', help = "help.arg.signer-contact")]
pub contact: Vec<ContactInfo>,
#[arg(long = "key")]
#[arg(long = "key", help = "help.arg.signer-key")]
pub keys: Vec<AnyVerifyingKey>,
#[arg(help = "help.arg.database-path")]
pub database: Option<PathBuf>,
}
@@ -339,6 +341,7 @@ pub async fn add_admin(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemoveAdminParams {
#[arg(help = "help.arg.signer-id")]
pub signer: Guid,
}
@@ -360,7 +363,9 @@ pub async fn remove_admin(
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliAddAdminParams {
#[arg(help = "help.arg.signer-id")]
pub signer: Guid,
#[arg(help = "help.arg.database-path")]
pub database: Option<PathBuf>,
}

View File

@@ -49,7 +49,7 @@ impl<Commitment> RegistryAsset<Commitment> {
}
}
Err(Error::new(
eyre!("Failed to load any http url"),
eyre!("{}", t!("registry.asset.failed-to-load-http-url")),
ErrorKind::Network,
))
}
@@ -64,7 +64,7 @@ impl<Commitment> RegistryAsset<Commitment> {
}
}
Err(Error::new(
eyre!("Failed to load any http url"),
eyre!("{}", t!("registry.asset.failed-to-load-http-url")),
ErrorKind::Network,
))
}
@@ -80,7 +80,7 @@ impl<Commitment> RegistryAsset<Commitment> {
}
}
Err(Error::new(
eyre!("Failed to load any http url"),
eyre!("{}", t!("registry.asset.failed-to-load-http-url")),
ErrorKind::Network,
))
}

View File

@@ -42,17 +42,17 @@ const DEFAULT_REGISTRY_LISTEN: SocketAddr =
#[serde(rename_all = "kebab-case")]
#[command(rename_all = "kebab-case")]
pub struct RegistryConfig {
#[arg(short = 'c', long = "config")]
#[arg(short = 'c', long = "config", help = "help.arg.config-file-path")]
pub config: Option<PathBuf>,
#[arg(short = 'l', long = "listen")]
#[arg(short = 'l', long = "listen", help = "help.arg.registry-listen-address")]
pub registry_listen: Option<SocketAddr>,
#[arg(short = 'H', long = "hostname")]
#[arg(short = 'H', long = "hostname", help = "help.arg.registry-hostname")]
pub registry_hostname: Vec<InternedString>,
#[arg(short = 'p', long = "tor-proxy")]
#[arg(short = 'p', long = "tor-proxy", help = "help.arg.tor-proxy-url")]
pub tor_proxy: Option<Url>,
#[arg(short = 'd', long = "datadir")]
#[arg(short = 'd', long = "datadir", help = "help.arg.data-directory")]
pub datadir: Option<PathBuf>,
#[arg(short = 'u', long = "pg-connection-url")]
#[arg(short = 'u', long = "pg-connection-url", help = "help.arg.postgres-connection-url")]
pub pg_connection_url: Option<String>,
}
impl ContextConfig for RegistryConfig {
@@ -124,7 +124,7 @@ impl RegistryContext {
};
if config.registry_hostname.is_empty() {
return Err(Error::new(
eyre!("missing required configuration: registry-hostname"),
eyre!("{}", t!("registry.context.missing-hostname")),
ErrorKind::NotFound,
));
}
@@ -165,6 +165,7 @@ impl Deref for RegistryContext {
#[derive(Debug, Deserialize, Serialize, Parser)]
pub struct RegistryUrlParams {
#[arg(help = "help.arg.registry-url")]
pub registry: Url,
}
@@ -195,7 +196,7 @@ impl CallRemote<RegistryContext> for CliContext {
url
} else {
return Err(
Error::new(eyre!("`--registry` required"), ErrorKind::InvalidRequest).into(),
Error::new(eyre!("{}", t!("registry.context.registry-required")), ErrorKind::InvalidRequest).into(),
);
};
@@ -330,7 +331,7 @@ impl SignatureAuthContext for RegistryContext {
}
}
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
Err(Error::new(eyre!("{}", t!("registry.context.unauthorized")), ErrorKind::Authorization))
}
async fn post_auth_hook(
&self,

View File

@@ -22,7 +22,7 @@ pub fn db_api<C: Context>() -> ParentHandler<C> {
"dump",
from_fn_async(cli_dump)
.with_display_serializable()
.with_about("Filter/query db to display tables and records"),
.with_about("about.filter-query-db"),
)
.subcommand(
"dump",
@@ -34,7 +34,7 @@ pub fn db_api<C: Context>() -> ParentHandler<C> {
"apply",
from_fn_async(cli_apply)
.no_display()
.with_about("Update a db record"),
.with_about("about.update-db-record"),
)
.subcommand(
"apply",
@@ -48,8 +48,9 @@ pub fn db_api<C: Context>() -> ParentHandler<C> {
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct CliDumpParams {
#[arg(long = "pointer", short = 'p')]
#[arg(long = "pointer", short = 'p', help = "help.arg.db-pointer")]
pointer: Option<JsonPointer>,
#[arg(help = "help.arg.database-path")]
path: Option<PathBuf>,
}
@@ -81,7 +82,7 @@ async fn cli_dump(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct DumpParams {
#[arg(long = "pointer", short = 'p')]
#[arg(long = "pointer", short = 'p', help = "help.arg.db-pointer")]
#[ts(type = "string | null")]
pointer: Option<JsonPointer>,
}
@@ -97,7 +98,9 @@ pub async fn dump(ctx: RegistryContext, DumpParams { pointer }: DumpParams) -> R
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct CliApplyParams {
#[arg(help = "help.arg.db-apply-expr")]
expr: String,
#[arg(help = "help.arg.database-path")]
path: Option<PathBuf>,
}
@@ -152,7 +155,9 @@ async fn cli_apply(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct ApplyParams {
#[arg(help = "help.arg.db-apply-expr")]
expr: String,
#[arg(help = "help.arg.database-path")]
path: Option<PathBuf>,
}

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

@@ -21,7 +21,7 @@ pub fn info_api<C: Context>() -> ParentHandler<C, WithIoFormat<Empty>> {
from_fn_async(get_info)
.with_metadata("authenticated", Value::Bool(false))
.with_display_serializable()
.with_about("Display registry name, icon, and package categories")
.with_about("about.display-registry-info")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -29,7 +29,7 @@ pub fn info_api<C: Context>() -> ParentHandler<C, WithIoFormat<Empty>> {
from_fn_async(set_name)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Set the name for the registry")
.with_about("about.set-registry-name")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -42,7 +42,7 @@ pub fn info_api<C: Context>() -> ParentHandler<C, WithIoFormat<Empty>> {
"set-icon",
from_fn_async(cli_set_icon)
.no_display()
.with_about("Set the icon for the registry"),
.with_about("about.set-registry-icon"),
)
}
@@ -69,6 +69,7 @@ pub async fn get_info(ctx: RegistryContext) -> Result<RegistryInfo, Error> {
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct SetNameParams {
#[arg(help = "help.arg.registry-name")]
pub name: String,
}
@@ -104,6 +105,7 @@ pub async fn set_icon(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct CliSetIconParams {
#[arg(help = "help.arg.icon-path")]
pub icon: PathBuf,
}

View File

@@ -76,26 +76,26 @@ pub fn registry_api<C: Context>() -> ParentHandler<C> {
"index",
from_fn_async(get_full_index)
.with_display_serializable()
.with_about("List info including registry name and packages")
.with_about("about.list-registry-info-packages")
.with_call_remote::<CliContext>(),
)
.subcommand("info", info::info_api::<C>())
// set info and categories
.subcommand(
"os",
os::os_api::<C>().with_about("Commands related to OS assets and versions"),
os::os_api::<C>().with_about("about.commands-os-assets-versions"),
)
.subcommand(
"package",
package::package_api::<C>().with_about("Commands to index, add, or get packages"),
package::package_api::<C>().with_about("about.commands-index-add-get-packages"),
)
.subcommand(
"admin",
admin::admin_api::<C>().with_about("Commands to add or list admins or signers"),
admin::admin_api::<C>().with_about("about.commands-add-list-admins-signers"),
)
.subcommand(
"db",
db::db_api::<C>().with_about("Commands to interact with the db i.e. dump and apply"),
db::db_api::<C>().with_about("about.commands-registry-db"),
)
}

View File

@@ -141,7 +141,7 @@ async fn add_asset(
.mutate(|s| {
if s.commitment != commitment {
Err(Error::new(
eyre!("commitment does not match"),
eyre!("{}", t!("registry.os.asset.commitment-mismatch")),
ErrorKind::InvalidSignature,
))
} else {
@@ -154,7 +154,7 @@ async fn add_asset(
})?;
Ok(())
} else {
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
Err(Error::new(eyre!("{}", t!("registry.os.asset.unauthorized")), ErrorKind::Authorization))
}
})
.await
@@ -179,11 +179,13 @@ pub async fn add_squashfs(ctx: RegistryContext, params: AddAssetParams) -> Resul
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliAddAssetParams {
#[arg(short = 'p', long = "platform")]
#[arg(short = 'p', long = "platform", help = "help.arg.platform")]
pub platform: InternedString,
#[arg(short = 'v', long = "version")]
#[arg(short = 'v', long = "version", help = "help.arg.os-version")]
pub version: Version,
#[arg(help = "help.arg.asset-file-path")]
pub file: PathBuf,
#[arg(help = "help.arg.asset-url")]
pub url: Url,
}
@@ -208,7 +210,7 @@ pub async fn cli_add_asset(
Some("squashfs") => "squashfs",
_ => {
return Err(Error::new(
eyre!("Unknown extension"),
eyre!("{}", t!("registry.os.asset.unknown-extension")),
ErrorKind::InvalidRequest,
));
}
@@ -232,7 +234,7 @@ pub async fn cli_add_asset(
let size = file
.size()
.await
.ok_or_else(|| Error::new(eyre!("failed to read file metadata"), ErrorKind::Filesystem))?;
.ok_or_else(|| Error::new(eyre!("{}", t!("registry.os.asset.failed-read-metadata")), ErrorKind::Filesystem))?;
let commitment = Blake3Commitment {
hash: Base64(*blake3.as_bytes()),
size,
@@ -334,7 +336,7 @@ async fn remove_asset(
.remove(&platform)?;
Ok(())
} else {
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
Err(Error::new(eyre!("{}", t!("registry.os.asset.unauthorized")), ErrorKind::Authorization))
}
})
.await

View File

@@ -34,7 +34,7 @@ pub fn get_api<C: Context>() -> ParentHandler<C> {
"iso",
from_fn_async(cli_get_os_asset)
.no_display()
.with_about("Download iso"),
.with_about("about.download-iso"),
)
.subcommand(
"img",
@@ -46,7 +46,7 @@ pub fn get_api<C: Context>() -> ParentHandler<C> {
"img",
from_fn_async(cli_get_os_asset)
.no_display()
.with_about("Download img"),
.with_about("about.download-img"),
)
.subcommand(
"squashfs",
@@ -58,7 +58,7 @@ pub fn get_api<C: Context>() -> ParentHandler<C> {
"squashfs",
from_fn_async(cli_get_os_asset)
.no_display()
.with_about("Download squashfs"),
.with_about("about.download-squashfs"),
)
}
@@ -121,18 +121,20 @@ pub async fn get_squashfs(
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliGetOsAssetParams {
#[arg(help = "help.arg.os-version")]
pub version: Version,
#[arg(help = "help.arg.platform")]
pub platform: InternedString,
#[arg(
long = "download",
short = 'd',
help = "The path of the directory to download to"
help = "help.arg.download-directory"
)]
pub download: Option<PathBuf>,
#[arg(
long = "reverify",
short = 'r',
help = "verify the hash of the file a second time after download"
help = "help.arg.reverify-hash"
)]
pub reverify: bool,
}

View File

@@ -11,7 +11,7 @@ pub fn asset_api<C: Context>() -> ParentHandler<C> {
"add",
from_fn_async(add::cli_add_asset)
.no_display()
.with_about("Add asset to registry"),
.with_about("about.add-asset-registry"),
)
.subcommand("remove", add::remove_api::<C>())
.subcommand("sign", sign::sign_api::<C>())
@@ -19,10 +19,10 @@ pub fn asset_api<C: Context>() -> ParentHandler<C> {
"sign",
from_fn_async(sign::cli_sign_asset)
.no_display()
.with_about("Sign file and add to registry index"),
.with_about("about.sign-file-add-registry"),
)
.subcommand(
"get",
get::get_api::<C>().with_about("Commands to download image, iso, or squashfs files"),
get::get_api::<C>().with_about("about.commands-download-assets"),
)
}

View File

@@ -89,7 +89,7 @@ async fn sign_asset(
.contains(&guid)
{
return Err(Error::new(
eyre!("signer {guid} is not authorized"),
eyre!("{}", t!("registry.os.asset.signer-not-authorized", guid = guid)),
ErrorKind::Authorization,
));
}
@@ -136,10 +136,11 @@ pub async fn sign_squashfs(ctx: RegistryContext, params: SignAssetParams) -> Res
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliSignAssetParams {
#[arg(short = 'p', long = "platform")]
#[arg(short = 'p', long = "platform", help = "help.arg.platform")]
pub platform: InternedString,
#[arg(short = 'v', long = "version")]
#[arg(short = 'v', long = "version", help = "help.arg.os-version")]
pub version: Version,
#[arg(help = "help.arg.asset-file-path")]
pub file: PathBuf,
}
@@ -163,7 +164,7 @@ pub async fn cli_sign_asset(
Some("squashfs") => "squashfs",
_ => {
return Err(Error::new(
eyre!("Unknown extension"),
eyre!("{}", t!("registry.os.asset.unknown-extension")),
ErrorKind::InvalidRequest,
));
}
@@ -186,7 +187,7 @@ pub async fn cli_sign_asset(
let size = file
.size()
.await
.ok_or_else(|| Error::new(eyre!("failed to read file metadata"), ErrorKind::Filesystem))?;
.ok_or_else(|| Error::new(eyre!("{}", t!("registry.os.asset.failed-read-metadata")), ErrorKind::Filesystem))?;
let commitment = Blake3Commitment {
hash: Base64(*blake3.as_bytes()),
size,

View File

@@ -17,16 +17,16 @@ pub fn os_api<C: Context>() -> ParentHandler<C> {
from_fn_async(index::get_os_index)
.with_metadata("authenticated", Value::Bool(false))
.with_display_serializable()
.with_about("List index of OS versions")
.with_about("about.list-os-versions-index")
.with_call_remote::<CliContext>(),
)
.subcommand(
"asset",
asset::asset_api::<C>().with_about("Commands to add, sign, or get registry assets"),
asset::asset_api::<C>().with_about("about.commands-add-sign-get-assets"),
)
.subcommand(
"version",
version::version_api::<C>()
.with_about("Commands to add, remove, or list versions or version signers"),
.with_about("about.commands-add-remove-list-versions"),
)
}

View File

@@ -27,7 +27,7 @@ pub fn version_api<C: Context>() -> ParentHandler<C> {
.with_metadata("admin", Value::Bool(true))
.with_metadata("get_signer", Value::Bool(true))
.no_display()
.with_about("Add OS version")
.with_about("about.add-os-version")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -35,12 +35,12 @@ pub fn version_api<C: Context>() -> ParentHandler<C> {
from_fn_async(remove_version)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Remove OS version")
.with_about("about.remove-os-version")
.with_call_remote::<CliContext>(),
)
.subcommand(
"signer",
signer::signer_api::<C>().with_about("Add, remove, and list version signers"),
signer::signer_api::<C>().with_about("about.add-remove-list-version-signers"),
)
.subcommand(
"get",
@@ -51,7 +51,7 @@ pub fn version_api<C: Context>() -> ParentHandler<C> {
.with_custom_display_fn(|handle, result| {
display_version_info(handle.params, result)
})
.with_about("Get OS versions and related version info")
.with_about("about.get-os-versions-info")
.with_call_remote::<CliContext>(),
)
}
@@ -62,10 +62,14 @@ pub fn version_api<C: Context>() -> ParentHandler<C> {
#[ts(export)]
pub struct AddVersionParams {
#[ts(type = "string")]
#[arg(help = "help.arg.os-version")]
pub version: Version,
#[arg(help = "help.arg.version-headline")]
pub headline: String,
#[arg(help = "help.arg.release-notes")]
pub release_notes: String,
#[ts(type = "string")]
#[arg(help = "help.arg.source-version-range")]
pub source_version: VersionRange,
#[arg(skip)]
#[ts(skip)]
@@ -110,6 +114,7 @@ pub async fn add_version(
#[ts(export)]
pub struct RemoveVersionParams {
#[ts(type = "string")]
#[arg(help = "help.arg.os-version")]
pub version: Version,
}
@@ -135,15 +140,15 @@ pub async fn remove_version(
#[ts(export)]
pub struct GetOsVersionParams {
#[ts(type = "string | null")]
#[arg(long = "src")]
#[arg(long = "src", help = "help.arg.source-version")]
pub source_version: Option<Version>,
#[ts(type = "string | null")]
#[arg(long)]
#[arg(long, help = "help.arg.target-version-range")]
pub target_version: Option<VersionRange>,
#[arg(long = "id")]
#[arg(long = "id", help = "help.arg.server-id")]
server_id: Option<String>,
#[ts(type = "string | null")]
#[arg(long)]
#[arg(long, help = "help.arg.platform")]
platform: Option<InternedString>,
#[ts(skip)]
#[arg(skip)]

View File

@@ -21,7 +21,7 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
from_fn_async(add_version_signer)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Add version signer")
.with_about("about.add-version-signer")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -29,7 +29,7 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
from_fn_async(remove_version_signer)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Remove version signer")
.with_about("about.remove-version-signer")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -38,7 +38,7 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
.with_metadata("authenticated", Value::Bool(false))
.with_display_serializable()
.with_custom_display_fn(|handle, result| display_signers(handle.params, result))
.with_about("List version signers and related signer info")
.with_about("about.list-version-signers")
.with_call_remote::<CliContext>(),
)
}
@@ -95,7 +95,7 @@ pub async fn remove_version_signer(
.mutate(|s| Ok(s.remove(&signer)))?
{
return Err(Error::new(
eyre!("signer {signer} is not authorized to sign for v{version}"),
eyre!("{}", t!("registry.os.version.signer-not-authorized", signer = signer, version = version)),
ErrorKind::NotFound,
));
}

View File

@@ -56,7 +56,7 @@ pub async fn add_package(
let Some(([url], rest)) = urls.split_at_checked(1) else {
return Err(Error::new(
eyre!("must specify at least 1 url"),
eyre!("{}", t!("registry.package.add.must-specify-url")),
ErrorKind::InvalidRequest,
));
};
@@ -112,7 +112,7 @@ pub async fn add_package(
Ok(())
} else {
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
Err(Error::new(eyre!("{}", t!("registry.package.add.unauthorized")), ErrorKind::Authorization))
}
})
.await
@@ -123,10 +123,11 @@ pub async fn add_package(
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliAddPackageParams {
#[arg(help = "help.arg.s9pk-file-path")]
pub file: PathBuf,
#[arg(long)]
#[arg(long, help = "help.arg.package-url")]
pub url: Vec<Url>,
#[arg(long)]
#[arg(long, help = "help.arg.no-verify")]
pub no_verify: bool,
}
@@ -205,9 +206,11 @@ pub async fn cli_add_package(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemovePackageParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
#[arg(help = "help.arg.package-version")]
pub version: VersionString,
#[arg(long)]
#[arg(long, help = "help.arg.signature-hash")]
pub sighash: Option<Base64<[u8; 32]>>,
#[ts(skip)]
#[arg(skip)]
@@ -226,7 +229,7 @@ pub async fn remove_package(
) -> Result<bool, Error> {
let peek = ctx.db.peek().await;
let signer =
signer.ok_or_else(|| Error::new(eyre!("missing signer"), ErrorKind::InvalidRequest))?;
signer.ok_or_else(|| Error::new(eyre!("{}", t!("registry.package.missing-signer")), ErrorKind::InvalidRequest))?;
let signer_guid = peek.as_index().as_signers().get_signer(&signer)?;
let rev = ctx
@@ -267,7 +270,7 @@ pub async fn remove_package(
}
Ok(())
} else {
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
Err(Error::new(eyre!("{}", t!("registry.package.unauthorized")), ErrorKind::Authorization))
}
})
.await;
@@ -342,7 +345,7 @@ pub async fn add_mirror(
Ok(())
} else {
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
Err(Error::new(eyre!("{}", t!("registry.package.add-mirror.unauthorized")), ErrorKind::Authorization))
}
})
.await
@@ -353,8 +356,11 @@ pub async fn add_mirror(
#[command(rename_all = "kebab-case")]
#[serde(rename_all = "camelCase")]
pub struct CliAddMirrorParams {
#[arg(help = "help.arg.s9pk-file-path")]
pub file: PathBuf,
#[arg(help = "help.arg.mirror-url")]
pub url: Url,
#[arg(long, help = "help.arg.no-verify")]
pub no_verify: bool,
}
@@ -432,9 +438,11 @@ pub async fn cli_add_mirror(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemoveMirrorParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
#[arg(help = "help.arg.package-version")]
pub version: VersionString,
#[arg(long)]
#[arg(long, help = "help.arg.mirror-url")]
#[ts(type = "string")]
pub url: Url,
#[ts(skip)]
@@ -454,7 +462,7 @@ pub async fn remove_mirror(
) -> Result<(), Error> {
let peek = ctx.db.peek().await;
let signer =
signer.ok_or_else(|| Error::new(eyre!("missing signer"), ErrorKind::InvalidRequest))?;
signer.ok_or_else(|| Error::new(eyre!("{}", t!("registry.package.missing-signer")), ErrorKind::InvalidRequest))?;
let signer_guid = peek.as_index().as_signers().get_signer(&signer)?;
ctx.db
@@ -483,7 +491,7 @@ pub async fn remove_mirror(
.for_each(|(_, asset)| asset.urls.retain(|u| u != &url));
if s.iter().any(|(_, asset)| asset.urls.is_empty()) {
Err(Error::new(
eyre!("cannot remove last mirror from an s9pk"),
eyre!("{}", t!("registry.package.cannot-remove-last-mirror")),
ErrorKind::InvalidRequest,
))
} else {
@@ -493,7 +501,7 @@ pub async fn remove_mirror(
}
Ok(())
} else {
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
Err(Error::new(eyre!("{}", t!("registry.package.remove-mirror.unauthorized")), ErrorKind::Authorization))
}
})
.await

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> {
@@ -20,7 +21,7 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
from_fn_async(add_category)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Add a category to the registry")
.with_about("about.add-category-registry")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -28,7 +29,7 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
from_fn_async(remove_category)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Remove a category from the registry")
.with_about("about.remove-category-registry")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -36,7 +37,7 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
from_fn_async(add_package)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Add a package to a category")
.with_about("about.add-package-category")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -44,7 +45,7 @@ pub fn category_api<C: Context>() -> ParentHandler<C> {
from_fn_async(remove_package)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Remove a package from a category")
.with_about("about.remove-package-category")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -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

@@ -51,17 +51,18 @@ pub struct PackageInfoShort {
#[ts(export)]
#[model = "Model<Self>"]
pub struct GetPackageParams {
#[arg(help = "help.arg.package-id")]
pub id: Option<PackageId>,
#[ts(type = "string | null")]
#[arg(long, short = 'v')]
#[arg(long, short = 'v', help = "help.arg.target-version-range")]
pub target_version: Option<VersionRange>,
#[arg(long)]
#[arg(long, help = "help.arg.source-version")]
pub source_version: Option<VersionString>,
#[ts(skip)]
#[arg(skip)]
#[serde(rename = "__DeviceInfo_device_info")]
pub device_info: Option<DeviceInfo>,
#[arg(default_value = "none")]
#[arg(default_value = "none", help = "help.arg.other-versions-detail")]
pub other_versions: Option<PackageDetailLevel>,
}
@@ -78,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() {
@@ -120,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
@@ -401,11 +406,12 @@ pub fn display_package_info(
#[derive(Debug, Deserialize, Serialize, TS, Parser)]
#[serde(rename_all = "camelCase")]
pub struct CliDownloadParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
#[arg(long, short = 'v')]
#[arg(long, short = 'v', help = "help.arg.target-version-range")]
#[ts(type = "string | null")]
pub target_version: Option<VersionRange>,
#[arg(short, long)]
#[arg(short, long, help = "help.arg.destination-path")]
pub dest: Option<PathBuf>,
}
@@ -441,8 +447,12 @@ pub async fn cli_download(
0 => {
return Err(Error::new(
eyre!(
"Could not find a version of {id} that satisfies {}",
target_version.unwrap_or(VersionRange::Any)
"{}",
t!(
"registry.package.get.version-not-found",
id = id,
version = target_version.unwrap_or(VersionRange::Any)
)
),
ErrorKind::NotFound,
));
@@ -462,8 +472,12 @@ pub async fn cli_download(
0 => {
return Err(Error::new(
eyre!(
"Could not find a version of {id} that satisfies {}",
target_version.unwrap_or(VersionRange::Any)
"{}",
t!(
"registry.package.get.version-not-found",
id = id,
version = target_version.unwrap_or(VersionRange::Any)
)
),
ErrorKind::NotFound,
));
@@ -551,7 +565,7 @@ pub async fn cli_download(
progress_tracker.complete();
progress.await.unwrap();
println!("Download Complete");
println!("{}", t!("registry.package.get.download-complete"));
Ok(())
}

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

@@ -17,7 +17,7 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
from_fn_async(index::get_package_index)
.with_metadata("authenticated", Value::Bool(false))
.with_display_serializable()
.with_about("List packages and categories")
.with_about("about.list-packages-categories")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -30,7 +30,7 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
"add",
from_fn_async(add::cli_add_package)
.no_display()
.with_about("Add package to registry index"),
.with_about("about.add-package-registry"),
)
.subcommand(
"add-mirror",
@@ -42,7 +42,7 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
"add-mirror",
from_fn_async(add::cli_add_mirror)
.no_display()
.with_about("Add a mirror for an s9pk"),
.with_about("about.add-mirror-s9pk"),
)
.subcommand(
"remove",
@@ -51,17 +51,17 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
.with_custom_display_fn(|args, changed| {
if !changed {
tracing::warn!(
"{}@{}{} does not exist, so not removed",
args.params.id,
args.params.version,
args.params
.sighash
.map_or(String::new(), |h| format!("#{h}"))
"{}",
t!("registry.package.remove-not-exist",
id = args.params.id,
version = args.params.version,
sighash = args.params.sighash.map_or(String::new(), |h| format!("#{h}"))
)
);
}
Ok(())
})
.with_about("Remove package from registry index")
.with_about("about.remove-package-registry")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -69,12 +69,12 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
from_fn_async(add::remove_mirror)
.with_metadata("get_signer", Value::Bool(true))
.no_display()
.with_about("Remove a mirror from a package")
.with_about("about.remove-mirror-package")
.with_call_remote::<CliContext>(),
)
.subcommand(
"signer",
signer::signer_api::<C>().with_about("Add, remove, and list package signers"),
signer::signer_api::<C>().with_about("about.add-remove-list-package-signers"),
)
.subcommand(
"get",
@@ -85,18 +85,18 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
.with_custom_display_fn(|handle, result| {
get::display_package_info(handle.params, result)
})
.with_about("List installation candidate package(s)")
.with_about("about.list-installation-candidates")
.with_call_remote::<CliContext>(),
)
.subcommand(
"download",
from_fn_async_local(get::cli_download)
.no_display()
.with_about("Download an s9pk"),
.with_about("about.download-s9pk"),
)
.subcommand(
"category",
category::category_api::<C>()
.with_about("Update the categories for packages on the registry"),
.with_about("about.update-categories-registry"),
)
}

View File

@@ -22,7 +22,7 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
from_fn_async(add_package_signer)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Add package signer")
.with_about("about.add-package-signer")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -30,7 +30,7 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
from_fn_async(remove_package_signer)
.with_metadata("admin", Value::Bool(true))
.no_display()
.with_about("Remove package signer")
.with_about("about.remove-package-signer")
.with_call_remote::<CliContext>(),
)
.subcommand(
@@ -41,7 +41,7 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
.with_custom_display_fn(|handle, result| {
display_package_signers(handle.params, result)
})
.with_about("List package signers and related signer info")
.with_about("about.list-package-signers")
.with_call_remote::<CliContext>(),
)
}
@@ -51,9 +51,11 @@ pub fn signer_api<C: Context>() -> ParentHandler<C> {
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct AddPackageSignerParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
#[arg(help = "help.arg.signer-id")]
pub signer: Guid,
#[arg(long)]
#[arg(long, help = "help.arg.version-range")]
#[ts(type = "string | null")]
pub versions: Option<VersionRange>,
}
@@ -93,7 +95,9 @@ pub async fn add_package_signer(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct RemovePackageSignerParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
#[arg(help = "help.arg.signer-id")]
pub signer: Guid,
}
@@ -114,7 +118,7 @@ pub async fn remove_package_signer(
.is_some()
{
return Err(Error::new(
eyre!("signer {signer} is not authorized to sign for {id}"),
eyre!("{}", t!("registry.package.signer.not-authorized", signer = signer, id = id)),
ErrorKind::NotFound,
));
}
@@ -130,6 +134,7 @@ pub async fn remove_package_signer(
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct ListPackageSignersParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
}

View File

@@ -90,7 +90,7 @@ impl AcceptSigners {
Ok(())
} else {
Err(Error::new(
eyre!("signer(s) not accepted"),
eyre!("{}", t!("registry.signer.not-accepted")),
ErrorKind::InvalidSignature,
))
}

View File

@@ -25,7 +25,7 @@ pub fn s9pk() -> ParentHandler<CliContext> {
"pack",
from_fn_async(super::v2::pack::pack)
.no_display()
.with_about("Package s9pk input files into valid s9pk"),
.with_about("about.package-s9pk-input-files-into-valid-s9pk"),
)
.subcommand(
"list-ingredients",
@@ -45,26 +45,27 @@ pub fn s9pk() -> ParentHandler<CliContext> {
println!();
Ok(())
})
.with_about("List paths of package ingredients"),
.with_about("about.list-paths-of-package-ingredients"),
)
.subcommand(
"edit",
edit().with_about("Commands to add an image to an s9pk or edit the manifest"),
edit().with_about("about.commands-add-image-or-edit-manifest"),
)
.subcommand(
"inspect",
inspect().with_about("Commands to display file paths, file contents, or manifest"),
inspect().with_about("about.commands-display-file-paths-contents-manifest"),
)
.subcommand(
"convert",
from_fn_async(convert)
.no_display()
.with_about("Convert s9pk from v1 to v2"),
.with_about("about.convert-s9pk-v1-to-v2"),
)
}
#[derive(Deserialize, Serialize, Parser)]
struct S9pkPath {
#[arg(help = "help.arg.s9pk-file-path")]
s9pk: PathBuf,
}
@@ -76,14 +77,14 @@ fn edit() -> ParentHandler<CliContext, S9pkPath> {
from_fn_async(add_image)
.with_inherited(only_parent)
.no_display()
.with_about("Add image to s9pk"),
.with_about("about.add-image-to-s9pk"),
)
.subcommand(
"manifest",
from_fn_async(edit_manifest)
.with_inherited(only_parent)
.with_display_serializable()
.with_about("Edit s9pk manifest"),
.with_about("about.edit-s9pk-manifest"),
)
}
@@ -95,26 +96,27 @@ fn inspect() -> ParentHandler<CliContext, S9pkPath> {
from_fn_async(file_tree)
.with_inherited(only_parent)
.with_display_serializable()
.with_about("Display list of paths"),
.with_about("about.display-list-of-paths"),
)
.subcommand(
"cat",
from_fn_async(cat)
.with_inherited(only_parent)
.no_display()
.with_about("Display file contents"),
.with_about("about.display-file-contents"),
)
.subcommand(
"manifest",
from_fn_async(inspect_manifest)
.with_inherited(only_parent)
.with_display_serializable()
.with_about("Display s9pk manifest"),
.with_about("about.display-s9pk-manifest"),
)
}
#[derive(Deserialize, Serialize, Parser, TS)]
struct AddImageParams {
#[arg(help = "help.arg.image-id")]
id: ImageId,
#[command(flatten)]
config: ImageConfig,
@@ -148,6 +150,7 @@ async fn add_image(
#[derive(Deserialize, Serialize, Parser, TS)]
struct EditManifestParams {
#[arg(help = "help.arg.db-apply-expr")]
expression: String,
}
async fn edit_manifest(
@@ -194,6 +197,7 @@ async fn file_tree(
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
struct CatParams {
#[arg(help = "help.arg.file-path")]
file_path: PathBuf,
}
async fn cat(

View File

@@ -16,5 +16,6 @@ pub const SIG_CONTEXT: &[u8] = b"s9pk";
#[serde(rename_all = "camelCase")]
#[command(rename_all = "kebab-case")]
pub struct VerifyParams {
#[arg(help = "help.arg.s9pk-file-path")]
pub path: PathBuf,
}

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;
@@ -155,20 +155,21 @@ impl From<PackSource> for DynFileSource {
#[derive(Deserialize, Serialize, Parser)]
pub struct PackParams {
#[arg(help = "help.arg.input-path")]
pub path: Option<PathBuf>,
#[arg(short, long)]
#[arg(short, long, help = "help.arg.output-path")]
pub output: Option<PathBuf>,
#[arg(long)]
#[arg(long, help = "help.arg.javascript-path")]
pub javascript: Option<PathBuf>,
#[arg(long)]
#[arg(long, help = "help.arg.icon-path")]
pub icon: Option<PathBuf>,
#[arg(long)]
#[arg(long, help = "help.arg.license-path")]
pub license: Option<PathBuf>,
#[arg(long, conflicts_with = "no-assets")]
#[arg(long, conflicts_with = "no-assets", help = "help.arg.assets-path")]
pub assets: Option<PathBuf>,
#[arg(long, conflicts_with = "assets")]
#[arg(long, conflicts_with = "assets", help = "help.arg.no-assets")]
pub no_assets: bool,
#[arg(long, help = "Architecture Mask")]
#[arg(long, help = "help.arg.architecture-mask")]
pub arch: Vec<InternedString>,
}
impl PackParams {
@@ -280,19 +281,19 @@ pub struct ImageConfig {
#[derive(Parser)]
struct CliImageConfig {
#[arg(long, conflicts_with("docker-tag"))]
#[arg(long, conflicts_with("docker-tag"), help = "help.arg.docker-build")]
docker_build: bool,
#[arg(long, requires("docker-build"))]
#[arg(long, requires("docker-build"), help = "help.arg.dockerfile-path")]
dockerfile: Option<PathBuf>,
#[arg(long, requires("docker-build"))]
#[arg(long, requires("docker-build"), help = "help.arg.workdir-path")]
workdir: Option<PathBuf>,
#[arg(long, conflicts_with_all(["dockerfile", "workdir"]))]
#[arg(long, conflicts_with_all(["dockerfile", "workdir"]), help = "help.arg.docker-tag")]
docker_tag: Option<String>,
#[arg(long)]
#[arg(long, help = "help.arg.architecture-mask")]
arch: Vec<InternedString>,
#[arg(long)]
#[arg(long, help = "help.arg.emulate-missing-arch")]
emulate_missing_as: Option<InternedString>,
#[arg(long)]
#[arg(long, help = "help.arg.nvidia-container")]
nvidia_container: bool,
}
impl TryFrom<CliImageConfig> for ImageConfig {
@@ -755,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

@@ -102,7 +102,7 @@ pub fn update_tasks(
}
}
None => {
tracing::error!("action request exists in an invalid state {:?}", v.task);
tracing::error!("{}", t!("service.action.action-request-invalid-state", task = format!("{:?}", v.task)));
}
},
}
@@ -151,7 +151,7 @@ impl Handler<RunAction> for ServiceActor {
.de()?;
if matches!(&action.visibility, ActionVisibility::Disabled(_)) {
return Err(Error::new(
eyre!("action {action_id} is disabled"),
eyre!("{}", t!("service.action.action-is-disabled", action_id = action_id)),
ErrorKind::Action,
));
}
@@ -162,7 +162,7 @@ impl Handler<RunAction> for ServiceActor {
_ => false,
} {
return Err(Error::new(
eyre!("service is not in allowed status for {action_id}"),
eyre!("{}", t!("service.action.service-not-in-allowed-status", action_id = action_id)),
ErrorKind::Action,
));
}

View File

@@ -14,7 +14,7 @@ use crate::service::effects::context::EffectContext;
#[derive(Debug, Default, Parser)]
pub struct ContainerClientConfig {
#[arg(long = "socket")]
#[arg(long = "socket", help = "help.arg.socket-path")]
pub socket: Option<PathBuf>,
}

View File

@@ -1,6 +1,7 @@
use std::collections::BTreeSet;
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
use rust_i18n::t;
use crate::action::{ActionInput, ActionResult, display_action_result};
use crate::db::model::package::{
@@ -80,7 +81,7 @@ pub async fn export_action(
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ClearActionsParams {
#[arg(long)]
#[arg(long, help = "help.arg.except-actions")]
pub except: Vec<ActionId>,
}
@@ -117,7 +118,9 @@ pub struct GetActionInputParams {
#[arg(skip)]
procedure_id: Guid,
#[ts(optional)]
#[arg(help = "help.arg.package-id")]
package_id: Option<PackageId>,
#[arg(help = "help.arg.action-id")]
action_id: ActionId,
}
async fn get_action_input(
@@ -155,9 +158,12 @@ pub struct RunActionParams {
#[arg(skip)]
procedure_id: Guid,
#[ts(optional)]
#[arg(help = "help.arg.package-id")]
package_id: Option<PackageId>,
#[arg(help = "help.arg.action-id")]
action_id: ActionId,
#[ts(type = "any")]
#[arg(help = "help.arg.action-input")]
input: Value,
}
async fn run_action(
@@ -175,7 +181,7 @@ async fn run_action(
if package_id != &context.seed.id {
return Err(Error::new(
eyre!("calling actions on other packages is unsupported at this time"),
eyre!("{}", t!("service.effects.action.calling-actions-on-other-packages-unsupported")),
ErrorKind::InvalidRequest,
));
context
@@ -220,7 +226,7 @@ async fn create_task(
TaskCondition::InputNotMatches => {
let Some(input) = task.input.as_ref() else {
return Err(Error::new(
eyre!("input-not-matches trigger requires input to be specified"),
eyre!("{}", t!("service.effects.action.input-not-matches-requires-input")),
ErrorKind::InvalidRequest,
));
};
@@ -238,9 +244,7 @@ async fn create_task(
else {
return Err(Error::new(
eyre!(
"action {} of {} has no input",
task.action_id,
task.package_id
"{}", t!("service.effects.action.action-has-no-input", action_id = task.action_id, package_id = task.package_id)
),
ErrorKind::InvalidRequest,
));
@@ -286,9 +290,9 @@ async fn create_task(
#[ts(type = "{ only: string[] } | { except: string[] }")]
#[ts(export)]
pub struct ClearTasksParams {
#[arg(long, conflicts_with = "except")]
#[arg(long, conflicts_with = "except", help = "help.arg.only-tasks")]
pub only: Option<Vec<ReplayId>>,
#[arg(long, conflicts_with = "only")]
#[arg(long, conflicts_with = "only", help = "help.arg.except-tasks")]
pub except: Option<Vec<ReplayId>>,
}

View File

@@ -319,9 +319,9 @@ impl CallbackHandlers {
#[ts(type = "{ only: number[] } | { except: number[] }")]
#[ts(export)]
pub struct ClearCallbacksParams {
#[arg(long, conflicts_with = "except")]
#[arg(long, conflicts_with = "except", help = "help.arg.only-callbacks")]
pub only: Option<Vec<CallbackId>>,
#[arg(long, conflicts_with = "only")]
#[arg(long, conflicts_with = "only", help = "help.arg.except-callbacks")]
pub except: Option<Vec<CallbackId>>,
}

View File

@@ -4,7 +4,7 @@ use std::str::FromStr;
use clap::builder::ValueParserFactory;
use exver::VersionRange;
use imbl_value::InternedString;
use rust_i18n::t;
use crate::db::model::package::{
CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, ManifestPreference,
@@ -148,13 +148,25 @@ impl FromStr for DependencyRequirement {
.map(|id| id.parse().map_err(Error::from))
.collect(),
Some((kind, _)) => Err(Error::new(
eyre!("unknown dependency 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!("unknown dependency kind {kind}"),
eyre!(
"{}",
t!(
"service.effects.dependency.unknown-dependency-kind",
kind = kind
)
),
ErrorKind::InvalidRequest,
)),
},
@@ -293,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,
@@ -334,7 +345,7 @@ pub async fn check_dependencies(
.collect();
results.push(CheckDependenciesResult {
package_id,
title,
title: title.map(|t| t.localized()),
installed_version: None,
satisfies: BTreeSet::new(),
is_running: false,
@@ -360,7 +371,7 @@ pub async fn check_dependencies(
.collect();
results.push(CheckDependenciesResult {
package_id,
title,
title: title.map(|t| t.localized()),
installed_version,
satisfies,
is_running,

View File

@@ -11,6 +11,6 @@ pub(super) use crate::service::effects::context::EffectContext;
#[ts(export)]
pub struct EventId {
#[serde(default)]
#[arg(default_value_t, long)]
#[arg(default_value_t, long, help = "help.arg.event-id")]
pub event_id: Guid,
}

View File

@@ -107,22 +107,23 @@ fn open_file_read(path: impl AsRef<Path>) -> Result<File, Error> {
#[derive(Debug, Clone, Serialize, Deserialize, Parser)]
pub struct ExecParams {
#[arg(long)]
#[arg(long, help = "help.arg.force-tty")]
force_tty: bool,
#[arg(long)]
#[arg(long, help = "help.arg.force-stderr-tty")]
force_stderr_tty: bool,
#[arg(long)]
#[arg(long, help = "help.arg.pty-size")]
pty_size: Option<TermSize>,
#[arg(short, long)]
#[arg(short, long, help = "help.arg.env-variable")]
env: Vec<String>,
#[arg(long)]
#[arg(long, help = "help.arg.env-file-path")]
env_file: Option<PathBuf>,
#[arg(short, long)]
#[arg(short, long, help = "help.arg.workdir-path")]
workdir: Option<PathBuf>,
#[arg(short, long)]
#[arg(short, long, help = "help.arg.user-name")]
user: Option<String>,
#[arg(help = "help.arg.chroot-path")]
chroot: PathBuf,
#[arg(trailing_var_arg = true)]
#[arg(trailing_var_arg = true, help = "help.arg.command-to-execute")]
command: Vec<OsString>,
}
impl ExecParams {

View File

@@ -28,6 +28,7 @@ use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
use ts_rs::TS;
use url::Url;
use crate::context::{CliContext, RpcContext};
use crate::db::model::package::{
InstalledState, ManifestPreference, PackageState, PackageStateMatchModelRef, TaskSeverity,
@@ -172,7 +173,7 @@ impl ServiceRef {
}
let service = Arc::try_unwrap(self.0).map_err(|_| {
Error::new(
eyre!("ServiceActor held somewhere after actor shutdown"),
eyre!("{}", t!("service.mod.service-actor-held-after-shutdown")),
ErrorKind::Unknown,
)
})?;
@@ -183,7 +184,7 @@ impl ServiceRef {
Arc::try_unwrap(service.seed)
.map_err(|_| {
Error::new(
eyre!("ServiceActorSeed held somewhere after actor shutdown"),
eyre!("{}", t!("service.mod.service-actor-seed-held-after-shutdown")),
ErrorKind::Unknown,
)
})?
@@ -375,7 +376,7 @@ impl Service {
{
Ok(PackageState::Installed(InstalledState { manifest }))
} else {
Err(Error::new(eyre!("Race condition detected - package state changed during load"), ErrorKind::Database))
Err(Error::new(eyre!("{}", t!("service.mod.race-condition-detected")), ErrorKind::Database))
}
})
}
@@ -446,7 +447,7 @@ impl Service {
handle_installed(S9pk::open(s9pk_path, Some(id)).await?).await
}
PackageStateMatchModelRef::Error(e) => Err(Error::new(
eyre!("Failed to parse PackageDataEntry, found {e:?}"),
eyre!("{}", t!("service.mod.failed-to-parse-package-data-entry", error = format!("{e:?}"))),
ErrorKind::Deserialization,
)),
}
@@ -552,7 +553,7 @@ impl Service {
true
} else {
tracing::warn!(
"Deleting task {id} because action no longer exists"
"{}", t!("service.mod.deleting-task-action-no-longer-exists", id = id)
);
false
}
@@ -684,6 +685,7 @@ struct ServiceActorSeed {
#[derive(Deserialize, Serialize, Parser, TS)]
pub struct RebuildParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
}
pub async fn rebuild(ctx: RpcContext, RebuildParams { id }: RebuildParams) -> Result<(), Error> {
@@ -789,7 +791,7 @@ pub async fn attach(
.join("\n");
return Err(Error::new(
eyre!(
"no matching subcontainers are running for {id}; some possible choices are:\n{subcontainers}"
"{}", t!("service.mod.no-matching-subcontainers", id = id, subcontainers = subcontainers)
),
ErrorKind::NotFound,
));
@@ -828,7 +830,7 @@ pub async fn attach(
.map(format_subcontainer_pair)
.join("\n");
return Err(Error::new(
eyre!("multiple subcontainers found for {id}: \n{subcontainer_ids}"),
eyre!("{}", t!("service.mod.multiple-subcontainers-found", id = id, subcontainer_ids = subcontainer_ids)),
ErrorKind::InvalidRequest,
));
}
@@ -985,7 +987,7 @@ pub async fn attach(
"signal" => {
if data.len() != 4 {
return Err(Error::new(
eyre!("invalid byte length for signal: {}", data.len()),
eyre!("{}", t!("service.mod.invalid-byte-length-for-signal", length = data.len())),
ErrorKind::InvalidRequest
));
}
@@ -1118,14 +1120,14 @@ async fn get_passwd_command(etc_passwd_path: PathBuf, user: &str) -> RootCommand
}
}
Err(Error::new(
eyre!("Could not parse /etc/passwd for shell: {}", contents),
eyre!("{}", t!("service.mod.could-not-parse-etc-passwd", contents = contents)),
ErrorKind::Filesystem,
))
}
.await
.map(RootCommand)
.unwrap_or_else(|e| {
tracing::error!("Could not get the /etc/passwd: {e}");
tracing::error!("{}", t!("service.mod.could-not-get-etc-passwd", error = e));
tracing::debug!("{e:?}");
RootCommand("/bin/sh".to_string())
})
@@ -1133,18 +1135,19 @@ async fn get_passwd_command(etc_passwd_path: PathBuf, user: &str) -> RootCommand
#[derive(Deserialize, Serialize, Parser)]
pub struct CliAttachParams {
#[arg(help = "help.arg.package-id")]
pub id: PackageId,
#[arg(long)]
#[arg(long, help = "help.arg.force-tty")]
pub force_tty: bool,
#[arg(trailing_var_arg = true)]
#[arg(trailing_var_arg = true, help = "help.arg.command-to-execute")]
pub command: Vec<OsString>,
#[arg(long, short)]
#[arg(long, short, help = "help.arg.subcontainer-name")]
subcontainer: Option<InternedString>,
#[arg(long, short)]
#[arg(long, short, help = "help.arg.container-name")]
name: Option<InternedString>,
#[arg(long, short)]
#[arg(long, short, help = "help.arg.user-name")]
user: Option<InternedString>,
#[arg(long, short)]
#[arg(long, short, help = "help.arg.image-id")]
image_id: Option<ImageId>,
}
#[instrument[skip_all]]
@@ -1287,7 +1290,7 @@ pub async fn cli_attach(
"exit" => {
if data.len() != 4 {
return Err(Error::new(
eyre!("invalid byte length for exit code: {}", data.len()),
eyre!("{}", t!("service.mod.invalid-byte-length-for-exit-code", length = data.len())),
ErrorKind::InvalidRequest
));
}

View File

@@ -318,7 +318,7 @@ impl PersistentContainer {
.get()
.ok_or_else(|| {
Error::new(
eyre!("PersistentContainer has been destroyed"),
eyre!("{}", t!("service.persistent-container.container-destroyed")),
ErrorKind::Incoherent,
)
})?
@@ -354,7 +354,7 @@ impl PersistentContainer {
.get()
.ok_or_else(|| {
Error::new(
eyre!("PersistentContainer has been destroyed"),
eyre!("{}", t!("service.persistent-container.container-destroyed")),
ErrorKind::Incoherent,
)
})?
@@ -364,7 +364,7 @@ impl PersistentContainer {
let handle = NonDetachingJoinHandle::from(tokio::spawn(async move {
let chown_status = async {
let res = server.run_unix(&path, |err| {
tracing::error!("error on unix socket {}: {err}", path.display())
tracing::error!("{}", t!("service.persistent-container.error-on-unix-socket", path = path.display(), error = err))
})?;
Command::new("chown")
.arg("100000:100000")
@@ -386,7 +386,7 @@ impl PersistentContainer {
}));
let shutdown = recv.await.map_err(|_| {
Error::new(
eyre!("unix socket server thread panicked"),
eyre!("{}", t!("service.persistent-container.unix-socket-server-panicked")),
ErrorKind::Unknown,
)
})??;
@@ -396,7 +396,7 @@ impl PersistentContainer {
.is_some()
{
return Err(Error::new(
eyre!("PersistentContainer already initialized"),
eyre!("{}", t!("service.persistent-container.already-initialized")),
ErrorKind::InvalidRequest,
));
}
@@ -473,7 +473,7 @@ impl PersistentContainer {
if let Some(destroy) = self.destroy(uninit) {
destroy.await?;
}
tracing::info!("Service for {} exited", self.s9pk.as_manifest().id);
tracing::info!("{}", t!("service.persistent-container.service-exited", id = self.s9pk.as_manifest().id));
Ok(())
}

View File

@@ -47,9 +47,9 @@ impl Actor for ServiceActor {
}
.await
{
tracing::error!("error synchronizing state of service: {e}");
tracing::error!("{}", t!("service.service-actor.error-synchronizing-state", error = e));
tracing::debug!("{e:?}");
tracing::error!("Retrying in {}s...", SYNC_RETRY_COOLDOWN_SECONDS);
tracing::error!("{}", t!("service.service-actor.retrying-in-seconds", seconds = SYNC_RETRY_COOLDOWN_SECONDS));
tokio::time::timeout(
Duration::from_secs(SYNC_RETRY_COOLDOWN_SECONDS),
async {

Some files were not shown because too many files have changed in this diff Show More