mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Compare commits
10 Commits
v0.3.4-rc.
...
v0.3.4.1-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d966e35054 | ||
|
|
1675570291 | ||
|
|
9b88de656e | ||
|
|
3d39b5653d | ||
|
|
eb5f7f64ad | ||
|
|
9fc0164c4d | ||
|
|
65eb520cca | ||
|
|
f7f07932b4 | ||
|
|
de52494039 | ||
|
|
4d87ee2bb6 |
2
backend/Cargo.lock
generated
2
backend/Cargo.lock
generated
@@ -1354,7 +1354,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "embassy-os"
|
||||
version = "0.3.4"
|
||||
version = "0.3.4-rev.1"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"async-compression",
|
||||
|
||||
@@ -14,7 +14,7 @@ keywords = [
|
||||
name = "embassy-os"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/Start9Labs/embassy-os"
|
||||
version = "0.3.4"
|
||||
version = "0.3.4-rev.1"
|
||||
|
||||
[lib]
|
||||
name = "embassy"
|
||||
|
||||
@@ -54,7 +54,8 @@ impl Drop for CliContextSeed {
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let store = self.cookie_store.lock().unwrap();
|
||||
let mut store = self.cookie_store.lock().unwrap();
|
||||
store.remove("localhost", "", "local");
|
||||
store.save_json(&mut *writer).unwrap();
|
||||
writer.sync_all().unwrap();
|
||||
std::fs::rename(tmp, &self.cookie_path).unwrap();
|
||||
@@ -101,19 +102,22 @@ impl CliContext {
|
||||
.unwrap_or(Path::new("/"))
|
||||
.join(".cookies.json")
|
||||
});
|
||||
let cookie_store = Arc::new(CookieStoreMutex::new(if cookie_path.exists() {
|
||||
let mut store = CookieStore::load_json(BufReader::new(File::open(&cookie_path)?))
|
||||
.map_err(|e| eyre!("{}", e))
|
||||
.with_kind(crate::ErrorKind::Deserialization)?;
|
||||
let cookie_store = Arc::new(CookieStoreMutex::new({
|
||||
let mut store = if cookie_path.exists() {
|
||||
CookieStore::load_json(BufReader::new(File::open(&cookie_path)?))
|
||||
.map_err(|e| eyre!("{}", e))
|
||||
.with_kind(crate::ErrorKind::Deserialization)?
|
||||
} else {
|
||||
CookieStore::default()
|
||||
};
|
||||
if let Ok(local) = std::fs::read_to_string(LOCAL_AUTH_COOKIE_PATH) {
|
||||
store
|
||||
.insert_raw(&Cookie::new("local", local), &"http://localhost".parse()?)
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
}
|
||||
store
|
||||
} else {
|
||||
CookieStore::default()
|
||||
}));
|
||||
|
||||
Ok(CliContext(Arc::new(CliContextSeed {
|
||||
base_url: url.clone(),
|
||||
rpc_url: {
|
||||
|
||||
@@ -1435,16 +1435,22 @@ pub fn load_images<'a, P: AsRef<Path> + 'a + Send + Sync>(
|
||||
copy_and_shutdown(&mut File::open(&path).await?, load_in)
|
||||
.await?
|
||||
}
|
||||
Some("s9pk") => {
|
||||
copy_and_shutdown(
|
||||
&mut S9pkReader::open(&path, false)
|
||||
.await?
|
||||
.docker_images()
|
||||
.await?,
|
||||
load_in,
|
||||
)
|
||||
.await?
|
||||
Some("s9pk") => match async {
|
||||
let mut reader = S9pkReader::open(&path, true).await?;
|
||||
copy_and_shutdown(&mut reader.docker_images().await?, load_in)
|
||||
.await?;
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
.await
|
||||
{
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Error loading docker images from s9pk: {e}"
|
||||
);
|
||||
tracing::debug!("{e:?}");
|
||||
}
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use itertools::Itertools;
|
||||
use patch_db::{DbHandle, LockReceipt, LockType};
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -111,6 +112,7 @@ pub async fn check<Db: DbHandle>(
|
||||
};
|
||||
|
||||
let health_results = if let Some(started) = started {
|
||||
tracing::debug!("Checking health of {}", id);
|
||||
manifest
|
||||
.health_checks
|
||||
.check_all(
|
||||
@@ -129,6 +131,24 @@ pub async fn check<Db: DbHandle>(
|
||||
if !should_commit.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !health_results
|
||||
.iter()
|
||||
.any(|(_, res)| matches!(res, HealthCheckResult::Failure { .. }))
|
||||
{
|
||||
tracing::debug!("All health checks succeeded for {}", id);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"Some health checks failed for {}: {}",
|
||||
id,
|
||||
health_results
|
||||
.iter()
|
||||
.filter(|(_, res)| matches!(res, HealthCheckResult::Failure { .. }))
|
||||
.map(|(id, _)| &*id)
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
let current_dependents = {
|
||||
let mut checkpoint = tx.begin().await?;
|
||||
let receipts = HealthCheckStatusReceipt::new(&mut checkpoint, id).await?;
|
||||
@@ -153,9 +173,7 @@ pub async fn check<Db: DbHandle>(
|
||||
current_dependents
|
||||
};
|
||||
|
||||
tracing::debug!("Checking health of {}", id);
|
||||
let receipts = crate::dependencies::BreakTransitiveReceipts::new(&mut tx).await?;
|
||||
tracing::debug!("Got receipts {}", id);
|
||||
|
||||
for (dependent, info) in (current_dependents).0.iter() {
|
||||
let failures: BTreeMap<HealthCheckId, HealthCheckResult> = health_results
|
||||
|
||||
@@ -226,7 +226,6 @@ impl DockerProcedure {
|
||||
let name = name.docker_name();
|
||||
let name: Option<&str> = name.as_ref().map(|x| &**x);
|
||||
let mut cmd = tokio::process::Command::new("docker");
|
||||
tracing::debug!("{:?} is run", name);
|
||||
let container_name = Self::container_name(pkg_id, name);
|
||||
cmd.arg("run")
|
||||
.arg("--rm")
|
||||
@@ -408,7 +407,6 @@ impl DockerProcedure {
|
||||
let name: Option<&str> = name.as_deref();
|
||||
let mut cmd = tokio::process::Command::new("docker");
|
||||
|
||||
tracing::debug!("{:?} is exec", name);
|
||||
cmd.arg("exec");
|
||||
|
||||
cmd.args(self.docker_args_inject(pkg_id).await?);
|
||||
|
||||
@@ -429,6 +429,7 @@ async fn migrate(
|
||||
ignore_existing: false,
|
||||
exclude: Vec::new(),
|
||||
no_permissions: false,
|
||||
no_owner: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -441,6 +442,7 @@ async fn migrate(
|
||||
ignore_existing: false,
|
||||
exclude: vec!["tmp".to_owned()],
|
||||
no_permissions: false,
|
||||
no_owner: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -306,6 +306,7 @@ async fn sync_boot() -> Result<(), Error> {
|
||||
ignore_existing: false,
|
||||
exclude: Vec::new(),
|
||||
no_permissions: false,
|
||||
no_owner: false,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
|
||||
@@ -20,8 +20,9 @@ mod v0_3_2;
|
||||
mod v0_3_2_1;
|
||||
mod v0_3_3;
|
||||
mod v0_3_4;
|
||||
mod v0_3_4_1;
|
||||
|
||||
pub type Current = v0_3_4::Version;
|
||||
pub type Current = v0_3_4_1::Version;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
@@ -37,6 +38,7 @@ enum Version {
|
||||
V0_3_2_1(Wrapper<v0_3_2_1::Version>),
|
||||
V0_3_3(Wrapper<v0_3_3::Version>),
|
||||
V0_3_4(Wrapper<v0_3_4::Version>),
|
||||
V0_3_4_1(Wrapper<v0_3_4_1::Version>),
|
||||
Other(emver::Version),
|
||||
}
|
||||
|
||||
@@ -63,6 +65,7 @@ impl Version {
|
||||
Version::V0_3_2_1(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_3(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_4(Wrapper(x)) => x.semver(),
|
||||
Version::V0_3_4_1(Wrapper(x)) => x.semver(),
|
||||
Version::Other(x) => x.clone(),
|
||||
}
|
||||
}
|
||||
@@ -244,6 +247,10 @@ pub async fn init<Db: DbHandle>(
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::V0_3_4_1(v) => {
|
||||
v.0.migrate_to(&Current::new(), db, secrets, receipts)
|
||||
.await?
|
||||
}
|
||||
Version::Other(_) => {
|
||||
return Err(Error::new(
|
||||
eyre!("Cannot downgrade"),
|
||||
@@ -287,6 +294,7 @@ mod tests {
|
||||
Just(Version::V0_3_2_1(Wrapper(v0_3_2_1::Version::new()))),
|
||||
Just(Version::V0_3_3(Wrapper(v0_3_3::Version::new()))),
|
||||
Just(Version::V0_3_4(Wrapper(v0_3_4::Version::new()))),
|
||||
Just(Version::V0_3_4_1(Wrapper(v0_3_4_1::Version::new()))),
|
||||
em_version().prop_map(Version::Other),
|
||||
]
|
||||
}
|
||||
|
||||
30
backend/src/version/v0_3_4_1.rs
Normal file
30
backend/src/version/v0_3_4_1.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use async_trait::async_trait;
|
||||
use emver::VersionRange;
|
||||
|
||||
use super::v0_3_0::V0_3_0_COMPAT;
|
||||
use super::*;
|
||||
|
||||
const V0_3_4_1: emver::Version = emver::Version::new(0, 3, 4, 1);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Version;
|
||||
|
||||
#[async_trait]
|
||||
impl VersionT for Version {
|
||||
type Previous = v0_3_4::Version;
|
||||
fn new() -> Self {
|
||||
Version
|
||||
}
|
||||
fn semver(&self) -> emver::Version {
|
||||
V0_3_4_1
|
||||
}
|
||||
fn compat(&self) -> &'static VersionRange {
|
||||
&*V0_3_0_COMPAT
|
||||
}
|
||||
async fn up<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
async fn down<Db: DbHandle>(&self, _db: &mut Db, _secrets: &PgPool) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -32,3 +32,4 @@ rsync
|
||||
systemd-timesyncd
|
||||
magic-wormhole
|
||||
nyx
|
||||
bash-completion
|
||||
@@ -13,17 +13,28 @@ mkdir -p /media/embassy/next/run
|
||||
mkdir -p /media/embassy/next/dev
|
||||
mkdir -p /media/embassy/next/sys
|
||||
mkdir -p /media/embassy/next/proc
|
||||
mkdir -p /media/embassy/next/boot
|
||||
mount --bind /run /media/embassy/next/run
|
||||
mount --bind /dev /media/embassy/next/dev
|
||||
mount --bind /sys /media/embassy/next/sys
|
||||
mount --bind /proc /media/embassy/next/proc
|
||||
mount --bind /boot /media/embassy/next/boot
|
||||
|
||||
chroot /media/embassy/next
|
||||
rm /media/embassy/next/usr/local/bin/apt
|
||||
rm /media/embassy/next/usr/local/bin/apt-get
|
||||
rm /media/embassy/next/usr/local/bin/aptitude
|
||||
|
||||
chroot /media/embassy/next $@
|
||||
|
||||
ln -sf /usr/lib/embassy/scripts/fake-apt /media/embassy/next/usr/local/bin/apt
|
||||
ln -sf /usr/lib/embassy/scripts/fake-apt /media/embassy/next/usr/local/bin/apt-get
|
||||
ln -sf /usr/lib/embassy/scripts/fake-apt /media/embassy/next/usr/local/bin/aptitude
|
||||
|
||||
umount /media/embassy/next/run
|
||||
umount /media/embassy/next/dev
|
||||
umount /media/embassy/next/sys
|
||||
umount /media/embassy/next/proc
|
||||
umount /media/embassy/next/boot
|
||||
|
||||
echo 'Upgrading...'
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
set -e
|
||||
|
||||
# install dependencies
|
||||
apt update
|
||||
apt install --no-install-recommends -y xserver-xorg x11-xserver-utils xinit firefox-esr matchbox-window-manager libnss3-tools
|
||||
/usr/bin/apt update
|
||||
/usr/bin/apt install --no-install-recommends -y xserver-xorg x11-xserver-utils xinit firefox-esr matchbox-window-manager libnss3-tools
|
||||
|
||||
# create kiosk script
|
||||
cat > /home/start9/kiosk.sh << 'EOF'
|
||||
|
||||
21
build/lib/scripts/fake-apt
Executable file
21
build/lib/scripts/fake-apt
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
>&2 echo 'THIS IS NOT A STANDARD DEBIAN SYSTEM'
|
||||
>&2 echo 'USING apt COULD CAUSE IRREPARABLE DAMAGE TO YOUR EMBASSY'
|
||||
>&2 echo 'PLEASE TURN BACK NOW!!!'
|
||||
if [ "$1" == "upgrade" ] && [ "$(whoami)" == "root" ]; then
|
||||
>&2 echo 'IF YOU THINK RUNNING "sudo apt upgrade" IS A REASONABLE THING TO DO ON THIS SYSTEM, YOU PROBABLY SHOULDN'"'"'T BE ON THE COMMAND LINE.'
|
||||
>&2 echo 'YOU ARE BEING REMOVED FROM THIS SESSION FOR YOUR OWN SAFETY.'
|
||||
pkill -9 -t $(tty | sed 's|^/dev/||g')
|
||||
fi
|
||||
>&2 echo
|
||||
>&2 echo 'If you are SURE you know what you are doing, and are willing to accept the DIRE CONSEQUENCES of doing so, you can run the following command to disable this protection:'
|
||||
>&2 echo ' sudo rm /usr/local/bin/apt'
|
||||
>&2 echo
|
||||
>&2 echo 'Otherwise, what you probably want to do is run:'
|
||||
>&2 echo ' sudo /usr/lib/embassy/scripts/chroot-and-upgrade'
|
||||
>&2 echo 'You can run apt in this context to add packages to your system.'
|
||||
>&2 echo 'When you are done with your changes, type "exit" and the device will reboot into a system with the changes applied.'
|
||||
>&2 echo 'This is still NOT RECOMMENDED if you don'"'"'t know what you are doing, but at least isn'"'"'t guaranteed to break things.'
|
||||
|
||||
exit 1
|
||||
20
build/lib/scripts/persist-apt-install
Executable file
20
build/lib/scripts/persist-apt-install
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
>&2 echo "usage: $0 <PACKAGE_NAME>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TO_INSTALL=()
|
||||
while [ -n "$1" ]; do
|
||||
if ! dpkg -s "$1"; then
|
||||
TO_INSTALL+=("$1")
|
||||
fi
|
||||
shift
|
||||
done
|
||||
|
||||
if [ ${#TO_INSTALL[@]} -ne 0 ]; then
|
||||
/usr/lib/embassy/scripts/chroot-and-upgrade << EOF
|
||||
apt-get update && apt-get install -y ${TO_INSTALL[@]}
|
||||
EOF
|
||||
fi
|
||||
@@ -112,3 +112,7 @@ rm -f /etc/motd
|
||||
ln -sf /usr/lib/embassy/motd /etc/update-motd.d/00-embassy
|
||||
chmod -x /etc/update-motd.d/*
|
||||
chmod +x /etc/update-motd.d/00-embassy
|
||||
|
||||
ln -sf /usr/lib/embassy/scripts/fake-apt /usr/local/bin/apt
|
||||
ln -sf /usr/lib/embassy/scripts/fake-apt /usr/local/bin/apt-get
|
||||
ln -sf /usr/lib/embassy/scripts/fake-apt /usr/local/bin/aptitude
|
||||
@@ -11,10 +11,9 @@ cat > /etc/rsyncd.conf << RD
|
||||
uid = root
|
||||
gid = root
|
||||
use chroot = yes
|
||||
max connections = 4
|
||||
max connections = 50
|
||||
pid file = /var/run/rsyncd.pid
|
||||
exclude = lost+found/
|
||||
transfer logging = yes
|
||||
timeout = 900
|
||||
ignore nonreadable = yes
|
||||
dont compress = *.gz *.tgz *.zip *.z *.Z *.rpm *.deb *.bz2
|
||||
@@ -35,12 +34,13 @@ do
|
||||
echo "Making new dir $new_dir"
|
||||
mkdir -p $new_dir
|
||||
|
||||
if ! test -n "$(mount -l | grep $new_dir)"
|
||||
then
|
||||
echo "Mounting $filename to $new_dir"
|
||||
mount $filename $new_dir
|
||||
if test -n "$(mount -l | grep $new_dir)"; then
|
||||
umount $new_dir
|
||||
fi
|
||||
|
||||
|
||||
echo "Mounting $filename to $new_dir"
|
||||
mount $filename $new_dir
|
||||
|
||||
cat >> /etc/rsyncd.conf << INSERTING
|
||||
[$version]
|
||||
path = $version_dir
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "embassy-os",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.4.1",
|
||||
"author": "Start9 Labs, Inc",
|
||||
"homepage": "https://start9.com/",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": null,
|
||||
"ack-welcome": "0.3.4",
|
||||
"ack-welcome": "0.3.4.1",
|
||||
"marketplace": {
|
||||
"selected-url": "https://registry.start9.com/",
|
||||
"known-hosts": {
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
[formControlName]="entry.key"
|
||||
(ionFocus)="presentAlertChangeWarning(entry.key, spec)"
|
||||
(ionChange)="handleInputChange()"
|
||||
>
|
||||
</ion-textarea>
|
||||
></ion-textarea>
|
||||
<ng-template #notTextArea>
|
||||
<ion-input
|
||||
type="text"
|
||||
@@ -38,8 +37,7 @@
|
||||
[formControlName]="entry.key"
|
||||
(ionFocus)="presentAlertChangeWarning(entry.key, spec)"
|
||||
(ionChange)="handleInputChange()"
|
||||
>
|
||||
</ion-input>
|
||||
></ion-input>
|
||||
</ng-template>
|
||||
<ion-button
|
||||
*ngIf="spec.type === 'string' && spec.masked"
|
||||
@@ -59,8 +57,9 @@
|
||||
slot="end"
|
||||
color="light"
|
||||
style="font-size: medium"
|
||||
>{{ spec.units }}</ion-note
|
||||
>
|
||||
{{ spec.units }}
|
||||
</ion-note>
|
||||
</ion-item>
|
||||
<p class="error-message">
|
||||
<span *ngIf="(formGroup | getControl: entry.key).errors as errors">
|
||||
@@ -92,11 +91,11 @@
|
||||
*ngIf="original?.[entry.key] === undefined"
|
||||
color="success"
|
||||
>
|
||||
(New)</ion-text
|
||||
>
|
||||
(New)
|
||||
</ion-text>
|
||||
<ion-text *ngIf="entry.value.dirty" color="warning">
|
||||
(Edited)</ion-text
|
||||
>
|
||||
(Edited)
|
||||
</ion-text>
|
||||
</b>
|
||||
</ion-label>
|
||||
<!-- boolean -->
|
||||
@@ -157,14 +156,9 @@
|
||||
></ion-icon>
|
||||
</ion-item-divider>
|
||||
<!-- body -->
|
||||
<div
|
||||
<tui-expand
|
||||
[expanded]="objectDisplay[entry.key].expanded"
|
||||
[id]="objectId | toElementId: entry.key"
|
||||
[ngStyle]="{
|
||||
'max-height': objectDisplay[entry.key].height,
|
||||
overflow: 'hidden',
|
||||
'transition-property': 'max-height',
|
||||
'transition-duration': '.42s'
|
||||
}"
|
||||
>
|
||||
<div class="nested-wrapper">
|
||||
<form-object
|
||||
@@ -172,11 +166,10 @@
|
||||
[formGroup]="$any(entry.value)"
|
||||
[current]="current?.[entry.key]"
|
||||
[original]="original?.[entry.key]"
|
||||
(onResize)="resize(entry.key)"
|
||||
(hasNewOptions)="setHasNew(entry.key)"
|
||||
></form-object>
|
||||
</div>
|
||||
</div>
|
||||
</tui-expand>
|
||||
</ng-container>
|
||||
<!-- union -->
|
||||
<form-union
|
||||
@@ -259,15 +252,10 @@
|
||||
></ion-icon>
|
||||
</ion-item>
|
||||
<!-- object/union body -->
|
||||
<div
|
||||
<tui-expand
|
||||
style="padding-left: 24px"
|
||||
[expanded]="objectListDisplay[entry.key][i].expanded"
|
||||
[id]="objectId | toElementId: entry.key:i"
|
||||
[ngStyle]="{
|
||||
'max-height': objectListDisplay[entry.key][i].height,
|
||||
overflow: 'hidden',
|
||||
'transition-property': 'max-height',
|
||||
'transition-duration': '.42s'
|
||||
}"
|
||||
>
|
||||
<form-object
|
||||
*ngIf="spec.subtype === 'object'"
|
||||
@@ -278,7 +266,6 @@
|
||||
(onInputChange)="
|
||||
updateLabel(entry.key, i, $any(spec.spec)['display-as'])
|
||||
"
|
||||
(onResize)="resize(entry.key, i)"
|
||||
></form-object>
|
||||
<form-union
|
||||
*ngIf="spec.subtype === 'union'"
|
||||
@@ -289,7 +276,6 @@
|
||||
(onInputChange)="
|
||||
updateLabel(entry.key, i, $any(spec.spec)['display-as'])
|
||||
"
|
||||
(onResize)="resize(entry.key, i)"
|
||||
></form-union>
|
||||
<div style="text-align: right; padding-top: 12px">
|
||||
<ion-button
|
||||
@@ -301,7 +287,7 @@
|
||||
Delete
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
</tui-expand>
|
||||
</ng-container>
|
||||
<!-- string or number -->
|
||||
<div
|
||||
@@ -318,8 +304,7 @@
|
||||
$any(spec.spec).placeholder || 'Enter ' + spec.name
|
||||
"
|
||||
[formControlName]="i"
|
||||
>
|
||||
</ion-input>
|
||||
></ion-input>
|
||||
<ion-button
|
||||
strong
|
||||
fill="clear"
|
||||
|
||||
@@ -18,6 +18,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
import { TuiElasticContainerModule } from '@taiga-ui/kit'
|
||||
import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module'
|
||||
import { TuiExpandModule } from '@taiga-ui/core'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -39,6 +40,7 @@ import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module'
|
||||
SharedPipesModule,
|
||||
EnumListPageModule,
|
||||
TuiElasticContainerModule,
|
||||
TuiExpandModule,
|
||||
],
|
||||
exports: [FormObjectComponent, FormLabelComponent],
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Inject,
|
||||
inject,
|
||||
SimpleChanges,
|
||||
} from '@angular/core'
|
||||
import { FormArray, UntypedFormArray, UntypedFormGroup } from '@angular/forms'
|
||||
import { AlertButton, AlertController, ModalController } from '@ionic/angular'
|
||||
@@ -41,15 +42,14 @@ export class FormObjectComponent {
|
||||
@Input() current?: Config
|
||||
@Input() original?: Config
|
||||
@Output() onInputChange = new EventEmitter<void>()
|
||||
@Output() onResize = new EventEmitter<void>()
|
||||
@Output() hasNewOptions = new EventEmitter<void>()
|
||||
warningAck: { [key: string]: boolean } = {}
|
||||
unmasked: { [key: string]: boolean } = {}
|
||||
objectDisplay: {
|
||||
[key: string]: { expanded: boolean; height: string; hasNewOptions: boolean }
|
||||
[key: string]: { expanded: boolean; hasNewOptions: boolean }
|
||||
} = {}
|
||||
objectListDisplay: {
|
||||
[key: string]: { expanded: boolean; height: string; displayAs: string }[]
|
||||
[key: string]: { expanded: boolean; displayAs: string }[]
|
||||
} = {}
|
||||
objectId = v4()
|
||||
|
||||
@@ -63,6 +63,37 @@ export class FormObjectComponent {
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.setDisplays()
|
||||
|
||||
// setTimeout hack to avoid ExpressionChangedAfterItHasBeenCheckedError
|
||||
setTimeout(() => {
|
||||
if (
|
||||
this.original &&
|
||||
Object.keys(this.current || {}).some(
|
||||
key => this.original![key] === undefined,
|
||||
)
|
||||
)
|
||||
this.hasNewOptions.emit()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
const specChanges = changes['objectSpec']
|
||||
|
||||
if (!specChanges) return
|
||||
|
||||
if (
|
||||
!specChanges.firstChange &&
|
||||
Object.keys({
|
||||
...specChanges.previousValue,
|
||||
...specChanges.currentValue,
|
||||
}).length !== Object.keys(specChanges.previousValue).length
|
||||
) {
|
||||
this.setDisplays()
|
||||
}
|
||||
}
|
||||
|
||||
private setDisplays() {
|
||||
Object.keys(this.objectSpec).forEach(key => {
|
||||
const spec = this.objectSpec[key]
|
||||
|
||||
@@ -74,7 +105,6 @@ export class FormObjectComponent {
|
||||
]
|
||||
this.objectListDisplay[key][index] = {
|
||||
expanded: false,
|
||||
height: '0px',
|
||||
displayAs: displayAs
|
||||
? (Mustache as any).render(displayAs, obj)
|
||||
: '',
|
||||
@@ -83,33 +113,10 @@ export class FormObjectComponent {
|
||||
} else if (spec.type === 'object') {
|
||||
this.objectDisplay[key] = {
|
||||
expanded: false,
|
||||
height: '0px',
|
||||
hasNewOptions: false,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// setTimeout hack to avoid ExpressionChangedAfterItHasBeenCheckedError
|
||||
setTimeout(() => {
|
||||
if (this.original) {
|
||||
Object.keys(this.current || {}).forEach(key => {
|
||||
if ((this.original as Config)[key] === undefined) {
|
||||
this.hasNewOptions.emit()
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 10)
|
||||
}
|
||||
|
||||
resize(key: string, i?: number): void {
|
||||
setTimeout(() => {
|
||||
if (i !== undefined) {
|
||||
this.objectListDisplay[key][i].height = this.getScrollHeight(key, i)
|
||||
} else {
|
||||
this.objectDisplay[key].height = this.getScrollHeight(key)
|
||||
}
|
||||
this.onResize.emit()
|
||||
}, 250) // 250 to match transition-duration defined in html, for smooth recursive resize
|
||||
}
|
||||
|
||||
addListItemWrapper<T extends ValueSpec>(
|
||||
@@ -121,20 +128,11 @@ export class FormObjectComponent {
|
||||
|
||||
toggleExpandObject(key: string) {
|
||||
this.objectDisplay[key].expanded = !this.objectDisplay[key].expanded
|
||||
this.objectDisplay[key].height = this.objectDisplay[key].expanded
|
||||
? this.getScrollHeight(key)
|
||||
: '0px'
|
||||
this.onResize.emit()
|
||||
}
|
||||
|
||||
toggleExpandListObject(key: string, i: number) {
|
||||
this.objectListDisplay[key][i].expanded =
|
||||
!this.objectListDisplay[key][i].expanded
|
||||
this.objectListDisplay[key][i].height = this.objectListDisplay[key][i]
|
||||
.expanded
|
||||
? this.getScrollHeight(key, i)
|
||||
: '0px'
|
||||
this.onResize.emit()
|
||||
}
|
||||
|
||||
updateLabel(key: string, i: number, displayAs: string) {
|
||||
@@ -275,7 +273,6 @@ export class FormObjectComponent {
|
||||
'display-as'
|
||||
]
|
||||
this.objectListDisplay[key].push({
|
||||
height: '0px',
|
||||
expanded: false,
|
||||
displayAs: displayAs ? Mustache.render(displayAs, newItem.value) : '',
|
||||
})
|
||||
@@ -296,8 +293,8 @@ export class FormObjectComponent {
|
||||
}
|
||||
|
||||
private deleteListItem(key: string, index: number, markDirty = true): void {
|
||||
if (this.objectListDisplay[key])
|
||||
this.objectListDisplay[key][index].height = '0px'
|
||||
// if (this.objectListDisplay[key])
|
||||
// this.objectListDisplay[key][index].height = '0px'
|
||||
const arr = this.formGroup.get(key) as UntypedFormArray
|
||||
if (markDirty) arr.markAsDirty()
|
||||
pauseFor(250).then(() => {
|
||||
@@ -328,13 +325,6 @@ export class FormObjectComponent {
|
||||
arr.markAsDirty()
|
||||
}
|
||||
|
||||
private getScrollHeight(key: string, index = 0): string {
|
||||
const element = this.document.getElementById(
|
||||
getElementId(this.objectId, key, index),
|
||||
)
|
||||
return `${element?.scrollHeight}px`
|
||||
}
|
||||
|
||||
asIsOrder() {
|
||||
return 0
|
||||
}
|
||||
@@ -352,8 +342,6 @@ export class FormUnionComponent {
|
||||
@Input() current?: Config
|
||||
@Input() original?: Config
|
||||
|
||||
@Output() onResize = new EventEmitter<void>()
|
||||
|
||||
get unionValue() {
|
||||
return this.formGroup.get(this.spec.tag.id)?.value
|
||||
}
|
||||
@@ -374,10 +362,7 @@ export class FormUnionComponent {
|
||||
|
||||
objectId = v4()
|
||||
|
||||
constructor(
|
||||
private readonly formService: FormService,
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
) {}
|
||||
constructor(private readonly formService: FormService) {}
|
||||
|
||||
updateUnion(e: any): void {
|
||||
const tagId = this.spec.tag.id
|
||||
@@ -397,12 +382,6 @@ export class FormUnionComponent {
|
||||
this.formGroup.addControl(control, unionGroup.controls[control])
|
||||
})
|
||||
}
|
||||
|
||||
resize(): void {
|
||||
setTimeout(() => {
|
||||
this.onResize.emit()
|
||||
}, 250) // 250 to match transition-duration, defined in html
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
[formGroup]="formGroup"
|
||||
[current]="current"
|
||||
[original]="original"
|
||||
(onResize)="resize()"
|
||||
></form-object>
|
||||
</tui-elastic-container>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,27 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<h2>This release</h2>
|
||||
<h2>This Release</h2>
|
||||
|
||||
<h4>0.3.4.1</h4>
|
||||
<p class="note-padding">
|
||||
View the complete
|
||||
<a
|
||||
href="https://github.com/Start9Labs/embassy-os/releases/tag/v0.3.4.1"
|
||||
target="_blank"
|
||||
noreferrer
|
||||
>
|
||||
release notes
|
||||
</a>
|
||||
for more details.
|
||||
</p>
|
||||
<h6>Highlights</h6>
|
||||
<ul class="spaced-list">
|
||||
<li>0.3.4 bug fixes</li>
|
||||
</ul>
|
||||
|
||||
<h2>Previous Releases</h2>
|
||||
|
||||
<h4>0.3.4</h4>
|
||||
<p class="note-padding">
|
||||
View the complete
|
||||
|
||||
@@ -1169,6 +1169,113 @@ export module Mock {
|
||||
} as any // @TODO why is this necessary?
|
||||
|
||||
export const ConfigSpec: RR.GetPackageConfigRes['spec'] = {
|
||||
bitcoin: {
|
||||
type: 'object',
|
||||
name: 'Bitcoin Settings',
|
||||
description:
|
||||
'RPC and P2P interface configuration options for Bitcoin Core',
|
||||
spec: {
|
||||
'bitcoind-p2p': {
|
||||
type: 'union',
|
||||
tag: {
|
||||
id: 'type',
|
||||
name: 'Bitcoin Core P2P',
|
||||
description:
|
||||
'<p>The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:</p><ul><li><strong>Bitcoin Core</strong>: The Bitcoin Core service installed on this device</li><li><strong>External Node</strong>: A Bitcoin node running on a different device</li></ul>',
|
||||
'variant-names': {
|
||||
internal: 'Bitcoin Core',
|
||||
external: 'External Node',
|
||||
},
|
||||
},
|
||||
default: 'internal',
|
||||
variants: {
|
||||
internal: {},
|
||||
external: {
|
||||
'p2p-host': {
|
||||
type: 'string',
|
||||
name: 'Public Address',
|
||||
description: 'The public address of your Bitcoin Core server',
|
||||
nullable: false,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
},
|
||||
'p2p-port': {
|
||||
type: 'number',
|
||||
name: 'P2P Port',
|
||||
description:
|
||||
'The port that your Bitcoin Core P2P server is bound to',
|
||||
nullable: false,
|
||||
range: '[0,65535]',
|
||||
integral: true,
|
||||
default: 8333,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
name: 'Advanced',
|
||||
type: 'object',
|
||||
description: 'Advanced settings',
|
||||
spec: {
|
||||
rpcsettings: {
|
||||
name: 'RPC Settings',
|
||||
type: 'object',
|
||||
description: 'rpc username and password',
|
||||
warning:
|
||||
'Adding RPC users gives them special permissions on your node.',
|
||||
spec: {
|
||||
rpcuser2: {
|
||||
name: 'RPC Username',
|
||||
type: 'string',
|
||||
description: 'rpc username',
|
||||
nullable: false,
|
||||
default: 'defaultrpcusername',
|
||||
pattern: '^[a-zA-Z]+$',
|
||||
'pattern-description': 'must contain only letters.',
|
||||
masked: false,
|
||||
copyable: true,
|
||||
},
|
||||
rpcuser: {
|
||||
name: 'RPC Username',
|
||||
type: 'string',
|
||||
description: 'rpc username',
|
||||
nullable: false,
|
||||
default: 'defaultrpcusername',
|
||||
pattern: '^[a-zA-Z]+$',
|
||||
'pattern-description': 'must contain only letters.',
|
||||
masked: false,
|
||||
copyable: true,
|
||||
},
|
||||
rpcpass: {
|
||||
name: 'RPC User Password',
|
||||
type: 'string',
|
||||
description: 'rpc password',
|
||||
nullable: false,
|
||||
default: {
|
||||
charset: 'a-z,A-Z,2-9',
|
||||
len: 20,
|
||||
},
|
||||
masked: true,
|
||||
copyable: true,
|
||||
},
|
||||
rpcpass2: {
|
||||
name: 'RPC User Password',
|
||||
type: 'string',
|
||||
description: 'rpc password',
|
||||
nullable: false,
|
||||
default: {
|
||||
charset: 'a-z,A-Z,2-9',
|
||||
len: 20,
|
||||
},
|
||||
masked: true,
|
||||
copyable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
testnet: {
|
||||
name: 'Testnet',
|
||||
type: 'boolean',
|
||||
@@ -1449,6 +1556,29 @@ export module Mock {
|
||||
},
|
||||
},
|
||||
external: {
|
||||
'emergency-contact': {
|
||||
name: 'Emergency Contact',
|
||||
type: 'object',
|
||||
description: 'The person to contact in case of emergency.',
|
||||
spec: {
|
||||
name: {
|
||||
type: 'string',
|
||||
name: 'Name',
|
||||
nullable: false,
|
||||
masked: false,
|
||||
copyable: false,
|
||||
pattern: '^[a-zA-Z]+$',
|
||||
'pattern-description': 'Must contain only letters.',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
name: 'Email',
|
||||
nullable: false,
|
||||
masked: false,
|
||||
copyable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
'public-domain': {
|
||||
name: 'Public Domain',
|
||||
type: 'string',
|
||||
@@ -1520,8 +1650,8 @@ export module Mock {
|
||||
copyable: false,
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
name: 'Advanced',
|
||||
'more-advanced': {
|
||||
name: 'More Advanced',
|
||||
type: 'object',
|
||||
description: 'Advanced settings',
|
||||
spec: {
|
||||
@@ -1726,8 +1856,7 @@ export module Mock {
|
||||
rulemakers: [],
|
||||
},
|
||||
'bitcoin-node': {
|
||||
type: 'external',
|
||||
'public-domain': 'hello.com',
|
||||
type: 'internal',
|
||||
},
|
||||
port: 20,
|
||||
rpcallowip: undefined,
|
||||
|
||||
@@ -21,13 +21,17 @@ export class ThemeSwitcherService extends BehaviorSubject<string> {
|
||||
.watch$('ui', 'theme')
|
||||
.pipe(take(1), filter(Boolean))
|
||||
.subscribe(theme => {
|
||||
this.next(theme)
|
||||
this.updateTheme(theme)
|
||||
})
|
||||
}
|
||||
|
||||
override next(currentTheme: string): void {
|
||||
this.embassyApi.setDbValue(['theme'], currentTheme)
|
||||
this.windowRef.document.body.setAttribute('data-theme', currentTheme)
|
||||
super.next(currentTheme)
|
||||
override next(theme: string): void {
|
||||
this.embassyApi.setDbValue(['theme'], theme)
|
||||
this.updateTheme(theme)
|
||||
}
|
||||
|
||||
private updateTheme(theme: string): void {
|
||||
this.windowRef.document.body.setAttribute('data-theme', theme)
|
||||
super.next(theme)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ pub struct RsyncOptions {
|
||||
pub exclude: Vec<String>,
|
||||
#[serde(default = "const_true")]
|
||||
pub no_permissions: bool,
|
||||
#[serde(default = "const_true")]
|
||||
pub no_owner: bool,
|
||||
}
|
||||
impl Default for RsyncOptions {
|
||||
fn default() -> Self {
|
||||
@@ -32,6 +34,7 @@ impl Default for RsyncOptions {
|
||||
ignore_existing: false,
|
||||
exclude: Vec::new(),
|
||||
no_permissions: false,
|
||||
no_owner: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +64,9 @@ impl Rsync {
|
||||
if options.no_permissions {
|
||||
cmd.arg("--no-perms");
|
||||
}
|
||||
if options.no_owner {
|
||||
cmd.arg("--no-owner");
|
||||
}
|
||||
for exclude in options.exclude {
|
||||
cmd.arg(format!("--exclude={}", exclude));
|
||||
}
|
||||
|
||||
2
system-images/compat/Cargo.lock
generated
2
system-images/compat/Cargo.lock
generated
@@ -1171,7 +1171,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "embassy-os"
|
||||
version = "0.3.4"
|
||||
version = "0.3.4-rev.1"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"async-compression",
|
||||
|
||||
Reference in New Issue
Block a user