mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d12a7f8931 | ||
|
|
8f9111ce3d | ||
|
|
7509c3a91e | ||
|
|
3126d6138e | ||
|
|
5d4837d942 | ||
|
|
660c0c5ff4 | ||
|
|
4c6c2768b3 | ||
|
|
6ddf7ce40b | ||
|
|
4f16d82294 | ||
|
|
7845044a3c | ||
|
|
20f91b10db | ||
|
|
ec2b707353 | ||
|
|
e609d3af1e | ||
|
|
5b5495cd51 | ||
|
|
8ba23f05a4 | ||
|
|
a4b1529dc4 | ||
|
|
da81aec9cc | ||
|
|
c440f637f3 | ||
|
|
9036c3ffed | ||
|
|
98a242229a | ||
|
|
74659d717a | ||
|
|
e6dbbf125c | ||
|
|
80f509a634 | ||
|
|
5d6a175585 | ||
|
|
6ef46ae309 | ||
|
|
13c94241c2 | ||
|
|
56041fd503 | ||
|
|
f52bb54a2f | ||
|
|
7f9f942eb1 | ||
|
|
1ca7a699c1 | ||
|
|
481accc9e6 | ||
|
|
11b007a31d | ||
|
|
5b8f27e53e | ||
|
|
9f4523676f | ||
|
|
bc5163d800 | ||
|
|
9b7fe03c19 | ||
|
|
9a2aaa08b8 | ||
|
|
8c87e6653c | ||
|
|
1c3b16e870 | ||
|
|
276085f084 | ||
|
|
52fc992090 | ||
|
|
af46a375a9 | ||
|
|
74a559eade | ||
|
|
f12d97122a | ||
|
|
ba9b3519de | ||
|
|
43e89df652 | ||
|
|
7bdc109bd4 | ||
|
|
ac5dec476d | ||
|
|
1f56be3cbf | ||
|
|
ed46ddbf44 | ||
|
|
2973c316a8 | ||
|
|
7e7a9dc140 | ||
|
|
b95686282d | ||
|
|
09f858d28d |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
.DS_Store
|
||||||
/*.img
|
/*.img
|
||||||
/buster.zip
|
/buster.zip
|
||||||
/product_key
|
/product_key
|
||||||
175
BuildGuide.md
Normal file
175
BuildGuide.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
##### Initial Notes & Recommendations
|
||||||
|
* Due to issues to cross-compile the image from a desktop, this guide will take you step-by-step through the process of compiling EmbassyOS directly on a Raspberry Pi 4 (4GB or 8GB)
|
||||||
|
* This process will go faster if you have an SSD/NVMe USB drive available.
|
||||||
|
* This build guide does **not** require a large microSD card, especially if your final build wil be used on an SSD/NVMe USB drive.
|
||||||
|
* Basic know-how of linux commands and terminal use is recommended.
|
||||||
|
* Follow the guide carefully and do not skip any steps.
|
||||||
|
|
||||||
|
# :hammer_and_wrench: Build Guide
|
||||||
|
1. Flash [Raspberry Pi OS Lite](https://www.raspberrypi.org/software/operating-systems/) to a microSD and configure your raspi to boot from SSD/NVMe USB drive
|
||||||
|
1. After flashing, create an empty text file called `ssh` in the `boot` partition of the microSD, then proceed with booting the raspi with the flashed microSD (check your router for the IP assigned to your raspi)
|
||||||
|
1. Do the usual initial update/config
|
||||||
|
```
|
||||||
|
sudo apt update
|
||||||
|
sudo raspi-config
|
||||||
|
```
|
||||||
|
1. Change `Advanced Options->Boot Order`
|
||||||
|
1. Select `USB Boot` *(it will try to boot from microSD first if it's available)*
|
||||||
|
1. Select `Finish`, then `Yes` to reboot
|
||||||
|
1. After reboot, `sudo shutdown now` to power off the raspi and remove the microSD
|
||||||
|
|
||||||
|
2. Flash the *Raspi OS Lite* (from step 1) to your SSD/NVMe drive
|
||||||
|
> :information_source: Don't worry about rootfs partition size (raspi will increase it for you on initial boot)
|
||||||
|
|
||||||
|
> :information_source: Every time you re-flash your SSD/NVMe you need to first boot with a microSD and set *Boot Order* again
|
||||||
|
|
||||||
|
1. Don't forget to create the empty `ssh` file
|
||||||
|
1. Connect the drive (remember to remove the microSD) to the raspi and start it up
|
||||||
|
1. Use `sudo raspi-config` to change the default password
|
||||||
|
1. Optional: `sudo apt upgrade -y`
|
||||||
|
1. Optional: `sudo nano /etc/apt/sources.list.d/vscode.list` comment the last line which contains `packages.microsoft.com`
|
||||||
|
|
||||||
|
3. Install GHC
|
||||||
|
```
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y ghc
|
||||||
|
|
||||||
|
#test:
|
||||||
|
ghc --version
|
||||||
|
|
||||||
|
#example of output:
|
||||||
|
The Glorious Glasgow Haskell Compilation System, version 8.4.4
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Compile Stack:
|
||||||
|
1. Install Stack v2.1.3
|
||||||
|
```
|
||||||
|
cd ~/
|
||||||
|
wget -qO- https://raw.githubusercontent.com/commercialhaskell/stack/v2.1.3/etc/scripts/get-stack.sh | sh
|
||||||
|
|
||||||
|
#test with
|
||||||
|
stack --version
|
||||||
|
|
||||||
|
#example output:
|
||||||
|
Version 2.1.3, Git revision 636e3a759d51127df2b62f90772def126cdf6d1f (7735 commits) arm hpack-0.31.2
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Use current Stack to compile Stack v2.5.1:
|
||||||
|
```
|
||||||
|
git clone --depth 1 --branch v2.5.1 https://github.com/commercialhaskell/stack.git
|
||||||
|
cd stack
|
||||||
|
sudo apt install -y screen
|
||||||
|
screen
|
||||||
|
```
|
||||||
|
> :information_source: Build (>=3.5h total... We are using `screen` in case of session timeout issues)
|
||||||
|
|
||||||
|
> :memo: If you get disconected you can reattach last sesion again by executing `screen -r`
|
||||||
|
```
|
||||||
|
stack build --stack-yaml=stack-ghc-84.yaml --system-ghc
|
||||||
|
|
||||||
|
#Install
|
||||||
|
stack install --stack-yaml=stack-ghc-84.yaml --system-ghc
|
||||||
|
export PATH=~/.local/bin:$PATH
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Clone EmbassyOS & try to *make* the `agent`:
|
||||||
|
1. First attempt
|
||||||
|
> :information_source: The first time you run **make** you'll get an error
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install -y llvm-9 libgmp-dev
|
||||||
|
export PATH=/usr/lib/llvm-9/bin:$PATH
|
||||||
|
cd ~/
|
||||||
|
git clone https://github.com/Start9Labs/embassy-os.git
|
||||||
|
cd embassy-os/
|
||||||
|
make agent
|
||||||
|
```
|
||||||
|
> :memo: This will install ghc-8.10.2, then attempt to build but will give errors (in next steps we deal with errors)
|
||||||
|
1. Confirm your cpu info
|
||||||
|
```
|
||||||
|
cat /proc/cpuinfo | grep Hardware
|
||||||
|
```
|
||||||
|
1. If your "Hardware" is [BCM2711](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711/README.md) then:
|
||||||
|
1. Change `C compiler flags` to `-marm -fno-stack-protector -mcpu=cortex-a7` in the GHC settings:
|
||||||
|
```
|
||||||
|
nano ~/.stack/programs/arm-linux/ghc-8.10.4/lib/ghc-8.10.4/settings
|
||||||
|
```
|
||||||
|
1. To prevent gcc errors we delete the `setup-exe-src` folder
|
||||||
|
```
|
||||||
|
rm -rf ~/.stack/setup-exe-src/
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Install requirements for step 7
|
||||||
|
1. Install NVM
|
||||||
|
```
|
||||||
|
cd ~/ && curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
|
||||||
|
export NVM_DIR="$HOME/.nvm"
|
||||||
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
|
||||||
|
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
|
||||||
|
nvm --version
|
||||||
|
```
|
||||||
|
1. Install Node.js & NPM
|
||||||
|
```
|
||||||
|
nvm install node
|
||||||
|
```
|
||||||
|
1. Install Ionic CLI
|
||||||
|
```
|
||||||
|
npm install -g @ionic/cli
|
||||||
|
```
|
||||||
|
1. Install Dependencies
|
||||||
|
```
|
||||||
|
sudo apt-get install -y build-essential openssl libssl-dev libc6-dev clang libclang-dev libavahi-client-dev upx ca-certificates
|
||||||
|
```
|
||||||
|
1. Install Rust
|
||||||
|
```
|
||||||
|
cd ~/ && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o- | bash
|
||||||
|
|
||||||
|
#Choose option 1
|
||||||
|
source $HOME/.cargo/env
|
||||||
|
|
||||||
|
#Check rust & cargo versions
|
||||||
|
rustc --version
|
||||||
|
cargo --version
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Finally, getting to build the **.img**
|
||||||
|
1. At this stage you hava a working development environment to build your **embassy.img**.
|
||||||
|
Before you do that you can choose to enable SSH login for user `pi` in case something will go wrong or just skip to the next step.
|
||||||
|
```
|
||||||
|
cd ~/embassy-os
|
||||||
|
sed -e '/passwd -l pi/ s/^#*/#/' -i setup.sh
|
||||||
|
```
|
||||||
|
> :warning: Default password for user `pi` is `raspberry`, change it the next you login.
|
||||||
|
1. Build the `embassy.img`
|
||||||
|
```
|
||||||
|
cd ~/embassy-os
|
||||||
|
make
|
||||||
|
|
||||||
|
#Depending on your hardware this can take 1-2h+
|
||||||
|
#Wait for the "DONE!" message and take note of your product_key
|
||||||
|
exit
|
||||||
|
```
|
||||||
|
8. Flash the `embassy.img` to a microSD
|
||||||
|
1. Copy `embassy.img` from the raspi to your PC with scp
|
||||||
|
```
|
||||||
|
scp pi@raspi_IP:~/embassy-os/embassy.img .
|
||||||
|
```
|
||||||
|
1. Connect to raspi again to do `sudo shutdown now`, after a complete shutdown disconnect SSD/NVMe drive
|
||||||
|
1. Flash `embassy.img` to a microSD (do this before flashing to the SSD/NVMe, to be sure it works)
|
||||||
|
|
||||||
|
9. Prepare for initial setup
|
||||||
|
1. Boot raspi using flashed microSD
|
||||||
|
1. After a few minutes, the raspi should reboot itself and make it's first [sounds](#embassy-sounds-explained).
|
||||||
|
> :information_source: If needed, you can check the `agent` log with: `journalctl -u agent -ef`
|
||||||
|
1. Proceed with the [initial setup process of EmbassyOS](https://docs.start9labs.com/user-manual/initial-setup.html)
|
||||||
|
1. If all went well you can safely flash `embassy.img` to an SSD/NVMe and repeat step 9
|
||||||
|
|
||||||
|
### Embassy sounds explained
|
||||||
|
Sound :notes: | Indicating
|
||||||
|
------- | --------
|
||||||
|
Bep | Device is powering on
|
||||||
|
Chime | Device is ready for setup
|
||||||
|
Mario "Coin" | EmbassyOS has started
|
||||||
|
Mario "Death" | Device is about to Shutdown/Reboot
|
||||||
|
Mario "Power Up" | EmbassyOS update sequence
|
||||||
|
Beethoven | Update failed :(
|
||||||
@@ -77,7 +77,7 @@ A good bug report shouldn't leave others needing to chase you up for more inform
|
|||||||
|
|
||||||
- Make sure that you are using the latest version.
|
- Make sure that you are using the latest version.
|
||||||
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://docs.start9labs.com). If you are looking for support, you might want to check [this section](#i-have-a-question)).
|
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://docs.start9labs.com). If you are looking for support, you might want to check [this section](#i-have-a-question)).
|
||||||
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/Start9Labs/embassy-osissues?q=label%3Abug).
|
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/Start9Labs/embassy-os/issues?q=label%3Abug).
|
||||||
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
|
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
|
||||||
- Collect information about the bug:
|
- Collect information about the bug:
|
||||||
- Stack trace (Traceback)
|
- Stack trace (Traceback)
|
||||||
@@ -225,9 +225,9 @@ When a pull request conflicts with the target branch, you may be asked to rebase
|
|||||||
This project aims to have a clean git history, where code changes are only made in non-merge commits. This simplifies auditability because merge commits can be assumed to not contain arbitrary code changes.
|
This project aims to have a clean git history, where code changes are only made in non-merge commits. This simplifies auditability because merge commits can be assumed to not contain arbitrary code changes.
|
||||||
|
|
||||||
## Join The Discussion
|
## Join The Discussion
|
||||||
Current or aspiring contributors? Join our community developer Matrix channel: `#community-dev:matrix.start9labs.com`.
|
Current or aspiring contributors? Join our community developer [Matrix channel](https://matrix.to/#/#community-dev:matrix.start9labs.com).
|
||||||
|
|
||||||
Just interested in or using the project? Join our community [Telegram](https://t.me/start9_labs) or Matrix channel: `#community:matrix.start9labs.com`.
|
Just interested in or using the project? Join our community [Telegram](https://t.me/start9_labs) or [Matrix](https://matrix.to/#/#community:matrix.start9labs.com).
|
||||||
|
|
||||||
## Join The Project Team
|
## Join The Project Team
|
||||||
Interested in becoming a part of the Start9 Labs team? Send an email to <jobs@start9labs.com>
|
Interested in becoming a part of the Start9 Labs team? Send an email to <jobs@start9labs.com>
|
||||||
|
|||||||
34
Makefile
34
Makefile
@@ -1,3 +1,15 @@
|
|||||||
|
UNAME := $(shell uname -m)
|
||||||
|
|
||||||
|
EMBASSY_SRC := buster.img product_key appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr ui/www agent/dist/agent agent/config/agent.service lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline lifeline/lifeline.service setup.sh setup.service docker-daemon.json
|
||||||
|
APPMGR_RELEASE_SRC := appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr
|
||||||
|
LIFELINE_RELEASE_SRC := lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline
|
||||||
|
|
||||||
|
ifeq ($(UNAME), armv7l)
|
||||||
|
EMBASSY_SRC := buster.img product_key appmgr/target/release/appmgr ui/www agent/dist/agent agent/config/agent.service lifeline/target/release/lifeline lifeline/lifeline.service setup.sh setup.service docker-daemon.json
|
||||||
|
APPMGR_RELEASE_SRC := appmgr/target/release/appmgr
|
||||||
|
LIFELINE_RELEASE_SRC := lifeline/target/release/lifeline
|
||||||
|
endif
|
||||||
|
|
||||||
APPMGR_SRC := $(shell find appmgr/src) appmgr/Cargo.toml appmgr/Cargo.lock
|
APPMGR_SRC := $(shell find appmgr/src) appmgr/Cargo.toml appmgr/Cargo.lock
|
||||||
LIFELINE_SRC := $(shell find lifeline/src) lifeline/Cargo.toml lifeline/Cargo.lock
|
LIFELINE_SRC := $(shell find lifeline/src) lifeline/Cargo.toml lifeline/Cargo.lock
|
||||||
AGENT_SRC := $(shell find agent/src) $(shell find agent/config) agent/stack.yaml agent/package.yaml agent/build.sh
|
AGENT_SRC := $(shell find agent/src) $(shell find agent/config) agent/stack.yaml agent/package.yaml agent/build.sh
|
||||||
@@ -13,7 +25,8 @@ UI_SRC := $(shell find ui/src) \
|
|||||||
|
|
||||||
all: embassy.img
|
all: embassy.img
|
||||||
|
|
||||||
embassy.img: buster.img product_key appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr ui/www agent/dist/agent agent/config/agent.service lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline lifeline/lifeline.service setup.sh setup.service docker-daemon.json
|
embassy.img: $(EMBASSY_SRC)
|
||||||
|
chmod +x make_image.sh
|
||||||
sudo ./make_image.sh
|
sudo ./make_image.sh
|
||||||
|
|
||||||
buster.img:
|
buster.img:
|
||||||
@@ -26,11 +39,16 @@ product_key:
|
|||||||
echo "X\c" > product_key
|
echo "X\c" > product_key
|
||||||
cat /dev/random | base32 | head -c11 | tr '[:upper:]' '[:lower:]' >> product_key
|
cat /dev/random | base32 | head -c11 | tr '[:upper:]' '[:lower:]' >> product_key
|
||||||
|
|
||||||
appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr: $(APPMGR_SRC)
|
$(APPMGR_RELEASE_SRC): $(APPMGR_SRC)
|
||||||
|
ifeq ($(UNAME), armv7l)
|
||||||
|
cd appmgr && cargo update && cargo build --release --features=production
|
||||||
|
arm-linux-gnueabihf-strip appmgr/target/release/appmgr
|
||||||
|
else
|
||||||
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest sh -c "(cd appmgr && cargo build --release --features=production)"
|
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest sh -c "(cd appmgr && cargo build --release --features=production)"
|
||||||
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest arm-linux-gnueabi-strip appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr
|
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest arm-linux-gnueabi-strip appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr
|
||||||
|
endif
|
||||||
|
|
||||||
appmgr: appmgr/target/armv7-unknown-linux-gnueabihf/release/appmgr
|
appmgr: $(APPMGR_RELEASE_SRC)
|
||||||
|
|
||||||
agent/dist/agent: $(AGENT_SRC)
|
agent/dist/agent: $(AGENT_SRC)
|
||||||
(cd agent && ./build.sh)
|
(cd agent && ./build.sh)
|
||||||
@@ -45,9 +63,13 @@ ui/www: $(UI_SRC) ui/node_modules
|
|||||||
|
|
||||||
ui: ui/www
|
ui: ui/www
|
||||||
|
|
||||||
lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline: $(LIFELINE_SRC)
|
$(LIFELINE_RELEASE_SRC): $(LIFELINE_SRC)
|
||||||
|
ifeq ($(UNAME), armv7l)
|
||||||
|
cd lifeline && cargo build --release
|
||||||
|
arm-linux-gnueabihf-strip lifeline/target/release/lifeline
|
||||||
|
else
|
||||||
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest sh -c "(cd lifeline && cargo build --release)"
|
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest sh -c "(cd lifeline && cargo build --release)"
|
||||||
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest arm-linux-gnueabi-strip lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline
|
docker run --rm -it -v ~/.cargo/registry:/root/.cargo/registry -v "$(shell pwd)":/home/rust/src start9/rust-arm-cross:latest arm-linux-gnueabi-strip lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline
|
||||||
|
endif
|
||||||
|
|
||||||
lifeline: lifeline/target/armv7-unknown-linux-gnueabihf/release/lifeline
|
lifeline: $(LIFELINE_RELEASE_SRC)
|
||||||
|
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -1,28 +1,30 @@
|
|||||||
# EmbassyOS
|
# EmbassyOS
|
||||||
[](https://github.com/Start9Labs/embassy-os/releases)
|
[](https://github.com/Start9Labs/embassy-os/releases)
|
||||||
|
[](https://matrix.to/#/#community:matrix.start9labs.com)
|
||||||
[](https://t.me/start9_labs)
|
[](https://t.me/start9_labs)
|
||||||
[](https://docs.start9labs.com)
|
[](https://docs.start9labs.com)
|
||||||
[](https://matrix.to/#/#community-dev:matrix.start9labs.com)
|
[](https://matrix.to/#/#community-dev:matrix.start9labs.com)
|
||||||
[](https://start9labs.com)
|
[](https://start9labs.com)
|
||||||
|
|
||||||
|
[](http://mastodon.start9labs.com)
|
||||||
[](https://twitter.com/start9labs)
|
[](https://twitter.com/start9labs)
|
||||||
|
|
||||||
### _Anyone can do it. No one can stop it._ ###
|
### _Anyone can do it. No one can stop it._ ###
|
||||||
|
|
||||||
EmbassyOS is a mass-market, graphical operating system designed to facilitate the discovery, installation, configuration, private self-hosting, and reliable operation of open-source software services and applications. It aims to eliminate trust and custodianship from personal computing.
|
EmbassyOS is a mass-market, graphical operating system designed to facilitate the discovery, installation, configuration, private self-hosting, and reliable operation of open-source software services and applications. It aims to eliminate trust and custodianship from personal computing.
|
||||||
|
|
||||||

|
<img src="assets/eos.png" width="100%">
|
||||||
|
|
||||||
## ⚠️ Caution
|
## :warning: Caution
|
||||||
Some technologies supported by this software, such as [Lightning](https://lightning.network/), are considered in active development and might experience issues. Do not commit any funds you are not willing to loose. Be #reckless at your own risk.
|
Some technologies supported by this software, such as [Lightning](https://lightning.network/), are considered in active development and might experience issues. Do not commit any funds you are not willing to lose. Be #reckless at your own risk.
|
||||||
|
|
||||||
## Running EmbassyOS
|
## Running EmbassyOS
|
||||||
There are multiple ways to obtain and begin using EmbassyOS.
|
There are multiple ways to obtain and begin using EmbassyOS.
|
||||||
|
|
||||||
### Buy an Embassy
|
### :moneybag: Buy an Embassy
|
||||||
This is the most convenient option. Simply [buy an Embassy](https://start9labs.com) from Start9 Labs and plug it in. Depending on where you live, shipping costs and import duties may vary.
|
This is the most convenient option. Simply [buy an Embassy](https://start9labs.com) from Start9 Labs and plug it in. Depending on where you live, shipping costs and import duties may vary.
|
||||||
|
|
||||||
### Build your own Embassy
|
### :construction_worker: Build your own Embassy
|
||||||
While not as convenient as buying an Embassy, this option is easier than you might imagine, and there are 4 reasons why you might prefer it:
|
While not as convenient as buying an Embassy, this option is easier than you might imagine, and there are 4 reasons why you might prefer it:
|
||||||
1. You already have a Raspberry Pi and would like to re-purpose it.
|
1. You already have a Raspberry Pi and would like to re-purpose it.
|
||||||
1. You want to save on shipping costs.
|
1. You want to save on shipping costs.
|
||||||
@@ -31,5 +33,15 @@ While not as convenient as buying an Embassy, this option is easier than you mig
|
|||||||
|
|
||||||
To pursue this option, follow this [guide](https://docs.start9labs.com/getting-started/diy.html).
|
To pursue this option, follow this [guide](https://docs.start9labs.com/getting-started/diy.html).
|
||||||
|
|
||||||
## Contributing
|
### :hammer_and_wrench: Build EmbassyOS from Source
|
||||||
To build EmbassyOS from source, or to contribute to its development, see [here](https://github.com/Start9Labs/embassy-os/blob/master/CONTRIBUTING.md#building-the-image).
|
|
||||||
|
EmbassyOS can be built from source, for personal use, for free.
|
||||||
|
A detailed guide for doing so can be found [here](https://github.com/Start9Labs/embassy-os/blob/master/BuildGuide.md).
|
||||||
|
|
||||||
|
## :heart: Contributing
|
||||||
|
To contribute to the development of EmbassyOS, see [here](https://github.com/Start9Labs/embassy-os/blob/master/CONTRIBUTING.md).
|
||||||
|
|
||||||
|
## UI Screenshots
|
||||||
|
<img src="assets/ServicesRunning.png" alt="Embassy Services" width="100%"> | <img src="assets/ServiceDetails.png" alt="Service Details" width="100%">
|
||||||
|
--- | ---
|
||||||
|
<img src="assets/Embassy.png" alt="EmbassyOS" width="100%"> | <img src="assets/Marketplace.png" alt="Marketplace" width="100%">
|
||||||
|
|||||||
2
agent/.gitignore
vendored
2
agent/.gitignore
vendored
@@ -19,9 +19,7 @@ cabal.sandbox.config
|
|||||||
*.keter
|
*.keter
|
||||||
*~
|
*~
|
||||||
.vscode
|
.vscode
|
||||||
*.cabal
|
|
||||||
\#*
|
\#*
|
||||||
start9-companion-server.cabal
|
|
||||||
stack.yaml.lock
|
stack.yaml.lock
|
||||||
*.env
|
*.env
|
||||||
agent_*
|
agent_*
|
||||||
|
|||||||
518
agent/ambassador-agent.cabal
Normal file
518
agent/ambassador-agent.cabal
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
cabal-version: 1.12
|
||||||
|
|
||||||
|
-- This file has been generated from package.yaml by hpack version 0.34.4.
|
||||||
|
--
|
||||||
|
-- see: https://github.com/sol/hpack
|
||||||
|
|
||||||
|
name: ambassador-agent
|
||||||
|
version: 0.2.13
|
||||||
|
build-type: Simple
|
||||||
|
extra-source-files:
|
||||||
|
./migrations/0.1.0::0.1.0
|
||||||
|
./migrations/0.1.0::0.1.1
|
||||||
|
./migrations/0.1.1::0.1.2
|
||||||
|
./migrations/0.1.2::0.1.3
|
||||||
|
./migrations/0.1.3::0.1.4
|
||||||
|
./migrations/0.1.4::0.1.5
|
||||||
|
./migrations/0.1.5::0.2.0
|
||||||
|
./migrations/0.2.0::0.2.1
|
||||||
|
./migrations/0.2.10::0.2.11
|
||||||
|
./migrations/0.2.11::0.2.12
|
||||||
|
./migrations/0.2.12::0.2.13
|
||||||
|
./migrations/0.2.1::0.2.2
|
||||||
|
./migrations/0.2.2::0.2.3
|
||||||
|
./migrations/0.2.3::0.2.4
|
||||||
|
./migrations/0.2.4::0.2.5
|
||||||
|
./migrations/0.2.5::0.2.6
|
||||||
|
./migrations/0.2.6::0.2.7
|
||||||
|
./migrations/0.2.7::0.2.8
|
||||||
|
./migrations/0.2.8::0.2.9
|
||||||
|
./migrations/0.2.9::0.2.10
|
||||||
|
|
||||||
|
flag dev
|
||||||
|
description: Turn on development settings, like auto-reload templates.
|
||||||
|
manual: False
|
||||||
|
default: False
|
||||||
|
|
||||||
|
flag disable-auth
|
||||||
|
description: disable authorization checks
|
||||||
|
manual: False
|
||||||
|
default: False
|
||||||
|
|
||||||
|
flag library-only
|
||||||
|
description: Build for use with "yesod devel"
|
||||||
|
manual: False
|
||||||
|
default: False
|
||||||
|
|
||||||
|
library
|
||||||
|
exposed-modules:
|
||||||
|
Application
|
||||||
|
Auth
|
||||||
|
Constants
|
||||||
|
Daemon.AppNotifications
|
||||||
|
Daemon.RefreshProcDev
|
||||||
|
Daemon.SslRenew
|
||||||
|
Daemon.TorHealth
|
||||||
|
Daemon.ZeroConf
|
||||||
|
Foundation
|
||||||
|
Handler.Apps
|
||||||
|
Handler.Authenticate
|
||||||
|
Handler.Backups
|
||||||
|
Handler.Hosts
|
||||||
|
Handler.Icons
|
||||||
|
Handler.Login
|
||||||
|
Handler.Network
|
||||||
|
Handler.Notifications
|
||||||
|
Handler.PasswordUpdate
|
||||||
|
Handler.PowerOff
|
||||||
|
Handler.Register
|
||||||
|
Handler.Register.Nginx
|
||||||
|
Handler.Register.Tor
|
||||||
|
Handler.SelfUpdate
|
||||||
|
Handler.SshKeys
|
||||||
|
Handler.Status
|
||||||
|
Handler.Tor
|
||||||
|
Handler.Types.Apps
|
||||||
|
Handler.Types.HmacSig
|
||||||
|
Handler.Types.Hosts
|
||||||
|
Handler.Types.Metrics
|
||||||
|
Handler.Types.Parse
|
||||||
|
Handler.Types.Register
|
||||||
|
Handler.Types.V0.Base
|
||||||
|
Handler.Types.V0.Specs
|
||||||
|
Handler.Types.V0.Ssh
|
||||||
|
Handler.Types.V0.Wifi
|
||||||
|
Handler.Util
|
||||||
|
Handler.V0
|
||||||
|
Handler.Wifi
|
||||||
|
Lib.Algebra.Domain.AppMgr
|
||||||
|
Lib.Algebra.Domain.AppMgr.TH
|
||||||
|
Lib.Algebra.Domain.AppMgr.Types
|
||||||
|
Lib.Algebra.State.RegistryUrl
|
||||||
|
Lib.Avahi
|
||||||
|
Lib.Background
|
||||||
|
Lib.ClientManifest
|
||||||
|
Lib.Crypto
|
||||||
|
Lib.Database
|
||||||
|
Lib.Error
|
||||||
|
Lib.External.AppManifest
|
||||||
|
Lib.External.AppMgr
|
||||||
|
Lib.External.Metrics.Df
|
||||||
|
Lib.External.Metrics.Iotop
|
||||||
|
Lib.External.Metrics.ProcDev
|
||||||
|
Lib.External.Metrics.Temperature
|
||||||
|
Lib.External.Metrics.Top
|
||||||
|
Lib.External.Metrics.Types
|
||||||
|
Lib.External.Registry
|
||||||
|
Lib.External.Specs.Common
|
||||||
|
Lib.External.Specs.CPU
|
||||||
|
Lib.External.Specs.Memory
|
||||||
|
Lib.External.Util
|
||||||
|
Lib.External.WpaSupplicant
|
||||||
|
Lib.IconCache
|
||||||
|
Lib.Metrics
|
||||||
|
Lib.Migration
|
||||||
|
Lib.Notifications
|
||||||
|
Lib.Password
|
||||||
|
Lib.ProductKey
|
||||||
|
Lib.SelfUpdate
|
||||||
|
Lib.Sound
|
||||||
|
Lib.Ssh
|
||||||
|
Lib.Ssl
|
||||||
|
Lib.Synchronizers
|
||||||
|
Lib.SystemCtl
|
||||||
|
Lib.SystemPaths
|
||||||
|
Lib.Tor
|
||||||
|
Lib.TyFam.ConditionalData
|
||||||
|
Lib.Types.Core
|
||||||
|
Lib.Types.Emver
|
||||||
|
Lib.Types.Emver.Orphans
|
||||||
|
Lib.Types.NetAddress
|
||||||
|
Lib.Types.ServerApp
|
||||||
|
Lib.Types.Url
|
||||||
|
Lib.WebServer
|
||||||
|
Model
|
||||||
|
Orphans.Digest
|
||||||
|
Orphans.UUID
|
||||||
|
Settings
|
||||||
|
Startlude
|
||||||
|
Startlude.ByteStream
|
||||||
|
Startlude.ByteStream.Char8
|
||||||
|
Util.Conduit
|
||||||
|
Util.File
|
||||||
|
Util.Function
|
||||||
|
Util.Text
|
||||||
|
other-modules:
|
||||||
|
Paths_ambassador_agent
|
||||||
|
hs-source-dirs:
|
||||||
|
src
|
||||||
|
default-extensions:
|
||||||
|
NoImplicitPrelude
|
||||||
|
BlockArguments
|
||||||
|
ConstraintKinds
|
||||||
|
DataKinds
|
||||||
|
DeriveAnyClass
|
||||||
|
DeriveFunctor
|
||||||
|
DeriveGeneric
|
||||||
|
DerivingStrategies
|
||||||
|
EmptyCase
|
||||||
|
FlexibleContexts
|
||||||
|
FlexibleInstances
|
||||||
|
GADTs
|
||||||
|
GeneralizedNewtypeDeriving
|
||||||
|
InstanceSigs
|
||||||
|
KindSignatures
|
||||||
|
LambdaCase
|
||||||
|
MultiParamTypeClasses
|
||||||
|
MultiWayIf
|
||||||
|
NamedFieldPuns
|
||||||
|
NumericUnderscores
|
||||||
|
OverloadedStrings
|
||||||
|
PolyKinds
|
||||||
|
RankNTypes
|
||||||
|
StandaloneDeriving
|
||||||
|
StandaloneKindSignatures
|
||||||
|
TupleSections
|
||||||
|
TypeApplications
|
||||||
|
TypeFamilies
|
||||||
|
TypeOperators
|
||||||
|
build-depends:
|
||||||
|
aeson
|
||||||
|
, aeson-flatten
|
||||||
|
, attoparsec
|
||||||
|
, base >=4.9.1.0 && <5
|
||||||
|
, bytestring
|
||||||
|
, casing
|
||||||
|
, comonad
|
||||||
|
, conduit
|
||||||
|
, conduit-extra
|
||||||
|
, connection
|
||||||
|
, containers
|
||||||
|
, cryptonite
|
||||||
|
, cryptonite-conduit
|
||||||
|
, data-default
|
||||||
|
, directory
|
||||||
|
, errors
|
||||||
|
, exceptions
|
||||||
|
, exinst
|
||||||
|
, fast-logger
|
||||||
|
, file-embed
|
||||||
|
, filelock
|
||||||
|
, filepath
|
||||||
|
, fused-effects
|
||||||
|
, fused-effects-th
|
||||||
|
, git-embed
|
||||||
|
, http-api-data
|
||||||
|
, http-client
|
||||||
|
, http-client-tls
|
||||||
|
, http-conduit
|
||||||
|
, http-types
|
||||||
|
, interpolate
|
||||||
|
, iso8601-time
|
||||||
|
, json-rpc
|
||||||
|
, lens
|
||||||
|
, lens-aeson
|
||||||
|
, lifted-async
|
||||||
|
, lifted-base
|
||||||
|
, memory
|
||||||
|
, mime-types
|
||||||
|
, monad-control
|
||||||
|
, monad-logger
|
||||||
|
, network
|
||||||
|
, persistent
|
||||||
|
, persistent-sqlite
|
||||||
|
, persistent-template
|
||||||
|
, process
|
||||||
|
, process-extras
|
||||||
|
, protolude
|
||||||
|
, regex-compat
|
||||||
|
, resourcet
|
||||||
|
, shell-conduit
|
||||||
|
, singletons
|
||||||
|
, stm
|
||||||
|
, streaming
|
||||||
|
, streaming-bytestring
|
||||||
|
, streaming-conduit
|
||||||
|
, streaming-utils
|
||||||
|
, tar-conduit
|
||||||
|
, template-haskell
|
||||||
|
, text >=0.11 && <2.0
|
||||||
|
, time
|
||||||
|
, transformers
|
||||||
|
, transformers-base
|
||||||
|
, typed-process
|
||||||
|
, unix
|
||||||
|
, unliftio
|
||||||
|
, unliftio-core
|
||||||
|
, unordered-containers
|
||||||
|
, uuid
|
||||||
|
, wai
|
||||||
|
, wai-cors
|
||||||
|
, wai-extra
|
||||||
|
, warp
|
||||||
|
, yaml
|
||||||
|
, yesod
|
||||||
|
, yesod-auth
|
||||||
|
, yesod-core
|
||||||
|
, yesod-form
|
||||||
|
, yesod-persistent
|
||||||
|
if (flag(dev)) || (flag(library-only))
|
||||||
|
ghc-options: -Wall -Wunused-packages -fwarn-tabs -O0 -fdefer-typed-holes
|
||||||
|
cpp-options: -DDEVELOPMENT
|
||||||
|
else
|
||||||
|
ghc-options: -Wall -Wunused-packages -fwarn-tabs -O2 -fdefer-typed-holes
|
||||||
|
if (flag(disable-auth))
|
||||||
|
cpp-options: -DDISABLE_AUTH
|
||||||
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
executable agent
|
||||||
|
main-is: main.hs
|
||||||
|
hs-source-dirs:
|
||||||
|
app
|
||||||
|
default-extensions:
|
||||||
|
NoImplicitPrelude
|
||||||
|
BlockArguments
|
||||||
|
ConstraintKinds
|
||||||
|
DataKinds
|
||||||
|
DeriveAnyClass
|
||||||
|
DeriveFunctor
|
||||||
|
DeriveGeneric
|
||||||
|
DerivingStrategies
|
||||||
|
EmptyCase
|
||||||
|
FlexibleContexts
|
||||||
|
FlexibleInstances
|
||||||
|
GADTs
|
||||||
|
GeneralizedNewtypeDeriving
|
||||||
|
InstanceSigs
|
||||||
|
KindSignatures
|
||||||
|
LambdaCase
|
||||||
|
MultiParamTypeClasses
|
||||||
|
MultiWayIf
|
||||||
|
NamedFieldPuns
|
||||||
|
NumericUnderscores
|
||||||
|
OverloadedStrings
|
||||||
|
PolyKinds
|
||||||
|
RankNTypes
|
||||||
|
StandaloneDeriving
|
||||||
|
StandaloneKindSignatures
|
||||||
|
TupleSections
|
||||||
|
TypeApplications
|
||||||
|
TypeFamilies
|
||||||
|
TypeOperators
|
||||||
|
ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -fdefer-typed-holes
|
||||||
|
build-depends:
|
||||||
|
aeson
|
||||||
|
, aeson-flatten
|
||||||
|
, ambassador-agent
|
||||||
|
, attoparsec
|
||||||
|
, base >=4.9.1.0 && <5
|
||||||
|
, bytestring
|
||||||
|
, casing
|
||||||
|
, comonad
|
||||||
|
, conduit
|
||||||
|
, conduit-extra
|
||||||
|
, connection
|
||||||
|
, containers
|
||||||
|
, cryptonite
|
||||||
|
, cryptonite-conduit
|
||||||
|
, data-default
|
||||||
|
, directory
|
||||||
|
, errors
|
||||||
|
, exceptions
|
||||||
|
, exinst
|
||||||
|
, fast-logger
|
||||||
|
, file-embed
|
||||||
|
, filelock
|
||||||
|
, filepath
|
||||||
|
, fused-effects
|
||||||
|
, fused-effects-th
|
||||||
|
, git-embed
|
||||||
|
, http-api-data
|
||||||
|
, http-client
|
||||||
|
, http-client-tls
|
||||||
|
, http-conduit
|
||||||
|
, http-types
|
||||||
|
, interpolate
|
||||||
|
, iso8601-time
|
||||||
|
, json-rpc
|
||||||
|
, lens
|
||||||
|
, lens-aeson
|
||||||
|
, lifted-async
|
||||||
|
, lifted-base
|
||||||
|
, memory
|
||||||
|
, mime-types
|
||||||
|
, monad-control
|
||||||
|
, monad-logger
|
||||||
|
, network
|
||||||
|
, persistent
|
||||||
|
, persistent-sqlite
|
||||||
|
, persistent-template
|
||||||
|
, process
|
||||||
|
, process-extras
|
||||||
|
, protolude
|
||||||
|
, regex-compat
|
||||||
|
, resourcet
|
||||||
|
, shell-conduit
|
||||||
|
, singletons
|
||||||
|
, stm
|
||||||
|
, streaming
|
||||||
|
, streaming-bytestring
|
||||||
|
, streaming-conduit
|
||||||
|
, streaming-utils
|
||||||
|
, tar-conduit
|
||||||
|
, template-haskell
|
||||||
|
, text >=0.11 && <2.0
|
||||||
|
, time
|
||||||
|
, transformers
|
||||||
|
, transformers-base
|
||||||
|
, typed-process
|
||||||
|
, unix
|
||||||
|
, unliftio
|
||||||
|
, unliftio-core
|
||||||
|
, unordered-containers
|
||||||
|
, uuid
|
||||||
|
, wai
|
||||||
|
, wai-cors
|
||||||
|
, wai-extra
|
||||||
|
, warp
|
||||||
|
, yaml
|
||||||
|
, yesod
|
||||||
|
, yesod-auth
|
||||||
|
, yesod-core
|
||||||
|
, yesod-form
|
||||||
|
, yesod-persistent
|
||||||
|
if flag(library-only)
|
||||||
|
buildable: False
|
||||||
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
test-suite agent-test
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
main-is: Main.hs
|
||||||
|
other-modules:
|
||||||
|
ChecklistSpec
|
||||||
|
Lib.External.AppManifestSpec
|
||||||
|
Lib.SoundSpec
|
||||||
|
Lib.Types.EmverProp
|
||||||
|
Live.Metrics
|
||||||
|
Live.Serialize
|
||||||
|
Spec
|
||||||
|
hs-source-dirs:
|
||||||
|
test
|
||||||
|
default-extensions:
|
||||||
|
NoImplicitPrelude
|
||||||
|
BlockArguments
|
||||||
|
ConstraintKinds
|
||||||
|
DataKinds
|
||||||
|
DeriveAnyClass
|
||||||
|
DeriveFunctor
|
||||||
|
DeriveGeneric
|
||||||
|
DerivingStrategies
|
||||||
|
EmptyCase
|
||||||
|
FlexibleContexts
|
||||||
|
FlexibleInstances
|
||||||
|
GADTs
|
||||||
|
GeneralizedNewtypeDeriving
|
||||||
|
InstanceSigs
|
||||||
|
KindSignatures
|
||||||
|
LambdaCase
|
||||||
|
MultiParamTypeClasses
|
||||||
|
MultiWayIf
|
||||||
|
NamedFieldPuns
|
||||||
|
NumericUnderscores
|
||||||
|
OverloadedStrings
|
||||||
|
PolyKinds
|
||||||
|
RankNTypes
|
||||||
|
StandaloneDeriving
|
||||||
|
StandaloneKindSignatures
|
||||||
|
TupleSections
|
||||||
|
TypeApplications
|
||||||
|
TypeFamilies
|
||||||
|
TypeOperators
|
||||||
|
ghc-options: -Wall -fdefer-typed-holes
|
||||||
|
build-depends:
|
||||||
|
aeson
|
||||||
|
, aeson-flatten
|
||||||
|
, ambassador-agent
|
||||||
|
, attoparsec
|
||||||
|
, base >=4.9.1.0 && <5
|
||||||
|
, bytestring
|
||||||
|
, casing
|
||||||
|
, comonad
|
||||||
|
, conduit
|
||||||
|
, conduit-extra
|
||||||
|
, connection
|
||||||
|
, containers
|
||||||
|
, cryptonite
|
||||||
|
, cryptonite-conduit
|
||||||
|
, data-default
|
||||||
|
, directory
|
||||||
|
, errors
|
||||||
|
, exceptions
|
||||||
|
, exinst
|
||||||
|
, fast-logger
|
||||||
|
, file-embed
|
||||||
|
, filelock
|
||||||
|
, filepath
|
||||||
|
, fused-effects
|
||||||
|
, fused-effects-th
|
||||||
|
, git-embed
|
||||||
|
, hedgehog
|
||||||
|
, hspec >=2.0.0
|
||||||
|
, hspec-expectations
|
||||||
|
, http-api-data
|
||||||
|
, http-client
|
||||||
|
, http-client-tls
|
||||||
|
, http-conduit
|
||||||
|
, http-types
|
||||||
|
, interpolate
|
||||||
|
, iso8601-time
|
||||||
|
, json-rpc
|
||||||
|
, lens
|
||||||
|
, lens-aeson
|
||||||
|
, lifted-async
|
||||||
|
, lifted-base
|
||||||
|
, memory
|
||||||
|
, mime-types
|
||||||
|
, monad-control
|
||||||
|
, monad-logger
|
||||||
|
, network
|
||||||
|
, persistent
|
||||||
|
, persistent-sqlite
|
||||||
|
, persistent-template
|
||||||
|
, process
|
||||||
|
, process-extras
|
||||||
|
, protolude
|
||||||
|
, random
|
||||||
|
, regex-compat
|
||||||
|
, resourcet
|
||||||
|
, shell-conduit
|
||||||
|
, singletons
|
||||||
|
, stm
|
||||||
|
, streaming
|
||||||
|
, streaming-bytestring
|
||||||
|
, streaming-conduit
|
||||||
|
, streaming-utils
|
||||||
|
, tar-conduit
|
||||||
|
, template-haskell
|
||||||
|
, text >=0.11 && <2.0
|
||||||
|
, time
|
||||||
|
, transformers
|
||||||
|
, transformers-base
|
||||||
|
, typed-process
|
||||||
|
, unix
|
||||||
|
, unliftio
|
||||||
|
, unliftio-core
|
||||||
|
, unordered-containers
|
||||||
|
, uuid
|
||||||
|
, wai
|
||||||
|
, wai-cors
|
||||||
|
, wai-extra
|
||||||
|
, warp
|
||||||
|
, yaml
|
||||||
|
, yesod
|
||||||
|
, yesod-auth
|
||||||
|
, yesod-core
|
||||||
|
, yesod-form
|
||||||
|
, yesod-persistent
|
||||||
|
, yesod-test
|
||||||
|
default-language: Haskell2010
|
||||||
23
agent/cabal.project
Normal file
23
agent/cabal.project
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
-- Generated by stackage-to-hackage
|
||||||
|
|
||||||
|
index-state: 2021-04-26T18:08:38Z
|
||||||
|
|
||||||
|
with-compiler: ghc-8.10.2
|
||||||
|
|
||||||
|
packages:
|
||||||
|
./
|
||||||
|
|
||||||
|
source-repository-package
|
||||||
|
type: git
|
||||||
|
location: https://github.com/ProofOfKeags/persistent.git
|
||||||
|
tag: 3b52b13d9ce79cdef14bb1c37cc527657a529462
|
||||||
|
subdir: persistent-sqlite
|
||||||
|
|
||||||
|
allow-older: *
|
||||||
|
allow-newer: *
|
||||||
|
|
||||||
|
package *
|
||||||
|
ghc-options: -haddock
|
||||||
|
|
||||||
|
package ambassador-agent
|
||||||
|
ghc-options: -fwrite-ide-info
|
||||||
2513
agent/cabal.project.freeze
Normal file
2513
agent/cabal.project.freeze
Normal file
File diff suppressed because it is too large
Load Diff
@@ -33,5 +33,5 @@ database:
|
|||||||
database: "start9_agent.sqlite3"
|
database: "start9_agent.sqlite3"
|
||||||
poolsize: "_env:YESOD_SQLITE_POOLSIZE:10"
|
poolsize: "_env:YESOD_SQLITE_POOLSIZE:10"
|
||||||
|
|
||||||
app-mgr-version-spec: "=0.2.10"
|
app-mgr-version-spec: "=0.2.13"
|
||||||
#analytics: UA-YOURCODE
|
#analytics: UA-YOURCODE
|
||||||
|
|||||||
1
agent/migrations/0.2.10::0.2.11
Normal file
1
agent/migrations/0.2.10::0.2.11
Normal file
@@ -0,0 +1 @@
|
|||||||
|
SELECT TRUE;
|
||||||
1
agent/migrations/0.2.11::0.2.12
Normal file
1
agent/migrations/0.2.11::0.2.12
Normal file
@@ -0,0 +1 @@
|
|||||||
|
SELECT TRUE;
|
||||||
1
agent/migrations/0.2.12::0.2.13
Normal file
1
agent/migrations/0.2.12::0.2.13
Normal file
@@ -0,0 +1 @@
|
|||||||
|
SELECT TRUE;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
name: ambassador-agent
|
name: ambassador-agent
|
||||||
version: 0.2.10
|
version: 0.2.13
|
||||||
|
|
||||||
default-extensions:
|
default-extensions:
|
||||||
- NoImplicitPrelude
|
- NoImplicitPrelude
|
||||||
@@ -182,3 +182,4 @@ executables:
|
|||||||
condition: flag(library-only)
|
condition: flag(library-only)
|
||||||
- condition: false
|
- condition: false
|
||||||
other-modules: Paths_ambassador_agent
|
other-modules: Paths_ambassador_agent
|
||||||
|
extra-source-files: ./migrations/*
|
||||||
@@ -186,6 +186,8 @@ cutoffDuringUpdate m = do
|
|||||||
path <- asks $ pathInfo . reqWaiRequest . handlerRequest
|
path <- asks $ pathInfo . reqWaiRequest . handlerRequest
|
||||||
case path of
|
case path of
|
||||||
[v] | v == "v" <> (show . major $ agentVersion) -> m
|
[v] | v == "v" <> (show . major $ agentVersion) -> m
|
||||||
|
[auth] | auth == "auth" -> m
|
||||||
|
(_:ssh:_) | ssh == "sshKeys" -> m
|
||||||
_ -> handleS9ErrT $ throwE UpdateInProgressE
|
_ -> handleS9ErrT $ throwE UpdateInProgressE
|
||||||
Nothing -> m
|
Nothing -> m
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,11 @@ type AllEffects m
|
|||||||
( Labelled
|
( Labelled
|
||||||
"databaseConnection"
|
"databaseConnection"
|
||||||
(ReaderT ConnectionPool)
|
(ReaderT ConnectionPool)
|
||||||
(ReaderT AgentCtx (ErrorC S9Error (LiftC m)))
|
( Labelled
|
||||||
|
"lanThread"
|
||||||
|
(ReaderT (MVar ThreadId))
|
||||||
|
(ReaderT AgentCtx (ErrorC S9Error (LiftC m)))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -122,6 +126,8 @@ intoHandler m = do
|
|||||||
runM
|
runM
|
||||||
. handleS9ErrC
|
. handleS9ErrC
|
||||||
. flip runReaderT ctx
|
. flip runReaderT ctx
|
||||||
|
. flip runReaderT (appLanThread ctx)
|
||||||
|
. runLabelled @"lanThread"
|
||||||
. flip runReaderT (appConnPool ctx)
|
. flip runReaderT (appConnPool ctx)
|
||||||
. runLabelled @"databaseConnection"
|
. runLabelled @"databaseConnection"
|
||||||
. flip runReaderT fsbase
|
. flip runReaderT fsbase
|
||||||
@@ -148,8 +154,7 @@ getAvailableAppsLogic :: ( Has (Reader AgentCtx) sig m
|
|||||||
getAvailableAppsLogic = do
|
getAvailableAppsLogic = do
|
||||||
jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO
|
jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO
|
||||||
let installCache = inspect SInstalling jobCache
|
let installCache = inspect SInstalling jobCache
|
||||||
(Reg.AppManifestRes apps, serverApps) <- LAsync.concurrently Reg.getAppManifest
|
(Reg.AppIndexRes apps, serverApps) <- LAsync.concurrently Reg.getAppIndex (AppMgr2.list [AppMgr2.flags|-s -d|])
|
||||||
(AppMgr2.list [AppMgr2.flags|-s -d|])
|
|
||||||
let remapped = remapAppMgrInfo jobCache serverApps
|
let remapped = remapAppMgrInfo jobCache serverApps
|
||||||
pure $ foreach apps $ \app@StoreApp { storeAppId } ->
|
pure $ foreach apps $ \app@StoreApp { storeAppId } ->
|
||||||
let installing =
|
let installing =
|
||||||
@@ -177,8 +182,9 @@ getAvailableAppByIdLogic appId = do
|
|||||||
let storeAppId' = storeAppId
|
let storeAppId' = storeAppId
|
||||||
jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO
|
jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO
|
||||||
let installCache = inspect SInstalling jobCache
|
let installCache = inspect SInstalling jobCache
|
||||||
(Reg.AppManifestRes storeApps, serverApps) <- LAsync.concurrently Reg.getAppManifest
|
((Reg.AppIndexRes storeApps, serverApps), AppManifest.AppManifest { appManifestLicenseName, appManifestLicenseLink }) <-
|
||||||
(AppMgr2.list [AppMgr2.flags|-s -d|])
|
LAsync.concurrently (LAsync.concurrently Reg.getAppIndex (AppMgr2.list [AppMgr2.flags|-s -d|]))
|
||||||
|
(Reg.getAppManifest appId)
|
||||||
StoreApp {..} <- pure (find ((== appId) . storeAppId) storeApps) `orThrowM` NotFoundE "appId" (show appId)
|
StoreApp {..} <- pure (find ((== appId) . storeAppId) storeApps) `orThrowM` NotFoundE "appId" (show appId)
|
||||||
let remapped = remapAppMgrInfo jobCache serverApps
|
let remapped = remapAppMgrInfo jobCache serverApps
|
||||||
let installingInfo =
|
let installingInfo =
|
||||||
@@ -207,6 +213,8 @@ getAvailableAppByIdLogic appId = do
|
|||||||
appId
|
appId
|
||||||
storeAppTitle
|
storeAppTitle
|
||||||
(storeIconUrl appId (storeAppVersionInfoVersion $ extract storeAppVersions))
|
(storeIconUrl appId (storeAppVersionInfoVersion $ extract storeAppVersions))
|
||||||
|
, appAvailableFullLicenseName = appManifestLicenseName
|
||||||
|
, appAvailableFullLicenseLink = appManifestLicenseLink
|
||||||
, appAvailableFullInstallInfo = installingInfo
|
, appAvailableFullInstallInfo = installingInfo
|
||||||
, appAvailableFullVersionLatest = storeAppVersionInfoVersion latest
|
, appAvailableFullVersionLatest = storeAppVersionInfoVersion latest
|
||||||
, appAvailableFullDescriptionShort = storeAppDescriptionShort
|
, appAvailableFullDescriptionShort = storeAppDescriptionShort
|
||||||
@@ -297,6 +305,8 @@ getInstalledAppByIdLogic appId = do
|
|||||||
backupTime <- lift $ LAsync.wait backupTime'
|
backupTime <- lift $ LAsync.wait backupTime'
|
||||||
hoistMaybe $ HM.lookup appId installCache <&> \(StoreApp {..}, StoreAppVersionInfo {..}) -> AppInstalledFull
|
hoistMaybe $ HM.lookup appId installCache <&> \(StoreApp {..}, StoreAppVersionInfo {..}) -> AppInstalledFull
|
||||||
{ appInstalledFullBase = AppBase appId storeAppTitle (iconUrl appId storeAppVersionInfoVersion)
|
{ appInstalledFullBase = AppBase appId storeAppTitle (iconUrl appId storeAppVersionInfoVersion)
|
||||||
|
, appInstalledFullLicenseName = Nothing
|
||||||
|
, appInstalledFullLicenseLink = Nothing
|
||||||
, appInstalledFullStatus = AppStatusTmp Installing
|
, appInstalledFullStatus = AppStatusTmp Installing
|
||||||
, appInstalledFullVersionInstalled = storeAppVersionInfoVersion
|
, appInstalledFullVersionInstalled = storeAppVersionInfoVersion
|
||||||
, appInstalledFullInstructions = Nothing
|
, appInstalledFullInstructions = Nothing
|
||||||
@@ -313,7 +323,7 @@ getInstalledAppByIdLogic appId = do
|
|||||||
}
|
}
|
||||||
serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|]
|
serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|]
|
||||||
let remapped = remapAppMgrInfo jobCache serverApps
|
let remapped = remapAppMgrInfo jobCache serverApps
|
||||||
appManifestFetchCached <- cached Reg.getAppManifest
|
appManifestFetchCached <- cached Reg.getAppIndex
|
||||||
let
|
let
|
||||||
installed = do
|
installed = do
|
||||||
(status, version, AppMgr2.InfoRes {..}) <- hoistMaybe (HM.lookup appId remapped)
|
(status, version, AppMgr2.InfoRes {..}) <- hoistMaybe (HM.lookup appId remapped)
|
||||||
@@ -327,7 +337,7 @@ getInstalledAppByIdLogic appId = do
|
|||||||
fromInstalled = (AppMgr2.infoResTitle &&& AppMgr2.infoResVersion)
|
fromInstalled = (AppMgr2.infoResTitle &&& AppMgr2.infoResVersion)
|
||||||
<$> hoistMaybe (HM.lookup depId serverApps)
|
<$> hoistMaybe (HM.lookup depId serverApps)
|
||||||
let fromStore = do
|
let fromStore = do
|
||||||
Reg.AppManifestRes res <- lift appManifestFetchCached
|
Reg.AppIndexRes res <- lift appManifestFetchCached
|
||||||
(storeAppTitle &&& storeAppVersionInfoVersion . extract . storeAppVersions)
|
(storeAppTitle &&& storeAppVersionInfoVersion . extract . storeAppVersions)
|
||||||
<$> hoistMaybe (find ((== depId) . storeAppId) res)
|
<$> hoistMaybe (find ((== depId) . storeAppId) res)
|
||||||
(title, v) <- fromInstalled <|> fromStore
|
(title, v) <- fromInstalled <|> fromStore
|
||||||
@@ -348,6 +358,8 @@ getInstalledAppByIdLogic appId = do
|
|||||||
guard (not . null $ lanConfs)
|
guard (not . null $ lanConfs)
|
||||||
pure $ LanAddress . (".onion" `Text.replace` ".local") . unTorAddress $ addrBase
|
pure $ LanAddress . (".onion" `Text.replace` ".local") . unTorAddress $ addrBase
|
||||||
pure AppInstalledFull { appInstalledFullBase = AppBase appId infoResTitle (iconUrl appId version)
|
pure AppInstalledFull { appInstalledFullBase = AppBase appId infoResTitle (iconUrl appId version)
|
||||||
|
, appInstalledFullLicenseName = AppManifest.appManifestLicenseName manifest
|
||||||
|
, appInstalledFullLicenseLink = AppManifest.appManifestLicenseLink manifest
|
||||||
, appInstalledFullStatus = status
|
, appInstalledFullStatus = status
|
||||||
, appInstalledFullVersionInstalled = version
|
, appInstalledFullVersionInstalled = version
|
||||||
, appInstalledFullInstructions = instructions
|
, appInstalledFullInstructions = instructions
|
||||||
@@ -376,6 +388,7 @@ postUninstallAppLogic :: ( HasFilesystemBase sig m
|
|||||||
, MonadIO m
|
, MonadIO m
|
||||||
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
|
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
|
||||||
, HasLabelled "iconTagCache" (Reader (TVar (HM.HashMap AppId (Digest MD5)))) sig m
|
, HasLabelled "iconTagCache" (Reader (TVar (HM.HashMap AppId (Digest MD5)))) sig m
|
||||||
|
, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m
|
||||||
)
|
)
|
||||||
=> AppId
|
=> AppId
|
||||||
-> AppMgr2.DryRun
|
-> AppMgr2.DryRun
|
||||||
@@ -413,6 +426,7 @@ postInstallNewAppR appId = do
|
|||||||
|
|
||||||
postInstallNewAppLogic :: forall sig m a
|
postInstallNewAppLogic :: forall sig m a
|
||||||
. ( Has (Reader AgentCtx) sig m
|
. ( Has (Reader AgentCtx) sig m
|
||||||
|
, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m
|
||||||
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
|
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
|
||||||
, HasLabelled "iconTagCache" (Reader (TVar (HM.HashMap AppId (Digest MD5)))) sig m
|
, HasLabelled "iconTagCache" (Reader (TVar (HM.HashMap AppId (Digest MD5)))) sig m
|
||||||
, Has (Error S9Error) sig m
|
, Has (Error S9Error) sig m
|
||||||
@@ -666,8 +680,8 @@ getAvailableAppVersionInfoLogic :: ( Has (Reader AgentCtx) sig m
|
|||||||
-> VersionRange
|
-> VersionRange
|
||||||
-> m AppVersionInfo
|
-> m AppVersionInfo
|
||||||
getAvailableAppVersionInfoLogic appId appVersionSpec = do
|
getAvailableAppVersionInfoLogic appId appVersionSpec = do
|
||||||
jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO
|
jobCache <- asks appBackgroundJobs >>= liftIO . readTVarIO
|
||||||
Reg.AppManifestRes storeApps <- Reg.getAppManifest
|
Reg.AppIndexRes storeApps <- Reg.getAppIndex
|
||||||
let titles =
|
let titles =
|
||||||
(storeAppTitle &&& storeAppVersionInfoVersion . extract . storeAppVersions) <$> indexBy storeAppId storeApps
|
(storeAppTitle &&& storeAppVersionInfoVersion . extract . storeAppVersions) <$> indexBy storeAppId storeApps
|
||||||
StoreApp {..} <- find ((== appId) . storeAppId) storeApps `orThrowPure` NotFoundE "appId" (show appId)
|
StoreApp {..} <- find ((== appId) . storeAppId) storeApps `orThrowPure` NotFoundE "appId" (show appId)
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import Startlude hiding ( Reader
|
|||||||
, runReader
|
, runReader
|
||||||
)
|
)
|
||||||
|
|
||||||
import Control.Effect.Labelled hiding ( Handler )
|
|
||||||
import Control.Effect.Reader.Labelled
|
|
||||||
import Control.Carrier.Error.Church
|
import Control.Carrier.Error.Church
|
||||||
import Control.Carrier.Lift
|
import Control.Carrier.Lift
|
||||||
import Control.Carrier.Reader ( runReader )
|
import Control.Carrier.Reader ( runReader )
|
||||||
|
import Control.Effect.Labelled hiding ( Handler )
|
||||||
|
import Control.Effect.Reader.Labelled
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import Data.UUID.V4
|
import Data.UUID.V4
|
||||||
@@ -20,8 +20,13 @@ import Yesod.Auth
|
|||||||
import Yesod.Core
|
import Yesod.Core
|
||||||
import Yesod.Core.Types
|
import Yesod.Core.Types
|
||||||
|
|
||||||
|
import Control.Concurrent.STM
|
||||||
|
import Exinst
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Handler.Network
|
||||||
import Handler.Util
|
import Handler.Util
|
||||||
|
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
||||||
|
import Lib.Background
|
||||||
import Lib.Error
|
import Lib.Error
|
||||||
import qualified Lib.External.AppMgr as AppMgr
|
import qualified Lib.External.AppMgr as AppMgr
|
||||||
import qualified Lib.Notifications as Notifications
|
import qualified Lib.Notifications as Notifications
|
||||||
@@ -29,10 +34,6 @@ import Lib.Password
|
|||||||
import Lib.Types.Core
|
import Lib.Types.Core
|
||||||
import Lib.Types.Emver
|
import Lib.Types.Emver
|
||||||
import Model
|
import Model
|
||||||
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
|
||||||
import Lib.Background
|
|
||||||
import Control.Concurrent.STM
|
|
||||||
import Exinst
|
|
||||||
|
|
||||||
|
|
||||||
data CreateBackupReq = CreateBackupReq
|
data CreateBackupReq = CreateBackupReq
|
||||||
@@ -59,7 +60,8 @@ instance FromJSON RestoreBackupReq where
|
|||||||
|
|
||||||
data EjectDiskReq = EjectDiskReq
|
data EjectDiskReq = EjectDiskReq
|
||||||
{ ejectDiskLogicalName :: Text
|
{ ejectDiskLogicalName :: Text
|
||||||
} deriving (Eq, Show)
|
}
|
||||||
|
deriving (Eq, Show)
|
||||||
instance FromJSON EjectDiskReq where
|
instance FromJSON EjectDiskReq where
|
||||||
parseJSON = withObject "Eject Disk Req" $ \o -> do
|
parseJSON = withObject "Eject Disk Req" $ \o -> do
|
||||||
ejectDiskLogicalName <- o .: "logicalName"
|
ejectDiskLogicalName <- o .: "logicalName"
|
||||||
@@ -100,6 +102,8 @@ postRestoreBackupR appId = disableEndpointOnFailedUpdate $ do
|
|||||||
& runReader appConnPool
|
& runReader appConnPool
|
||||||
& runLabelled @"backgroundJobCache"
|
& runLabelled @"backgroundJobCache"
|
||||||
& runReader appBackgroundJobs
|
& runReader appBackgroundJobs
|
||||||
|
& runLabelled @"lanThread"
|
||||||
|
& runReader appLanThread
|
||||||
& handleS9ErrC
|
& handleS9ErrC
|
||||||
& runM
|
& runM
|
||||||
|
|
||||||
@@ -173,6 +177,7 @@ stopBackupLogic appId = do
|
|||||||
|
|
||||||
restoreBackupLogic :: ( HasLabelled "backgroundJobCache" (Reader (TVar JobCache)) sig m
|
restoreBackupLogic :: ( HasLabelled "backgroundJobCache" (Reader (TVar JobCache)) sig m
|
||||||
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
|
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
|
||||||
|
, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m
|
||||||
, Has (Error S9Error) sig m
|
, Has (Error S9Error) sig m
|
||||||
, Has AppMgr2.AppMgr sig m
|
, Has AppMgr2.AppMgr sig m
|
||||||
, MonadIO m
|
, MonadIO m
|
||||||
@@ -181,10 +186,11 @@ restoreBackupLogic :: ( HasLabelled "backgroundJobCache" (Reader (TVar JobCache)
|
|||||||
-> RestoreBackupReq
|
-> RestoreBackupReq
|
||||||
-> m ()
|
-> m ()
|
||||||
restoreBackupLogic appId RestoreBackupReq {..} = do
|
restoreBackupLogic appId RestoreBackupReq {..} = do
|
||||||
jobCache <- ask @"backgroundJobCache"
|
lanThread <- ask @"lanThread"
|
||||||
db <- ask @"databaseConnection"
|
jobCache <- ask @"backgroundJobCache"
|
||||||
version <- fmap AppMgr2.infoResVersion $ AppMgr2.info [AppMgr2.flags| |] appId `orThrowM` NotFoundE "appId"
|
db <- ask @"databaseConnection"
|
||||||
(show appId)
|
version <- fmap AppMgr2.infoResVersion $ AppMgr2.info [AppMgr2.flags| |] appId `orThrowM` NotFoundE "appId"
|
||||||
|
(show appId)
|
||||||
res <- liftIO . atomically $ do
|
res <- liftIO . atomically $ do
|
||||||
(JobCache jobs) <- readTVar jobCache
|
(JobCache jobs) <- readTVar jobCache
|
||||||
case HM.lookup appId jobs of
|
case HM.lookup appId jobs of
|
||||||
@@ -206,10 +212,13 @@ restoreBackupLogic appId RestoreBackupReq {..} = do
|
|||||||
let notif = case appmgrRes of
|
let notif = case appmgrRes of
|
||||||
Left e -> Notifications.RestoreFailed e
|
Left e -> Notifications.RestoreFailed e
|
||||||
Right _ -> Notifications.RestoreSucceeded
|
Right _ -> Notifications.RestoreSucceeded
|
||||||
|
resetRes <- runExceptT @S9Error $ runReader lanThread . runLabelled @"lanThread" $ postResetLanLogic
|
||||||
|
case resetRes of
|
||||||
|
Left _ -> pure () -- temporarily forbidden is the only possible thing here so ignore it
|
||||||
|
Right () -> pure ()
|
||||||
flip runSqlPool db $ void $ Notifications.emit appId version notif
|
flip runSqlPool db $ void $ Notifications.emit appId version notif
|
||||||
liftIO . atomically $ modifyTVar jobCache (insertJob appId Restore tid)
|
liftIO . atomically $ modifyTVar jobCache (insertJob appId Restore tid)
|
||||||
|
|
||||||
|
|
||||||
listDisksLogic :: (Has (Error S9Error) sig m, MonadIO m) => m [AppMgr.DiskInfo]
|
listDisksLogic :: (Has (Error S9Error) sig m, MonadIO m) => m [AppMgr.DiskInfo]
|
||||||
listDisksLogic = runExceptT AppMgr.diskShow >>= liftEither
|
listDisksLogic = runExceptT AppMgr.diskShow >>= liftEither
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ import Network.HTTP.Simple
|
|||||||
import System.FilePath.Posix
|
import System.FilePath.Posix
|
||||||
import Yesod.Core
|
import Yesod.Core
|
||||||
|
|
||||||
|
import Control.Carrier.Reader hiding ( asks )
|
||||||
|
import Control.Concurrent.STM ( modifyTVar
|
||||||
|
, readTVarIO
|
||||||
|
)
|
||||||
|
import Control.Effect.Labelled ( runLabelled )
|
||||||
|
import Crypto.Hash.Conduit ( hashFile )
|
||||||
|
import qualified Data.HashMap.Strict as HM
|
||||||
import Foundation
|
import Foundation
|
||||||
import Lib.Algebra.State.RegistryUrl
|
import Lib.Algebra.State.RegistryUrl
|
||||||
import Lib.Error
|
import Lib.Error
|
||||||
@@ -21,16 +28,9 @@ import qualified Lib.External.Registry as Reg
|
|||||||
import Lib.IconCache
|
import Lib.IconCache
|
||||||
import Lib.SystemPaths hiding ( (</>) )
|
import Lib.SystemPaths hiding ( (</>) )
|
||||||
import Lib.Types.Core
|
import Lib.Types.Core
|
||||||
|
import Lib.Types.Emver
|
||||||
import Lib.Types.ServerApp
|
import Lib.Types.ServerApp
|
||||||
import Settings
|
import Settings
|
||||||
import Control.Carrier.Reader hiding ( asks )
|
|
||||||
import Control.Effect.Labelled ( runLabelled )
|
|
||||||
import qualified Data.HashMap.Strict as HM
|
|
||||||
import Control.Concurrent.STM ( modifyTVar
|
|
||||||
, readTVarIO
|
|
||||||
)
|
|
||||||
import Crypto.Hash.Conduit ( hashFile )
|
|
||||||
import Lib.Types.Emver
|
|
||||||
|
|
||||||
iconUrl :: AppId -> Version -> Text
|
iconUrl :: AppId -> Version -> Text
|
||||||
iconUrl appId version = (foldMap (T.cons '/') . fst . renderRoute . AppIconR $ appId) <> "?" <> show version
|
iconUrl appId version = (foldMap (T.cons '/') . fst . renderRoute . AppIconR $ appId) <> "?" <> show version
|
||||||
@@ -63,7 +63,7 @@ getAppIconR appId = handleS9ErrT $ do
|
|||||||
lift $ respondSource (parseContentType path) $ CB.sourceFile path .| awaitForever sendChunkBS
|
lift $ respondSource (parseContentType path) $ CB.sourceFile path .| awaitForever sendChunkBS
|
||||||
where
|
where
|
||||||
fetchIcon = do
|
fetchIcon = do
|
||||||
url <- find ((== appId) . storeAppId) . Reg.storeApps <$> Reg.getAppManifest >>= \case
|
url <- find ((== appId) . storeAppId) . Reg.storeApps <$> Reg.getAppIndex >>= \case
|
||||||
Nothing -> throwError $ NotFoundE "icon" (show appId)
|
Nothing -> throwError $ NotFoundE "icon" (show appId)
|
||||||
Just x -> pure . toS $ storeAppIconUrl x
|
Just x -> pure . toS $ storeAppIconUrl x
|
||||||
bp <- getAbsoluteLocationFor iconBasePath
|
bp <- getAbsoluteLocationFor iconBasePath
|
||||||
@@ -84,7 +84,7 @@ getAvailableAppIconR :: AppId -> Handler TypedContent
|
|||||||
getAvailableAppIconR appId = handleS9ErrT $ do
|
getAvailableAppIconR appId = handleS9ErrT $ do
|
||||||
s <- getsYesod appSettings
|
s <- getsYesod appSettings
|
||||||
url <- do
|
url <- do
|
||||||
find ((== appId) . storeAppId) . Reg.storeApps <$> interp s Reg.getAppManifest >>= \case
|
find ((== appId) . storeAppId) . Reg.storeApps <$> interp s Reg.getAppIndex >>= \case
|
||||||
Nothing -> throwE $ NotFoundE "icon" (show appId)
|
Nothing -> throwE $ NotFoundE "icon" (show appId)
|
||||||
Just x -> pure . toS $ storeAppIconUrl x
|
Just x -> pure . toS $ storeAppIconUrl x
|
||||||
req <- case parseRequest url of
|
req <- case parseRequest url of
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
module Handler.Network where
|
module Handler.Network where
|
||||||
|
|
||||||
import Startlude hiding ( Reader
|
import Startlude hiding ( Reader
|
||||||
|
, ask
|
||||||
, asks
|
, asks
|
||||||
, runReader
|
, runReader
|
||||||
)
|
)
|
||||||
|
|
||||||
import Control.Carrier.Lift ( runM )
|
import Control.Carrier.Lift ( runM )
|
||||||
import Control.Effect.Error
|
import Control.Effect.Error
|
||||||
import Control.Carrier.Reader
|
|
||||||
import Lib.Error
|
import Lib.Error
|
||||||
import Yesod.Core ( getYesod )
|
import Yesod.Core ( getYesod )
|
||||||
|
|
||||||
|
import Control.Carrier.Reader ( runReader )
|
||||||
|
import Control.Effect.Labelled ( runLabelled )
|
||||||
|
import Control.Effect.Reader.Labelled
|
||||||
import Foundation
|
import Foundation
|
||||||
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
||||||
import Lib.Types.Core
|
import Lib.Types.Core
|
||||||
@@ -18,11 +21,12 @@ import Lib.Types.Core
|
|||||||
postResetLanR :: Handler ()
|
postResetLanR :: Handler ()
|
||||||
postResetLanR = do
|
postResetLanR = do
|
||||||
ctx <- getYesod
|
ctx <- getYesod
|
||||||
runM . handleS9ErrC . runReader ctx $ postResetLanLogic
|
runM . handleS9ErrC . runReader (appLanThread ctx) . runLabelled @"lanThread" $ postResetLanLogic
|
||||||
|
|
||||||
postResetLanLogic :: (MonadIO m, Has (Reader AgentCtx) sig m, Has (Error S9Error) sig m) => m ()
|
postResetLanLogic :: (MonadIO m, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m, Has (Error S9Error) sig m)
|
||||||
|
=> m ()
|
||||||
postResetLanLogic = do
|
postResetLanLogic = do
|
||||||
threadVar <- asks appLanThread
|
threadVar <- ask @"lanThread"
|
||||||
mtid <- liftIO . tryTakeMVar $ threadVar
|
mtid <- liftIO . tryTakeMVar $ threadVar
|
||||||
case mtid of
|
case mtid of
|
||||||
Nothing -> throwError $ TemporarilyForbiddenE (AppId "LAN") "reset" "being reset"
|
Nothing -> throwError $ TemporarilyForbiddenE (AppId "LAN") "reset" "being reset"
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ instance FromJSON InstallNewAppReq where
|
|||||||
|
|
||||||
data AppAvailableFull = AppAvailableFull
|
data AppAvailableFull = AppAvailableFull
|
||||||
{ appAvailableFullBase :: AppBase
|
{ appAvailableFullBase :: AppBase
|
||||||
|
, appAvailableFullLicenseName :: Maybe Text
|
||||||
|
, appAvailableFullLicenseLink :: Maybe Text
|
||||||
, appAvailableFullInstallInfo :: Maybe (Version, AppStatus)
|
, appAvailableFullInstallInfo :: Maybe (Version, AppStatus)
|
||||||
, appAvailableFullVersionLatest :: Version
|
, appAvailableFullVersionLatest :: Version
|
||||||
, appAvailableFullDescriptionShort :: Text
|
, appAvailableFullDescriptionShort :: Text
|
||||||
@@ -88,7 +90,9 @@ instance ToJSON AppAvailableFull where
|
|||||||
toJSON AppAvailableFull {..} = mergeTo
|
toJSON AppAvailableFull {..} = mergeTo
|
||||||
(toJSON appAvailableFullBase)
|
(toJSON appAvailableFullBase)
|
||||||
(object
|
(object
|
||||||
[ "versionInstalled" .= fmap fst appAvailableFullInstallInfo
|
[ "licenseName" .= appAvailableFullLicenseName
|
||||||
|
, "licenseLink" .= appAvailableFullLicenseLink
|
||||||
|
, "versionInstalled" .= fmap fst appAvailableFullInstallInfo
|
||||||
, "status" .= fmap snd appAvailableFullInstallInfo
|
, "status" .= fmap snd appAvailableFullInstallInfo
|
||||||
, "versionLatest" .= appAvailableFullVersionLatest
|
, "versionLatest" .= appAvailableFullVersionLatest
|
||||||
, "descriptionShort" .= appAvailableFullDescriptionShort
|
, "descriptionShort" .= appAvailableFullDescriptionShort
|
||||||
@@ -131,6 +135,8 @@ instance ToJSON (AppDependencyRequirement Keep) where
|
|||||||
-- mute violations downstream of version for installing apps
|
-- mute violations downstream of version for installing apps
|
||||||
data AppInstalledFull = AppInstalledFull
|
data AppInstalledFull = AppInstalledFull
|
||||||
{ appInstalledFullBase :: AppBase
|
{ appInstalledFullBase :: AppBase
|
||||||
|
, appInstalledFullLicenseName :: Maybe Text
|
||||||
|
, appInstalledFullLicenseLink :: Maybe Text
|
||||||
, appInstalledFullStatus :: AppStatus
|
, appInstalledFullStatus :: AppStatus
|
||||||
, appInstalledFullVersionInstalled :: Version
|
, appInstalledFullVersionInstalled :: Version
|
||||||
, appInstalledFullTorAddress :: Maybe TorAddress
|
, appInstalledFullTorAddress :: Maybe TorAddress
|
||||||
@@ -156,6 +162,8 @@ instance ToJSON AppInstalledFull where
|
|||||||
, "lanUi" .= appInstalledFullLanUi
|
, "lanUi" .= appInstalledFullLanUi
|
||||||
, "id" .= appBaseId appInstalledFullBase
|
, "id" .= appBaseId appInstalledFullBase
|
||||||
, "title" .= appBaseTitle appInstalledFullBase
|
, "title" .= appBaseTitle appInstalledFullBase
|
||||||
|
, "licenseName" .= appInstalledFullLicenseName
|
||||||
|
, "licenseLink" .= appInstalledFullLicenseLink
|
||||||
, "iconURL" .= appBaseIconUrl appInstalledFullBase
|
, "iconURL" .= appBaseIconUrl appInstalledFullBase
|
||||||
, "versionInstalled" .= appInstalledFullVersionInstalled
|
, "versionInstalled" .= appInstalledFullVersionInstalled
|
||||||
, "status" .= appInstalledFullStatus
|
, "status" .= appInstalledFullStatus
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ module Lib.Algebra.Domain.AppMgr
|
|||||||
( module Lib.Algebra.Domain.AppMgr
|
( module Lib.Algebra.Domain.AppMgr
|
||||||
, module Lib.Algebra.Domain.AppMgr.Types
|
, module Lib.Algebra.Domain.AppMgr.Types
|
||||||
, module Lib.Algebra.Domain.AppMgr.TH
|
, module Lib.Algebra.Domain.AppMgr.TH
|
||||||
)
|
) where
|
||||||
where
|
|
||||||
|
|
||||||
import Startlude
|
import Startlude
|
||||||
|
|
||||||
@@ -26,31 +25,31 @@ import Data.Singletons.Prelude hiding ( Error )
|
|||||||
import Data.Singletons.Prelude.Either
|
import Data.Singletons.Prelude.Either
|
||||||
import qualified Data.String as String
|
import qualified Data.String as String
|
||||||
|
|
||||||
import Lib.Algebra.Domain.AppMgr.Types
|
import Control.Monad.Base ( MonadBase(..) )
|
||||||
|
import Control.Monad.Fail ( MonadFail(fail) )
|
||||||
|
import Control.Monad.Trans.Class ( MonadTrans )
|
||||||
|
import Control.Monad.Trans.Control ( MonadBaseControl(..)
|
||||||
|
, MonadTransControl(..)
|
||||||
|
, defaultLiftBaseWith
|
||||||
|
, defaultRestoreM
|
||||||
|
)
|
||||||
|
import Control.Monad.Trans.Resource ( MonadResource(..) )
|
||||||
|
import qualified Data.ByteString.Char8 as C8
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
import Data.String.Interpolate.IsString
|
||||||
|
( i )
|
||||||
import Lib.Algebra.Domain.AppMgr.TH
|
import Lib.Algebra.Domain.AppMgr.TH
|
||||||
|
import Lib.Algebra.Domain.AppMgr.Types
|
||||||
import Lib.Error
|
import Lib.Error
|
||||||
import qualified Lib.External.AppManifest as Manifest
|
import qualified Lib.External.AppManifest as Manifest
|
||||||
import Lib.TyFam.ConditionalData
|
import Lib.TyFam.ConditionalData
|
||||||
import Lib.Types.Core ( AppId(..)
|
import Lib.Types.Core ( AppContainerStatus(..)
|
||||||
, AppContainerStatus(..)
|
, AppId(..)
|
||||||
)
|
)
|
||||||
import Lib.Types.NetAddress
|
|
||||||
import Lib.Types.Emver
|
import Lib.Types.Emver
|
||||||
import Control.Monad.Trans.Class ( MonadTrans )
|
import Lib.Types.NetAddress
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
|
||||||
import System.Process.Typed
|
|
||||||
import Data.String.Interpolate.IsString
|
|
||||||
( i )
|
|
||||||
import Control.Monad.Base ( MonadBase(..) )
|
|
||||||
import Control.Monad.Fail ( MonadFail(fail) )
|
|
||||||
import Control.Monad.Trans.Resource ( MonadResource(..) )
|
|
||||||
import Control.Monad.Trans.Control ( defaultLiftBaseWith
|
|
||||||
, defaultRestoreM
|
|
||||||
, MonadTransControl(..)
|
|
||||||
, MonadBaseControl(..)
|
|
||||||
)
|
|
||||||
import qualified Data.ByteString.Char8 as C8
|
|
||||||
import System.Process
|
import System.Process
|
||||||
|
import System.Process.Typed
|
||||||
|
|
||||||
|
|
||||||
type InfoRes :: Either OnlyInfoFlag [IncludeInfoFlag] -> Type
|
type InfoRes :: Either OnlyInfoFlag [IncludeInfoFlag] -> Type
|
||||||
@@ -371,13 +370,16 @@ instance (Has (Error S9Error) sig m, Algebra sig m, MonadIO m) => Algebra (AppMg
|
|||||||
(L (List (SRight flags))) -> do
|
(L (List (SRight flags))) -> do
|
||||||
let renderedFlags = (genInclusiveFlag <$> fromSing flags) <> ["--json"]
|
let renderedFlags = (genInclusiveFlag <$> fromSing flags) <> ["--json"]
|
||||||
let args = "list" : renderedFlags
|
let args = "list" : renderedFlags
|
||||||
(ec, out) <- readProcessInheritStderr "appmgr" args ""
|
let runIt retryCount = do
|
||||||
res <- case ec of
|
(ec, out) <- readProcessInheritStderr "appmgr" args ""
|
||||||
ExitSuccess -> case withSingI flags $ eitherDecodeStrict out of
|
case ec of
|
||||||
Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e
|
ExitSuccess -> case withSingI flags $ eitherDecodeStrict out of
|
||||||
Right x -> pure x
|
Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e
|
||||||
ExitFailure n -> throwError $ AppMgrE "list" n
|
Right x -> pure $ ctx $> x
|
||||||
pure $ ctx $> res
|
ExitFailure 6 ->
|
||||||
|
if retryCount > 0 then runIt (retryCount - 1) else throwError $ AppMgrE "list" 6
|
||||||
|
ExitFailure n -> throwError $ AppMgrE "list" n
|
||||||
|
runIt (1 :: Word) -- with 1 retry
|
||||||
(L (Remove dryorpurge appId)) -> do
|
(L (Remove dryorpurge appId)) -> do
|
||||||
let args = "remove" : case dryorpurge of
|
let args = "remove" : case dryorpurge of
|
||||||
Left (DryRun True) -> ["--dry-run", show appId, "--json"]
|
Left (DryRun True) -> ["--dry-run", show appId, "--json"]
|
||||||
|
|||||||
4
agent/src/Lib/External/AppManifest.hs
vendored
4
agent/src/Lib/External/AppManifest.hs
vendored
@@ -78,6 +78,8 @@ data AppManifest where
|
|||||||
AppManifest ::{ appManifestId :: AppId
|
AppManifest ::{ appManifestId :: AppId
|
||||||
, appManifestVersion :: Version
|
, appManifestVersion :: Version
|
||||||
, appManifestTitle :: Text
|
, appManifestTitle :: Text
|
||||||
|
, appManifestLicenseName :: Maybe Text
|
||||||
|
, appManifestLicenseLink :: Maybe Text
|
||||||
, appManifestDescShort :: Text
|
, appManifestDescShort :: Text
|
||||||
, appManifestDescLong :: Text
|
, appManifestDescLong :: Text
|
||||||
, appManifestReleaseNotes :: Text
|
, appManifestReleaseNotes :: Text
|
||||||
@@ -109,6 +111,8 @@ instance FromJSON AppManifest where
|
|||||||
appManifestId <- o .: "id"
|
appManifestId <- o .: "id"
|
||||||
appManifestVersion <- o .: "version"
|
appManifestVersion <- o .: "version"
|
||||||
appManifestTitle <- o .: "title"
|
appManifestTitle <- o .: "title"
|
||||||
|
appManifestLicenseName <- o .:? "license-info" >>= traverse (.: "license")
|
||||||
|
appManifestLicenseLink <- o .:? "license-info" >>= traverse (.: "url")
|
||||||
appManifestDescShort <- o .: "description" >>= (.: "short")
|
appManifestDescShort <- o .: "description" >>= (.: "short")
|
||||||
appManifestDescLong <- o .: "description" >>= (.: "long")
|
appManifestDescLong <- o .: "description" >>= (.: "long")
|
||||||
appManifestReleaseNotes <- o .: "release-notes"
|
appManifestReleaseNotes <- o .: "release-notes"
|
||||||
|
|||||||
27
agent/src/Lib/External/Registry.hs
vendored
27
agent/src/Lib/External/Registry.hs
vendored
@@ -13,8 +13,8 @@ import Startlude.ByteStream hiding ( count )
|
|||||||
|
|
||||||
import Conduit
|
import Conduit
|
||||||
import Control.Algebra
|
import Control.Algebra
|
||||||
import Control.Effect.Lift
|
|
||||||
import Control.Effect.Error
|
import Control.Effect.Error
|
||||||
|
import Control.Effect.Lift
|
||||||
import Control.Effect.Reader.Labelled
|
import Control.Effect.Reader.Labelled
|
||||||
import Control.Monad.Fail ( fail )
|
import Control.Monad.Fail ( fail )
|
||||||
import Control.Monad.Trans.Resource
|
import Control.Monad.Trans.Resource
|
||||||
@@ -30,15 +30,17 @@ import System.Directory
|
|||||||
import System.Process
|
import System.Process
|
||||||
|
|
||||||
import Constants
|
import Constants
|
||||||
|
import qualified Data.Aeson.Types ( parseEither )
|
||||||
|
import Data.Time.ISO8601 ( parseISO8601 )
|
||||||
import Lib.Algebra.State.RegistryUrl
|
import Lib.Algebra.State.RegistryUrl
|
||||||
import Lib.Error
|
import Lib.Error
|
||||||
|
import Lib.External.AppManifest
|
||||||
import Lib.SystemPaths
|
import Lib.SystemPaths
|
||||||
import Lib.Types.Core
|
import Lib.Types.Core
|
||||||
import Lib.Types.Emver
|
import Lib.Types.Emver
|
||||||
import Lib.Types.ServerApp
|
import Lib.Types.ServerApp
|
||||||
import Data.Time.ISO8601 ( parseISO8601 )
|
|
||||||
|
|
||||||
newtype AppManifestRes = AppManifestRes
|
newtype AppIndexRes = AppIndexRes
|
||||||
{ storeApps :: [StoreApp] } deriving (Eq, Show)
|
{ storeApps :: [StoreApp] } deriving (Eq, Show)
|
||||||
|
|
||||||
newtype RegistryVersionForSpecRes = RegistryVersionForSpecRes
|
newtype RegistryVersionForSpecRes = RegistryVersionForSpecRes
|
||||||
@@ -85,8 +87,8 @@ getLifelineBinary avs = do
|
|||||||
liftIO $ runConduitRes $ httpSource request getResponseBody .| sinkFile (toS lifelineTarget)
|
liftIO $ runConduitRes $ httpSource request getResponseBody .| sinkFile (toS lifelineTarget)
|
||||||
liftIO $ void $ readProcessWithExitCode "chmod" ["700", toS lifelineTarget] ""
|
liftIO $ void $ readProcessWithExitCode "chmod" ["700", toS lifelineTarget] ""
|
||||||
|
|
||||||
getAppManifest :: (MonadIO m, Has (Error S9Error) sig m, Has RegistryUrl sig m) => m AppManifestRes
|
getAppIndex :: (MonadIO m, Has (Error S9Error) sig m, Has RegistryUrl sig m) => m AppIndexRes
|
||||||
getAppManifest = do
|
getAppIndex = do
|
||||||
manifestPath <- registryManifestUrl
|
manifestPath <- registryManifestUrl
|
||||||
req <- liftIO $ fmap setUserAgent . parseRequestThrow $ toS manifestPath
|
req <- liftIO $ fmap setUserAgent . parseRequestThrow $ toS manifestPath
|
||||||
val <- (liftIO . try @SomeException) (httpBS req) >>= \case
|
val <- (liftIO . try @SomeException) (httpBS req) >>= \case
|
||||||
@@ -96,22 +98,29 @@ getAppManifest = do
|
|||||||
Left e -> throwError $ RegistryParseE manifestPath . toS $ e
|
Left e -> throwError $ RegistryParseE manifestPath . toS $ e
|
||||||
Right a -> pure a
|
Right a -> pure a
|
||||||
|
|
||||||
|
getAppManifest :: (MonadIO m, Has (Error S9Error) sig m, Has RegistryUrl sig m) => AppId -> m AppManifest
|
||||||
|
getAppManifest appId = do
|
||||||
|
let path = "/apps/manifest/" <> unAppId appId
|
||||||
|
v <- registryRequest path
|
||||||
|
case Data.Aeson.Types.parseEither parseJSON v of
|
||||||
|
Left e -> throwError $ RegistryParseE path . toS $ e
|
||||||
|
Right a -> pure a
|
||||||
|
|
||||||
getStoreAppInfo :: (MonadIO m, Has RegistryUrl sig m, Has (Error S9Error) sig m) => AppId -> m (Maybe StoreApp)
|
getStoreAppInfo :: (MonadIO m, Has RegistryUrl sig m, Has (Error S9Error) sig m) => AppId -> m (Maybe StoreApp)
|
||||||
getStoreAppInfo name = find ((== name) . storeAppId) . storeApps <$> getAppManifest
|
getStoreAppInfo name = find ((== name) . storeAppId) . storeApps <$> getAppIndex
|
||||||
|
|
||||||
parseBsManifest :: Has RegistryUrl sig m => ByteString -> m (Either String AppManifestRes)
|
parseBsManifest :: Has RegistryUrl sig m => ByteString -> m (Either String AppIndexRes)
|
||||||
parseBsManifest bs = do
|
parseBsManifest bs = do
|
||||||
parseRegistryRes' <- parseRegistryRes
|
parseRegistryRes' <- parseRegistryRes
|
||||||
pure $ parseEither parseRegistryRes' . fromJust . decodeThrow $ bs
|
pure $ parseEither parseRegistryRes' . fromJust . decodeThrow $ bs
|
||||||
|
|
||||||
parseRegistryRes :: Has RegistryUrl sig m => m (Value -> Parser AppManifestRes)
|
parseRegistryRes :: Has RegistryUrl sig m => m (Value -> Parser AppIndexRes)
|
||||||
parseRegistryRes = do
|
parseRegistryRes = do
|
||||||
parseAppData' <- parseAppData
|
parseAppData' <- parseAppData
|
||||||
pure $ withObject "app registry response" $ \obj -> do
|
pure $ withObject "app registry response" $ \obj -> do
|
||||||
let keyVals = HM.toList obj
|
let keyVals = HM.toList obj
|
||||||
let mManifestApps = fmap (\(k, v) -> parseMaybe (parseAppData' (AppId k)) v) keyVals
|
let mManifestApps = fmap (\(k, v) -> parseMaybe (parseAppData' (AppId k)) v) keyVals
|
||||||
pure . AppManifestRes . catMaybes $ mManifestApps
|
pure . AppIndexRes . catMaybes $ mManifestApps
|
||||||
|
|
||||||
registryUrl :: (Has RegistryUrl sig m) => m Text
|
registryUrl :: (Has RegistryUrl sig m) => m Text
|
||||||
registryUrl = maybe "https://registry.start9labs.com:443" show <$> getRegistryUrl
|
registryUrl = maybe "https://registry.start9labs.com:443" show <$> getRegistryUrl
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import System.Process ( callCommand )
|
|||||||
|
|
||||||
import Constants
|
import Constants
|
||||||
import Control.Effect.Error hiding ( run )
|
import Control.Effect.Error hiding ( run )
|
||||||
|
import Control.Effect.Labelled ( runLabelled )
|
||||||
import Daemon.ZeroConf ( getStart9AgentHostname )
|
import Daemon.ZeroConf ( getStart9AgentHostname )
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -97,12 +98,12 @@ parseKernelVersion = do
|
|||||||
pure $ KernelVersion (Version (major', minor', patch', 0)) arch
|
pure $ KernelVersion (Version (major', minor', patch', 0)) arch
|
||||||
|
|
||||||
synchronizer :: Synchronizer
|
synchronizer :: Synchronizer
|
||||||
synchronizer = sync_0_2_10
|
synchronizer = sync_0_2_13
|
||||||
{-# INLINE synchronizer #-}
|
{-# INLINE synchronizer #-}
|
||||||
|
|
||||||
sync_0_2_10 :: Synchronizer
|
sync_0_2_13 :: Synchronizer
|
||||||
sync_0_2_10 = Synchronizer
|
sync_0_2_13 = Synchronizer
|
||||||
"0.2.10"
|
"0.2.13"
|
||||||
[ syncCreateAgentTmp
|
[ syncCreateAgentTmp
|
||||||
, syncCreateSshDir
|
, syncCreateSshDir
|
||||||
, syncRemoveAvahiSystemdDependency
|
, syncRemoveAvahiSystemdDependency
|
||||||
@@ -126,6 +127,7 @@ sync_0_2_10 = Synchronizer
|
|||||||
, syncRestarterService
|
, syncRestarterService
|
||||||
, syncInstallEject
|
, syncInstallEject
|
||||||
, syncDropCertificateUniqueness
|
, syncDropCertificateUniqueness
|
||||||
|
, syncRemoveDefaultNginxCfg
|
||||||
]
|
]
|
||||||
|
|
||||||
syncCreateAgentTmp :: SyncOp
|
syncCreateAgentTmp :: SyncOp
|
||||||
@@ -437,10 +439,11 @@ syncInstallAppMgr = SyncOp "Install AppMgr" check migrate False
|
|||||||
Left _ -> pure True
|
Left _ -> pure True
|
||||||
Right v -> not . (v <||) <$> asks (appMgrVersionSpec . appSettings)
|
Right v -> not . (v <||) <$> asks (appMgrVersionSpec . appSettings)
|
||||||
migrate = fmap (either absurd id) . runExceptT . flip catchE failUpdate $ do
|
migrate = fmap (either absurd id) . runExceptT . flip catchE failUpdate $ do
|
||||||
|
lan <- asks appLanThread
|
||||||
avs <- asks $ appMgrVersionSpec . appSettings
|
avs <- asks $ appMgrVersionSpec . appSettings
|
||||||
av <- AppMgr.installNewAppMgr avs
|
av <- AppMgr.installNewAppMgr avs
|
||||||
unless (av <|| avs) $ throwE $ AppMgrVersionE av avs
|
unless (av <|| avs) $ throwE $ AppMgrVersionE av avs
|
||||||
postResetLanLogic -- to accommodate 0.2.x -> 0.2.9 where previous appmgr didn't correctly set up lan
|
flip runReaderT lan $ runLabelled @"lanThread" $ postResetLanLogic -- to accommodate 0.2.x -> 0.2.9 where previous appmgr didn't correctly set up lan
|
||||||
|
|
||||||
syncUpgradeLifeline :: SyncOp
|
syncUpgradeLifeline :: SyncOp
|
||||||
syncUpgradeLifeline = SyncOp "Upgrade Lifeline" check migrate False
|
syncUpgradeLifeline = SyncOp "Upgrade Lifeline" check migrate False
|
||||||
@@ -582,11 +585,11 @@ syncRestarterService = SyncOp "Install Restarter Service" check migrate True
|
|||||||
liftIO $ callCommand "systemctl enable restarter.timer"
|
liftIO $ callCommand "systemctl enable restarter.timer"
|
||||||
|
|
||||||
syncUpgradeTor :: SyncOp
|
syncUpgradeTor :: SyncOp
|
||||||
syncUpgradeTor = SyncOp "Install Tor 0.3.5.12-1" check migrate False
|
syncUpgradeTor = SyncOp "Install Tor 0.3.5.14-1" check migrate False
|
||||||
where
|
where
|
||||||
check =
|
check =
|
||||||
liftIO
|
liftIO
|
||||||
$ ( run (shell [i|dpkg -l|] $| shell [i|grep tor|] $| shell [i|grep 0.3.5.12-1|] $| conduit await)
|
$ ( run (shell [i|dpkg -l|] $| shell [i|grep tor|] $| shell [i|grep 0.3.5.14-1|] $| conduit await)
|
||||||
$> False
|
$> False
|
||||||
)
|
)
|
||||||
`catch` \(e :: ProcessException) -> case e of
|
`catch` \(e :: ProcessException) -> case e of
|
||||||
@@ -594,7 +597,7 @@ syncUpgradeTor = SyncOp "Install Tor 0.3.5.12-1" check migrate False
|
|||||||
_ -> throwIO e
|
_ -> throwIO e
|
||||||
migrate = liftIO . run $ do
|
migrate = liftIO . run $ do
|
||||||
shell "apt-get update"
|
shell "apt-get update"
|
||||||
shell "apt-get install -y tor=0.3.5.12-1"
|
shell "apt-get install -y tor=0.3.5.14-1"
|
||||||
|
|
||||||
syncDropCertificateUniqueness :: SyncOp
|
syncDropCertificateUniqueness :: SyncOp
|
||||||
syncDropCertificateUniqueness = SyncOp "Eliminate OpenSSL unique_subject=yes" check migrate False
|
syncDropCertificateUniqueness = SyncOp "Eliminate OpenSSL unique_subject=yes" check migrate False
|
||||||
@@ -602,14 +605,33 @@ syncDropCertificateUniqueness = SyncOp "Eliminate OpenSSL unique_subject=yes" ch
|
|||||||
uni = "unique_subject = no\n"
|
uni = "unique_subject = no\n"
|
||||||
check = do
|
check = do
|
||||||
base <- asks $ appFilesystemBase . appSettings
|
base <- asks $ appFilesystemBase . appSettings
|
||||||
contentsRoot <- liftIO . BS.readFile . toS $ (rootCaDirectory <> "index.txt.attr") `relativeTo` base
|
contentsRoot <-
|
||||||
contentsInt <- liftIO . BS.readFile . toS $ (intermediateCaDirectory <> "index.txt.attr") `relativeTo` base
|
liftIO
|
||||||
pure $ uni /= contentsRoot || uni /= contentsInt
|
$ (fmap Just . BS.readFile . toS $ (rootCaDirectory <> "index.txt.attr") `relativeTo` base)
|
||||||
|
`catch` \(e :: IOException) -> if isDoesNotExistError e then pure Nothing else throwIO e
|
||||||
|
contentsInt <-
|
||||||
|
liftIO
|
||||||
|
$ (fmap Just . BS.readFile . toS $ (intermediateCaDirectory <> "index.txt.attr") `relativeTo` base)
|
||||||
|
`catch` \(e :: IOException) -> if isDoesNotExistError e then pure Nothing else throwIO e
|
||||||
|
case (contentsRoot, contentsInt) of
|
||||||
|
(Just root, Just int) -> pure $ uni /= root || uni /= int
|
||||||
|
_ -> pure True
|
||||||
migrate = do
|
migrate = do
|
||||||
base <- asks $ appFilesystemBase . appSettings
|
base <- asks $ appFilesystemBase . appSettings
|
||||||
liftIO $ BS.writeFile (toS $ (rootCaDirectory <> "index.txt.attr") `relativeTo` base) uni
|
liftIO $ BS.writeFile (toS $ (rootCaDirectory <> "index.txt.attr") `relativeTo` base) uni
|
||||||
liftIO $ BS.writeFile (toS $ (intermediateCaDirectory <> "index.txt.attr") `relativeTo` base) uni
|
liftIO $ BS.writeFile (toS $ (intermediateCaDirectory <> "index.txt.attr") `relativeTo` base) uni
|
||||||
|
|
||||||
|
syncRemoveDefaultNginxCfg :: SyncOp
|
||||||
|
syncRemoveDefaultNginxCfg = SyncOp "Remove Default Nginx Configuration" check migrate False
|
||||||
|
where
|
||||||
|
check = do
|
||||||
|
base <- asks $ appFilesystemBase . appSettings
|
||||||
|
liftIO $ doesPathExist (toS $ nginxSitesEnabled "default" `relativeTo` base)
|
||||||
|
migrate = do
|
||||||
|
base <- asks $ appFilesystemBase . appSettings
|
||||||
|
liftIO $ removeFileIfExists (toS $ nginxSitesEnabled "default" `relativeTo` base)
|
||||||
|
liftIO $ systemCtl RestartService "nginx" $> ()
|
||||||
|
|
||||||
failUpdate :: S9Error -> ExceptT Void (ReaderT AgentCtx IO) ()
|
failUpdate :: S9Error -> ExceptT Void (ReaderT AgentCtx IO) ()
|
||||||
failUpdate e = do
|
failUpdate e = do
|
||||||
ref <- asks appIsUpdateFailed
|
ref <- asks appIsUpdateFailed
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
-- {-# OPTIONS_GHC -fno-warn-unused-imports #-}
|
-- {-# OPTIONS_GHC -fno-warn-unused-imports #-}
|
||||||
module Startlude.ByteStream
|
module Startlude.ByteStream
|
||||||
( module Startlude.ByteStream
|
( module BS
|
||||||
, module BS
|
) where
|
||||||
)
|
|
||||||
where
|
|
||||||
|
|
||||||
import Data.ByteString.Streaming as BS
|
import Streaming.ByteString as BS
|
||||||
hiding ( ByteString )
|
hiding ( ByteString )
|
||||||
import Data.ByteString.Streaming as X
|
|
||||||
( ByteString )
|
|
||||||
|
|
||||||
type ByteStream m = X.ByteString m
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
module Startlude.ByteStream.Char8
|
module Startlude.ByteStream.Char8
|
||||||
( module X
|
( module X
|
||||||
)
|
) where
|
||||||
where
|
|
||||||
|
|
||||||
import Data.ByteString.Streaming.Char8
|
import Streaming.ByteString.Char8 as X
|
||||||
as X
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
resolver: nightly-2020-09-29
|
resolver: lts-17.10
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
- .
|
- .
|
||||||
|
|
||||||
extra-deps:
|
extra-deps:
|
||||||
- aeson-1.4.7.1
|
# - aeson-1.4.7.1
|
||||||
- aeson-flatten-0.1.0.2
|
- aeson-flatten-0.1.0.2
|
||||||
- exinst-0.8
|
- exinst-0.8
|
||||||
- fused-effects-1.1.0.0
|
- fused-effects-1.1.0.0
|
||||||
@@ -12,13 +12,14 @@ extra-deps:
|
|||||||
- git-embed-0.1.0
|
- git-embed-0.1.0
|
||||||
- json-stream-0.4.2.4
|
- json-stream-0.4.2.4
|
||||||
- protolude-0.3.0
|
- protolude-0.3.0
|
||||||
|
- streaming-bytestring-0.1.7
|
||||||
- streaming-conduit-0.1.2.2
|
- streaming-conduit-0.1.2.2
|
||||||
- streaming-utils-0.2.0.0
|
- streaming-utils-0.2.0.0
|
||||||
# to avoid the ridiculous bug where stat64 is not found (only affects development)
|
# to avoid the ridiculous bug where stat64 is not found (only affects development)
|
||||||
- git: https://github.com/ProofOfKeags/persistent.git
|
# - git: https://github.com/ProofOfKeags/persistent.git
|
||||||
commit: 3b52b13d9ce79cdef14bb1c37cc527657a529462
|
# commit: 3b52b13d9ce79cdef14bb1c37cc527657a529462
|
||||||
subdirs:
|
# subdirs:
|
||||||
- persistent-sqlite
|
# - persistent-sqlite
|
||||||
|
|
||||||
ghc-options:
|
ghc-options:
|
||||||
"$locals": -fwrite-ide-info
|
"$locals": -fwrite-ide-info
|
||||||
|
|||||||
1
appmgr/.gitignore
vendored
1
appmgr/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
.DS_Store
|
||||||
2
appmgr/Cargo.lock
generated
2
appmgr/Cargo.lock
generated
@@ -41,7 +41,7 @@ checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "appmgr"
|
name = "appmgr"
|
||||||
version = "0.2.10"
|
version = "0.2.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"avahi-sys",
|
"avahi-sys",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
authors = ["Aiden McClelland <me@drbonez.dev>"]
|
authors = ["Aiden McClelland <me@drbonez.dev>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
name = "appmgr"
|
name = "appmgr"
|
||||||
version = "0.2.10"
|
version = "0.2.13"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "appmgrlib"
|
name = "appmgrlib"
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
# appmgr
|
# appmgr
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
|
||||||
|
Clone the repo and enter the appmgr directory
|
||||||
|
|
||||||
|
`git clone https://github.com/Start9Labs/embassy-os.git`
|
||||||
|
|
||||||
|
`cd embassy-os/appmgr`
|
||||||
|
|
||||||
|
Install the portable version of appmgr
|
||||||
|
|
||||||
|
`cargo install --path=. --features=portable --no-default-features`
|
||||||
|
|
||||||
## Exit Codes
|
## Exit Codes
|
||||||
1. General Error
|
1. General Error
|
||||||
2. File System IO Error
|
2. File System IO Error
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
set -e
|
set -e
|
||||||
shopt -s expand_aliases
|
shopt -s expand_aliases
|
||||||
|
|
||||||
|
if [ "$0" != "./build-dev.sh" ]; then
|
||||||
|
>&2 echo "Must be run from appmgr directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest'
|
alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest'
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
|
|||||||
15
appmgr/build-portable.sh
Executable file
15
appmgr/build-portable.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
shopt -s expand_aliases
|
||||||
|
|
||||||
|
if [ "$0" != "./build-portable.sh" ]; then
|
||||||
|
>&2 echo "Must be run from appmgr directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
alias 'rust-musl-builder'='docker run --rm -it -v "$HOME"/.cargo/registry:/root/.cargo/registry -v "$(pwd)":/home/rust/src messense/rust-musl-cross:x86_64-musl'
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
rust-musl-builder sh -c "(cd appmgr && cargo build --release --target=x86_64-unknown-linux-musl --features=portable,production --no-default-features)"
|
||||||
|
cd appmgr
|
||||||
@@ -3,6 +3,11 @@
|
|||||||
set -e
|
set -e
|
||||||
shopt -s expand_aliases
|
shopt -s expand_aliases
|
||||||
|
|
||||||
|
if [ "$0" != "./build-prod.sh" ]; then
|
||||||
|
>&2 echo "Must be run from appmgr directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest'
|
alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest'
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::os::unix::process::ExitStatusExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use argon2::Config;
|
use argon2::Config;
|
||||||
@@ -10,6 +11,7 @@ use serde::Serialize;
|
|||||||
use crate::util::from_yaml_async_reader;
|
use crate::util::from_yaml_async_reader;
|
||||||
use crate::util::to_yaml_async_writer;
|
use crate::util::to_yaml_async_writer;
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
|
use crate::util::PersistencePath;
|
||||||
use crate::version::VersionT;
|
use crate::version::VersionT;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::ResultExt;
|
use crate::ResultExt;
|
||||||
@@ -224,6 +226,28 @@ pub async fn restore_backup<P: AsRef<Path>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
crate::tor::restart().await?;
|
crate::tor::restart().await?;
|
||||||
|
// Delete the fullchain certificate, so it can be regenerated with the restored tor pubkey address
|
||||||
|
PersistencePath::from_ref("apps")
|
||||||
|
.join(&app_id)
|
||||||
|
.join("cert-local.fullchain.crt.pem")
|
||||||
|
.delete()
|
||||||
|
.await?;
|
||||||
|
crate::tor::write_lan_services(
|
||||||
|
&crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let svc_exit = std::process::Command::new("service")
|
||||||
|
.args(&["nginx", "reload"])
|
||||||
|
.status()?;
|
||||||
|
crate::ensure_code!(
|
||||||
|
svc_exit.success(),
|
||||||
|
crate::error::GENERAL_ERROR,
|
||||||
|
"Failed to Reload Nginx: {}",
|
||||||
|
svc_exit
|
||||||
|
.code()
|
||||||
|
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||||
|
.unwrap_or(0)
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ pub async fn start_app(name: &str, update_metadata: bool) -> Result<(), Error> {
|
|||||||
if status == crate::apps::DockerStatus::Stopped {
|
if status == crate::apps::DockerStatus::Stopped {
|
||||||
if update_metadata {
|
if update_metadata {
|
||||||
crate::config::configure(name, None, None, false).await?;
|
crate::config::configure(name, None, None, false).await?;
|
||||||
crate::dependencies::update_shared(name).await?;
|
|
||||||
crate::dependencies::update_binds(name).await?;
|
crate::dependencies::update_binds(name).await?;
|
||||||
}
|
}
|
||||||
crate::apps::set_needs_restart(name, false).await?;
|
crate::apps::set_needs_restart(name, false).await?;
|
||||||
|
|||||||
@@ -188,31 +188,6 @@ pub async fn auto_configure(
|
|||||||
crate::config::configure(dependency, Some(dependency_config), None, dry_run).await
|
crate::config::configure(dependency, Some(dependency_config), None, dry_run).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_shared(dependency_id: &str) -> Result<(), Error> {
|
|
||||||
let dependency_manifest = crate::apps::manifest(dependency_id).await?;
|
|
||||||
if let Some(shared) = dependency_manifest.shared {
|
|
||||||
for dependent_id in &crate::apps::dependents(dependency_id, false).await? {
|
|
||||||
let dependent_manifest = crate::apps::manifest(&dependent_id).await?;
|
|
||||||
if dependent_manifest
|
|
||||||
.dependencies
|
|
||||||
.0
|
|
||||||
.get(dependency_id)
|
|
||||||
.ok_or_else(|| failure::format_err!("failed to index dependent: {}", dependent_id))?
|
|
||||||
.mount_shared
|
|
||||||
{
|
|
||||||
tokio::fs::create_dir_all(
|
|
||||||
Path::new(crate::VOLUMES)
|
|
||||||
.join(dependency_id)
|
|
||||||
.join(&shared)
|
|
||||||
.join(&dependent_id),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update_binds(dependent_id: &str) -> Result<(), Error> {
|
pub async fn update_binds(dependent_id: &str) -> Result<(), Error> {
|
||||||
let dependent_manifest = crate::apps::manifest(dependent_id).await?;
|
let dependent_manifest = crate::apps::manifest(dependent_id).await?;
|
||||||
let dependency_manifests = futures::future::try_join_all(
|
let dependency_manifests = futures::future::try_join_all(
|
||||||
@@ -222,12 +197,19 @@ pub async fn update_binds(dependent_id: &str) -> Result<(), Error> {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|(_, info)| info.mount_public || info.mount_shared)
|
.filter(|(_, info)| info.mount_public || info.mount_shared)
|
||||||
.map(|(id, info)| async {
|
.map(|(id, info)| async {
|
||||||
crate::apps::manifest(&id).await.map(|man| (id, info, man))
|
Ok::<_, Error>(if crate::apps::list_info().await?.contains_key(&id) {
|
||||||
|
let man = crate::apps::manifest(&id).await?;
|
||||||
|
Some((id, info, man))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
// i just have a gut feeling this shouldn't be concurrent
|
// i just have a gut feeling this shouldn't be concurrent
|
||||||
for (dependency_id, info, dependency_manifest) in dependency_manifests {
|
for (dependency_id, info, dependency_manifest) in
|
||||||
|
dependency_manifests.into_iter().filter_map(|a| a)
|
||||||
|
{
|
||||||
match (dependency_manifest.public, info.mount_public) {
|
match (dependency_manifest.public, info.mount_public) {
|
||||||
(Some(public), true) => {
|
(Some(public), true) => {
|
||||||
let public_path = Path::new(crate::VOLUMES).join(&dependency_id).join(public);
|
let public_path = Path::new(crate::VOLUMES).join(&dependency_id).join(public);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use failure::ResultExt as _;
|
||||||
use futures::future::try_join_all;
|
use futures::future::try_join_all;
|
||||||
|
|
||||||
use crate::util::Invoke;
|
use crate::util::Invoke;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::ResultExt;
|
use crate::ResultExt as _;
|
||||||
|
|
||||||
pub const FSTAB: &'static str = "/etc/fstab";
|
pub const FSTAB: &'static str = "/etc/fstab";
|
||||||
|
|
||||||
@@ -153,6 +154,11 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
|
|||||||
dst: P1,
|
dst: P1,
|
||||||
read_only: bool,
|
read_only: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
log::info!(
|
||||||
|
"Binding {} to {}",
|
||||||
|
src.as_ref().display(),
|
||||||
|
dst.as_ref().display()
|
||||||
|
);
|
||||||
let is_mountpoint = tokio::process::Command::new("mountpoint")
|
let is_mountpoint = tokio::process::Command::new("mountpoint")
|
||||||
.arg(dst.as_ref())
|
.arg(dst.as_ref())
|
||||||
.stdout(std::process::Stdio::null())
|
.stdout(std::process::Stdio::null())
|
||||||
@@ -185,6 +191,7 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unmount<P: AsRef<Path>>(mount_point: P) -> Result<(), Error> {
|
pub async fn unmount<P: AsRef<Path>>(mount_point: P) -> Result<(), Error> {
|
||||||
|
log::info!("Unmounting {}.", mount_point.as_ref().display());
|
||||||
let umount_output = tokio::process::Command::new("umount")
|
let umount_output = tokio::process::Command::new("umount")
|
||||||
.arg(mount_point.as_ref())
|
.arg(mount_point.as_ref())
|
||||||
.output()
|
.output()
|
||||||
@@ -192,10 +199,14 @@ pub async fn unmount<P: AsRef<Path>>(mount_point: P) -> Result<(), Error> {
|
|||||||
crate::ensure_code!(
|
crate::ensure_code!(
|
||||||
umount_output.status.success(),
|
umount_output.status.success(),
|
||||||
crate::error::FILESYSTEM_ERROR,
|
crate::error::FILESYSTEM_ERROR,
|
||||||
"Error Unmounting Drive: {}",
|
"Error Unmounting Drive: {}: {}",
|
||||||
|
mount_point.as_ref().display(),
|
||||||
std::str::from_utf8(&umount_output.stderr).unwrap_or("Unknown Error")
|
std::str::from_utf8(&umount_output.stderr).unwrap_or("Unknown Error")
|
||||||
);
|
);
|
||||||
tokio::fs::remove_dir_all(mount_point.as_ref()).await?;
|
tokio::fs::remove_dir_all(mount_point.as_ref())
|
||||||
|
.await
|
||||||
|
.with_context(|e| format!("rm {}: {}", mount_point.as_ref().display(), e))
|
||||||
|
.with_code(crate::error::FILESYSTEM_ERROR)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -561,14 +561,17 @@ pub async fn install_v0<R: AsyncRead + Unpin + Send + Sync>(
|
|||||||
crate::config::configure(&manifest.id, Some(empty_config), None, false).await?;
|
crate::config::configure(&manifest.id, Some(empty_config), None, false).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
crate::dependencies::update_binds(&manifest.id).await?;
|
||||||
for (dep_id, dep_info) in manifest.dependencies.0 {
|
for (dep_id, dep_info) in manifest.dependencies.0 {
|
||||||
if dep_info.mount_shared
|
if dep_info.mount_shared
|
||||||
&& crate::apps::list_info().await?.get(&dep_id).is_some()
|
&& crate::apps::list_info().await?.get(&dep_id).is_some()
|
||||||
&& crate::apps::manifest(&dep_id).await?.shared.is_some()
|
&& crate::apps::manifest(&dep_id).await?.shared.is_some()
|
||||||
&& crate::apps::status(&dep_id, false).await?.status
|
|
||||||
!= crate::apps::DockerStatus::Stopped
|
|
||||||
{
|
{
|
||||||
crate::apps::set_needs_restart(&dep_id, true).await?;
|
match crate::apps::status(&dep_id, false).await?.status {
|
||||||
|
crate::apps::DockerStatus::Stopped => (),
|
||||||
|
crate::apps::DockerStatus::Running => crate::control::restart_app(&dep_id).await?,
|
||||||
|
_ => crate::apps::set_needs_restart(&dep_id, true).await?,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
map $http_x_forwarded_proto $real_proto {{
|
||||||
|
ext+onions ext+onions;
|
||||||
|
ext+onion ext+onion;
|
||||||
|
https https;
|
||||||
|
http http;
|
||||||
|
default $scheme;
|
||||||
|
}}
|
||||||
server {{
|
server {{
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name {hostname}.local;
|
server_name {hostname}.local;
|
||||||
@@ -8,7 +15,10 @@ server {{
|
|||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $real_proto;
|
||||||
|
client_max_body_size 0;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
proxy_buffering off;
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
server {{
|
server {{
|
||||||
|
|||||||
@@ -4,5 +4,8 @@ server {{
|
|||||||
location / {{
|
location / {{
|
||||||
proxy_pass http://{app_ip}:{internal_port}/;
|
proxy_pass http://{app_ip}:{internal_port}/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
client_max_body_size 0;
|
||||||
|
proxy_request_buffering off;
|
||||||
|
proxy_buffering off;
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
use crate::failure::ResultExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use linear_map::LinearMap;
|
use linear_map::LinearMap;
|
||||||
|
|
||||||
use crate::dependencies::{DependencyError, TaggedDependencyError};
|
use crate::dependencies::{DependencyError, TaggedDependencyError};
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
use crate::ResultExt as _;
|
||||||
|
|
||||||
pub async fn remove(
|
pub async fn remove(
|
||||||
name: &str,
|
name: &str,
|
||||||
@@ -55,48 +57,79 @@ pub async fn remove(
|
|||||||
log::info!("Removing tor hidden service.");
|
log::info!("Removing tor hidden service.");
|
||||||
crate::tor::rm_svc(name).await?;
|
crate::tor::rm_svc(name).await?;
|
||||||
log::info!("Removing app metadata.");
|
log::info!("Removing app metadata.");
|
||||||
tokio::fs::remove_dir_all(Path::new(crate::PERSISTENCE_DIR).join("apps").join(name))
|
let metadata_path = Path::new(crate::PERSISTENCE_DIR).join("apps").join(name);
|
||||||
.await?;
|
tokio::fs::remove_dir_all(&metadata_path)
|
||||||
log::info!("Destroying mounted volume.");
|
.await
|
||||||
|
.with_context(|e| format!("rm {}: {}", metadata_path.display(), e))
|
||||||
|
.with_code(crate::error::FILESYSTEM_ERROR)?;
|
||||||
log::info!("Unbinding shared filesystem.");
|
log::info!("Unbinding shared filesystem.");
|
||||||
for (dep, info) in manifest.dependencies.0.iter() {
|
let installed_apps = crate::apps::list_info().await?;
|
||||||
if info.mount_public {
|
for (dep, _) in manifest.dependencies.0.iter() {
|
||||||
crate::disks::unmount(
|
let path = Path::new(crate::VOLUMES)
|
||||||
Path::new(crate::VOLUMES)
|
.join(name)
|
||||||
.join(name)
|
.join("start9")
|
||||||
.join("start9")
|
.join("public")
|
||||||
.join("public")
|
.join(&dep);
|
||||||
.join(&dep),
|
if path.exists() {
|
||||||
)
|
crate::disks::unmount(&path).await?;
|
||||||
.await?;
|
} else {
|
||||||
|
log::warn!("{} does not exist, skipping...", path.display());
|
||||||
}
|
}
|
||||||
if info.mount_shared {
|
let path = Path::new(crate::VOLUMES)
|
||||||
if let Some(shared) = match crate::apps::manifest(dep).await {
|
.join(name)
|
||||||
Ok(man) => man.shared,
|
.join("start9")
|
||||||
Err(e) => {
|
.join("shared")
|
||||||
log::error!("Failed to Fetch Dependency Manifest: {}", e);
|
.join(&dep);
|
||||||
None
|
if path.exists() {
|
||||||
}
|
crate::disks::unmount(&path).await?;
|
||||||
} {
|
} else {
|
||||||
let path = Path::new(crate::VOLUMES)
|
log::warn!("{} does not exist, skipping...", path.display());
|
||||||
.join(name)
|
}
|
||||||
.join("start9")
|
if installed_apps.contains_key(dep) {
|
||||||
.join("shared")
|
let dep_man = crate::apps::manifest(dep).await?;
|
||||||
.join(&dep);
|
if let Some(shared) = dep_man.shared {
|
||||||
if path.exists() {
|
|
||||||
crate::disks::unmount(&path).await?;
|
|
||||||
}
|
|
||||||
let path = Path::new(crate::VOLUMES).join(dep).join(&shared).join(name);
|
let path = Path::new(crate::VOLUMES).join(dep).join(&shared).join(name);
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
tokio::fs::remove_dir_all(
|
tokio::fs::remove_dir_all(&path)
|
||||||
Path::new(crate::VOLUMES).join(dep).join(&shared).join(name),
|
.await
|
||||||
)
|
.with_context(|e| format!("rm {}: {}", path.display(), e))
|
||||||
.await?;
|
.with_code(crate::error::FILESYSTEM_ERROR)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!("{} is not installed, skipping...", dep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if manifest.public.is_some() || manifest.shared.is_some() {
|
||||||
|
for dependent in crate::apps::dependents(name, false).await? {
|
||||||
|
let path = Path::new(crate::VOLUMES)
|
||||||
|
.join(&dependent)
|
||||||
|
.join("start9")
|
||||||
|
.join("public")
|
||||||
|
.join(name);
|
||||||
|
if path.exists() {
|
||||||
|
crate::disks::unmount(&path).await?;
|
||||||
|
} else {
|
||||||
|
log::warn!("{} does not exist, skipping...", path.display());
|
||||||
|
}
|
||||||
|
let path = Path::new(crate::VOLUMES)
|
||||||
|
.join(dependent)
|
||||||
|
.join("start9")
|
||||||
|
.join("shared")
|
||||||
|
.join(name);
|
||||||
|
if path.exists() {
|
||||||
|
crate::disks::unmount(&path).await?;
|
||||||
|
} else {
|
||||||
|
log::warn!("{} does not exist, skipping...", path.display());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tokio::fs::remove_dir_all(Path::new(crate::VOLUMES).join(name)).await?;
|
log::info!("Destroying mounted volume.");
|
||||||
|
let volume_path = Path::new(crate::VOLUMES).join(name);
|
||||||
|
tokio::fs::remove_dir_all(&volume_path)
|
||||||
|
.await
|
||||||
|
.with_context(|e| format!("rm {}: {}", volume_path.display(), e))
|
||||||
|
.with_code(crate::error::FILESYSTEM_ERROR)?;
|
||||||
log::info!("Pruning unused docker images.");
|
log::info!("Pruning unused docker images.");
|
||||||
crate::ensure_code!(
|
crate::ensure_code!(
|
||||||
std::process::Command::new("docker")
|
std::process::Command::new("docker")
|
||||||
|
|||||||
@@ -110,6 +110,14 @@ impl PersistencePath {
|
|||||||
pub async fn for_update(self) -> Result<UpdateHandle<ForRead>, Error> {
|
pub async fn for_update(self) -> Result<UpdateHandle<ForRead>, Error> {
|
||||||
UpdateHandle::new(self).await
|
UpdateHandle::new(self).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete(&self) -> Result<(), Error> {
|
||||||
|
match tokio::fs::remove_file(self.path()).await {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(k) if k.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||||
|
e => e.with_code(crate::error::FILESYSTEM_ERROR),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ mod v0_1_4;
|
|||||||
mod v0_1_5;
|
mod v0_1_5;
|
||||||
mod v0_2_0;
|
mod v0_2_0;
|
||||||
mod v0_2_1;
|
mod v0_2_1;
|
||||||
mod v0_2_10;
|
|
||||||
mod v0_2_2;
|
mod v0_2_2;
|
||||||
mod v0_2_3;
|
mod v0_2_3;
|
||||||
mod v0_2_4;
|
mod v0_2_4;
|
||||||
@@ -27,7 +26,12 @@ mod v0_2_7;
|
|||||||
mod v0_2_8;
|
mod v0_2_8;
|
||||||
mod v0_2_9;
|
mod v0_2_9;
|
||||||
|
|
||||||
pub use v0_2_10::Version as Current;
|
mod v0_2_10;
|
||||||
|
mod v0_2_11;
|
||||||
|
mod v0_2_12;
|
||||||
|
mod v0_2_13;
|
||||||
|
|
||||||
|
pub use v0_2_13::Version as Current;
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize)]
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
@@ -50,6 +54,9 @@ enum Version {
|
|||||||
V0_2_8(Wrapper<v0_2_8::Version>),
|
V0_2_8(Wrapper<v0_2_8::Version>),
|
||||||
V0_2_9(Wrapper<v0_2_9::Version>),
|
V0_2_9(Wrapper<v0_2_9::Version>),
|
||||||
V0_2_10(Wrapper<v0_2_10::Version>),
|
V0_2_10(Wrapper<v0_2_10::Version>),
|
||||||
|
V0_2_11(Wrapper<v0_2_11::Version>),
|
||||||
|
V0_2_12(Wrapper<v0_2_12::Version>),
|
||||||
|
V0_2_13(Wrapper<v0_2_13::Version>),
|
||||||
Other(emver::Version),
|
Other(emver::Version),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +169,9 @@ pub async fn init() -> Result<(), failure::Error> {
|
|||||||
Version::V0_2_8(v) => v.0.migrate_to(&Current::new()).await?,
|
Version::V0_2_8(v) => v.0.migrate_to(&Current::new()).await?,
|
||||||
Version::V0_2_9(v) => v.0.migrate_to(&Current::new()).await?,
|
Version::V0_2_9(v) => v.0.migrate_to(&Current::new()).await?,
|
||||||
Version::V0_2_10(v) => v.0.migrate_to(&Current::new()).await?,
|
Version::V0_2_10(v) => v.0.migrate_to(&Current::new()).await?,
|
||||||
|
Version::V0_2_11(v) => v.0.migrate_to(&Current::new()).await?,
|
||||||
|
Version::V0_2_12(v) => v.0.migrate_to(&Current::new()).await?,
|
||||||
|
Version::V0_2_13(v) => v.0.migrate_to(&Current::new()).await?,
|
||||||
Version::Other(_) => (),
|
Version::Other(_) => (),
|
||||||
// TODO find some way to automate this?
|
// TODO find some way to automate this?
|
||||||
}
|
}
|
||||||
@@ -253,6 +263,9 @@ pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error>
|
|||||||
Version::V0_2_8(v) => Current::new().migrate_to(&v.0).await?,
|
Version::V0_2_8(v) => Current::new().migrate_to(&v.0).await?,
|
||||||
Version::V0_2_9(v) => Current::new().migrate_to(&v.0).await?,
|
Version::V0_2_9(v) => Current::new().migrate_to(&v.0).await?,
|
||||||
Version::V0_2_10(v) => Current::new().migrate_to(&v.0).await?,
|
Version::V0_2_10(v) => Current::new().migrate_to(&v.0).await?,
|
||||||
|
Version::V0_2_11(v) => Current::new().migrate_to(&v.0).await?,
|
||||||
|
Version::V0_2_12(v) => Current::new().migrate_to(&v.0).await?,
|
||||||
|
Version::V0_2_13(v) => Current::new().migrate_to(&v.0).await?,
|
||||||
Version::Other(_) => (),
|
Version::Other(_) => (),
|
||||||
// TODO find some way to automate this?
|
// TODO find some way to automate this?
|
||||||
};
|
};
|
||||||
|
|||||||
38
appmgr/src/version/v0_2_11.rs
Normal file
38
appmgr/src/version/v0_2_11.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use super::*;
|
||||||
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
|
||||||
|
const V0_2_11: emver::Version = emver::Version::new(0, 2, 11, 0);
|
||||||
|
|
||||||
|
pub struct Version;
|
||||||
|
#[async_trait]
|
||||||
|
impl VersionT for Version {
|
||||||
|
type Previous = v0_2_10::Version;
|
||||||
|
fn new() -> Self {
|
||||||
|
Version
|
||||||
|
}
|
||||||
|
fn semver(&self) -> &'static emver::Version {
|
||||||
|
&V0_2_11
|
||||||
|
}
|
||||||
|
async fn up(&self) -> Result<(), Error> {
|
||||||
|
crate::tor::write_lan_services(
|
||||||
|
&crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let svc_exit = std::process::Command::new("service")
|
||||||
|
.args(&["nginx", "reload"])
|
||||||
|
.status()?;
|
||||||
|
crate::ensure_code!(
|
||||||
|
svc_exit.success(),
|
||||||
|
crate::error::GENERAL_ERROR,
|
||||||
|
"Failed to Reload Nginx: {}",
|
||||||
|
svc_exit
|
||||||
|
.code()
|
||||||
|
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||||
|
.unwrap_or(0)
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn down(&self) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
38
appmgr/src/version/v0_2_12.rs
Normal file
38
appmgr/src/version/v0_2_12.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use super::*;
|
||||||
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
|
||||||
|
const V0_2_12: emver::Version = emver::Version::new(0, 2, 12, 0);
|
||||||
|
|
||||||
|
pub struct Version;
|
||||||
|
#[async_trait]
|
||||||
|
impl VersionT for Version {
|
||||||
|
type Previous = v0_2_11::Version;
|
||||||
|
fn new() -> Self {
|
||||||
|
Version
|
||||||
|
}
|
||||||
|
fn semver(&self) -> &'static emver::Version {
|
||||||
|
&V0_2_12
|
||||||
|
}
|
||||||
|
async fn up(&self) -> Result<(), Error> {
|
||||||
|
crate::tor::write_lan_services(
|
||||||
|
&crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let svc_exit = std::process::Command::new("service")
|
||||||
|
.args(&["nginx", "reload"])
|
||||||
|
.status()?;
|
||||||
|
crate::ensure_code!(
|
||||||
|
svc_exit.success(),
|
||||||
|
crate::error::GENERAL_ERROR,
|
||||||
|
"Failed to Reload Nginx: {}",
|
||||||
|
svc_exit
|
||||||
|
.code()
|
||||||
|
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||||
|
.unwrap_or(0)
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn down(&self) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
21
appmgr/src/version/v0_2_13.rs
Normal file
21
appmgr/src/version/v0_2_13.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
const V0_2_13: emver::Version = emver::Version::new(0, 2, 13, 0);
|
||||||
|
|
||||||
|
pub struct Version;
|
||||||
|
#[async_trait]
|
||||||
|
impl VersionT for Version {
|
||||||
|
type Previous = v0_2_12::Version;
|
||||||
|
fn new() -> Self {
|
||||||
|
Version
|
||||||
|
}
|
||||||
|
fn semver(&self) -> &'static emver::Version {
|
||||||
|
&V0_2_13
|
||||||
|
}
|
||||||
|
async fn up(&self) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn down(&self) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
assets/Embassy.png
Normal file
BIN
assets/Embassy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
BIN
assets/Marketplace.png
Normal file
BIN
assets/Marketplace.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
BIN
assets/ServiceDetails.png
Normal file
BIN
assets/ServiceDetails.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
BIN
assets/ServicesRunning.png
Normal file
BIN
assets/ServicesRunning.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
BIN
assets/eos.png
Normal file
BIN
assets/eos.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 MiB |
@@ -1,5 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
arch=$(uname -m)
|
||||||
|
if [[ $arch == armv7l ]]; then
|
||||||
|
dev_target="target"
|
||||||
|
else
|
||||||
|
dev_target="target/armv7-unknown-linux-musleabihf"
|
||||||
|
fi
|
||||||
mv buster.img embassy.img
|
mv buster.img embassy.img
|
||||||
product_key=$(cat product_key)
|
product_key=$(cat product_key)
|
||||||
loopdev=$(losetup -f -P embassy.img --show)
|
loopdev=$(losetup -f -P embassy.img --show)
|
||||||
@@ -9,6 +14,12 @@ mkdir -p "${root_mountpoint}"
|
|||||||
mkdir -p "${boot_mountpoint}"
|
mkdir -p "${boot_mountpoint}"
|
||||||
mount "${loopdev}p2" "${root_mountpoint}"
|
mount "${loopdev}p2" "${root_mountpoint}"
|
||||||
mount "${loopdev}p1" "${boot_mountpoint}"
|
mount "${loopdev}p1" "${boot_mountpoint}"
|
||||||
|
mkdir -p "${root_mountpoint}/root/agent"
|
||||||
|
mkdir -p "${root_mountpoint}/etc/docker"
|
||||||
|
mkdir -p "${root_mountpoint}/home/pi/.ssh"
|
||||||
|
echo -n "" > "${root_mountpoint}/home/pi/.ssh/authorized_keys"
|
||||||
|
chown -R pi:pi "${root_mountpoint}/home/pi/.ssh"
|
||||||
|
echo -n "" > "${boot_mountpoint}/ssh"
|
||||||
echo "${product_key}" > "${root_mountpoint}/root/agent/product_key"
|
echo "${product_key}" > "${root_mountpoint}/root/agent/product_key"
|
||||||
echo -n "start9-" > "${root_mountpoint}/etc/hostname"
|
echo -n "start9-" > "${root_mountpoint}/etc/hostname"
|
||||||
echo -n "${product_key}" | shasum -t -a 256 | cut -c1-8 >> "${root_mountpoint}/etc/hostname"
|
echo -n "${product_key}" | shasum -t -a 256 | cut -c1-8 >> "${root_mountpoint}/etc/hostname"
|
||||||
@@ -18,20 +29,23 @@ echo -n "${product_key}" | shasum -t -a 256 | cut -c1-8 >> "${root_mountpoint}/e
|
|||||||
mv "${root_mountpoint}/etc/hosts.tmp" "${root_mountpoint}/etc/hosts"
|
mv "${root_mountpoint}/etc/hosts.tmp" "${root_mountpoint}/etc/hosts"
|
||||||
cp agent/dist/agent "${root_mountpoint}/usr/local/bin/agent"
|
cp agent/dist/agent "${root_mountpoint}/usr/local/bin/agent"
|
||||||
chmod 700 "${root_mountpoint}/usr/local/bin/agent"
|
chmod 700 "${root_mountpoint}/usr/local/bin/agent"
|
||||||
cp appmgr/target/armv7-unknown-linux-musleabihf/release/appmgr "${root_mountpoint}/usr/local/bin/appmgr"
|
cp "appmgr/${dev_target}/release/appmgr" "${root_mountpoint}/usr/local/bin/appmgr"
|
||||||
chmod 700 "${root_mountpoint}/usr/local/bin/appmgr"
|
chmod 700 "${root_mountpoint}/usr/local/bin/appmgr"
|
||||||
cp lifeline/target/armv7-unknown-linux-musleabihf/release/lifeline "${root_mountpoint}/usr/local/bin/lifeline"
|
cp "lifeline/${dev_target}/release/lifeline" "${root_mountpoint}/usr/local/bin/lifeline"
|
||||||
chmod 700 "${root_mountpoint}/usr/local/bin/lifeline"
|
chmod 700 "${root_mountpoint}/usr/local/bin/lifeline"
|
||||||
cp docker-daemon.json "${root_mountpoint}/etc/docker/daemon.json"
|
cp docker-daemon.json "${root_mountpoint}/etc/docker/daemon.json"
|
||||||
cp setup.sh "${root_mountpoint}/root/setup.sh"
|
cp setup.sh "${root_mountpoint}/root/setup.sh"
|
||||||
chmod 700 "${root_mountpoint}/root/setup.sh"
|
chmod 700 "${root_mountpoint}/root/setup.sh"
|
||||||
cp setup.service /etc/systemd/system/setup.service
|
cp setup.service "${root_mountpoint}/etc/systemd/system/setup.service"
|
||||||
cp lifeline/lifeline.service /etc/systemd/system/lifeline.service
|
ln -s /etc/systemd/system/setup.service "${root_mountpoint}/etc/systemd/system/getty.target.wants/setup.service"
|
||||||
cp agent/config/agent.service /etc/systemd/system/agent.service
|
cp lifeline/lifeline.service "${root_mountpoint}/etc/systemd/system/lifeline.service"
|
||||||
cat "${boot_mountpoint}/config.txt" | grep -v "dtoverlay=pwm-2chan" > "${boot_mountpoint}/config.txt.tmp"
|
cp agent/config/agent.service "${root_mountpoint}/etc/systemd/system/agent.service"
|
||||||
|
cat "${boot_mountpoint}/config.txt" | grep -v "dtoverlay=" > "${boot_mountpoint}/config.txt.tmp"
|
||||||
echo "dtoverlay=pwm-2chan" >> "${boot_mountpoint}/config.txt.tmp"
|
echo "dtoverlay=pwm-2chan" >> "${boot_mountpoint}/config.txt.tmp"
|
||||||
|
mv "${boot_mountpoint}/config.txt.tmp" "${boot_mountpoint}/config.txt"
|
||||||
umount "${root_mountpoint}"
|
umount "${root_mountpoint}"
|
||||||
rm -r "${root_mountpoint}"
|
rm -r "${root_mountpoint}"
|
||||||
umount "${boot_mountpoint}"
|
umount "${boot_mountpoint}"
|
||||||
rm -r "${boot_mountpoint}"
|
rm -r "${boot_mountpoint}"
|
||||||
losetup -d ${loopdev}
|
losetup -d ${loopdev}
|
||||||
|
echo "DONE! Here is your EmbassyOS key: ${product_key}"
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=Boot process for system setup.
|
Description=Boot process for system setup.
|
||||||
|
After=rc-local.service
|
||||||
|
Before=getty.target
|
||||||
|
ConditionFileNotEmpty=/root/setup.sh
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
ExecStart=/root/setup.sh
|
ExecStart=/root/setup.sh
|
||||||
|
ExecStartPost=/root/setup-s2.sh
|
||||||
RemainAfterExit=true
|
RemainAfterExit=true
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=basic.target
|
||||||
|
|
||||||
|
|||||||
30
setup.sh
30
setup.sh
@@ -1,15 +1,26 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
apt update
|
|
||||||
apt install -y libsecp256k1-0
|
|
||||||
apt install -y tor
|
|
||||||
apt install -y docker.io
|
|
||||||
apt install -y iotop
|
|
||||||
apt install -y bmon
|
|
||||||
apt autoremove -y
|
|
||||||
mkdir -p /root/volumes
|
mkdir -p /root/volumes
|
||||||
mkdir -p /root/tmp/appmgr
|
mkdir -p /root/tmp/appmgr
|
||||||
mkdir -p /root/agent
|
mkdir -p /root/agent
|
||||||
mkdir -p /root/appmgr/tor
|
mkdir -p /root/appmgr/tor
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y tor
|
||||||
|
apt-get install -y iotop
|
||||||
|
apt-get install -y bmon
|
||||||
|
apt-get install -y libavahi-client3
|
||||||
|
apt-get install -y libsecp256k1-0
|
||||||
|
apt-get install -y docker.io needrestart-
|
||||||
|
mv /root/setup.sh /root/setup-s1.sh.done
|
||||||
|
cat <<EOT >> /root/setup-s2.sh
|
||||||
|
#!/bin/bash
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y tor
|
||||||
|
apt-get install -y iotop
|
||||||
|
apt-get install -y bmon
|
||||||
|
apt-get install -y libavahi-client3
|
||||||
|
apt-get install -y libsecp256k1-0
|
||||||
|
apt-get install -y docker.io needrestart-
|
||||||
|
apt-get autoremove -y
|
||||||
systemctl enable lifeline
|
systemctl enable lifeline
|
||||||
systemctl enable agent
|
systemctl enable agent
|
||||||
systemctl enable ssh
|
systemctl enable ssh
|
||||||
@@ -17,5 +28,8 @@ systemctl enable avahi-daemon
|
|||||||
passwd -l root
|
passwd -l root
|
||||||
passwd -l pi
|
passwd -l pi
|
||||||
sync
|
sync
|
||||||
systemctl disable setup.service
|
systemctl disable setup
|
||||||
|
mv /root/setup-s2.sh /root/setup-s2.sh.done
|
||||||
reboot
|
reboot
|
||||||
|
EOT
|
||||||
|
chmod +x /root/setup-s2.sh
|
||||||
41
ui/build-send-alpha.sh
Executable file
41
ui/build-send-alpha.sh
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "turn off mocks"
|
||||||
|
echo "$( jq '.useMocks = false' use-mocks.json )" > use-mocks.json
|
||||||
|
echo "$( jq '.skipStartupAlerts = false' use-mocks.json )" > use-mocks.json
|
||||||
|
|
||||||
|
echo "FILTER: rm -rf www"
|
||||||
|
rm -rf www
|
||||||
|
|
||||||
|
echo "FILTER: ionic build"
|
||||||
|
npm run build-prod
|
||||||
|
|
||||||
|
echo "FILTER: cp client-manifest.yaml www"
|
||||||
|
cp client-manifest.yaml www
|
||||||
|
|
||||||
|
echo "FILTER: git hash"
|
||||||
|
touch git-hash.txt
|
||||||
|
git log | head -n1 > git-hash.txt
|
||||||
|
mv git-hash.txt www
|
||||||
|
|
||||||
|
echo "FILTER: removing mock icons"
|
||||||
|
rm -rf www/assets/img/service-icons
|
||||||
|
|
||||||
|
echo "FILTER: tar -zcvf ambassador-ui.tar.gz www"
|
||||||
|
tar -zcvf ambassador-ui.tar.gz www
|
||||||
|
|
||||||
|
SHA_SUM=$(sha1sum ambassador-ui.tar.gz)
|
||||||
|
echo "${SHA_SUM}"
|
||||||
|
|
||||||
|
echo "Set version"
|
||||||
|
VERSION=$(jq ".version" package.json)
|
||||||
|
echo "${VERSION}"
|
||||||
|
|
||||||
|
echo "FILTER: mkdir alpha-reg"
|
||||||
|
ssh root@alpha-registry.start9labs.com "mkdir -p /var/www/html/resources/sys/ambassador-ui.tar.gz/${VERSION}"
|
||||||
|
|
||||||
|
echo "FILTER: scp ambassador-ui.tar.gz"
|
||||||
|
scp ambassador-ui.tar.gz root@alpha-registry.start9labs.com:/var/www/html/resources/sys/ambassador-ui.tar.gz/${VERSION}/ambassador-ui.tar.gz
|
||||||
|
|
||||||
|
echo "FILTER: fin"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
manifest-version: 0
|
manifest-version: 0
|
||||||
app-id: start9-ambassador
|
app-id: start9-ambassador
|
||||||
app-version: 0.2.10
|
app-version: 0.2.13
|
||||||
uri-rewrites:
|
uri-rewrites:
|
||||||
- =/api -> http://{{start9-ambassador}}:5959/authenticate
|
- =/api -> http://{{start9-ambassador}}:5959/authenticate
|
||||||
- /api/ -> http://{{start9-ambassador}}:5959/
|
- /api/ -> http://{{start9-ambassador}}:5959/
|
||||||
|
|||||||
3960
ui/package-lock.json
generated
3960
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "embassy-ui",
|
"name": "embassy-ui",
|
||||||
"version": "0.2.10",
|
"version": "0.2.13",
|
||||||
"description": "GUI for EmbassyOS",
|
"description": "GUI for EmbassyOS",
|
||||||
"author": "Start9 Labs",
|
"author": "Start9 Labs",
|
||||||
"homepage": "https://github.com/Start9Labs/embassy-ui",
|
"homepage": "https://github.com/Start9Labs/embassy-ui",
|
||||||
|
|||||||
@@ -298,11 +298,11 @@ export class ConfigCursor<T extends ValueType> {
|
|||||||
const mappedCfg = this.mappedConfig()
|
const mappedCfg = this.mappedConfig()
|
||||||
if (cfg && mappedCfg && typeof cfg === 'object' && typeof mappedCfg === 'object') {
|
if (cfg && mappedCfg && typeof cfg === 'object' && typeof mappedCfg === 'object') {
|
||||||
const spec = this.spec()
|
const spec = this.spec()
|
||||||
let allKeys
|
let allKeys: Set<string>
|
||||||
if (spec.type === 'union') {
|
if (spec.type === 'union') {
|
||||||
let unionSpec = spec as ValueSpecOf<'union'>
|
let unionSpec = spec as ValueSpecOf<'union'>
|
||||||
const labelForSelection = unionSpec.tag.id
|
const labelForSelection = unionSpec.tag.id
|
||||||
allKeys = new Set([...Object.keys(unionSpec.variants[cfg[labelForSelection]])])
|
allKeys = new Set([labelForSelection, ...Object.keys(unionSpec.variants[cfg[labelForSelection]])])
|
||||||
} else {
|
} else {
|
||||||
allKeys = new Set([...Object.keys(cfg), ...Object.keys(mappedCfg)])
|
allKeys = new Set([...Object.keys(cfg), ...Object.keys(mappedCfg)])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,15 @@
|
|||||||
<ion-item-group>
|
<ion-item-group>
|
||||||
<ion-item-divider></ion-item-divider>
|
<ion-item-divider></ion-item-divider>
|
||||||
<ion-item>
|
<ion-item>
|
||||||
|
<ion-icon size="small" slot="start" *ngIf="!edited"
|
||||||
|
style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);"
|
||||||
|
name="ellipse"></ion-icon>
|
||||||
|
<ion-icon size="small" slot="start" *ngIf="edited" style="margin-right: 15px" color="primary" name="ellipse">
|
||||||
|
</ion-icon>
|
||||||
<ion-label>{{ spec.tag.name }}</ion-label>
|
<ion-label>{{ spec.tag.name }}</ion-label>
|
||||||
<ion-select slot="end" [interfaceOptions]="setSelectOptions()" placeholder="Select One" [(ngModel)]="value[spec.tag.id]" [selectedText]="spec.tag.variantNames[value[spec.tag.id]]" (ngModelChange)="handleUnionChange()">
|
<ion-select slot="end" [interfaceOptions]="setSelectOptions()" placeholder="Select One"
|
||||||
|
[(ngModel)]="value[spec.tag.id]" [selectedText]="spec.tag.variantNames[value[spec.tag.id]]"
|
||||||
|
(ngModelChange)="handleUnionChange()">
|
||||||
<ion-select-option *ngFor="let option of spec.variants | keyvalue: asIsOrder" [value]="option.key">
|
<ion-select-option *ngFor="let option of spec.variants | keyvalue: asIsOrder" [value]="option.key">
|
||||||
{{ spec.tag.variantNames[option.key] }}
|
{{ spec.tag.variantNames[option.key] }}
|
||||||
<span *ngIf="option.key === spec.default"> (default)</span>
|
<span *ngIf="option.key === spec.default"> (default)</span>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export class AppConfigUnionPage {
|
|||||||
spec: ValueSpecUnion
|
spec: ValueSpecUnion
|
||||||
value: object
|
value: object
|
||||||
error: string
|
error: string
|
||||||
|
edited: boolean
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly modalCtrl: ModalController,
|
private readonly modalCtrl: ModalController,
|
||||||
@@ -28,6 +29,7 @@ export class AppConfigUnionPage {
|
|||||||
this.spec = this.cursor.spec()
|
this.spec = this.cursor.spec()
|
||||||
this.value = this.cursor.config()
|
this.value = this.cursor.config()
|
||||||
this.error = this.cursor.checkInvalid()
|
this.error = this.cursor.checkInvalid()
|
||||||
|
this.edited = this.cursor.seekNext(this.spec.tag.id).isEdited()
|
||||||
}
|
}
|
||||||
|
|
||||||
async dismiss () {
|
async dismiss () {
|
||||||
@@ -37,6 +39,8 @@ export class AppConfigUnionPage {
|
|||||||
async handleUnionChange () {
|
async handleUnionChange () {
|
||||||
this.value = mapUnionSpec(this.spec, this.value)
|
this.value = mapUnionSpec(this.spec, this.value)
|
||||||
this.objectConfig.annotations = this.objectConfig.cursor.getAnnotations()
|
this.objectConfig.annotations = this.objectConfig.cursor.getAnnotations()
|
||||||
|
this.error = this.cursor.checkInvalid()
|
||||||
|
this.edited = this.cursor.seekNext(this.spec.tag.id).isEdited()
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectOptions () {
|
setSelectOptions () {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<ion-header>
|
<ion-header>
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-title >
|
<ion-title >
|
||||||
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.10!</ion-label>
|
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.13!</ion-label>
|
||||||
</ion-title>
|
</ion-title>
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
@@ -9,14 +9,10 @@
|
|||||||
<ion-content class="ion-padding">
|
<ion-content class="ion-padding">
|
||||||
<div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
|
<div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
|
||||||
<h2>Highlights</h2>
|
<h2>Highlights</h2>
|
||||||
<p class="main-content">
|
<div class="main-content">
|
||||||
0.2.10 introduces LAN support for services running on the Embassy. A service's LAN address (.local URL) can be accessed while connected to the same network.
|
<p>At long last, Matrix has arrived!</p>
|
||||||
This is useful for two reasons: (1) LAN connections are significantly faster than Tor, and (2) if the Tor network is experiencing connectivity issues, you will not be locked out of your services.
|
<p>This release also enables displaying Service license information and contains utilities to facilitate the next major release of EmbassyOS.</p>
|
||||||
|
</div>
|
||||||
It also introduces support for services to define one-time actions that are exposed to the user. This
|
|
||||||
can be useful for password resets or other types of operations where doing it through the service UI would be
|
|
||||||
insecure or otherwise undesirable.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="close-button">
|
<div class="close-button">
|
||||||
<ion-button fill="outline" (click)="dismiss()">
|
<ion-button fill="outline" (click)="dismiss()">
|
||||||
|
|||||||
@@ -18,9 +18,11 @@ export interface AppAvailablePreview extends BaseApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AppAvailableFull =
|
export type AppAvailableFull =
|
||||||
AppAvailablePreview &
|
AppAvailablePreview & {
|
||||||
{ descriptionLong: string
|
descriptionLong: string
|
||||||
versions: string[]
|
versions: string[]
|
||||||
|
licenseName?: string // @TODO required for 0.3.0
|
||||||
|
licenseLink?: string // @TODO required for 0.3.0
|
||||||
} &
|
} &
|
||||||
AppAvailableVersionSpecificInfo
|
AppAvailableVersionSpecificInfo
|
||||||
|
|
||||||
@@ -45,6 +47,8 @@ export interface AppInstalledPreview extends BaseApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AppInstalledFull extends AppInstalledPreview {
|
export interface AppInstalledFull extends AppInstalledPreview {
|
||||||
|
licenseName?: string // @TODO required for 0.3.0
|
||||||
|
licenseLink?: string // @TODO required for 0.3.0
|
||||||
instructions: string | null
|
instructions: string | null
|
||||||
lastBackup: string | null
|
lastBackup: string | null
|
||||||
configuredRequirements: AppDependency[] | null // null if not yet configured
|
configuredRequirements: AppDependency[] | null // null if not yet configured
|
||||||
|
|||||||
@@ -59,20 +59,23 @@ export class AppActionsPage extends Cleanup {
|
|||||||
})
|
})
|
||||||
await alert.present()
|
await alert.present()
|
||||||
} else {
|
} else {
|
||||||
const joinStatuses = (statuses: AppStatus[]) => {
|
const statuses = [...action.allowedStatuses]
|
||||||
const last = statuses.pop()
|
const last = statuses.pop()
|
||||||
let s = statuses.join(', ')
|
let statusesStr = statuses.join(', ')
|
||||||
if (last) {
|
let error = null
|
||||||
if (statuses.length > 1) { // oxford comma
|
if (statuses.length) {
|
||||||
s += ','
|
if (statuses.length > 1) { // oxford comma
|
||||||
}
|
statusesStr += ','
|
||||||
s += ` or ${last}`
|
|
||||||
}
|
}
|
||||||
return s
|
statusesStr += ` or ${last}`
|
||||||
|
} else if (last) {
|
||||||
|
statusesStr = `${last}`
|
||||||
|
} else {
|
||||||
|
error = `There is state for which this action may be run. This is a bug. Please file an issue with the service maintainer.`
|
||||||
}
|
}
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
header: 'Forbidden',
|
header: 'Forbidden',
|
||||||
message: `Action "${action.name}" can only be executed when service is ${joinStatuses(action.allowedStatuses)}`,
|
message: error || `Action "${action.name}" can only be executed when service is ${statusesStr}`,
|
||||||
buttons: ['OK'],
|
buttons: ['OK'],
|
||||||
cssClass: 'alert-error-message',
|
cssClass: 'alert-error-message',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,6 +21,26 @@
|
|||||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
<ion-card *ngIf="v1Status.status === 'instructions'" [href]="'https://start9.com/eos-' + v1Status.version" target="_blank" class="instructions-card">
|
||||||
|
<ion-card-header>
|
||||||
|
<ion-card-subtitle>Coming Soon...</ion-card-subtitle>
|
||||||
|
<ion-card-title>EmbassyOS Version {{ v1Status.version }}</ion-card-title>
|
||||||
|
</ion-card-header>
|
||||||
|
<ion-card-content>
|
||||||
|
<b>Get ready. View the update instructions.</b>
|
||||||
|
</ion-card-content>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<ion-card *ngIf="v1Status.status === 'available'" [href]="'https://start9.com/eos-' + v1Status.version" target="_blank" class="available-card">
|
||||||
|
<ion-card-header>
|
||||||
|
<ion-card-subtitle>Now Available...</ion-card-subtitle>
|
||||||
|
<ion-card-title>EmbassyOS Version {{ v1Status.version }}</ion-card-title>
|
||||||
|
</ion-card-header>
|
||||||
|
<ion-card-content>
|
||||||
|
<b>View the update instructions.</b>
|
||||||
|
</ion-card-content>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
<ion-card *ngFor="let app of apps" style="margin: 10px 10px;" [routerLink]="[app.id]">
|
<ion-card *ngFor="let app of apps" style="margin: 10px 10px;" [routerLink]="[app.id]">
|
||||||
<ion-item style="--inner-border-width: 0 0 .4px 0; --border-color: #525252;" *ngIf="{ installing: (app.subject.status | async) === 'INSTALLING', installComparison: app.subject | compareInstalledAndLatest | async } as l">
|
<ion-item style="--inner-border-width: 0 0 .4px 0; --border-color: #525252;" *ngIf="{ installing: (app.subject.status | async) === 'INSTALLING', installComparison: app.subject | compareInstalledAndLatest | async } as l">
|
||||||
<ion-avatar style="margin-top: 8px;" slot="start">
|
<ion-avatar style="margin-top: 8px;" slot="start">
|
||||||
|
|||||||
@@ -4,3 +4,13 @@
|
|||||||
font-family: 'Open Sans';
|
font-family: 'Open Sans';
|
||||||
padding: 1px 0px 1.5px 0px;
|
padding: 1px 0px 1.5px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.instructions-card {
|
||||||
|
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-medium) 150%);
|
||||||
|
margin: 16px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-card {
|
||||||
|
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-danger) 150%);
|
||||||
|
margin: 16px 10px;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { Subscription, BehaviorSubject, combineLatest } from 'rxjs'
|
|||||||
import { take } from 'rxjs/operators'
|
import { take } from 'rxjs/operators'
|
||||||
import { markAsLoadingDuringP } from 'src/app/services/loader.service'
|
import { markAsLoadingDuringP } from 'src/app/services/loader.service'
|
||||||
import { OsUpdateService } from 'src/app/services/os-update.service'
|
import { OsUpdateService } from 'src/app/services/os-update.service'
|
||||||
|
import { V1Status } from 'src/app/services/api/api-types'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-available-list',
|
selector: 'app-available-list',
|
||||||
@@ -20,6 +21,7 @@ export class AppAvailableListPage {
|
|||||||
installedAppDeltaSubscription: Subscription
|
installedAppDeltaSubscription: Subscription
|
||||||
apps: PropertySubjectId<AppAvailablePreview>[] = []
|
apps: PropertySubjectId<AppAvailablePreview>[] = []
|
||||||
appsInstalled: PropertySubjectId<AppInstalledPreview>[] = []
|
appsInstalled: PropertySubjectId<AppInstalledPreview>[] = []
|
||||||
|
v1Status: V1Status = { status: 'nothing', version: '' }
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private readonly apiService: ApiService,
|
private readonly apiService: ApiService,
|
||||||
@@ -35,6 +37,7 @@ export class AppAvailableListPage {
|
|||||||
|
|
||||||
markAsLoadingDuringP(this.$loading$, Promise.all([
|
markAsLoadingDuringP(this.$loading$, Promise.all([
|
||||||
this.getApps(),
|
this.getApps(),
|
||||||
|
this.checkV1Status(),
|
||||||
this.osUpdateService.checkWhenNotAvailable$().toPromise(), // checks for an os update, banner component renders conditionally
|
this.osUpdateService.checkWhenNotAvailable$().toPromise(), // checks for an os update, banner component renders conditionally
|
||||||
pauseFor(600),
|
pauseFor(600),
|
||||||
]))
|
]))
|
||||||
@@ -44,6 +47,14 @@ export class AppAvailableListPage {
|
|||||||
this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id))
|
this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkV1Status () {
|
||||||
|
try {
|
||||||
|
this.v1Status = await this.apiService.checkV1Status()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mergeInstalledProps (appInstalledId: string) {
|
mergeInstalledProps (appInstalledId: string) {
|
||||||
const appAvailable = this.apps.find(app => app.id === appInstalledId)
|
const appAvailable = this.apps.find(app => app.id === appInstalledId)
|
||||||
if (!appAvailable) return
|
if (!appAvailable) return
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
versionInstalled: $app$.versionInstalled | async,
|
versionInstalled: $app$.versionInstalled | async,
|
||||||
versionViewing: $app$.versionViewing | async,
|
versionViewing: $app$.versionViewing | async,
|
||||||
descriptionLong: $app$.descriptionLong | async,
|
descriptionLong: $app$.descriptionLong | async,
|
||||||
|
licenseName: $app$.licenseName | async,
|
||||||
|
licenseLink: $app$.licenseLink | async,
|
||||||
serviceRequirements: $app$.serviceRequirements | async,
|
serviceRequirements: $app$.serviceRequirements | async,
|
||||||
iconURL: $app$.iconURL | async,
|
iconURL: $app$.iconURL | async,
|
||||||
releaseNotes: $app$.releaseNotes | async
|
releaseNotes: $app$.releaseNotes | async
|
||||||
@@ -112,9 +114,14 @@
|
|||||||
<dependency-list [$loading$]="$dependenciesLoading$" [hostApp]="$app$ | peekProperties" [dependencies]="vars.serviceRequirements"></dependency-list>
|
<dependency-list [$loading$]="$dependenciesLoading$" [hostApp]="$app$ | peekProperties" [dependencies]="vars.serviceRequirements"></dependency-list>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ion-item-divider></ion-item-divider>
|
<ion-item-divider></ion-item-divider>
|
||||||
|
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
|
||||||
|
<ion-icon slot="start" name="newspaper-outline"></ion-icon>
|
||||||
|
<ion-label>License</ion-label>
|
||||||
|
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
|
||||||
|
</ion-item>
|
||||||
<ion-item lines="none" button (click)="presentAlertVersions()">
|
<ion-item lines="none" button (click)="presentAlertVersions()">
|
||||||
<ion-icon color="medium" slot="start" name="file-tray-stacked-outline"></ion-icon>
|
<ion-icon slot="start" name="file-tray-stacked-outline"></ion-icon>
|
||||||
<ion-label color="medium">Other versions</ion-label>
|
<ion-label>Other versions</ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-item-group>
|
</ion-item-group>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
torAddress: app.torAddress | async,
|
torAddress: app.torAddress | async,
|
||||||
status: app.status | async,
|
status: app.status | async,
|
||||||
versionInstalled: app.versionInstalled | async,
|
versionInstalled: app.versionInstalled | async,
|
||||||
|
licenseName: app.licenseName | async,
|
||||||
|
licenseLink: app.licenseLink | async,
|
||||||
configuredRequirements: app.configuredRequirements | async,
|
configuredRequirements: app.configuredRequirements | async,
|
||||||
lastBackup: app.lastBackup | async,
|
lastBackup: app.lastBackup | async,
|
||||||
hasFetchedFull: app.hasFetchedFull | async,
|
hasFetchedFull: app.hasFetchedFull | async,
|
||||||
@@ -157,6 +159,12 @@
|
|||||||
<ion-icon slot="start" name="storefront-outline" color="primary"></ion-icon>
|
<ion-icon slot="start" name="storefront-outline" color="primary"></ion-icon>
|
||||||
<ion-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label>
|
<ion-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
<!-- license -->
|
||||||
|
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
|
||||||
|
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
|
||||||
|
<ion-label><ion-text color="primary">License</ion-text></ion-label>
|
||||||
|
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
<!-- dependencies -->
|
<!-- dependencies -->
|
||||||
<ng-container *ngIf="vars.configuredRequirements && vars.configuredRequirements.length">
|
<ng-container *ngIf="vars.configuredRequirements && vars.configuredRequirements.length">
|
||||||
|
|||||||
@@ -37,3 +37,7 @@ export interface ApiAppConfig {
|
|||||||
|
|
||||||
export type Unit = { never?: never; } // hack for the unit typ
|
export type Unit = { never?: never; } // hack for the unit typ
|
||||||
|
|
||||||
|
export type V1Status = {
|
||||||
|
status: 'nothing' | 'instructions' | 'available'
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Rules } from '../../models/app-model'
|
|||||||
import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
|
import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
|
||||||
import { S9Notification, SSHFingerprint, ServerMetrics, DiskInfo } from '../../models/server-model'
|
import { S9Notification, SSHFingerprint, ServerMetrics, DiskInfo } from '../../models/server-model'
|
||||||
import { Subject, Observable } from 'rxjs'
|
import { Subject, Observable } from 'rxjs'
|
||||||
import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull, ApiAppInstalledPreview } from './api-types'
|
import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull, ApiAppInstalledPreview, V1Status } from './api-types'
|
||||||
import { AppMetrics, AppMetricsVersioned } from 'src/app/util/metrics.util'
|
import { AppMetrics, AppMetricsVersioned } from 'src/app/util/metrics.util'
|
||||||
import { ConfigSpec } from 'src/app/app-config/config-types'
|
import { ConfigSpec } from 'src/app/app-config/config-types'
|
||||||
|
|
||||||
@@ -64,6 +64,7 @@ export abstract class ApiService {
|
|||||||
abstract ejectExternalDisk (logicalName: string): Promise<Unit>
|
abstract ejectExternalDisk (logicalName: string): Promise<Unit>
|
||||||
abstract serviceAction (appId: string, serviceAction: ServiceAction): Promise<ReqRes.ServiceActionResponse>
|
abstract serviceAction (appId: string, serviceAction: ServiceAction): Promise<ReqRes.ServiceActionResponse>
|
||||||
abstract refreshLAN (): Promise<Unit>
|
abstract refreshLAN (): Promise<Unit>
|
||||||
|
abstract checkV1Status (): Promise<V1Status>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRpcFailure<Error, Result> (arg: { error: Error } | { result: Result }): arg is { error: Error } {
|
export function isRpcFailure<Error, Result> (arg: { error: Error } | { result: Result }): arg is { error: Error } {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AppModel, AppStatus } from '../../models/app-model'
|
|||||||
import { AppAvailablePreview, AppAvailableFull, AppInstalledFull, AppInstalledPreview, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
|
import { AppAvailablePreview, AppAvailableFull, AppInstalledFull, AppInstalledPreview, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
|
||||||
import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model'
|
import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model'
|
||||||
import { ApiService, ReqRes } from './api.service'
|
import { ApiService, ReqRes } from './api.service'
|
||||||
import { ApiAppInstalledPreview, ApiServer, Unit } from './api-types'
|
import { ApiAppInstalledPreview, ApiServer, Unit, V1Status } from './api-types'
|
||||||
import { HttpErrorResponse } from '@angular/common/http'
|
import { HttpErrorResponse } from '@angular/common/http'
|
||||||
import { isUnauthorized } from 'src/app/util/web.util'
|
import { isUnauthorized } from 'src/app/util/web.util'
|
||||||
import { Replace } from 'src/app/util/types.util'
|
import { Replace } from 'src/app/util/types.util'
|
||||||
@@ -17,7 +17,7 @@ import { ConfigService } from '../config.service'
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LiveApiService extends ApiService {
|
export class LiveApiService extends ApiService {
|
||||||
constructor(
|
constructor (
|
||||||
private readonly http: HttpService,
|
private readonly http: HttpService,
|
||||||
// TODO remove app + server model from here. updates to state should be done in a separate class wrapping ApiService + App/ServerModel
|
// TODO remove app + server model from here. updates to state should be done in a separate class wrapping ApiService + App/ServerModel
|
||||||
private readonly appModel: AppModel,
|
private readonly appModel: AppModel,
|
||||||
@@ -25,40 +25,40 @@ export class LiveApiService extends ApiService {
|
|||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
) { super() }
|
) { super() }
|
||||||
|
|
||||||
testConnection(url: string): Promise<true> {
|
testConnection (url: string): Promise<true> {
|
||||||
return this.http.raw.get(url).pipe(mapTo(true as true), catchError(e => catchHttpStatusError(e))).toPromise()
|
return this.http.raw.get(url).pipe(mapTo(true as true), catchError(e => catchHttpStatusError(e))).toPromise()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to check whether password or key is valid. If so, it will be used implicitly by all other calls.
|
// Used to check whether password or key is valid. If so, it will be used implicitly by all other calls.
|
||||||
async getCheckAuth(): Promise<Unit> {
|
async getCheckAuth (): Promise<Unit> {
|
||||||
return this.http.serverRequest<Unit>({ method: Method.GET, url: '/authenticate' }, { version: '' })
|
return this.http.serverRequest<Unit>({ method: Method.GET, url: '/authenticate' }, { version: '' })
|
||||||
}
|
}
|
||||||
|
|
||||||
async postLogin(password: string): Promise<Unit> {
|
async postLogin (password: string): Promise<Unit> {
|
||||||
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/login', data: { password } }, { version: '' })
|
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/login', data: { password } }, { version: '' })
|
||||||
}
|
}
|
||||||
|
|
||||||
async postLogout(): Promise<Unit> {
|
async postLogout (): Promise<Unit> {
|
||||||
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/logout' }, { version: '' }).then(() => { this.authenticatedRequestsEnabled = false; return {} })
|
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/logout' }, { version: '' }).then(() => { this.authenticatedRequestsEnabled = false; return { } })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getServer(timeout?: number): Promise<ApiServer> {
|
async getServer (timeout?: number): Promise<ApiServer> {
|
||||||
return this.authRequest<ReqRes.GetServerRes>({ method: Method.GET, url: '/', readTimeout: timeout })
|
return this.authRequest<ReqRes.GetServerRes>({ method: Method.GET, url: '/', readTimeout: timeout })
|
||||||
}
|
}
|
||||||
|
|
||||||
async acknowledgeOSWelcome(version: string): Promise<Unit> {
|
async acknowledgeOSWelcome (version: string): Promise<Unit> {
|
||||||
return this.authRequest<Unit>({ method: Method.POST, url: `/welcome/${version}` })
|
return this.authRequest<Unit>({ method: Method.POST, url: `/welcome/${version}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVersionLatest(): Promise<ReqRes.GetVersionLatestRes> {
|
async getVersionLatest (): Promise<ReqRes.GetVersionLatestRes> {
|
||||||
return this.authRequest<ReqRes.GetVersionLatestRes>({ method: Method.GET, url: '/versionLatest' }, { version: '' })
|
return this.authRequest<ReqRes.GetVersionLatestRes>({ method: Method.GET, url: '/versionLatest' }, { version: '' })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getServerMetrics(): Promise<ReqRes.GetServerMetricsRes> {
|
async getServerMetrics (): Promise<ReqRes.GetServerMetricsRes> {
|
||||||
return this.authRequest<ReqRes.GetServerMetricsRes>({ method: Method.GET, url: `/metrics` })
|
return this.authRequest<ReqRes.GetServerMetricsRes>({ method: Method.GET, url: `/metrics` })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNotifications(page: number, perPage: number): Promise<S9Notification[]> {
|
async getNotifications (page: number, perPage: number): Promise<S9Notification[]> {
|
||||||
const params: ReqRes.GetNotificationsReq = {
|
const params: ReqRes.GetNotificationsReq = {
|
||||||
page: String(page),
|
page: String(page),
|
||||||
perPage: String(perPage),
|
perPage: String(perPage),
|
||||||
@@ -66,27 +66,27 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.authRequest<ReqRes.GetNotificationsRes>({ method: Method.GET, url: `/notifications`, params })
|
return this.authRequest<ReqRes.GetNotificationsRes>({ method: Method.GET, url: `/notifications`, params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteNotification(id: string): Promise<Unit> {
|
async deleteNotification (id: string): Promise<Unit> {
|
||||||
return this.authRequest({ method: Method.DELETE, url: `/notifications/${id}` })
|
return this.authRequest({ method: Method.DELETE, url: `/notifications/${id}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getExternalDisks(): Promise<DiskInfo[]> {
|
async getExternalDisks (): Promise<DiskInfo[]> {
|
||||||
return this.authRequest<ReqRes.GetExternalDisksRes>({ method: Method.GET, url: `/disks` })
|
return this.authRequest<ReqRes.GetExternalDisksRes>({ method: Method.GET, url: `/disks` })
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: EJECT-DISKS
|
// TODO: EJECT-DISKS
|
||||||
async ejectExternalDisk(logicalName: string): Promise<Unit> {
|
async ejectExternalDisk (logicalName: string): Promise<Unit> {
|
||||||
return this.authRequest({ method: Method.POST, url: `/disks/eject`, data: { logicalName } })
|
return this.authRequest({ method: Method.POST, url: `/disks/eject`, data: { logicalName } })
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAgent(version: string): Promise<Unit> {
|
async updateAgent (version: string): Promise<Unit> {
|
||||||
const data: ReqRes.PostUpdateAgentReq = {
|
const data: ReqRes.PostUpdateAgentReq = {
|
||||||
version: `=${version}`,
|
version: `=${version}`,
|
||||||
}
|
}
|
||||||
return this.authRequest({ method: Method.POST, url: '/update', data })
|
return this.authRequest({ method: Method.POST, url: '/update', data })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailableAppVersionSpecificInfo(appId: string, versionSpec: string): Promise<AppAvailableVersionSpecificInfo> {
|
async getAvailableAppVersionSpecificInfo (appId: string, versionSpec: string): Promise<AppAvailableVersionSpecificInfo> {
|
||||||
return this
|
return this
|
||||||
.authRequest<Replace<ReqRes.GetAppAvailableVersionInfoRes, 'versionViewing', 'version'>>({ method: Method.GET, url: `/apps/${appId}/store/${versionSpec}` })
|
.authRequest<Replace<ReqRes.GetAppAvailableVersionInfoRes, 'versionViewing', 'version'>>({ method: Method.GET, url: `/apps/${appId}/store/${versionSpec}` })
|
||||||
.then(res => ({ ...res, versionViewing: res.version }))
|
.then(res => ({ ...res, versionViewing: res.version }))
|
||||||
@@ -96,7 +96,7 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailableApps(): Promise<AppAvailablePreview[]> {
|
async getAvailableApps (): Promise<AppAvailablePreview[]> {
|
||||||
const res = await this.authRequest<ReqRes.GetAppsAvailableRes>({ method: Method.GET, url: '/apps/store' })
|
const res = await this.authRequest<ReqRes.GetAppsAvailableRes>({ method: Method.GET, url: '/apps/store' })
|
||||||
return res.map(a => {
|
return res.map(a => {
|
||||||
const latestVersionTimestamp = new Date(a.latestVersionTimestamp)
|
const latestVersionTimestamp = new Date(a.latestVersionTimestamp)
|
||||||
@@ -105,7 +105,7 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailableApp(appId: string): Promise<AppAvailableFull> {
|
async getAvailableApp (appId: string): Promise<AppAvailableFull> {
|
||||||
return this.authRequest<ReqRes.GetAppAvailableRes>({ method: Method.GET, url: `/apps/${appId}/store` })
|
return this.authRequest<ReqRes.GetAppAvailableRes>({ method: Method.GET, url: `/apps/${appId}/store` })
|
||||||
.then(res => {
|
.then(res => {
|
||||||
return {
|
return {
|
||||||
@@ -115,7 +115,7 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInstalledApp(appId: string): Promise<AppInstalledFull> {
|
async getInstalledApp (appId: string): Promise<AppInstalledFull> {
|
||||||
return this.authRequest<ReqRes.GetAppInstalledRes>({ method: Method.GET, url: `/apps/${appId}/installed` })
|
return this.authRequest<ReqRes.GetAppInstalledRes>({ method: Method.GET, url: `/apps/${appId}/installed` })
|
||||||
.then(app => {
|
.then(app => {
|
||||||
return {
|
return {
|
||||||
@@ -127,7 +127,7 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInstalledApps(): Promise<AppInstalledPreview[]> {
|
async getInstalledApps (): Promise<AppInstalledPreview[]> {
|
||||||
return this.authRequest<ReqRes.GetAppsInstalledRes>({ method: Method.GET, url: `/apps/installed` })
|
return this.authRequest<ReqRes.GetAppsInstalledRes>({ method: Method.GET, url: `/apps/installed` })
|
||||||
.then(apps => {
|
.then(apps => {
|
||||||
return apps.map(app => {
|
return apps.map(app => {
|
||||||
@@ -140,24 +140,24 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAppConfig(appId: string): Promise<ReqRes.GetAppConfigRes> {
|
async getAppConfig (appId: string): Promise<ReqRes.GetAppConfigRes> {
|
||||||
return this.authRequest<ReqRes.GetAppConfigRes>({ method: Method.GET, url: `/apps/${appId}/config` })
|
return this.authRequest<ReqRes.GetAppConfigRes>({ method: Method.GET, url: `/apps/${appId}/config` })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAppLogs(appId: string, params: ReqRes.GetAppLogsReq = {}): Promise<string[]> {
|
async getAppLogs (appId: string, params: ReqRes.GetAppLogsReq = { }): Promise<string[]> {
|
||||||
return this.authRequest<ReqRes.GetAppLogsRes>({ method: Method.GET, url: `/apps/${appId}/logs`, params: params as any })
|
return this.authRequest<ReqRes.GetAppLogsRes>({ method: Method.GET, url: `/apps/${appId}/logs`, params: params as any })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getServerLogs(): Promise<string[]> {
|
async getServerLogs (): Promise<string[]> {
|
||||||
return this.authRequest<ReqRes.GetServerLogsRes>({ method: Method.GET, url: `/logs` })
|
return this.authRequest<ReqRes.GetServerLogsRes>({ method: Method.GET, url: `/logs` })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAppMetrics(appId: string): Promise<AppMetrics> {
|
async getAppMetrics (appId: string): Promise<AppMetrics> {
|
||||||
return this.authRequest<ReqRes.GetAppMetricsRes | string>({ method: Method.GET, url: `/apps/${appId}/metrics` })
|
return this.authRequest<ReqRes.GetAppMetricsRes | string>({ method: Method.GET, url: `/apps/${appId}/metrics` })
|
||||||
.then(parseMetricsPermissive)
|
.then(parseMetricsPermissive)
|
||||||
}
|
}
|
||||||
|
|
||||||
async installApp(appId: string, version: string, dryRun: boolean = false): Promise<AppInstalledFull & { breakages: DependentBreakage[] }> {
|
async installApp (appId: string, version: string, dryRun: boolean = false): Promise<AppInstalledFull & { breakages: DependentBreakage[] }> {
|
||||||
const data: ReqRes.PostInstallAppReq = {
|
const data: ReqRes.PostInstallAppReq = {
|
||||||
version,
|
version,
|
||||||
}
|
}
|
||||||
@@ -172,94 +172,94 @@ export class LiveApiService extends ApiService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async uninstallApp(appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
|
async uninstallApp (appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
|
||||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/uninstall${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
|
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/uninstall${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
async startApp(appId: string): Promise<Unit> {
|
async startApp (appId: string): Promise<Unit> {
|
||||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/start`, readTimeout: 60000 })
|
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/start`, readTimeout: 60000 })
|
||||||
.then(() => this.appModel.update({ id: appId, status: AppStatus.RUNNING }))
|
.then(() => this.appModel.update({ id: appId, status: AppStatus.RUNNING }))
|
||||||
.then(() => ({}))
|
.then(() => ({ }))
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopApp(appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
|
async stopApp (appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
|
||||||
const res = await this.authRequest<{ breakages: DependentBreakage[] }>({ method: Method.POST, url: `/apps/${appId}/stop${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
|
const res = await this.authRequest<{ breakages: DependentBreakage[] }>({ method: Method.POST, url: `/apps/${appId}/stop${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
|
||||||
if (!dryRun) this.appModel.update({ id: appId, status: AppStatus.STOPPING }, modulateTime(new Date(), 5, 'seconds'))
|
if (!dryRun) this.appModel.update({ id: appId, status: AppStatus.STOPPING }, modulateTime(new Date(), 5, 'seconds'))
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
async restartApp(appId: string): Promise<Unit> {
|
async restartApp (appId: string): Promise<Unit> {
|
||||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/restart`, readTimeout: 60000 })
|
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/restart`, readTimeout: 60000 })
|
||||||
.then(() => ({} as any))
|
.then(() => ({ } as any))
|
||||||
}
|
}
|
||||||
|
|
||||||
async createAppBackup(appId: string, logicalname: string, password?: string): Promise<Unit> {
|
async createAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit> {
|
||||||
const data: ReqRes.PostAppBackupCreateReq = {
|
const data: ReqRes.PostAppBackupCreateReq = {
|
||||||
password: password || undefined,
|
password: password || undefined,
|
||||||
logicalname,
|
logicalname,
|
||||||
}
|
}
|
||||||
return this.authRequest<ReqRes.PostAppBackupCreateRes>({ method: Method.POST, url: `/apps/${appId}/backup`, data, readTimeout: 60000 })
|
return this.authRequest<ReqRes.PostAppBackupCreateRes>({ method: Method.POST, url: `/apps/${appId}/backup`, data, readTimeout: 60000 })
|
||||||
.then(() => this.appModel.update({ id: appId, status: AppStatus.CREATING_BACKUP }))
|
.then(() => this.appModel.update({ id: appId, status: AppStatus.CREATING_BACKUP }))
|
||||||
.then(() => ({}))
|
.then(() => ({ }))
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopAppBackup(appId: string): Promise<Unit> {
|
async stopAppBackup (appId: string): Promise<Unit> {
|
||||||
return this.authRequest<ReqRes.PostAppBackupStopRes>({ method: Method.POST, url: `/apps/${appId}/backup/stop`, readTimeout: 60000 })
|
return this.authRequest<ReqRes.PostAppBackupStopRes>({ method: Method.POST, url: `/apps/${appId}/backup/stop`, readTimeout: 60000 })
|
||||||
.then(() => this.appModel.update({ id: appId, status: AppStatus.STOPPED }))
|
.then(() => this.appModel.update({ id: appId, status: AppStatus.STOPPED }))
|
||||||
.then(() => ({}))
|
.then(() => ({ }))
|
||||||
}
|
}
|
||||||
|
|
||||||
async restoreAppBackup(appId: string, logicalname: string, password?: string): Promise<Unit> {
|
async restoreAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit> {
|
||||||
const data: ReqRes.PostAppBackupRestoreReq = {
|
const data: ReqRes.PostAppBackupRestoreReq = {
|
||||||
password: password || undefined,
|
password: password || undefined,
|
||||||
logicalname,
|
logicalname,
|
||||||
}
|
}
|
||||||
return this.authRequest<ReqRes.PostAppBackupRestoreRes>({ method: Method.POST, url: `/apps/${appId}/backup/restore`, data, readTimeout: 60000 })
|
return this.authRequest<ReqRes.PostAppBackupRestoreRes>({ method: Method.POST, url: `/apps/${appId}/backup/restore`, data, readTimeout: 60000 })
|
||||||
.then(() => this.appModel.update({ id: appId, status: AppStatus.RESTORING_BACKUP }))
|
.then(() => this.appModel.update({ id: appId, status: AppStatus.RESTORING_BACKUP }))
|
||||||
.then(() => ({}))
|
.then(() => ({ }))
|
||||||
}
|
}
|
||||||
|
|
||||||
async patchAppConfig(app: AppInstalledPreview, config: object, dryRun = false): Promise<{ breakages: DependentBreakage[] }> {
|
async patchAppConfig (app: AppInstalledPreview, config: object, dryRun = false): Promise<{ breakages: DependentBreakage[] }> {
|
||||||
const data: ReqRes.PatchAppConfigReq = {
|
const data: ReqRes.PatchAppConfigReq = {
|
||||||
config,
|
config,
|
||||||
}
|
}
|
||||||
return this.authRequest({ method: Method.PATCH, url: `/apps/${app.id}/config${dryRunParam(dryRun, true)}`, data, readTimeout: 60000 })
|
return this.authRequest({ method: Method.PATCH, url: `/apps/${app.id}/config${dryRunParam(dryRun, true)}`, data, readTimeout: 60000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
async postConfigureDependency(dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }> {
|
async postConfigureDependency (dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }> {
|
||||||
return this.authRequest({ method: Method.POST, url: `/apps/${dependencyId}/autoconfig/${dependentId}${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
|
return this.authRequest({ method: Method.POST, url: `/apps/${dependencyId}/autoconfig/${dependentId}${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
async patchServerConfig(attr: string, value: any): Promise<Unit> {
|
async patchServerConfig (attr: string, value: any): Promise<Unit> {
|
||||||
const data: ReqRes.PatchServerConfigReq = {
|
const data: ReqRes.PatchServerConfigReq = {
|
||||||
value,
|
value,
|
||||||
}
|
}
|
||||||
return this.authRequest({ method: Method.PATCH, url: `/${attr}`, data, readTimeout: 60000 })
|
return this.authRequest({ method: Method.PATCH, url: `/${attr}`, data, readTimeout: 60000 })
|
||||||
.then(() => this.serverModel.update({ [attr]: value }))
|
.then(() => this.serverModel.update({ [attr]: value }))
|
||||||
.then(() => ({}))
|
.then(() => ({ }))
|
||||||
}
|
}
|
||||||
|
|
||||||
async wipeAppData(app: AppInstalledPreview): Promise<Unit> {
|
async wipeAppData (app: AppInstalledPreview): Promise<Unit> {
|
||||||
return this.authRequest({ method: Method.POST, url: `/apps/${app.id}/wipe`, readTimeout: 60000 }).then((res) => {
|
return this.authRequest({ method: Method.POST, url: `/apps/${app.id}/wipe`, readTimeout: 60000 }).then((res) => {
|
||||||
this.appModel.update({ id: app.id, status: AppStatus.NEEDS_CONFIG })
|
this.appModel.update({ id: app.id, status: AppStatus.NEEDS_CONFIG })
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleAppLAN(appId: string, toggle: 'enable' | 'disable'): Promise<Unit> {
|
async toggleAppLAN (appId: string, toggle: 'enable' | 'disable'): Promise<Unit> {
|
||||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/lan/${toggle}` })
|
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/lan/${toggle}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
async addSSHKey(sshKey: string): Promise<Unit> {
|
async addSSHKey (sshKey: string): Promise<Unit> {
|
||||||
const data: ReqRes.PostAddSSHKeyReq = {
|
const data: ReqRes.PostAddSSHKeyReq = {
|
||||||
sshKey,
|
sshKey,
|
||||||
}
|
}
|
||||||
const fingerprint = await this.authRequest<ReqRes.PostAddSSHKeyRes>({ method: Method.POST, url: `/sshKeys`, data })
|
const fingerprint = await this.authRequest<ReqRes.PostAddSSHKeyRes>({ method: Method.POST, url: `/sshKeys`, data })
|
||||||
this.serverModel.update({ ssh: [...this.serverModel.peek().ssh, fingerprint] })
|
this.serverModel.update({ ssh: [...this.serverModel.peek().ssh, fingerprint] })
|
||||||
return {}
|
return { }
|
||||||
}
|
}
|
||||||
|
|
||||||
async addWifi(ssid: string, password: string, country: string, connect: boolean): Promise<Unit> {
|
async addWifi (ssid: string, password: string, country: string, connect: boolean): Promise<Unit> {
|
||||||
const data: ReqRes.PostAddWifiReq = {
|
const data: ReqRes.PostAddWifiReq = {
|
||||||
ssid,
|
ssid,
|
||||||
password,
|
password,
|
||||||
@@ -269,30 +269,30 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.authRequest({ method: Method.POST, url: `/wifi`, data })
|
return this.authRequest({ method: Method.POST, url: `/wifi`, data })
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectWifi(ssid: string): Promise<Unit> {
|
async connectWifi (ssid: string): Promise<Unit> {
|
||||||
return this.authRequest({ method: Method.POST, url: encodeURI(`/wifi/${ssid}`) })
|
return this.authRequest({ method: Method.POST, url: encodeURI(`/wifi/${ssid}`) })
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteWifi(ssid: string): Promise<Unit> {
|
async deleteWifi (ssid: string): Promise<Unit> {
|
||||||
return this.authRequest({ method: Method.DELETE, url: encodeURI(`/wifi/${ssid}`) })
|
return this.authRequest({ method: Method.DELETE, url: encodeURI(`/wifi/${ssid}`) })
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteSSHKey(fingerprint: SSHFingerprint): Promise<Unit> {
|
async deleteSSHKey (fingerprint: SSHFingerprint): Promise<Unit> {
|
||||||
await this.authRequest({ method: Method.DELETE, url: `/sshKeys/${fingerprint.hash}` })
|
await this.authRequest({ method: Method.DELETE, url: `/sshKeys/${fingerprint.hash}` })
|
||||||
const ssh = this.serverModel.peek().ssh
|
const ssh = this.serverModel.peek().ssh
|
||||||
this.serverModel.update({ ssh: ssh.filter(s => s !== fingerprint) })
|
this.serverModel.update({ ssh: ssh.filter(s => s !== fingerprint) })
|
||||||
return {}
|
return { }
|
||||||
}
|
}
|
||||||
|
|
||||||
async restartServer(): Promise<Unit> {
|
async restartServer (): Promise<Unit> {
|
||||||
return this.authRequest({ method: Method.POST, url: '/restart', readTimeout: 60000 })
|
return this.authRequest({ method: Method.POST, url: '/restart', readTimeout: 60000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
async shutdownServer(): Promise<Unit> {
|
async shutdownServer (): Promise<Unit> {
|
||||||
return this.authRequest({ method: Method.POST, url: '/shutdown', readTimeout: 60000 })
|
return this.authRequest({ method: Method.POST, url: '/shutdown', readTimeout: 60000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
async serviceAction(appId: string, s: ServiceAction): Promise<ReqRes.ServiceActionResponse> {
|
async serviceAction (appId: string, s: ServiceAction): Promise<ReqRes.ServiceActionResponse> {
|
||||||
const data: ReqRes.ServiceActionRequest = {
|
const data: ReqRes.ServiceActionRequest = {
|
||||||
jsonrpc: '2.0',
|
jsonrpc: '2.0',
|
||||||
id: uuid.v4(),
|
id: uuid.v4(),
|
||||||
@@ -301,11 +301,15 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/actions`, data, readTimeout: 300000 })
|
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/actions`, data, readTimeout: 300000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshLAN(): Promise<Unit> {
|
async refreshLAN (): Promise<Unit> {
|
||||||
return this.authRequest({ method: Method.POST, url: '/network/lan/reset' })
|
return this.authRequest({ method: Method.POST, url: '/network/lan/reset' })
|
||||||
}
|
}
|
||||||
|
|
||||||
private async authRequest<T>(opts: HttpOptions, overrides: Partial<{ version: string }> = {}): Promise<T> {
|
async checkV1Status (): Promise<V1Status> {
|
||||||
|
return this.http.request({ method: Method.GET, url: 'https://registry.start9labs.com/sys/status' })
|
||||||
|
}
|
||||||
|
|
||||||
|
private async authRequest<T> (opts: HttpOptions, overrides: Partial<{ version: string }> = { }): Promise<T> {
|
||||||
if (!this.authenticatedRequestsEnabled) throw new Error(`Authenticated requests are not enabled. Do you need to login?`)
|
if (!this.authenticatedRequestsEnabled) throw new Error(`Authenticated requests are not enabled. Do you need to login?`)
|
||||||
|
|
||||||
opts.withCredentials = true
|
opts.withCredentials = true
|
||||||
@@ -324,7 +328,7 @@ const dryRunParam = (dryRun: boolean, first: boolean) => {
|
|||||||
return first ? `?dryrun` : `&dryrun`
|
return first ? `?dryrun` : `&dryrun`
|
||||||
}
|
}
|
||||||
|
|
||||||
function catchHttpStatusError(error: HttpErrorResponse): Observable<true> {
|
function catchHttpStatusError (error: HttpErrorResponse): Observable<true> {
|
||||||
if (error.error instanceof ErrorEvent) {
|
if (error.error instanceof ErrorEvent) {
|
||||||
// A client-side or network error occurred. Handle it accordingly.
|
// A client-side or network error occurred. Handle it accordingly.
|
||||||
return throwError('Not Connected')
|
return throwError('Not Connected')
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalle
|
|||||||
import { S9Notification, SSHFingerprint, ServerStatus, ServerModel, DiskInfo } from '../../models/server-model'
|
import { S9Notification, SSHFingerprint, ServerStatus, ServerModel, DiskInfo } from '../../models/server-model'
|
||||||
import { pauseFor } from '../../util/misc.util'
|
import { pauseFor } from '../../util/misc.util'
|
||||||
import { ApiService, ReqRes } from './api.service'
|
import { ApiService, ReqRes } from './api.service'
|
||||||
import { ApiAppInstalledFull, ApiAppInstalledPreview, ApiServer, Unit as EmptyResponse, Unit } from './api-types'
|
import { ApiAppInstalledFull, ApiAppInstalledPreview, ApiServer, Unit as EmptyResponse, Unit, V1Status } from './api-types'
|
||||||
import { AppMetrics, AppMetricsVersioned, parseMetricsPermissive } from 'src/app/util/metrics.util'
|
import { AppMetrics, AppMetricsVersioned, parseMetricsPermissive } from 'src/app/util/metrics.util'
|
||||||
import { mockApiAppAvailableFull, mockApiAppAvailableVersionInfo, mockApiAppInstalledFull, mockAppDependentBreakages, toInstalledPreview } from './mock-app-fixures'
|
import { mockApiAppAvailableFull, mockApiAppAvailableVersionInfo, mockApiAppInstalledFull, mockAppDependentBreakages, toInstalledPreview } from './mock-app-fixures'
|
||||||
import { ConfigService } from '../config.service'
|
import { ConfigService } from '../config.service'
|
||||||
@@ -273,6 +273,13 @@ export class MockApiService extends ApiService {
|
|||||||
return mockRefreshLAN()
|
return mockRefreshLAN()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkV1Status (): Promise<V1Status> {
|
||||||
|
return {
|
||||||
|
status: 'instructions',
|
||||||
|
version: '1.0.0',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private hasUI (app: ApiAppInstalledPreview): boolean {
|
private hasUI (app: ApiAppInstalledPreview): boolean {
|
||||||
return app.lanUi || app.torUi
|
return app.lanUi || app.torUi
|
||||||
}
|
}
|
||||||
@@ -355,7 +362,7 @@ async function mockGetServerLogs (): Promise<ReqRes.GetServerLogsRes> {
|
|||||||
|
|
||||||
async function mockGetAppMetrics (): Promise<ReqRes.GetAppMetricsRes> {
|
async function mockGetAppMetrics (): Promise<ReqRes.GetAppMetricsRes> {
|
||||||
await pauseFor(1000)
|
await pauseFor(1000)
|
||||||
return mockApiAppMetricsV1
|
return mockApiAppMetricsV1 as ReqRes.GetAppMetricsRes
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mockGetAvailableAppVersionInfo (): Promise<ReqRes.GetAppAvailableVersionInfoRes> {
|
async function mockGetAvailableAppVersionInfo (): Promise<ReqRes.GetAppAvailableVersionInfoRes> {
|
||||||
@@ -492,8 +499,8 @@ const mockApiNotifications: ReqRes.GetNotificationsRes = [
|
|||||||
const mockApiServer: () => ReqRes.GetServerRes = () => ({
|
const mockApiServer: () => ReqRes.GetServerRes = () => ({
|
||||||
serverId: 'start9-mockxyzab',
|
serverId: 'start9-mockxyzab',
|
||||||
name: 'Embassy:12345678',
|
name: 'Embassy:12345678',
|
||||||
versionInstalled: '0.2.10',
|
versionInstalled: '0.2.13',
|
||||||
versionLatest: '0.2.11',
|
versionLatest: '0.2.13',
|
||||||
status: ServerStatus.RUNNING,
|
status: ServerStatus.RUNNING,
|
||||||
alternativeRegistryUrl: 'beta-registry.start9labs.com',
|
alternativeRegistryUrl: 'beta-registry.start9labs.com',
|
||||||
welcomeAck: true,
|
welcomeAck: true,
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export const bitcoinI: ApiAppInstalledFull = {
|
|||||||
versionInstalled: '0.18.1',
|
versionInstalled: '0.18.1',
|
||||||
lanAddress: undefined,
|
lanAddress: undefined,
|
||||||
title: 'Bitcoin Core',
|
title: 'Bitcoin Core',
|
||||||
|
licenseName: 'MIT',
|
||||||
|
licenseLink: 'https://github.com/bitcoin/bitcoin/blob/master/COPYING',
|
||||||
torAddress: '4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpbi7utijcltosqemad.onion',
|
torAddress: '4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpbi7utijcltosqemad.onion',
|
||||||
startAlert: 'Bitcoind could take a loooooong time to start. Please be patient.',
|
startAlert: 'Bitcoind could take a loooooong time to start. Please be patient.',
|
||||||
status: AppStatus.STOPPED,
|
status: AppStatus.STOPPED,
|
||||||
@@ -66,6 +68,7 @@ export const bitcoinI: ApiAppInstalledFull = {
|
|||||||
restoreAlert: 'if you restore this app horrible things will happen to the people you love.',
|
restoreAlert: 'if you restore this app horrible things will happen to the people you love.',
|
||||||
actions: [
|
actions: [
|
||||||
{ id: 'sync-chain', name: 'Sync Chain', description: 'this will sync with the chain like from Avatar', allowedStatuses: [ AppStatus.RUNNING, AppStatus.RUNNING, AppStatus.RUNNING, AppStatus.RUNNING ]},
|
{ id: 'sync-chain', name: 'Sync Chain', description: 'this will sync with the chain like from Avatar', allowedStatuses: [ AppStatus.RUNNING, AppStatus.RUNNING, AppStatus.RUNNING, AppStatus.RUNNING ]},
|
||||||
|
{ id: 'single-status-action', name: 'Single Status Action', description: 'This action has only one allowed status', allowedStatuses: [ AppStatus.RUNNING ]},
|
||||||
{ id: 'off-sync-chain', name: 'Off Sync Chain', description: 'this will off sync with the chain like from Avatar', allowedStatuses: [ AppStatus.STOPPED ]}
|
{ id: 'off-sync-chain', name: 'Off Sync Chain', description: 'this will off sync with the chain like from Avatar', allowedStatuses: [ AppStatus.STOPPED ]}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -146,6 +149,8 @@ export const bitcoinA: AppAvailableFull = {
|
|||||||
id: 'bitcoind',
|
id: 'bitcoind',
|
||||||
versionLatest: '0.19.1.1',
|
versionLatest: '0.19.1.1',
|
||||||
versionInstalled: '0.19.0',
|
versionInstalled: '0.19.0',
|
||||||
|
licenseName: 'MIT',
|
||||||
|
licenseLink: 'https://github.com/bitcoin/bitcoin/blob/master/COPYING',
|
||||||
status: AppStatus.UNKNOWN,
|
status: AppStatus.UNKNOWN,
|
||||||
title: 'Bitcoin Core',
|
title: 'Bitcoin Core',
|
||||||
descriptionShort: 'Bitcoin is an innovative payment network and new kind of money.',
|
descriptionShort: 'Bitcoin is an innovative payment network and new kind of money.',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { WizardBaker } from '../components/install-wizard/prebaked-wizards'
|
|||||||
import { OSWelcomePage } from '../modals/os-welcome/os-welcome.page'
|
import { OSWelcomePage } from '../modals/os-welcome/os-welcome.page'
|
||||||
import { S9Server } from '../models/server-model'
|
import { S9Server } from '../models/server-model'
|
||||||
import { displayEmver } from '../pipes/emver.pipe'
|
import { displayEmver } from '../pipes/emver.pipe'
|
||||||
|
import { V1Status } from './api/api-types'
|
||||||
import { ApiService, ReqRes } from './api/api.service'
|
import { ApiService, ReqRes } from './api/api.service'
|
||||||
import { ConfigService } from './config.service'
|
import { ConfigService } from './config.service'
|
||||||
import { Emver } from './emver.service'
|
import { Emver } from './emver.service'
|
||||||
@@ -36,6 +37,13 @@ export class StartupAlertsNotifier {
|
|||||||
display: vl => this.displayOsUpdateCheck(vl),
|
display: vl => this.displayOsUpdateCheck(vl),
|
||||||
hasRun: this.config.skipStartupAlerts,
|
hasRun: this.config.skipStartupAlerts,
|
||||||
}
|
}
|
||||||
|
const v1StatusUpdate: Check<V1Status> = {
|
||||||
|
name: 'v1Status',
|
||||||
|
shouldRun: s => this.shouldRunOsUpdateCheck(s),
|
||||||
|
check: () => this.v1StatusCheck(),
|
||||||
|
display: s => this.displayV1Check(s),
|
||||||
|
hasRun: this.config.skipStartupAlerts,
|
||||||
|
}
|
||||||
const apps: Check<boolean> = {
|
const apps: Check<boolean> = {
|
||||||
name: 'apps',
|
name: 'apps',
|
||||||
shouldRun: s => this.shouldRunAppsCheck(s),
|
shouldRun: s => this.shouldRunAppsCheck(s),
|
||||||
@@ -43,7 +51,7 @@ export class StartupAlertsNotifier {
|
|||||||
display: () => this.displayAppsCheck(),
|
display: () => this.displayAppsCheck(),
|
||||||
hasRun: this.config.skipStartupAlerts,
|
hasRun: this.config.skipStartupAlerts,
|
||||||
}
|
}
|
||||||
this.checks = [welcome, osUpdate, apps]
|
this.checks = [welcome, osUpdate, v1StatusUpdate, apps]
|
||||||
}
|
}
|
||||||
|
|
||||||
// This takes our three checks and filters down to those that should run.
|
// This takes our three checks and filters down to those that should run.
|
||||||
@@ -85,6 +93,10 @@ export class StartupAlertsNotifier {
|
|||||||
return server.autoCheckUpdates
|
return server.autoCheckUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async v1StatusCheck (): Promise<V1Status> {
|
||||||
|
return this.apiService.checkV1Status()
|
||||||
|
}
|
||||||
|
|
||||||
private async osUpdateCheck (s: Readonly<S9Server>): Promise<ReqRes.GetVersionLatestRes | undefined> {
|
private async osUpdateCheck (s: Readonly<S9Server>): Promise<ReqRes.GetVersionLatestRes | undefined> {
|
||||||
const res = await this.apiService.getVersionLatest()
|
const res = await this.apiService.getVersionLatest()
|
||||||
return this.osUpdateService.updateIsAvailable(s.versionInstalled, res) ? res : undefined
|
return this.osUpdateService.updateIsAvailable(s.versionInstalled, res) ? res : undefined
|
||||||
@@ -131,6 +143,33 @@ export class StartupAlertsNotifier {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async displayV1Check (s: V1Status): Promise<boolean> {
|
||||||
|
return new Promise(async resolve => {
|
||||||
|
if (s.status !== 'available') return resolve(true)
|
||||||
|
const alert = await this.alertCtrl.create({
|
||||||
|
backdropDismiss: true,
|
||||||
|
header: `EmbassyOS ${s.version} Now Available!`,
|
||||||
|
message: `Version ${s.version} introduces SSD support and a whole lot more.`,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Cancel',
|
||||||
|
role: 'cancel',
|
||||||
|
handler: () => resolve(true),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'View Instructions',
|
||||||
|
handler: () => {
|
||||||
|
window.open(`https://start9.com/eos-${s.version}`, '_blank')
|
||||||
|
resolve(false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
await alert.present()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private async displayAppsCheck (): Promise<boolean> {
|
private async displayAppsCheck (): Promise<boolean> {
|
||||||
return new Promise(async resolve => {
|
return new Promise(async resolve => {
|
||||||
const alert = await this.alertCtrl.create({
|
const alert = await this.alertCtrl.create({
|
||||||
|
|||||||
Reference in New Issue
Block a user