Compare commits

..

37 Commits

Author SHA1 Message Date
Alex Inkin
9f66ccec17 fix: fix dynamic status and instructions modal (#1382) 2022-04-05 08:56:02 -06:00
Matt Hill
905aaafa2b dont hang on update and downgrade 2022-04-04 17:01:07 -06:00
J M
12ca3e0aea fix: Be able to port forward and see frontend (#1380) 2022-04-04 15:22:37 -06:00
Aiden McClelland
f523a68e72 fix migrations not running in the to block 2022-04-01 20:51:43 -06:00
Aiden McClelland
6ac87a51e4 allow for multiple disk repairs (#1375)
* allow for multiple disk repairs

* actually reboot if reboot required

* must_use
2022-04-01 11:39:03 -06:00
Alex Inkin
3b930060a8 fix(app-list): fix recovered services spinner (#1376) 2022-04-01 08:20:16 -06:00
Aiden McClelland
04f1511b52 remove "(Current Version)" (#1372)
* remove "(Current Version)"

* remove index variable
2022-03-31 15:28:21 -06:00
Matt Hill
01f14061ec better eager loading and better error messaging for backup flow (#1368)
* better eager loading and better error messaging for backup flow

* add arch qp to marketplace proxy requests

* better styling for eos release notes
2022-03-31 13:05:59 -04:00
Lucy C
e79b27e0bb Fix/misc 0.3.0.1 (#1366)
* add solution for os error 2 case

* make awaiting result text blue

* remove mask button on text inputs until proper solution implemented
2022-03-31 11:19:10 -04:00
Aiden McClelland
8bc1ef415f add compression to rip-image script 2022-03-30 17:49:27 -06:00
Aiden McClelland
c49fe9744e actually include index.html during build 2022-03-30 17:49:27 -06:00
Aiden McClelland
604d0ae052 fix issue where boot label reverts on quirk change 2022-03-30 15:20:44 -06:00
Aiden McClelland
4960aeedad update quirks during migration 2022-03-29 14:42:09 -06:00
Lucy C
1e8aa569b3 update packages (#1361)
* update frontend packages

* update system images as well

* proper update of package lock file
2022-03-29 12:44:31 -06:00
Aiden McClelland
eeb557860e wait for time sync up to 60s 2022-03-29 12:41:00 -06:00
Aiden McClelland
fdf473016b bump version for 0.3.0.1 (#1353)
* bump version for 0.3.0.1

* fix mock versioning

* update welcome message

* remove old release note

Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
2022-03-29 12:40:41 -06:00
Lucy C
e53bf81cbc Feature/diagnostic repair disk (#1358)
* add disk repair actions to diagnostic ui and server menu

* only display repair disk button when activated

* fix typo

* add repairDrive fn with restart to diagnostic ui

* fix copy

* add alert before repairing disk in diagnostic ui

* fix repair disk message spacing and hidden display

* fix version comparisons and enable dismissable refresh modal

* eager load medkit and fix storefront to outline icon
2022-03-28 17:31:32 -06:00
Aiden McClelland
8ef1584a4d only remove if exists 2022-03-28 14:23:29 -05:00
Lucy C
c9676ff018 rename conflicting attribute for proper display of action buttons in marketplace (#1357) 2022-03-28 10:32:26 -06:00
Lucy C
e13fab7d9f convert all inputs to text except login (#1356) 2022-03-28 10:32:09 -06:00
Aiden McClelland
a182b0c260 update sqlx directly, and allow patch bumps from other packages 2022-03-24 18:09:21 -05:00
Aiden McClelland
f3a30e40fe run fsck on startup (#1350)
* add code to un e2fsck on startup

* fix imports

* don't auto undo

* switch enum to newtype

* more explicit caller variable
2022-03-23 17:24:46 -05:00
Aiden McClelland
e7f4aefb72 allow for charset (#1354) 2022-03-23 14:31:45 -06:00
Lucy Cifferello
476b9a3c9c add kernel log display to server show page 2022-03-23 14:59:45 -05:00
Aiden McClelland
659af734eb add endpoint for dmesg logs 2022-03-23 14:59:45 -05:00
Lucy C
39a2685506 Fix/config obj list (#1351)
* remove deprecated build check

* fixed config types for object and union; cleaned up mock api fixtures

* make form object element transition times consistent and emit expand event on list object items
2022-03-23 09:39:55 -06:00
Aiden McClelland
5e0b83fa4a fix init 2022-03-22 14:23:46 -05:00
Alex Inkin
7ea3aefdd5 feat(marketplace): extract common components (#1338)
* feat(marketplace): extract common components

* chore: fix service provide

* feat(markdown): allow Observable content

* chore: remove unnecessary module import

* minor styling for marketplacee list

* only show loading for marketplace show if version change

* chore: get rid of unnecessary server request

* chore: fix version switching

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
2022-03-21 13:50:06 -06:00
Aiden McClelland
8b286431e6 handle new content-types from marketplace.get 2022-03-17 17:17:02 -05:00
Aiden McClelland
c640749c7c allow websockets to containers (#1340)
* allow websockets to containers

* fix format string
2022-03-17 16:51:45 -05:00
Aiden McClelland
cb5bb34ed8 whitelist samsung T7 (#1333)
* whitelist samsung T7

* add drive model to comment

Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>

Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>
2022-03-16 10:26:53 -06:00
Matt Hill
90d72fb0d4 include shared scss in setup-wizard and diagnostic-ui (#1334) 2022-03-15 15:07:39 -06:00
Aiden McClelland
20b3ab98df use consistent config file type 2022-03-15 13:19:36 -06:00
J M
227b7a03d7 Fix/wifi (#1326)
* fix: Fix the missing wifi's still list.

* chore: Exclude the interface maybe

* chore: Don't add if already there, just modify

* chore: Minor changes

* no timeouts, regex on pw input (#1327)

Co-authored-by: Drew Ansbacher <drew@start9labs.com>

* fix: Allow more than 8

* Update wifi.page.ts

Co-authored-by: Drew Ansbacher <drew.ansbacher@gmail.com>
Co-authored-by: Drew Ansbacher <drew@start9labs.com>
2022-03-15 13:09:03 -06:00
Alex Inkin
8942c29229 feat(marketplace): add separate package and move some entities in it (#1283)
* feat(marketplace): add separate package and move some entities in it

* feat(marketplace): refactor release notes and list

* feat(marketplace): refactor showing a package

* chore: fix install progress

* chore: fix angular.json

* chore: properly share stream
2022-03-15 11:11:54 -06:00
Drew Ansbacher
72cb451f5a fix back button 2022-03-08 11:32:02 -07:00
Aiden McClelland
8a7181a21c fix logic for validating image tags 2022-03-03 15:56:41 -07:00
226 changed files with 6276 additions and 4645 deletions

View File

@@ -4,7 +4,7 @@ EMBASSY_SRC := raspios.img product_key.txt $(EMBASSY_BINS) backend/embassyd.serv
COMPAT_SRC := $(shell find system-images/compat/src)
UTILS_SRC := $(shell find system-images/utils/Dockerfile)
BACKEND_SRC := $(shell find backend/src) $(shell find patch-db/*/src) $(shell find rpc-toolkit/*/src) backend/Cargo.toml backend/Cargo.lock
FRONTEND_SRC := $(shell find frontend/projects) $(shell find frontend/styles) $(shell find frontend/assets)
FRONTEND_SRC := $(shell find frontend/projects) $(shell find frontend/assets)
PATCH_DB_CLIENT_SRC = $(shell find patch-db/client -not -path patch-db/client/dist)
GIT_REFS := $(shell find .git/refs/heads)
TMP_FILE := $(shell mktemp)

1229
backend/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@ keywords = [
name = "embassy-os"
readme = "README.md"
repository = "https://github.com/Start9Labs/embassy-os"
version = "0.3.0"
version = "0.3.0-rev.1"
[lib]
name = "embassy"
@@ -51,6 +51,7 @@ avahi-sys = { git = "https://github.com/Start9Labs/avahi-sys", version = "0.10.0
"dynamic",
], optional = true }
base32 = "0.4.0"
base64 = "0.13.0"
basic-cookies = "0.1.4"
bollard = "0.11.0"
chrono = { version = "0.4.19", features = ["serde"] }
@@ -107,7 +108,7 @@ serde_toml = { package = "toml", version = "0.5.8" }
serde_yaml = "0.8.21"
sha2 = "0.9.8"
simple-logging = "2.0"
sqlx = { version = "0.5", features = [
sqlx = { version = "0.5.11", features = [
"chrono",
"offline",
"runtime-tokio-rustls",

View File

@@ -1,8 +1,8 @@
[Unit]
Description=Embassy Init
After=network.target ntp.service
After=network.target
Requires=network.target
Wants=avahi-daemon.service nginx.service tor.service ntp.service
Wants=avahi-daemon.service nginx.service tor.service
[Service]
Type=oneshot

View File

@@ -4,7 +4,9 @@ use std::time::Duration;
use embassy::context::rpc::RpcContextConfig;
use embassy::context::{DiagnosticContext, SetupContext};
use embassy::disk::fsck::RepairStrategy;
use embassy::disk::main::DEFAULT_PASSWORD;
use embassy::disk::REPAIR_DISK_PATH;
use embassy::hostname::get_product_key;
use embassy::middleware::cors::cors;
use embassy::middleware::diagnostic::diagnostic;
@@ -74,14 +76,31 @@ async fn setup_or_init(cfg_path: Option<&str>) -> Result<(), Error> {
.with_kind(embassy::ErrorKind::Network)?;
} else {
let cfg = RpcContextConfig::load(cfg_path).await?;
embassy::disk::main::import(
tokio::fs::read_to_string("/embassy-os/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
.await?
.trim(),
let guid_string = tokio::fs::read_to_string("/embassy-os/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy
.await?;
let guid = guid_string.trim();
let reboot = embassy::disk::main::import(
guid,
cfg.datadir(),
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
RepairStrategy::Aggressive
} else {
RepairStrategy::Preen
},
DEFAULT_PASSWORD,
)
.await?;
if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() {
tokio::fs::remove_file(REPAIR_DISK_PATH)
.await
.with_ctx(|_| (embassy::ErrorKind::Filesystem, REPAIR_DISK_PATH))?;
}
if reboot.0 {
embassy::disk::main::export(guid, cfg.datadir()).await?;
Command::new("reboot")
.invoke(embassy::ErrorKind::Unknown)
.await?;
}
tracing::info!("Loaded Disk");
embassy::init::init(&cfg, &get_product_key().await?).await?;
}

View File

@@ -2,12 +2,11 @@ use std::collections::{BTreeMap, VecDeque};
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use bollard::Docker;
use chrono::Utc;
use color_eyre::eyre::eyre;
use patch_db::json_ptr::JsonPointer;
use patch_db::{DbHandle, LockType, PatchDb, Revision};
@@ -35,7 +34,7 @@ use crate::notifications::NotificationManager;
use crate::setup::password_hash;
use crate::shutdown::Shutdown;
use crate::status::{MainStatus, Status};
use crate::util::io::from_toml_async_reader;
use crate::util::io::from_yaml_async_reader;
use crate::util::{AsyncFileExt, Invoke};
use crate::{Error, ResultExt};
@@ -61,7 +60,7 @@ impl RpcContextConfig {
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, cfg_path.display().to_string()))?
{
from_toml_async_reader(f).await
from_yaml_async_reader(f).await
} else {
Ok(Self::default())
}

View File

@@ -21,7 +21,7 @@ use crate::db::model::Database;
use crate::hostname::{derive_hostname, derive_id, get_product_key};
use crate::net::tor::os_key;
use crate::setup::{password_hash, RecoveryStatus};
use crate::util::io::from_toml_async_reader;
use crate::util::io::from_yaml_async_reader;
use crate::util::AsyncFileExt;
use crate::{Error, ResultExt};
@@ -50,7 +50,7 @@ impl SetupContextConfig {
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, cfg_path.display().to_string()))?
{
from_toml_async_reader(f).await
from_yaml_async_reader(f).await
} else {
Ok(Self::default())
}

View File

@@ -5,6 +5,7 @@ use rpc_toolkit::command;
use rpc_toolkit::yajrc::RpcError;
use crate::context::DiagnosticContext;
use crate::disk::repair;
use crate::logs::{display_logs, fetch_logs, LogResponse, LogSource};
use crate::shutdown::Shutdown;
use crate::util::display_none;
@@ -12,7 +13,7 @@ use crate::Error;
pub const SYSTEMD_UNIT: &'static str = "embassy-init";
#[command(subcommands(error, logs, exit, restart, forget_disk))]
#[command(subcommands(error, logs, exit, restart, forget_disk, disk))]
pub fn diagnostic() -> Result<(), Error> {
Ok(())
}
@@ -56,7 +57,12 @@ pub fn restart(#[context] ctx: DiagnosticContext) -> Result<(), Error> {
Ok(())
}
#[command(rename = "forget-disk", display(display_none))]
#[command(subcommands(forget_disk, repair))]
pub fn disk() -> Result<(), Error> {
Ok(())
}
#[command(rename = "forget", display(display_none))]
pub async fn forget_disk() -> Result<(), Error> {
let disk_guid = Path::new("/embassy-os/disk.guid");
if tokio::fs::metadata(disk_guid).await.is_ok() {

120
backend/src/disk/fsck.rs Normal file
View File

@@ -0,0 +1,120 @@
use std::ffi::OsStr;
use std::path::Path;
use color_eyre::eyre::eyre;
use futures::future::BoxFuture;
use futures::FutureExt;
use tokio::process::Command;
use tracing::instrument;
use crate::{Error, ResultExt};
#[derive(Debug, Clone, Copy)]
#[must_use]
pub struct RequiresReboot(pub bool);
impl std::ops::BitOrAssign for RequiresReboot {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0
}
}
#[derive(Debug, Clone, Copy)]
pub enum RepairStrategy {
Preen,
Aggressive,
}
impl RepairStrategy {
pub async fn e2fsck(
&self,
logicalname: impl AsRef<Path> + std::fmt::Debug,
) -> Result<RequiresReboot, Error> {
match self {
RepairStrategy::Preen => e2fsck_preen(logicalname).await,
RepairStrategy::Aggressive => e2fsck_aggressive(logicalname).await,
}
}
}
#[instrument]
pub async fn e2fsck_preen(
logicalname: impl AsRef<Path> + std::fmt::Debug,
) -> Result<RequiresReboot, Error> {
e2fsck_runner(Command::new("e2fsck").arg("-p"), logicalname).await
}
fn backup_existing_undo_file<'a>(path: &'a Path) -> BoxFuture<'a, Result<(), Error>> {
async move {
if tokio::fs::metadata(path).await.is_ok() {
let bak = path.with_extension(format!(
"{}.bak",
path.extension()
.and_then(|s| s.to_str())
.unwrap_or_default()
));
backup_existing_undo_file(&bak).await?;
tokio::fs::rename(path, &bak).await?;
}
Ok(())
}
.boxed()
}
#[instrument]
pub async fn e2fsck_aggressive(
logicalname: impl AsRef<Path> + std::fmt::Debug,
) -> Result<RequiresReboot, Error> {
let undo_path = Path::new("/embassy-os")
.join(
logicalname
.as_ref()
.file_name()
.unwrap_or(OsStr::new("unknown")),
)
.with_extension("e2undo");
backup_existing_undo_file(&undo_path).await?;
e2fsck_runner(
Command::new("e2fsck").arg("-y").arg("-z").arg(undo_path),
logicalname,
)
.await
}
async fn e2fsck_runner(
e2fsck_cmd: &mut Command,
logicalname: impl AsRef<Path> + std::fmt::Debug,
) -> Result<RequiresReboot, Error> {
let e2fsck_out = e2fsck_cmd.arg(logicalname.as_ref()).output().await?;
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"),
crate::ErrorKind::DiskManagement,
)
})?;
if code & 4 != 0 {
tracing::error!(
"some filesystem errors NOT corrected on {}:\n{}",
logicalname.as_ref().display(),
e2fsck_stderr,
);
} else if code & 1 != 0 {
tracing::warn!(
"filesystem errors corrected on {}:\n{}",
logicalname.as_ref().display(),
e2fsck_stderr,
);
}
if code < 8 {
if code & 2 != 0 {
tracing::warn!("reboot required");
Ok(RequiresReboot(true))
} else {
Ok(RequiresReboot(false))
}
} else {
Err(Error::new(
eyre!("e2fsck: {}", e2fsck_stderr),
crate::ErrorKind::DiskManagement,
))
}
}

View File

@@ -5,6 +5,7 @@ use color_eyre::eyre::eyre;
use tokio::process::Command;
use tracing::instrument;
use super::fsck::{RepairStrategy, RequiresReboot};
use super::util::pvscan;
use crate::disk::mount::filesystem::block_dev::mount;
use crate::disk::mount::filesystem::ReadWrite;
@@ -196,7 +197,12 @@ pub async fn export<P: AsRef<Path>>(guid: &str, datadir: P) -> Result<(), Error>
}
#[instrument(skip(datadir, password))]
pub async fn import<P: AsRef<Path>>(guid: &str, datadir: P, password: &str) -> Result<(), Error> {
pub async fn import<P: AsRef<Path>>(
guid: &str,
datadir: P,
repair: RepairStrategy,
password: &str,
) -> Result<RequiresReboot, Error> {
let scan = pvscan().await?;
if scan
.values()
@@ -244,8 +250,7 @@ pub async fn import<P: AsRef<Path>>(guid: &str, datadir: P, password: &str) -> R
.arg(guid)
.invoke(crate::ErrorKind::DiskManagement)
.await?;
mount_all_fs(guid, datadir, password).await?;
Ok(())
mount_all_fs(guid, datadir, repair, password).await
}
#[instrument(skip(datadir, password))]
@@ -253,8 +258,9 @@ pub async fn mount_fs<P: AsRef<Path>>(
guid: &str,
datadir: P,
name: &str,
repair: RepairStrategy,
password: &str,
) -> Result<(), Error> {
) -> Result<RequiresReboot, Error> {
tokio::fs::write(PASSWORD_PATH, password)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
@@ -267,27 +273,26 @@ pub async fn mount_fs<P: AsRef<Path>>(
.arg(format!("{}_{}", guid, name))
.invoke(crate::ErrorKind::DiskManagement)
.await?;
mount(
Path::new("/dev/mapper").join(format!("{}_{}", guid, name)),
datadir.as_ref().join(name),
ReadWrite,
)
.await?;
let mapper_path = Path::new("/dev/mapper").join(format!("{}_{}", guid, name));
let reboot = repair.e2fsck(&mapper_path).await?;
mount(&mapper_path, datadir.as_ref().join(name), ReadWrite).await?;
tokio::fs::remove_file(PASSWORD_PATH)
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, PASSWORD_PATH))?;
Ok(())
Ok(reboot)
}
#[instrument(skip(datadir, password))]
pub async fn mount_all_fs<P: AsRef<Path>>(
guid: &str,
datadir: P,
repair: RepairStrategy,
password: &str,
) -> Result<(), Error> {
mount_fs(guid, &datadir, "main", password).await?;
mount_fs(guid, &datadir, "package-data", password).await?;
Ok(())
) -> Result<RequiresReboot, Error> {
let mut reboot = RequiresReboot(false);
reboot |= mount_fs(guid, &datadir, "main", repair, password).await?;
reboot |= mount_fs(guid, &datadir, "package-data", repair, password).await?;
Ok(reboot)
}

View File

@@ -2,17 +2,20 @@ use clap::ArgMatches;
use rpc_toolkit::command;
use self::util::DiskListResponse;
use crate::util::display_none;
use crate::util::serde::{display_serializable, IoFormat};
use crate::Error;
pub mod fsck;
pub mod main;
pub mod mount;
pub mod quirks;
pub mod util;
pub const BOOT_RW_PATH: &'static str = "/media/boot-rw";
pub const BOOT_RW_PATH: &str = "/media/boot-rw";
pub const REPAIR_DISK_PATH: &str = "/embassy-os/repair-disk";
#[command(subcommands(list))]
#[command(subcommands(list, repair))]
pub fn disk() -> Result<(), Error> {
Ok(())
}
@@ -79,3 +82,9 @@ pub async fn list(
) -> Result<DiskListResponse, Error> {
crate::disk::util::list().await
}
#[command(display(display_none))]
pub async fn repair() -> Result<(), Error> {
tokio::fs::write(REPAIR_DISK_PATH, b"").await?;
Ok(())
}

View File

@@ -1,3 +1,4 @@
use std::collections::BTreeSet;
use std::num::ParseIntError;
use std::path::Path;
@@ -11,14 +12,15 @@ use crate::Error;
pub const QUIRK_PATH: &'static str = "/sys/module/usb_storage/parameters/quirks";
pub const WHITELIST: [(VendorId, ProductId); 4] = [
pub const WHITELIST: [(VendorId, ProductId); 5] = [
(VendorId(0x1d6b), ProductId(0x0002)), // root hub usb2
(VendorId(0x1d6b), ProductId(0x0003)), // root hub usb3
(VendorId(0x2109), ProductId(0x3431)),
(VendorId(0x1058), ProductId(0x262f)), // western digital black HDD
(VendorId(0x04e8), ProductId(0x4001)), // Samsung T7
];
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct VendorId(u16);
impl std::str::FromStr for VendorId {
type Err = ParseIntError;
@@ -32,7 +34,7 @@ impl std::fmt::Display for VendorId {
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct ProductId(u16);
impl std::str::FromStr for ProductId {
type Err = ParseIntError;
@@ -47,10 +49,13 @@ impl std::fmt::Display for ProductId {
}
#[derive(Clone, Debug)]
pub struct Quirks(Vec<(VendorId, ProductId)>);
pub struct Quirks(BTreeSet<(VendorId, ProductId)>);
impl Quirks {
pub fn add(&mut self, vendor: VendorId, product: ProductId) {
self.0.push((vendor, product));
self.0.insert((vendor, product));
}
pub fn remove(&mut self, vendor: VendorId, product: ProductId) {
self.0.remove(&(vendor, product));
}
pub fn contains(&self, vendor: VendorId, product: ProductId) -> bool {
self.0.contains(&(vendor, product))
@@ -74,10 +79,10 @@ impl std::str::FromStr for Quirks {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
let mut quirks = Vec::new();
let mut quirks = BTreeSet::new();
for item in s.split(",") {
if let [vendor, product, "u"] = item.splitn(3, ":").collect::<Vec<_>>().as_slice() {
quirks.push((vendor.parse()?, product.parse()?));
quirks.insert((vendor.parse()?, product.parse()?));
} else {
return Err(Error::new(
eyre!("Invalid quirk: `{}`", item),
@@ -106,7 +111,11 @@ pub async fn update_quirks(quirks: &mut Quirks) -> Result<Vec<String>, Error> {
let product = tokio::fs::read_to_string(usb_device.path().join("idProduct"))
.await?
.parse()?;
if WHITELIST.contains(&(vendor, product)) || quirks.contains(vendor, product) {
if WHITELIST.contains(&(vendor, product)) {
quirks.remove(vendor, product);
continue;
}
if quirks.contains(vendor, product) {
continue;
}
quirks.add(vendor, product);

View File

@@ -1,3 +1,5 @@
use std::time::Duration;
use tokio::process::Command;
use crate::context::rpc::RpcContextConfig;
@@ -8,6 +10,19 @@ use crate::Error;
pub const SYSTEM_REBUILD_PATH: &str = "/embassy-os/system-rebuild";
pub async fn check_time_is_synchronized() -> Result<bool, Error> {
Ok(String::from_utf8(
Command::new("timedatectl")
.arg("show")
.arg("-p")
.arg("NTPSynchronized")
.invoke(crate::ErrorKind::Unknown)
.await?,
)?
.trim()
== "NTPSynchronized=yes")
}
pub async fn init(cfg: &RpcContextConfig, product_key: &str) -> Result<(), Error> {
let should_rebuild = tokio::fs::metadata(SYSTEM_REBUILD_PATH).await.is_ok();
let secret_store = cfg.secret_store().await?;
@@ -100,6 +115,18 @@ pub async fn init(cfg: &RpcContextConfig, product_key: &str) -> Result<(), Error
};
info.save(&mut handle).await?;
let mut warn_time_not_synced = true;
for _ in 0..60 {
if check_time_is_synchronized().await? {
warn_time_not_synced = false;
break;
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
if warn_time_not_synced {
tracing::warn!("Timed out waiting for system time to synchronize");
}
crate::version::init(&mut handle).await?;
if should_rebuild {

View File

@@ -9,8 +9,8 @@ use std::time::{Duration, Instant};
use color_eyre::eyre::eyre;
use emver::VersionRange;
use futures::future::{self, BoxFuture};
use futures::{stream, FutureExt, StreamExt, TryStreamExt};
use futures::future::BoxFuture;
use futures::{FutureExt, StreamExt, TryStreamExt};
use http::header::CONTENT_LENGTH;
use http::{Request, Response, StatusCode};
use hyper::Body;
@@ -365,13 +365,13 @@ pub async fn sideload(
match pde.take() {
Some(PackageDataEntry::Installed {
installed,
manifest,
static_files,
..
}) => {
*pde = Some(PackageDataEntry::Updating {
install_progress: progress.clone(),
installed,
manifest,
manifest: manifest.clone(),
static_files,
})
}
@@ -1197,34 +1197,33 @@ pub async fn install_s9pk<R: AsyncRead + AsyncSeek + Unpin>(
dep_errs.save(&mut tx).await?;
if let PackageDataEntry::Updating {
installed: prev,
manifest: prev_manifest,
..
installed: prev, ..
} = prev
{
let prev_is_configured = prev.status.configured;
let prev_migration = prev_manifest
let prev_migration = prev
.manifest
.migrations
.to(
ctx,
version,
pkg_id,
&prev_manifest.version,
&prev_manifest.volumes,
&prev.manifest.version,
&prev.manifest.volumes,
)
.map(futures::future::Either::Left);
let migration = manifest
.migrations
.from(
ctx,
&prev_manifest.version,
&prev.manifest.version,
pkg_id,
version,
&manifest.volumes,
)
.map(futures::future::Either::Right);
let viable_migration = if prev_manifest.version > manifest.version {
let viable_migration = if prev.manifest.version > manifest.version {
prev_migration.or(migration)
} else {
migration.or(prev_migration)

View File

@@ -77,6 +77,7 @@ pub fn main_api() -> Result<(), RpcError> {
#[command(subcommands(
system::logs,
system::kernel_logs,
system::metrics,
shutdown::shutdown,
shutdown::restart,

View File

@@ -106,6 +106,7 @@ fn deserialize_string_or_utf8_array<'de, D: serde::de::Deserializer<'de>>(
#[derive(Debug)]
pub enum LogSource {
Kernel,
Service(&'static str),
Container(PackageId),
}
@@ -139,37 +140,42 @@ pub async fn fetch_logs(
cursor: Option<String>,
before_flag: bool,
) -> Result<LogResponse, Error> {
let limit = limit.unwrap_or(50);
let limit_formatted = format!("-n{}", limit);
let mut cmd = Command::new("journalctl");
let mut args = vec!["--output=json", "--output-fields=MESSAGE", &limit_formatted];
let id_formatted = match id {
let limit = limit.unwrap_or(50);
cmd.arg("--output=json");
cmd.arg("--output-fields=MESSAGE");
cmd.arg(format!("-n{}", limit));
match id {
LogSource::Kernel => {
cmd.arg("-k");
}
LogSource::Service(id) => {
args.push("-u");
id.to_owned()
cmd.arg("-u");
cmd.arg(id);
}
LogSource::Container(id) => {
format!("CONTAINER_NAME={}", DockerAction::container_name(&id, None))
cmd.arg(format!(
"CONTAINER_NAME={}",
DockerAction::container_name(&id, None)
));
}
};
args.push(&id_formatted);
let cursor_formatted = format!("--after-cursor={}", cursor.clone().unwrap_or("".to_owned()));
let mut get_prev_logs_and_reverse = false;
if cursor.is_some() {
args.push(&cursor_formatted);
cmd.arg(&cursor_formatted);
if before_flag {
get_prev_logs_and_reverse = true;
}
}
if get_prev_logs_and_reverse {
args.push("--reverse");
cmd.arg("--reverse");
}
let mut child = Command::new("journalctl")
.args(args)
.stdout(Stdio::piped())
.spawn()?;
let mut child = cmd.stdout(Stdio::piped()).spawn()?;
let out = BufReader::new(
child
.stdout

View File

@@ -12,15 +12,45 @@ pub fn marketplace() -> Result<(), Error> {
#[command]
pub async fn get(#[arg] url: Url) -> Result<Value, Error> {
let response = reqwest::get(url)
let mut response = reqwest::get(url)
.await
.with_kind(crate::ErrorKind::Network)?;
let status = response.status();
if status.is_success() {
response
.json()
.await
.with_kind(crate::ErrorKind::Deserialization)
match response
.headers_mut()
.remove("Content-Type")
.as_ref()
.and_then(|h| h.to_str().ok())
.and_then(|h| h.split(";").next())
.map(|h| h.trim())
{
Some("application/json") => response
.json()
.await
.with_kind(crate::ErrorKind::Deserialization),
Some("text/plain") => Ok(Value::String(
response
.text()
.await
.with_kind(crate::ErrorKind::Registry)?,
)),
Some(ctype) => Ok(Value::String(format!(
"data:{};base64,{}",
ctype,
base64::encode_config(
&response
.bytes()
.await
.with_kind(crate::ErrorKind::Registry)?,
base64::URL_SAFE
)
))),
_ => Err(Error::new(
eyre!("missing Content-Type"),
crate::ErrorKind::Registry,
)),
}
} else {
let message = response.text().await.with_kind(crate::ErrorKind::Network)?;
Err(Error::new(

View File

@@ -11,5 +11,8 @@ server {{
client_max_body_size 0;
proxy_request_buffering off;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}}
}}

View File

@@ -1,4 +1,4 @@
use std::collections::{BTreeMap, HashMap};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
@@ -79,6 +79,7 @@ pub async fn add(
.await
{
tracing::error!("Failed to add new WiFi network '{}': {}", ssid, err);
tracing::debug!("{:?}", err);
return Err(Error::new(
color_eyre::eyre::eyre!("Failed adding {}", ssid),
ErrorKind::Wifi,
@@ -292,12 +293,12 @@ pub async fn get(
wpa_supplicant.list_wifi_low()
);
let signal_strengths = signal_strengths?;
let list_networks = list_networks?;
let list_networks: BTreeSet<_> = list_networks?.into_iter().map(|(_, x)| x.ssid).collect();
let available_wifi = {
let mut wifi_list: Vec<WifiListOut> = signal_strengths
.clone()
.into_iter()
.filter(|(ssid, _)| !list_networks.contains_key(ssid))
.filter(|(ssid, _)| !list_networks.contains(ssid))
.map(|(ssid, info)| WifiListOut {
ssid,
strength: info.strength,
@@ -309,13 +310,13 @@ pub async fn get(
wifi_list
};
let ssids: HashMap<Ssid, SignalStrength> = list_networks
.into_keys()
.map(|x| {
.into_iter()
.map(|ssid| {
let signal_strength = signal_strengths
.get(&x)
.get(&ssid)
.map(|x| x.strength)
.unwrap_or_default();
(x, signal_strength)
(ssid, signal_strength)
})
.collect();
let current = current_res?;
@@ -341,10 +342,13 @@ pub async fn get_available(
wpa_supplicant.list_wifi_low(),
wpa_supplicant.list_networks_low()
);
let network_list = network_list?;
let network_list = network_list?
.into_iter()
.map(|(_, info)| info.ssid)
.collect::<BTreeSet<_>>();
let mut wifi_list: Vec<WifiListOut> = wifi_list?
.into_iter()
.filter(|(ssid, _)| !network_list.contains_key(ssid))
.filter(|(ssid, _)| !network_list.contains(ssid))
.map(|(ssid, info)| WifiListOut {
ssid,
strength: info.strength,
@@ -369,7 +373,7 @@ pub async fn set_country(
}
let mut wpa_supplicant = ctx.wifi_manager.write().await;
wpa_supplicant.set_country_low(country.alpha2()).await?;
for (_ssid, network_id) in wpa_supplicant.list_networks_low().await? {
for (network_id, _wifi_info) in wpa_supplicant.list_networks_low().await? {
wpa_supplicant.remove_network_low(network_id).await?;
}
wpa_supplicant.remove_all_connections().await?;
@@ -421,6 +425,12 @@ impl SignalStrength {
}
}
#[derive(Debug, Clone)]
pub struct WifiInfo {
ssid: Ssid,
device: Option<String>,
}
#[derive(Clone, Debug)]
pub struct Psk(String);
impl WpaCli {
@@ -446,20 +456,20 @@ impl WpaCli {
}
#[instrument(skip(self, psk))]
pub async fn add_network_low(&mut self, ssid: &Ssid, psk: &Psk) -> Result<(), Error> {
let _ = Command::new("nmcli")
.arg("con")
.arg("add")
.arg("con-name")
.arg(&ssid.0)
.arg("ifname")
.arg(&self.interface)
.arg("type")
.arg("wifi")
.arg("ssid")
.arg(&ssid.0)
.invoke(ErrorKind::Wifi)
.await?;
let _ = Command::new("nmcli")
if self.find_networks(ssid).await?.is_empty() {
Command::new("nmcli")
.arg("con")
.arg("add")
.arg("con-name")
.arg(&ssid.0)
.arg("type")
.arg("wifi")
.arg("ssid")
.arg(&ssid.0)
.invoke(ErrorKind::Wifi)
.await?;
}
Command::new("nmcli")
.arg("con")
.arg("modify")
.arg(&ssid.0)
@@ -467,7 +477,20 @@ impl WpaCli {
.arg("wpa-psk")
.invoke(ErrorKind::Wifi)
.await?;
let _ = Command::new("nmcli")
Command::new("nmcli")
.arg("con")
.arg("modify")
.arg(&ssid.0)
.arg("ifname")
.arg(&self.interface)
.invoke(ErrorKind::Wifi)
.await
.map(|_| ())
.unwrap_or_else(|e| {
tracing::warn!("Failed to set interface {} for {}", self.interface, ssid.0);
tracing::debug!("{:?}", e);
});
Command::new("nmcli")
.arg("con")
.arg("modify")
.arg(&ssid.0)
@@ -526,27 +549,32 @@ impl WpaCli {
Ok(())
}
#[instrument]
pub async fn list_networks_low(&self) -> Result<BTreeMap<Ssid, NetworkId>, Error> {
pub async fn list_networks_low(&self) -> Result<BTreeMap<NetworkId, WifiInfo>, Error> {
let r = Command::new("nmcli")
.arg("-t")
.arg("c")
.arg("show")
.invoke(ErrorKind::Wifi)
.await?;
Ok(String::from_utf8(r)?
.lines()
let r = String::from_utf8(r)?;
tracing::info!("JCWM: all the networks: {:?}", r);
Ok(r.lines()
.filter_map(|l| {
let mut cs = l.split(':');
let name = Ssid(cs.next()?.to_owned());
let uuid = NetworkId(cs.next()?.to_owned());
let connection_type = cs.next()?;
let device = cs.next()?;
if !device.contains("wlan0") || !connection_type.contains("wireless") {
let device = cs.next();
if !connection_type.contains("wireless") {
return None;
}
Some((name, uuid))
let info = WifiInfo {
ssid: name,
device: device.map(|x| x.to_owned()),
};
Some((uuid, info))
})
.collect::<BTreeMap<Ssid, NetworkId>>())
.collect::<BTreeMap<NetworkId, WifiInfo>>())
}
#[instrument]
@@ -606,12 +634,37 @@ impl WpaCli {
.await?;
Ok(())
}
pub async fn check_network(&self, ssid: &Ssid) -> Result<Option<NetworkId>, Error> {
Ok(self.list_networks_low().await?.remove(ssid))
async fn check_active_network(&self, ssid: &Ssid) -> Result<Option<NetworkId>, Error> {
Ok(self
.list_networks_low()
.await?
.iter()
.find_map(|(network_id, wifi_info)| {
wifi_info.device.as_ref()?;
if wifi_info.ssid == *ssid {
Some(network_id.clone())
} else {
None
}
}))
}
pub async fn find_networks(&self, ssid: &Ssid) -> Result<Vec<NetworkId>, Error> {
Ok(self
.list_networks_low()
.await?
.iter()
.filter_map(|(network_id, wifi_info)| {
if wifi_info.ssid == *ssid {
Some(network_id.clone())
} else {
None
}
})
.collect())
}
#[instrument(skip(db))]
pub async fn select_network(&mut self, db: impl DbHandle, ssid: &Ssid) -> Result<bool, Error> {
let m_id = self.check_network(ssid).await?;
let m_id = self.check_active_network(ssid).await?;
match m_id {
None => Err(Error::new(
color_eyre::eyre::eyre!("SSID Not Found"),
@@ -663,14 +716,15 @@ impl WpaCli {
}
#[instrument(skip(db))]
pub async fn remove_network(&mut self, db: impl DbHandle, ssid: &Ssid) -> Result<bool, Error> {
match self.check_network(ssid).await? {
None => Ok(false),
Some(x) => {
self.remove_network_low(x).await?;
self.save_config(db).await?;
Ok(true)
}
let found_networks = self.find_networks(ssid).await?;
if found_networks.is_empty() {
return Ok(true);
}
for network_id in found_networks {
self.remove_network_low(network_id).await?;
}
self.save_config(db).await?;
Ok(true)
}
#[instrument(skip(psk, db))]
pub async fn set_add_network(

View File

@@ -1,4 +1,8 @@
map $http_upgrade $connection_upgrade {{
default upgrade;
'' $http_connection;
}}
server {{
listen 443 ssl default_server;

View File

@@ -68,7 +68,7 @@ impl ImageTag {
crate::ErrorKind::ValidateS9pk,
));
}
if id != &self.package_id {
if version != &self.version {
return Err(Error::new(
eyre!(
"Contains image with incorrect version: expected {} received {}",

View File

@@ -28,6 +28,7 @@ use crate::context::rpc::RpcContextConfig;
use crate::context::setup::SetupResult;
use crate::context::SetupContext;
use crate::db::model::RecoveredPackageInfo;
use crate::disk::fsck::RepairStrategy;
use crate::disk::main::DEFAULT_PASSWORD;
use crate::disk::mount::filesystem::block_dev::BlockDev;
use crate::disk::mount::filesystem::cifs::Cifs;
@@ -96,7 +97,13 @@ pub async fn attach(
#[context] ctx: SetupContext,
#[arg] guid: Arc<String>,
) -> Result<SetupResult, Error> {
crate::disk::main::import(&*guid, &ctx.datadir, DEFAULT_PASSWORD).await?;
crate::disk::main::import(
&*guid,
&ctx.datadir,
RepairStrategy::Preen,
DEFAULT_PASSWORD,
)
.await?;
let product_key_path = Path::new("/embassy-data/main/product_key.txt");
if tokio::fs::metadata(product_key_path).await.is_ok() {
let pkey = tokio::fs::read_to_string(product_key_path).await?;
@@ -301,7 +308,13 @@ pub async fn execute_inner(
)
.await?,
);
crate::disk::main::import(&*guid, &ctx.datadir, DEFAULT_PASSWORD).await?;
crate::disk::main::import(
&*guid,
&ctx.datadir,
RepairStrategy::Preen,
DEFAULT_PASSWORD,
)
.await?;
let res = if let Some(recovery_source) = recovery_source {
let (tor_addr, root_ca, recover_fut) = recover(

View File

@@ -8,7 +8,7 @@ use tokio::sync::RwLock;
use tracing::instrument;
use crate::context::RpcContext;
use crate::disk::util::{get_available, get_percentage, get_used};
use crate::disk::util::{get_available, get_used};
use crate::logs::{display_logs, fetch_logs, LogResponse, LogSource};
use crate::shutdown::Shutdown;
use crate::util::serde::{display_serializable, IoFormat};
@@ -31,6 +31,21 @@ pub async fn logs(
.await?)
}
#[command(rename = "kernel-logs", display(display_logs))]
pub async fn kernel_logs(
#[arg] limit: Option<usize>,
#[arg] cursor: Option<String>,
#[arg] before_flag: Option<bool>,
) -> Result<LogResponse, Error> {
Ok(fetch_logs(
LogSource::Kernel,
limit,
cursor,
before_flag.unwrap_or(false),
)
.await?)
}
#[derive(Serialize, Deserialize)]
pub struct MetricLeaf<T> {
value: T,

View File

@@ -88,40 +88,40 @@ fn display_update_result(status: WithRevision<UpdateResult>, _: &ArgMatches<'_>)
const HEADER_KEY: &str = "x-eos-hash";
#[derive(Debug, Clone, Copy)]
enum WritableDrives {
pub enum WritableDrives {
Green,
Blue,
}
impl WritableDrives {
fn label(&self) -> &'static str {
pub fn label(&self) -> &'static str {
match self {
Self::Green => "green",
Self::Blue => "blue",
}
}
fn block_dev(&self) -> &'static Path {
pub fn block_dev(&self) -> &'static Path {
Path::new(match self {
Self::Green => "/dev/mmcblk0p3",
Self::Blue => "/dev/mmcblk0p4",
})
}
fn part_uuid(&self) -> &'static str {
pub fn part_uuid(&self) -> &'static str {
match self {
Self::Green => "cb15ae4d-03",
Self::Blue => "cb15ae4d-04",
}
}
fn as_fs(&self) -> impl FileSystem {
pub fn as_fs(&self) -> impl FileSystem {
BlockDev::new(self.block_dev())
}
}
/// This will be where we are going to be putting the new update
#[derive(Debug, Clone, Copy)]
struct NewLabel(WritableDrives);
pub struct NewLabel(pub WritableDrives);
/// This is our current label where the os is running
struct CurrentLabel(WritableDrives);
pub struct CurrentLabel(pub WritableDrives);
lazy_static! {
static ref PARSE_COLOR: Regex = Regex::new("LABEL=(\\w+)[ \t]+/").unwrap();
@@ -259,7 +259,7 @@ async fn do_update(
}
#[instrument]
async fn query_mounted_label() -> Result<(NewLabel, CurrentLabel), Error> {
pub async fn query_mounted_label() -> Result<(NewLabel, CurrentLabel), Error> {
let output = tokio::fs::read_to_string("/etc/fstab")
.await
.with_ctx(|_| (crate::ErrorKind::Filesystem, "/etc/fstab"))?;
@@ -445,9 +445,18 @@ async fn swap_boot_label(new_label: NewLabel) -> Result<(), Error> {
new_label.0.label()
))
.arg(mounted.as_ref().join("etc/fstab"))
.output()
.invoke(crate::ErrorKind::Filesystem)
.await?;
mounted.unmount().await?;
Command::new("sed")
.arg("-i")
.arg(&format!(
"s/PARTUUID=cb15ae4d-\\(03\\|04\\)/PARTUUID={}/g",
new_label.0.part_uuid()
))
.arg(Path::new(BOOT_RW_PATH).join("cmdline.txt.orig"))
.invoke(crate::ErrorKind::Filesystem)
.await?;
Command::new("sed")
.arg("-i")
.arg(&format!(
@@ -455,7 +464,7 @@ async fn swap_boot_label(new_label: NewLabel) -> Result<(), Error> {
new_label.0.part_uuid()
))
.arg(Path::new(BOOT_RW_PATH).join("cmdline.txt"))
.output()
.invoke(crate::ErrorKind::Filesystem)
.await?;
UPDATED.store(true, Ordering::SeqCst);

View File

@@ -9,13 +9,15 @@ use rpc_toolkit::command;
use crate::{Error, ResultExt};
mod v0_3_0;
mod v0_3_0_1;
pub type Current = v0_3_0::Version;
pub type Current = v0_3_0_1::Version;
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
enum Version {
V0_3_0(Wrapper<v0_3_0::Version>),
V0_3_0_1(Wrapper<v0_3_0_1::Version>),
Other(emver::Version),
}
@@ -111,6 +113,7 @@ pub async fn init<Db: DbHandle>(db: &mut Db) -> Result<(), Error> {
let version: Version = db.get(&ptr).await?;
match version {
Version::V0_3_0(v) => v.0.migrate_to(&Current::new(), db).await?,
Version::V0_3_0_1(v) => v.0.migrate_to(&Current::new(), db).await?,
Version::Other(_) => {
return Err(Error::new(
eyre!("Cannot downgrade"),

View File

@@ -5,7 +5,7 @@ use super::*;
const V0_3_0: emver::Version = emver::Version::new(0, 3, 0, 0);
lazy_static! {
static ref V0_3_0_COMPAT: VersionRange = VersionRange::Conj(
pub static ref V0_3_0_COMPAT: VersionRange = VersionRange::Conj(
Box::new(VersionRange::Anchor(
emver::GTE,
emver::Version::new(0, 3, 0, 0),

View File

@@ -0,0 +1,46 @@
use std::path::Path;
use emver::VersionRange;
use tokio::process::Command;
use super::*;
use crate::disk::quirks::{fetch_quirks, save_quirks, update_quirks};
use crate::disk::BOOT_RW_PATH;
use crate::update::query_mounted_label;
use crate::util::Invoke;
const V0_3_0_1: emver::Version = emver::Version::new(0, 3, 0, 1);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_3_0::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> emver::Version {
V0_3_0_1
}
fn compat(&self) -> &'static VersionRange {
&*v0_3_0::V0_3_0_COMPAT
}
async fn up<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
let (_, current) = query_mounted_label().await?;
Command::new("sed")
.arg("-i")
.arg(&format!(
"s/PARTUUID=cb15ae4d-\\(03\\|04\\)/PARTUUID={}/g",
current.0.part_uuid()
))
.arg(Path::new(BOOT_RW_PATH).join("cmdline.txt.orig"))
.invoke(crate::ErrorKind::Filesystem)
.await?;
let mut q = fetch_quirks().await?;
update_quirks(&mut q).await?;
save_quirks(&q).await?;
Ok(())
}
async fn down<Db: DbHandle>(&self, _db: &mut Db) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -16,6 +16,9 @@ passwd -l start9
! test -f /etc/docker/daemon.json || rm /etc/docker/daemon.json
mount -o remount,rw /boot
apt-mark hold raspberrypi-bootloader
apt-mark hold raspberrypi-kernel
apt-get update
apt-get install -y \
tor \
@@ -34,7 +37,6 @@ apt-get install -y \
ecryptfs-utils \
cifs-utils \
samba-common-bin \
ntp \
network-manager \
vim \
jq \
@@ -87,9 +89,9 @@ rm -rf /var/lib/tor/*
raspi-config nonint enable_overlayfs
# create a copy of the cmdline *without* the quirk string, so that it can be easily amended
sudo sed -i 's/usb-storage.quirks=152d:0562:u,14cd:121c:u,0781:cfcb:u //g' /boot/cmdline.txt
sudo cp /boot/cmdline.txt /boot/cmdline.txt.orig
sudo sed -i 's/^/usb-storage.quirks=152d:0562:u,14cd:121c:u,0781:cfcb:u /g' /boot/cmdline.txt
sed -i 's/usb-storage.quirks=152d:0562:u,14cd:121c:u,0781:cfcb:u //g' /boot/cmdline.txt
cp /boot/cmdline.txt /boot/cmdline.txt.orig
sed -i 's/^/usb-storage.quirks=152d:0562:u,14cd:121c:u,0781:cfcb:u /g' /boot/cmdline.txt
systemctl disable initialization.service
sudo systemctl restart NetworkManager

View File

@@ -96,3 +96,10 @@ echo "Verification Succeeded"
sudo e2label update.img red
echo "Image Relabeled to \"red\""
echo "Compressing..."
if which pv > /dev/null; then
cat update.img | pv -s $FS_SIZE | gzip > update.img.gz
else
cat update.img | gzip > update.img.gz
fi

View File

@@ -74,6 +74,7 @@ sudo mkdir -p /tmp/eos-mnt/var/www/html
sudo cp -R frontend/dist/diagnostic-ui /tmp/eos-mnt/var/www/html/diagnostic
sudo cp -R frontend/dist/setup-wizard /tmp/eos-mnt/var/www/html/setup
sudo cp -R frontend/dist/ui /tmp/eos-mnt/var/www/html/main
sudo cp index.html /tmp/eos-mnt/var/www/html/index.html
# Make the .ssh directory
sudo mkdir -p /tmp/eos-mnt/root/.ssh

View File

@@ -38,8 +38,9 @@
}
],
"styles": [
"styles/variables.scss",
"styles/global.scss",
"projects/shared/styles/variables.scss",
"projects/shared/styles/global.scss",
"projects/shared/styles/shared.scss",
"projects/ui/src/styles.scss"
],
"scripts": []
@@ -157,8 +158,9 @@
}
],
"styles": [
"styles/variables.scss",
"styles/global.scss",
"projects/shared/styles/variables.scss",
"projects/shared/styles/global.scss",
"projects/shared/styles/shared.scss",
"projects/setup-wizard/src/styles.scss"
],
"scripts": []
@@ -276,8 +278,9 @@
}
],
"styles": [
"styles/variables.scss",
"styles/global.scss",
"projects/shared/styles/variables.scss",
"projects/shared/styles/global.scss",
"projects/shared/styles/shared.scss",
"projects/diagnostic-ui/src/styles.scss"
],
"scripts": []
@@ -376,6 +379,29 @@
}
}
},
"marketplace": {
"projectType": "library",
"root": "projects/marketplace",
"sourceRoot": "projects/marketplace/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "projects/marketplace/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/marketplace/tsconfig.prod.json"
},
"development": {
"tsConfig": "projects/marketplace/tsconfig.json"
}
},
"defaultConfiguration": "production"
}
}
},
"shared": {
"projectType": "library",
"root": "projects/shared",

View File

@@ -13,6 +13,7 @@
"mocks": {
"maskAs": "tor",
"skipStartupAlerts": true
}
},
"targetArch": "aarch64"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "embassy-os",
"version": "0.3.0",
"version": "0.3.0.1",
"author": "Start9 Labs, Inc",
"homepage": "https://start9.com/",
"scripts": {
@@ -15,21 +15,21 @@
"build:setup-wizard": "ng run setup-wizard:build",
"build:ui": "ng run ui:build && tsc projects/ui/postprocess.ts && node projects/ui/postprocess.js && git log | head -n1 > dist/ui/git-hash.txt",
"build:all": "npm run build:deps && npm run build:diagnostic-ui && npm run build:setup-wizard && npm run build:ui",
"start:diagnostic-ui": "npm run-script build-config && ionic serve --project diagnostic-ui",
"start:setup-wizard": "npm run-script build-config && ionic serve --project setup-wizard",
"start:ui": "npm run-script build-config && ionic serve --project ui",
"start:diagnostic-ui": "npm run-script build-config && ionic serve --project diagnostic-ui --address 0.0.0.0",
"start:setup-wizard": "npm run-script build-config && ionic serve --project setup-wizard --address 0.0.0.0",
"start:ui": "npm run-script build-config && ionic serve --project ui --ip --address 0.0.0.0",
"build-config": "node build-config.js"
},
"dependencies": {
"@angular/animations": "^13.2.0",
"@angular/common": "^13.2.0",
"@angular/compiler": "^13.2.0",
"@angular/core": "^13.2.0",
"@angular/forms": "^13.2.0",
"@angular/platform-browser": "^13.2.0",
"@angular/platform-browser-dynamic": "^13.2.0",
"@angular/router": "^13.2.0",
"@ionic/angular": "^6.0.3",
"@angular/animations": "^13.3.0",
"@angular/common": "^13.3.0",
"@angular/compiler": "^13.3.0",
"@angular/core": "^13.3.0",
"@angular/forms": "^13.3.0",
"@angular/platform-browser": "^13.3.0",
"@angular/platform-browser-dynamic": "^13.3.0",
"@angular/router": "^13.3.0",
"@ionic/angular": "^6.0.13",
"@ionic/storage-angular": "^3.0.6",
"@materia-ui/ngx-monaco-editor": "^6.0.0",
"@start9labs/argon2": "^0.1.0",
@@ -37,13 +37,13 @@
"aes-js": "^3.1.2",
"ajv": "^6.12.6",
"ansi-to-html": "^0.7.2",
"core-js": "^3.17.2",
"dompurify": "^2.3.3",
"fast-json-patch": "^3.1.0",
"core-js": "^3.21.1",
"dompurify": "^2.3.6",
"fast-json-patch": "^3.1.1",
"fuse.js": "^6.4.6",
"js-yaml": "^4.1.0",
"marked": "^4.0.0",
"monaco-editor": "^0.32.0",
"monaco-editor": "^0.33.0",
"mustache": "^4.2.0",
"ng-qrcode": "^6.0.0",
"patch-db-client": "file: ../../../patch-db/client",
@@ -51,32 +51,32 @@
"rxjs": "^6.6.7",
"tslib": "^2.3.0",
"uuid": "^8.3.2",
"zone.js": "^0.11.4"
"zone.js": "^0.11.5"
},
"devDependencies": {
"@angular-devkit/build-angular": "^13.1.4",
"@angular/cli": "^13.1.4",
"@angular/compiler-cli": "^13.2.0",
"@angular/language-service": "^13.2.0",
"@ionic/cli": "^6.18.1",
"@angular/compiler-cli": "^13.3.0",
"@angular/language-service": "^13.3.0",
"@ionic/cli": "^6.19.0",
"@types/aes-js": "^3.1.1",
"@types/dompurify": "^2.3.3",
"@types/estree": "^0.0.51",
"@types/js-yaml": "^4.0.5",
"@types/marked": "^4.0.0",
"@types/marked": "^4.0.3",
"@types/mustache": "^4.1.2",
"@types/node": "^16.9.1",
"@types/pbkdf2": "^3.1.0",
"@types/uuid": "^8.3.1",
"husky": "^4.3.8",
"lint-staged": "^12.1.2",
"ng-packagr": "^13.0.0",
"node-html-parser": "^5.0.0",
"prettier": "^2.5.1",
"lint-staged": "^12.3.7",
"ng-packagr": "^13.3.0",
"node-html-parser": "^5.3.3",
"prettier": "^2.6.1",
"raw-loader": "^4.0.2",
"ts-node": "^10.4.0",
"ts-node": "^10.7.0",
"tslint": "^6.1.3",
"typescript": "^4.5.2"
"typescript": "^4.6.3"
},
"husky": {
"hooks": {

View File

@@ -8,7 +8,6 @@ import { HttpClientModule } from '@angular/common/http'
import { ApiService } from './services/api/api.service'
import { MockApiService } from './services/api/mock-api.service'
import { LiveApiService } from './services/api/live-api.service'
import { HttpService } from './services/http.service'
import { GlobalErrorHandler } from './services/global-error-handler.service'
import { WorkspaceConfig } from '@start9labs/shared'
@@ -29,14 +28,7 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{
provide: ApiService,
useFactory: (http: HttpService) => {
if (useMocks) {
return new MockApiService()
} else {
return new LiveApiService(http)
}
},
deps: [HttpService],
useClass: useMocks ? MockApiService : LiveApiService,
},
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
],

View File

@@ -49,6 +49,12 @@
}}
</ion-button>
</div>
<div
*ngIf="error.code === 2 || error.code === 48"
class="ion-padding-top"
>
<ion-button (click)="repairDrive()"> {{ 'Repair Drive' }} </ion-button>
</div>
</ng-container>
<ng-template #refresh>

View File

@@ -1,5 +1,9 @@
import { Component } from '@angular/core'
import { LoadingController } from '@ionic/angular'
import {
AlertController,
IonicSafeString,
LoadingController,
} from '@ionic/angular'
import { ApiService } from 'src/app/services/api/api.service'
@Component({
@@ -20,6 +24,7 @@ export class HomePage {
constructor(
private readonly loadingCtrl: LoadingController,
private readonly api: ApiService,
private readonly alertCtrl: AlertController,
) {}
async ngOnInit() {
@@ -48,16 +53,34 @@ export class HomePage {
this.error = {
code: 25,
problem:
'Storage drive corrupted. This could be the result of data corruption or a physical damage.',
'Storage drive corrupted. This could be the result of data corruption or physical damage.',
solution:
'It may or may not be possible to re-use this drive by reformatting and recovering from backup. To enter recovery mode, click ENTER RECOVERY MODE below, then follow instructions. No data will be erased during this step.',
details: error.data?.details,
}
// filesystem I/O error - disk needs repair
} else if (error.code === 2) {
this.error = {
code: 2,
problem: 'Filesystem I/O error.',
solution:
'Repairing the disk could help resolve this issue. This will occur on a restart between the bep and chime. Please DO NOT unplug the drive or Embassy during this time or the situation will become worse.',
details: error.data?.details,
}
// disk management error - disk needs repair
} else if (error.code === 48) {
this.error = {
code: 48,
problem: 'Disk management error.',
solution:
'Repairing the disk could help resolve this issue. This will occur on a restart between the bep and chime. Please DO NOT unplug the drive or Embassy during this time or the situation will become worse.',
details: error.data?.details,
}
} else {
this.error = {
code: error.code,
problem: error.message,
solution: 'Please conact support.',
solution: 'Please contact support.',
details: error.data?.details,
}
}
@@ -101,6 +124,53 @@ export class HomePage {
}
}
async repairDrive(): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
cssClass: 'loader',
})
await loader.present()
try {
await this.api.repairDisk()
await this.api.restart()
this.restarted = true
} catch (e) {
console.error(e)
} finally {
loader.dismiss()
}
}
async presentAlertRepairDisk() {
const alert = await this.alertCtrl.create({
header: 'RepairDisk',
message: new IonicSafeString(
`<ion-text color="warning">Warning:</ion-text> This action will attempt to preform a disk repair operation and system reboot. No data will be deleted. This action should only be executed if directed by a Start9 support specialist. We recommend backing up your device before preforming this action. If anything happens to the device during the reboot (between the bep and chime), such as loosing power, a power surge, unplugging the drive, or unplugging the Embassy, the filesystem *will* be in an unrecoverable state. Please proceed with caution.`,
),
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Repair',
handler: () => {
try {
this.api.repairDisk().then(_ => {
this.restart()
})
} catch (e) {
console.error(e)
}
},
cssClass: 'enter-click',
},
],
})
await alert.present()
}
refreshPage(): void {
window.location.reload()
}

View File

@@ -1,22 +1,31 @@
export abstract class ApiService {
abstract getError (): Promise<GetErrorRes>
abstract restart (): Promise<void>
abstract forgetDrive (): Promise<void>
abstract getLogs (params: GetLogsReq): Promise<GetLogsRes>
abstract getError(): Promise<GetErrorRes>
abstract restart(): Promise<void>
abstract forgetDrive(): Promise<void>
abstract repairDisk(): Promise<void>
abstract getLogs(params: GetLogsReq): Promise<GetLogsRes>
}
export interface GetErrorRes {
code: number,
message: string,
code: number
message: string
data: { details: string }
}
export type GetLogsReq = { cursor?: string, before_flag?: boolean, limit?: number }
export type GetLogsReq = {
cursor?: string
before_flag?: boolean
limit?: number
}
export type GetLogsRes = LogsRes
export type LogsRes = { entries: Log[], 'start-cursor'?: string, 'end-cursor'?: string }
export type LogsRes = {
entries: Log[]
'start-cursor'?: string
'end-cursor'?: string
}
export interface Log {
timestamp: string
message: string
}
}

View File

@@ -24,7 +24,14 @@ export class LiveApiService extends ApiService {
forgetDrive(): Promise<void> {
return this.http.rpcRequest<void>({
method: 'diagnostic.forget-disk',
method: 'diagnostic.disk.forget',
params: {},
})
}
repairDisk(): Promise<void> {
return this.http.rpcRequest<void>({
method: 'diagnostic.disk.repair',
params: {},
})
}

View File

@@ -33,6 +33,11 @@ export class MockApiService extends ApiService {
return null
}
async repairDisk(): Promise<void> {
await pauseFor(1000)
return null
}
async getLogs(params: GetLogsReq): Promise<GetLogsRes> {
await pauseFor(1000)
let entries: Log[]

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/marketplace",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,13 @@
{
"name": "@start9labs/marketplace",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^13.2.0",
"@angular/core": "^13.2.0",
"@start9labs/shared": "^0.0.1",
"fuse.js": "^6.4.6"
},
"dependencies": {
"tslib": "^2.3.0"
}
}

View File

@@ -0,0 +1,9 @@
<ion-button
*ngFor="let cat of categories"
fill="clear"
class="category"
[class.category_selected]="cat === category"
(click)="switchCategory(cat)"
>
{{ cat }}
</ion-button>

View File

@@ -0,0 +1,14 @@
:host {
display: block;
}
.category {
font-weight: 300;
color: var(--ion-color-dark-shade);
&_selected {
font-weight: bold;
font-size: 17px;
color: var(--color);
}
}

View File

@@ -0,0 +1,32 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
} from '@angular/core'
@Component({
selector: 'marketplace-categories',
templateUrl: 'categories.component.html',
styleUrls: ['categories.component.scss'],
host: {
class: 'hidden-scrollbar ion-text-center',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CategoriesComponent {
@Input()
categories = new Set<string>()
@Input()
category = ''
@Output()
readonly categoryChange = new EventEmitter<string>()
switchCategory(category: string): void {
this.category = category
this.categoryChange.emit(category)
}
}

View File

@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { CategoriesComponent } from './categories.component'
@NgModule({
imports: [CommonModule, IonicModule],
declarations: [CategoriesComponent],
exports: [CategoriesComponent],
})
export class CategoriesModule {}

View File

@@ -0,0 +1,10 @@
<ion-item [routerLink]="['/marketplace', pkg.manifest.id]">
<ion-thumbnail slot="start">
<img alt="" [src]="'data:image/png;base64,' + pkg.icon | trustUrl" />
</ion-thumbnail>
<ion-label>
<h2 class="pkg-title">{{ pkg.manifest.title }}</h2>
<h3>{{ pkg.manifest.description.short }}</h3>
<ng-content></ng-content>
</ion-label>
</ion-item>

View File

@@ -0,0 +1,4 @@
.pkg-title {
font-family: 'Montserrat', sans-serif;
font-weight: bold;
}

View File

@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '../../../types/marketplace-pkg'
@Component({
selector: 'marketplace-item',
templateUrl: 'item.component.html',
styleUrls: ['item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ItemComponent {
@Input()
pkg: MarketplacePkg
}

View File

@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharedPipesModule } from '@start9labs/shared'
import { ItemComponent } from './item.component'
@NgModule({
imports: [IonicModule, RouterModule, SharedPipesModule],
declarations: [ItemComponent],
exports: [ItemComponent],
})
export class ItemModule {}

View File

@@ -0,0 +1,15 @@
<ion-grid>
<ion-row>
<ion-col sizeSm="8" offset-sm="2">
<ion-toolbar color="transparent">
<ion-searchbar
enterkeyhint="search"
color="dark"
debounce="250"
[ngModel]="query"
(ngModelChange)="onModelChange($event)"
></ion-searchbar>
</ion-toolbar>
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -0,0 +1,4 @@
:host {
display: block;
padding-bottom: 32px;
}

View File

@@ -0,0 +1,26 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
} from '@angular/core'
@Component({
selector: 'marketplace-search',
templateUrl: 'search.component.html',
styleUrls: ['search.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchComponent {
@Input()
query = ''
@Output()
readonly queryChange = new EventEmitter<string>()
onModelChange(query: string) {
this.query = query
this.queryChange.emit(query)
}
}

View File

@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { IonicModule } from '@ionic/angular'
import { SearchComponent } from './search.component'
@NgModule({
imports: [IonicModule, FormsModule],
declarations: [SearchComponent],
exports: [SearchComponent],
})
export class SearchModule {}

View File

@@ -0,0 +1,38 @@
<div class="hidden-scrollbar ion-text-center">
<ion-button *ngFor="let cat of ['', '', '', '', '', '', '']" fill="clear">
<ion-skeleton-text
animated
style="width: 80px; border-radius: 0"
></ion-skeleton-text>
</ion-button>
</div>
<div class="divider" style="margin: 24px 0"></div>
<ion-grid>
<ion-row>
<ion-col
*ngFor="let pkg of ['', '', '', '']"
sizeXs="12"
sizeSm="12"
sizeMd="6"
>
<ion-item>
<ion-thumbnail slot="start">
<ion-skeleton-text
style="border-radius: 100%"
animated
></ion-skeleton-text>
</ion-thumbnail>
<ion-label>
<ion-skeleton-text
animated
style="width: 150px; height: 18px; margin-bottom: 8px"
></ion-skeleton-text>
<ion-skeleton-text animated style="width: 400px"></ion-skeleton-text>
<ion-skeleton-text animated style="width: 100px"></ion-skeleton-text>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -0,0 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
@Component({
selector: 'marketplace-skeleton',
templateUrl: 'skeleton.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SkeletonComponent {}

View File

@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { SkeletonComponent } from './skeleton.component'
@NgModule({
imports: [CommonModule, IonicModule],
declarations: [SkeletonComponent],
exports: [SkeletonComponent],
})
export class SkeletonModule {}

View File

@@ -0,0 +1,32 @@
<ion-content>
<ng-container *ngIf="notes$ | async as notes; else loading">
<div *ngFor="let note of notes | keyvalue: asIsOrder">
<ion-button
expand="full"
color="light"
class="version-button"
[class.ion-activated]="isSelected(note.key)"
(click)="setSelected(note.key)"
>
<p class="version">{{ note.key | displayEmver }}</p>
</ion-button>
<ion-card
elementRef
#element="elementRef"
class="panel"
color="light"
[id]="note.key"
[style.maxHeight.px]="getDocSize(note.key, element)"
>
<ion-text
id="release-notes"
[innerHTML]="note.value | markdown"
></ion-text>
</ion-card>
</div>
</ng-container>
<ng-template #loading>
<text-spinner text="Loading Release Notes"></text-spinner>
</ng-template>
</ion-content>

View File

@@ -0,0 +1,23 @@
:host {
flex: 1 1 0;
}
.panel {
margin: 0;
padding: 0 24px;
transition: max-height 0.2s ease-out;
}
.active {
border: 5px solid #4d4d4d;
}
.version-button {
height: 50px;
margin: 1px;
}
.version {
position: absolute;
left: 10px;
}

View File

@@ -0,0 +1,38 @@
import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { AbstractMarketplaceService } from '../../services/marketplace.service'
@Component({
selector: 'release-notes',
templateUrl: './release-notes.component.html',
styleUrls: ['./release-notes.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ReleaseNotesComponent {
private readonly pkgId = this.route.snapshot.paramMap.get('pkgId')
private selected: string | null = null
readonly notes$ = this.marketplaceService.getReleaseNotes(this.pkgId)
constructor(
private readonly route: ActivatedRoute,
private readonly marketplaceService: AbstractMarketplaceService,
) {}
isSelected(key: string): boolean {
return this.selected === key
}
setSelected(selected: string) {
this.selected = this.isSelected(selected) ? null : selected
}
getDocSize(key: string, { nativeElement }: ElementRef<HTMLElement>) {
return this.isSelected(key) ? nativeElement.scrollHeight : 0
}
asIsOrder(a: any, b: any) {
return 0
}
}

View File

@@ -1,20 +1,25 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { MarkdownPage } from './markdown.page'
import {
EmverPipesModule,
MarkdownPipeModule,
TextSpinnerComponentModule,
ElementModule,
} from '@start9labs/shared'
import { ReleaseNotesComponent } from './release-notes.component'
@NgModule({
declarations: [MarkdownPage],
imports: [
CommonModule,
IonicModule,
MarkdownPipeModule,
TextSpinnerComponentModule,
EmverPipesModule,
MarkdownPipeModule,
ElementModule,
],
exports: [MarkdownPage],
declarations: [ReleaseNotesComponent],
exports: [ReleaseNotesComponent],
})
export class MarkdownPageModule {}
export class ReleaseNotesModule {}

View File

@@ -0,0 +1,23 @@
<!-- release notes -->
<ion-item-divider>
New in {{ pkg.manifest.version | displayEmver }}
<ion-button routerLink="notes" class="all-notes" fill="clear" color="dark">
All Release Notes
<ion-icon slot="end" name="arrow-forward"></ion-icon>
</ion-button>
</ion-item-divider>
<ion-item lines="none" color="transparent">
<ion-label>
<div
class="release-notes"
[innerHTML]="pkg.manifest['release-notes'] | markdown"
></div>
</ion-label>
</ion-item>
<!-- description -->
<ion-item-divider>Description</ion-item-divider>
<ion-item lines="none" color="transparent">
<ion-label>
<div class="release-notes">{{ pkg.manifest.description.long }}</div>
</ion-label>
</ion-item>

View File

@@ -0,0 +1,9 @@
.all-notes {
position: absolute;
right: 10px;
}
.release-notes {
overflow: auto;
max-height: 120px;
}

View File

@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '../../../types/marketplace-pkg'
@Component({
selector: 'marketplace-about',
templateUrl: 'about.component.html',
styleUrls: ['about.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AboutComponent {
@Input()
pkg: MarketplacePkg
}

View File

@@ -0,0 +1,20 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { EmverPipesModule, MarkdownPipeModule } from '@start9labs/shared'
import { AboutComponent } from './about.component'
@NgModule({
imports: [
CommonModule,
RouterModule,
IonicModule,
MarkdownPipeModule,
EmverPipesModule,
],
declarations: [AboutComponent],
exports: [AboutComponent],
})
export class AboutModule {}

View File

@@ -0,0 +1,76 @@
<ion-item-divider>Additional Info</ion-item-divider>
<ion-card>
<ion-grid>
<ion-row>
<ion-col sizeSm="12" sizeMd="6">
<ion-item-group>
<ion-item button detail="false" (click)="presentAlertVersions()">
<ion-label>
<h2>Other Versions</h2>
<p>Click to view other versions</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
</ion-item>
<ion-item button detail="false" (click)="presentModalMd('license')">
<ion-label>
<h2>License</h2>
<p>{{ pkg.manifest.license }}</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
</ion-item>
<ion-item
button
detail="false"
(click)="presentModalMd('instructions')"
>
<ion-label>
<h2>Instructions</h2>
<p>Click to view instructions</p>
</ion-label>
<ion-icon slot="end" name="chevron-forward"></ion-icon>
</ion-item>
</ion-item-group>
</ion-col>
<ion-col sizeSm="12" sizeMd="6">
<ion-item-group>
<ion-item
[href]="pkg.manifest['upstream-repo']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Source Repository</h2>
<p>{{ pkg.manifest['upstream-repo'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="pkg.manifest['wrapper-repo']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Wrapper Repository</h2>
<p>{{ pkg.manifest['wrapper-repo'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
<ion-item
[href]="pkg.manifest['support-site']"
target="_blank"
rel="noreferrer"
detail="false"
>
<ion-label>
<h2>Support Site</h2>
<p>{{ pkg.manifest['support-site'] }}</p>
</ion-label>
<ion-icon slot="end" name="open-outline"></ion-icon>
</ion-item>
</ion-item-group>
</ion-col>
</ion-row>
</ion-grid>
</ion-card>

View File

@@ -0,0 +1,74 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
} from '@angular/core'
import { AlertController, ModalController } from '@ionic/angular'
import { displayEmver, Emver, MarkdownComponent } from '@start9labs/shared'
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
import { MarketplacePkg } from '../../../types/marketplace-pkg'
@Component({
selector: 'marketplace-additional',
templateUrl: 'additional.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AdditionalComponent {
@Input()
pkg: MarketplacePkg
@Output()
version = new EventEmitter<string>()
constructor(
private readonly alertCtrl: AlertController,
private readonly modalCtrl: ModalController,
private readonly emver: Emver,
private readonly marketplaceService: AbstractMarketplaceService,
) {}
async presentAlertVersions() {
const alert = await this.alertCtrl.create({
header: 'Versions',
inputs: this.pkg.versions
.sort((a, b) => -1 * this.emver.compare(a, b))
.map(v => ({
name: v, // for CSS
type: 'radio',
label: displayEmver(v), // appearance on screen
value: v, // literal SEM version value
checked: this.pkg.manifest.version === v,
})),
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Ok',
handler: (version: string) => this.version.emit(version),
cssClass: 'enter-click',
},
],
})
await alert.present()
}
async presentModalMd(title: string) {
const content = this.marketplaceService.getPackageMarkdown(
title,
this.pkg.manifest.id,
)
const modal = await this.modalCtrl.create({
componentProps: { title, content },
component: MarkdownComponent,
})
await modal.present()
}
}

View File

@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { MarkdownModule } from '@start9labs/shared'
import { AdditionalComponent } from './additional.component'
@NgModule({
imports: [IonicModule, MarkdownModule],
declarations: [AdditionalComponent],
exports: [AdditionalComponent],
})
export class AdditionalModule {}

View File

@@ -0,0 +1,30 @@
<ion-item-divider>Dependencies</ion-item-divider>
<ion-grid>
<ion-row>
<ion-col
*ngFor="let dep of pkg.manifest.dependencies | keyvalue"
sizeSm="12"
sizeMd="6"
>
<ion-item [routerLink]="['/marketplace', dep.key]">
<ion-thumbnail slot="start">
<img alt="" [src]="getImg(dep.key) | trustUrl" />
</ion-thumbnail>
<ion-label>
<h2>
{{ pkg['dependency-metadata'][dep.key].title }}
<ng-container [ngSwitch]="dep.value.requirement.type">
<span *ngSwitchCase="'required'">(required)</span>
<span *ngSwitchCase="'opt-out'">(required by default)</span>
<span *ngSwitchCase="'opt-in'">(optional)</span>
</ng-container>
</h2>
<p>
<small>{{ dep.value.version | displayEmver }}</small>
</p>
<p>{{ dep.value.description }}</p>
</ion-label>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -0,0 +1,17 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '../../../types/marketplace-pkg'
@Component({
selector: 'marketplace-dependencies',
templateUrl: 'dependencies.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DependenciesComponent {
@Input()
pkg: MarketplacePkg
getImg(key: string): string {
return 'data:image/png;base64,' + this.pkg['dependency-metadata'][key].icon
}
}

View File

@@ -0,0 +1,20 @@
import { CommonModule } from '@angular/common'
import { NgModule } from '@angular/core'
import { RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared'
import { DependenciesComponent } from './dependencies.component'
@NgModule({
imports: [
CommonModule,
RouterModule,
IonicModule,
SharedPipesModule,
EmverPipesModule,
],
declarations: [DependenciesComponent],
exports: [DependenciesComponent],
})
export class DependenciesModule {}

View File

@@ -0,0 +1,22 @@
<ion-grid>
<ion-row>
<ion-col sizeXs="12" sizeMd="9">
<div class="header">
<img
class="logo"
alt=""
[src]="'data:image/png;base64,' + pkg.icon | trustUrl"
/>
<div class="text">
<h1 class="title">{{ pkg.manifest.title }}</h1>
<p class="version">{{ pkg.manifest.version | displayEmver }}</p>
<ng-content></ng-content>
</div>
</div>
</ion-col>
<ion-col sizeMd="3" sizeXs="12" class="ion-align-self-center">
<ng-content select="[position=controls]"></ng-content>
</ion-col>
</ion-row>
<ng-content select="ion-row"></ng-content>
</ion-grid>

View File

@@ -0,0 +1,26 @@
.header {
font-family: 'Montserrat', sans-serif;
padding: 2%;
}
.logo {
min-width: 15%;
max-width: 18%;
}
.text {
margin-left: 5%;
display: inline-block;
vertical-align: top;
}
.title {
margin: 0 0 0 -2px;
font-size: calc(20px + 3vw);
}
.version {
padding: 4px 0 12px 0;
margin: 0;
font-size: calc(10px + 1vw);
}

View File

@@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { MarketplacePkg } from '../../../types/marketplace-pkg'
@Component({
selector: 'marketplace-package',
templateUrl: 'package.component.html',
styleUrls: ['package.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PackageComponent {
@Input()
pkg: MarketplacePkg
}

View File

@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core'
import { IonicModule } from '@ionic/angular'
import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared'
import { PackageComponent } from './package.component'
@NgModule({
imports: [IonicModule, SharedPipesModule, EmverPipesModule],
declarations: [PackageComponent],
exports: [PackageComponent],
})
export class PackageModule {}

View File

@@ -0,0 +1,71 @@
import { NgModule, Pipe, PipeTransform } from '@angular/core'
import Fuse from 'fuse.js/dist/fuse.min.js'
import { MarketplacePkg } from '../types/marketplace-pkg'
import { MarketplaceManifest } from '../types/marketplace-manifest'
const defaultOps = {
isCaseSensitive: false,
includeScore: true,
shouldSort: true,
includeMatches: false,
findAllMatches: false,
minMatchCharLength: 1,
location: 0,
threshold: 0.6,
distance: 100,
useExtendedSearch: false,
ignoreLocation: false,
ignoreFieldNorm: false,
keys: [
'manifest.id',
'manifest.title',
'manifest.description.short',
'manifest.description.long',
],
}
@Pipe({
name: 'filterPackages',
})
export class FilterPackagesPipe implements PipeTransform {
transform(
packages: MarketplacePkg[] | null,
query: string,
category: string,
local: Record<string, { manifest: MarketplaceManifest }> = {},
): MarketplacePkg[] | null {
if (!packages) {
return null
}
if (query) {
const fuse = new Fuse(packages, defaultOps)
return fuse.search(query).map(p => p.item)
}
if (category === 'updates') {
return packages.filter(
({ manifest }) =>
local[manifest.id] &&
manifest.version !== local[manifest.id].manifest.version,
)
}
const pkgsToSort = packages.filter(
p => category === 'all' || p.categories.includes(category),
)
const fuse = new Fuse(pkgsToSort, { ...defaultOps, threshold: 1 })
return fuse
.search(category !== 'all' ? category || '' : 'bit')
.map(p => p.item)
}
}
@NgModule({
declarations: [FilterPackagesPipe],
exports: [FilterPackagesPipe],
})
export class FilterPackagesPipeModule {}

View File

@@ -0,0 +1,32 @@
/*
* Public API Surface of @start9labs/marketplace
*/
export * from './pages/list/categories/categories.component'
export * from './pages/list/categories/categories.module'
export * from './pages/list/item/item.component'
export * from './pages/list/item/item.module'
export * from './pages/list/search/search.component'
export * from './pages/list/search/search.module'
export * from './pages/list/skeleton/skeleton.component'
export * from './pages/list/skeleton/skeleton.module'
export * from './pages/release-notes/release-notes.component'
export * from './pages/release-notes/release-notes.module'
export * from './pages/show/about/about.component'
export * from './pages/show/about/about.module'
export * from './pages/show/additional/additional.component'
export * from './pages/show/additional/additional.module'
export * from './pages/show/dependencies/dependencies.component'
export * from './pages/show/dependencies/dependencies.module'
export * from './pages/show/package/package.component'
export * from './pages/show/package/package.module'
export * from './pipes/filter-packages.pipe'
export * from './services/marketplace.service'
export * from './types/dependency'
export * from './types/marketplace'
export * from './types/marketplace-data'
export * from './types/marketplace-manifest'
export * from './types/marketplace-pkg'

View File

@@ -0,0 +1,19 @@
import { Observable } from 'rxjs'
import { MarketplacePkg } from '../types/marketplace-pkg'
import { Marketplace } from '../types/marketplace'
export abstract class AbstractMarketplaceService {
abstract install(id: string, version?: string): Observable<unknown>
abstract getMarketplace(): Observable<Marketplace>
abstract getReleaseNotes(id: string): Observable<Record<string, string>>
abstract getCategories(): Observable<string[]>
abstract getPackages(): Observable<MarketplacePkg[]>
abstract getPackageMarkdown(type: string, pkgId: string): Observable<string>
abstract getPackage(id: string, version: string): Observable<MarketplacePkg>
}

View File

@@ -0,0 +1,17 @@
export interface Dependency<T> {
version: string
requirement:
| {
type: 'opt-in'
how: string
}
| {
type: 'opt-out'
how: string
}
| {
type: 'required'
}
description: string | null
config: T
}

View File

@@ -0,0 +1,4 @@
export interface MarketplaceData {
categories: string[]
name: string
}

View File

@@ -0,0 +1,28 @@
import { Url } from '@start9labs/shared'
import { Dependency } from './dependency'
export interface MarketplaceManifest<T = unknown> {
id: string
title: string
version: string
description: {
short: string
long: string
}
'release-notes': string
license: string // name
'wrapper-repo': Url
'upstream-repo': Url
'support-site': Url
'marketing-site': Url
'donation-url': Url | null
alerts: {
install: string | null
uninstall: string | null
restore: string | null
start: string | null
stop: string | null
}
dependencies: Record<string, Dependency<T>>
}

View File

@@ -0,0 +1,17 @@
import { Url } from '@start9labs/shared'
import { MarketplaceManifest } from './marketplace-manifest'
export interface MarketplacePkg {
icon: Url
license: Url
instructions: Url
manifest: MarketplaceManifest
categories: string[]
versions: string[]
'dependency-metadata': {
[id: string]: {
title: string
icon: Url
}
}
}

View File

@@ -0,0 +1,4 @@
export interface Marketplace {
url: string
name: string
}

View File

@@ -0,0 +1,13 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "../../out-tsc/lib",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": ["src/test.ts", "**/*.spec.ts"]
}

View File

@@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}

View File

@@ -1,7 +1,7 @@
import { Component } from '@angular/core'
import { NavController } from '@ionic/angular'
import { ApiService } from './services/api/api.service'
import { ErrorToastService } from './services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
import { StateService } from './services/state.service'
@Component({
@@ -30,7 +30,7 @@ export class AppComponent {
await this.navCtrl.navigateForward(`/recover`)
}
} catch (e) {
this.errorToastService.present(e.message)
this.errorToastService.present(e)
}
}
}

View File

@@ -10,7 +10,7 @@ import {
DiskInfo,
DiskRecoverySource,
} from 'src/app/services/api/api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../../modals/password/password.page'
@@ -74,7 +74,7 @@ export class EmbassyPage {
await alert.present()
}
} catch (e) {
this.errorToastService.present(e.message)
this.errorToastService.present(e)
} finally {
this.loading = false
}
@@ -142,9 +142,9 @@ export class EmbassyPage {
await this.navCtrl.navigateForward(`/success`)
}
} catch (e) {
this.errorToastService.present(
`${e.message}\n\nRestart Embassy to try again.`,
)
this.errorToastService.present({
message: `${e.message}\n\nRestart Embassy to try again.`,
})
console.error(e)
} finally {
loader.dismiss()

View File

@@ -8,7 +8,7 @@ import {
} from '@ionic/angular'
import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page'
import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service'
import { PasswordPage } from '../../modals/password/password.page'
import { ProdKeyModal } from '../../modals/prod-key-modal/prod-key-modal.page'
@@ -120,7 +120,7 @@ export class RecoverPage {
this.hasShownGuidAlert = true
}
} catch (e) {
this.errorToastService.present(e.message)
this.errorToastService.present(e)
} finally {
this.loading = false
}
@@ -206,7 +206,7 @@ export class RecoverPage {
await this.stateService.importDrive(guid)
await this.navCtrl.navigateForward(`/success`)
} catch (e) {
this.errorToastService.present(e.message)
this.errorToastService.present(e)
} finally {
loader.dismiss()
}

View File

@@ -1,6 +1,6 @@
import { Component, EventEmitter, Output } from '@angular/core'
import { ToastController } from '@ionic/angular'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { ErrorToastService } from '@start9labs/shared'
import { StateService } from 'src/app/services/state.service'
@Component({

View File

@@ -1,43 +0,0 @@
import { Injectable } from '@angular/core'
import { ToastController } from '@ionic/angular'
@Injectable({
providedIn: 'root',
})
export class ErrorToastService {
private toast: HTMLIonToastElement
constructor (
private readonly toastCtrl: ToastController,
) { }
async present (message: string): Promise<void> {
if (this.toast) return
this.toast = await this.toastCtrl.create({
header: 'Error',
message,
duration: 0,
position: 'top',
cssClass: 'error-toast',
animated: true,
buttons: [
{
side: 'end',
icon: 'close',
handler: () => {
this.dismiss()
},
},
],
})
await this.toast.present()
}
async dismiss (): Promise<void> {
if (this.toast) {
await this.toast.dismiss()
this.toast = undefined
}
}
}

View File

@@ -5,8 +5,7 @@ import {
CifsRecoverySource,
DiskRecoverySource,
} from './api/api.service'
import { ErrorToastService } from './error-toast.service'
import { pauseFor } from '@start9labs/shared'
import { pauseFor, ErrorToastService } from '@start9labs/shared'
@Injectable({
providedIn: 'root',
@@ -51,9 +50,9 @@ export class StateService {
try {
progress = await this.apiService.getRecoveryStatus()
} catch (e) {
this.errorToastService.present(
`${e.message}\n\nRestart Embassy to try again.`,
)
this.errorToastService.present({
message: `${e.message}\n\nRestart Embassy to try again.`,
})
}
if (progress) {
this.dataTransferProgress = {

View File

@@ -1,6 +1,7 @@
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/shared",
"assets": ["styles"],
"lib": {
"entryFile": "src/public-api.ts"
}

View File

@@ -4,6 +4,7 @@
"peerDependencies": {
"@angular/common": "^13.2.0",
"@angular/core": "^13.2.0",
"@ionic/angular": "^6.0.3",
"@start9labs/emver": "^0.1.5"
},
"dependencies": {

View File

@@ -0,0 +1,29 @@
<ion-header>
<ion-toolbar>
<ion-title>{{ title | titlecase }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismiss()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item *ngIf="error$ | async as error">
<ion-label>
<ion-text safeLinks color="danger">{{ error }}</ion-text>
</ion-label>
</ion-item>
<div
*ngIf="content$ | async as result; else loading"
safeLinks
class="content-padding"
[innerHTML]="result | markdown"
></div>
<ng-template #loading>
<text-spinner [text]="'Loading ' + title | titlecase"></text-spinner>
</ng-template>
</ion-content>

View File

@@ -0,0 +1,33 @@
import { Component, Input } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { defer, isObservable, Observable, of } from 'rxjs'
import { catchError, ignoreElements, share } from 'rxjs/operators'
import { getErrorMessage } from '../../services/error-toast.service'
@Component({
selector: 'markdown',
templateUrl: './markdown.component.html',
styleUrls: ['./markdown.component.scss'],
})
export class MarkdownComponent {
@Input() content?: string | Observable<string>
@Input() title = ''
private readonly data$ = defer(() =>
isObservable(this.content) ? this.content : of(this.content),
).pipe(share())
readonly error$ = this.data$.pipe(
ignoreElements(),
catchError(e => of(getErrorMessage(e))),
)
readonly content$ = this.data$.pipe(catchError(() => of([])))
constructor(private readonly modalCtrl: ModalController) {}
async dismiss() {
return this.modalCtrl.dismiss(true)
}
}

View File

@@ -0,0 +1,21 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { MarkdownPipeModule } from '../../pipes/markdown/markdown.module'
import { SafeLinksModule } from '../../directives/safe-links/safe-links.module'
import { TextSpinnerComponentModule } from '../text-spinner/text-spinner.component.module'
import { MarkdownComponent } from './markdown.component'
@NgModule({
declarations: [MarkdownComponent],
imports: [
CommonModule,
IonicModule,
MarkdownPipeModule,
TextSpinnerComponentModule,
SafeLinksModule,
],
exports: [MarkdownComponent],
})
export class MarkdownModule {}

View File

@@ -0,0 +1,11 @@
import { Directive, ElementRef, Inject } from '@angular/core'
@Directive({
selector: '[elementRef]',
exportAs: 'elementRef',
})
export class ElementDirective<T extends Element> extends ElementRef<T> {
constructor(@Inject(ElementRef) { nativeElement }: ElementRef<T>) {
super(nativeElement)
}
}

View File

@@ -0,0 +1,9 @@
import { NgModule } from '@angular/core'
import { ElementDirective } from './element.directive'
@NgModule({
declarations: [ElementDirective],
exports: [ElementDirective],
})
export class ElementModule {}

View File

@@ -0,0 +1,27 @@
import { AfterViewInit, Directive, ElementRef, Inject } from '@angular/core'
import { DOCUMENT } from '@angular/common'
// TODO: Refactor to use `MutationObserver` so it works with dynamic content
@Directive({
selector: '[safeLinks]',
})
export class SafeLinksDirective implements AfterViewInit {
constructor(
@Inject(DOCUMENT) private readonly document: Document,
private readonly elementRef: ElementRef<HTMLElement>,
) {}
ngAfterViewInit() {
Array.from(this.document.links)
.filter(
link =>
link.hostname !== this.document.location.hostname &&
this.elementRef.nativeElement.contains(link),
)
.forEach(link => {
link.target = '_blank'
link.setAttribute('rel', 'noreferrer')
link.classList.add('externalLink')
})
}
}

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