Compare commits

...

169 Commits

Author SHA1 Message Date
Keagan McClelland
183f91859a fixes apt-update issue 100 where Suites changed, kills update sound t… (#422)
* fixes apt-update issue 100 where Suites changed, kills update sound thread, sets updating to false if synchronization fails

* cleans up code

* appmgr changes

* ui changes

* updates required appmgr in agent

* actually upgrade current version in appmgr
2021-08-23 14:30:08 -06:00
Keagan McClelland
18a069e6fd Release/0.2.15 (#411)
* remove all apt-get calls from check phase of startup

* Fix "n is undefined" config spec change bug (#392)

Fixes the issue where an old config with a new config spec sometimes renders the config inaccessible, with the error `n is undefined`

* updates version numbers

* version change for appmgr

* updates version numbers and welcome message

* adds migration for 0.2.15 to the agent

* actually add 0.2.15 migration to appmgr

Co-authored-by: Chris Guida <chrisguida@users.noreply.github.com>
2021-08-18 15:28:32 -06:00
Keagan McClelland
46643cb3a4 fixes post process build failure step (#381) 2021-07-27 08:00:09 -06:00
Keagan McClelland
70397eaf10 fix agent code review 2021-07-20 16:54:50 -06:00
Keagan McClelland
5caf6b3d90 fix build issues 2021-07-20 16:54:50 -06:00
Keagan McClelland
5e3e330bb3 change release notes 2021-07-20 16:54:50 -06:00
Keagan McClelland
7989f12511 alter semantics of tor update 2021-07-20 16:54:50 -06:00
Keagan McClelland
0ac0da0ebf preps 0.2.14 messaging and version bumps 2021-07-20 16:54:50 -06:00
Keagan McClelland
6334d79c01 updates appmgr to 0.2.14 ceremonial 2021-07-20 16:54:50 -06:00
Keagan McClelland
943c898a3e update appmgr dependency 2021-07-20 16:54:50 -06:00
Keagan McClelland
39f85c7199 agent 0.2.14 2021-07-20 16:54:50 -06:00
kn0wmad
57e9a97d44 Build guide edit 2021-06-29 16:03:13 -06:00
Lucy C
d12a7f8931 fix cabal version and update welcome copy (#327)
* fix cabal version and update welcome copy

* fixes autogen'ed cabal file

Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>
2021-05-21 13:01:18 -06:00
Matt Hill
8f9111ce3d Integration/0.2.13 (#324)
* updates to 8.10.4, adjusts dependencies, adds license info feature

* add toJSON

* add licesne info to services

* remove mocks

* adds license info to available show

* prepare upgrade messaging

* better welcome message

* update backend versioning to 0.2.13

* add version migration file

* update ui build scripts

* update eos image

* update eos image with embassy

* add migration files

* update welcome page

* explicity add migration files

Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>
Co-authored-by: Lucy Cifferello <12953208+elvece@users.noreply.github.com>
2021-05-19 11:46:37 -06:00
Lucy Cifferello
7509c3a91e update build guide instructions for ghc 8.10.4 2021-05-18 15:34:40 -06:00
kn0wmad
3126d6138e Update README.md (#318) 2021-05-05 10:01:31 -06:00
Julian Ospald
5d4837d942 Add proper cabal support 2021-04-26 13:50:01 -06:00
@RandyMcMillan
660c0c5ff4 Update BuildGuide.md 2021-04-23 11:28:31 -06:00
Mariusz Kogen
4c6c2768b3 👷 Cleaner look 2021-04-07 15:31:34 -06:00
Chris Guida
6ddf7ce40b Update README.md 2021-04-02 17:21:26 -06:00
Aiden McClelland
4f16d82294 Update README.md 2021-04-02 13:49:03 -06:00
kn0wmad
7845044a3c Added screenshots to README 2021-04-02 13:49:03 -06:00
kn0wmad
20f91b10db Added screenshots to README 2021-04-02 13:49:03 -06:00
kn0wmad
ec2b707353 README edit 2021-04-02 13:49:03 -06:00
kn0wmad
e609d3af1e README edit 2021-04-02 13:49:03 -06:00
kn0wmad
5b5495cd51 Added Instructions to appmgr README 2021-04-02 13:49:03 -06:00
kn0wmad
8ba23f05a4 Added Instructions to appmgr README 2021-04-02 13:49:03 -06:00
Aiden McClelland
a4b1529dc4 Update README.md 2021-04-02 13:44:09 -06:00
Chris Guida
da81aec9cc add ui mock action with only one allowed status 2021-04-02 10:55:52 -06:00
Aiden McClelland
c440f637f3 ui: fix allowed statuses message 2021-04-02 10:55:52 -06:00
Matt Hill
9036c3ffed update welcome message 2021-04-02 10:55:52 -06:00
Keagan McClelland
98a242229a package lock 2021-04-02 10:55:52 -06:00
Keagan McClelland
74659d717a ui ceremonial changes for 0.2.12 2021-04-02 10:55:52 -06:00
Keagan McClelland
e6dbbf125c pin appmgr 0.2.12 2021-04-02 10:55:52 -06:00
Keagan McClelland
80f509a634 0.2.12 ceremonial release 2021-04-02 10:55:52 -06:00
Aiden McClelland
5d6a175585 ui: bump version 2021-04-02 10:55:52 -06:00
Aiden McClelland
6ef46ae309 bump version 2021-04-02 10:55:52 -06:00
Aiden McClelland
13c94241c2 retain x-forwarded-proto 2021-04-02 10:55:52 -06:00
Chris Guida
56041fd503 disable buffering in nginx 2021-03-30 20:14:02 -06:00
Mariusz Kogen
f52bb54a2f 🌠 Image display fix (#274)
Added community matrix and mastodon shields
2021-03-30 08:45:10 -06:00
Mariusz Kogen
7f9f942eb1 🛠️ Adding BuildGuide branch for the ease of use (#252)
* Create BuildGuide.md

* bug fixes

* Update BuildGuide.md

* Update make_image.sh

* Update BuildGuide.md

* Update make_image.sh

additional improvements and "done" message added. Thanks @k0gen!

* Update BuildGuide.md

Added intro notes and made minor adjustments to the guide.

* Update setup.sh

Required adjustments to prevent reboot when following BuildGuide.md

* Update BuildGuide.md

Improvements to final setup steps

* bug fix

additional improvements and "done" message added. Thanks @k0gen!

* Update Makefile

Changes to facilitate building process when using the BuildGuide

* Update BuildGuide.md

Avoiding manual changes to Makefile and cleaning up step 7

* Update BuildGuide.md

Switching from sftp to cp for one line command simplification

* Update BuildGuide.md

Simplified method of transferring .img to desktop. Thanks @k0gen!

* Update BuildGuide.md

update to latest openssl https://www.openssl.org/news/openssl-1.1.1-notes.html

* Update BuildGuide.md

Simplified step 6 and added new required dependency

* Update BuildGuide.md

Added hint on how to check `agent` log

* Update setup.sh

Added missing dependency

* Update BuildGuide.md

Simplified step 6

* Simplifying Rust installation

One line install, reboot is no longer needed.

* make_image.sh +x 

Make it executable before running

* Step no longer needed

chmod +x done by Makefile

* Update BuildGuide.md

Added dependency for Rust setup

* Adding BuildGuide branch for the ease of use

* Forgot about the guide file :)

* Update BuildGuide.md

apt -y by default and some environment add-ons

Co-authored-by: Tommy Smith <63304263+t0mmysm1th@users.noreply.github.com>
2021-03-29 14:21:01 -06:00
Matt Hill
1ca7a699c1 Update use-mocks.json 2021-03-25 10:49:26 -06:00
Mariusz Kogen
481accc9e6 Update CONTRIBUTING.md
Fixing bug tracker broken link, adding active matrix links
2021-03-23 12:32:37 -06:00
Lucy C
11b007a31d Fix/integration/0.2.11 (#265)
* backports tor security fix to 0.2.10, adds functionality to allow for ssh key management during an update (#263)

* actually upgrade to 0.3.5.14-1

* update lan services on backup restore

* reload nginx, update welcome message, move reset lan to handler

* moves lan refresh after backup restore to asynchronous part of restore

* fix certificate generation

* match guards

Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>
2021-03-19 16:50:52 -06:00
Lucy C
5b8f27e53e Appmgr/fix/restart dep on install (#262)
* appmgr: fix: restart dep on install

* appmgr: fix: bind before restart

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2021-03-18 00:46:29 -06:00
Aiden McClelland
9f4523676f appmgr: fix: restart dep on install (#261) 2021-03-17 22:08:12 -06:00
Lucy C
bc5163d800 Appmgr/debug/uninstall (#260)
* with context debuggging

* appmgr: fix errors

* appmgr: add more logging

* appmgr: more logs

* appmgr: make unbind more robust

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2021-03-17 18:55:51 -06:00
Lucy C
9b7fe03c19 copy updates for 0.2.11 (#248)
* copy updates for 0.2.11

* fix moderate npm vulns

* fix mocks and welcome page layout
2021-03-17 12:30:41 -06:00
Lucy Cifferello
9a2aaa08b8 ignore ds_store 2021-03-17 12:30:41 -06:00
Lucy Cifferello
8c87e6653c fix import 2021-03-17 12:30:41 -06:00
Chris Guida
1c3b16e870 Appmgr/bugfix/nginx file too large migrations (#258)
* Nginx: allow infinite body sizes

* add nginx refresh to migration script
2021-03-17 12:30:41 -06:00
Lucy Cifferello
276085f084 fix mocks file 2021-03-17 12:30:41 -06:00
Lucy Cifferello
52fc992090 update welcome page release notes 2021-03-17 12:30:41 -06:00
Chris Guida
af46a375a9 Nginx: allow infinite body sizes (#256) 2021-03-17 12:30:41 -06:00
Aiden McClelland
74a559eade ui: add elements to union page 2021-03-17 12:30:41 -06:00
Aiden McClelland
f12d97122a ui: edited dot on union enum 2021-03-17 12:30:41 -06:00
Aiden McClelland
ba9b3519de ui: fix isEdited for unions 2021-03-17 12:30:41 -06:00
Aiden McClelland
43e89df652 appmgr: handle optional dep unmounts on uninstall 2021-03-17 12:30:41 -06:00
Keagan McClelland
7bdc109bd4 retry once on exit 6 for list 2021-03-16 11:20:33 -06:00
Lucy C
ac5dec476d copy updates for 0.2.11 (#248)
* copy updates for 0.2.11

* fix moderate npm vulns

* fix mocks and welcome page layout
2021-03-12 15:42:18 -07:00
Keagan McClelland
1f56be3cbf 0.2.11 bump 2021-03-12 15:42:18 -07:00
Keagan McClelland
ed46ddbf44 removes default welcome to nginx page (#247) 2021-03-12 15:42:18 -07:00
Keagan McClelland
2973c316a8 catches if either file doesn't exist and runs the sync if so (#245) 2021-03-12 15:42:18 -07:00
Aiden McClelland
7e7a9dc140 appmgr: version bump 2021-03-12 15:42:18 -07:00
Aiden McClelland
b95686282d ui: fix union error message 2021-03-12 15:42:18 -07:00
Aiden McClelland
09f858d28d appmgr: only mount binds if installed
also improve build scripts
2021-03-12 15:42:18 -07:00
Keagan McClelland
424afb3d1c adds actions copy (#240)
* adds actions copy

* punctuation

* punctuation

* removes superfluous language
2021-03-08 17:51:27 -07:00
Matt Hill
a056f6d318 fix instructions link (#239) 2021-03-08 17:25:24 -07:00
Keagan McClelland
5339b23ea6 0.2.10 version bump stuff (#238)
* agent -> 0.2.10

* appmgr -> 0.2.10

* updates to 0.2.10 for the UI

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2021-03-08 16:38:24 -07:00
Aiden McClelland
bd61510c24 Hotfix/rewrite certs (#237)
* appmgr: rewrite full cert if missing

* rewrite if key missing
2021-03-08 16:25:05 -07:00
Keagan McClelland
91557c39e5 drops uniqueness requirement (#236) 2021-03-08 16:21:21 -07:00
Keagan McClelland
894fa21002 Agent/sync/tor upgrade (#234)
* adds tor upgrade sync

* use the right version

* I hate all of you
2021-03-06 19:11:05 -07:00
Matt Hill
d611c69b0c add messaging for missing LAN 2021-03-06 11:29:20 -07:00
Keagan McClelland
d430986403 adds availability to installed full 2021-03-05 17:24:36 -07:00
Aiden McClelland
c8aafbdbc9 appmgr: fix lan service deserialization 2021-03-05 16:33:53 -07:00
Matt Hill
2e3e1401f5 Update ui/src/app/services/api/mock-app-fixures.ts
Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>
2021-03-05 15:42:31 -07:00
Matt Hill
deb0b1e561 rework LAN display and service launchability 2021-03-05 15:42:31 -07:00
Aiden McClelland
daf701a76c appmgr: disable zeroconf if no lan 2021-03-05 15:39:43 -07:00
Keagan McClelland
43035e7271 adds 80 2021-03-05 14:22:12 -07:00
Keagan McClelland
e37db33d62 fixes custom parse 2021-03-05 14:22:12 -07:00
Keagan McClelland
adab9e7fca optional lan 2021-03-05 14:22:12 -07:00
Keagan McClelland
a21bd91460 actually install libavahi 2021-03-04 14:40:35 -07:00
Aiden McClelland
acc2722586 appmgr: add proxy headers for lan services 2021-03-04 12:18:54 -07:00
Keagan McClelland
d8d6541b11 fix mocks 2021-03-03 16:03:04 -07:00
Keagan McClelland
1a7d40afa9 render correctly 2021-03-03 16:03:04 -07:00
Keagan McClelland
b152a93dd8 should fix the logs api 2021-03-03 16:03:04 -07:00
Keagan McClelland
ae90b70348 exposes start alerts 2021-03-03 16:03:04 -07:00
Aiden McClelland
21f982e9a6 appmgr: start-alert 2021-03-03 16:03:04 -07:00
Aiden McClelland
4100d4ca97 ui: fallback to json 2021-03-03 11:13:31 -07:00
Aiden McClelland
f5ae93c999 ui: better error message 2021-03-03 11:13:31 -07:00
Aiden McClelland
2f5ad4d82b stringify error 2021-03-03 11:13:31 -07:00
Keagan McClelland
6e2a332bcd adds sync for libavahi-client3 2021-03-03 10:29:09 -07:00
Aiden McClelland
4a2e496e8a ui: better error handing and messaging 2021-03-03 10:29:09 -07:00
Matt Hill
e69a936fb8 show newlines in action res and longer timeout 2021-03-03 10:29:09 -07:00
Aiden McClelland
d9894d4082 appmgr: pin yajrc to c2952a4a 2021-03-03 10:29:09 -07:00
Aiden McClelland
6b3fa54551 appmgr: fix deleted certs 2021-03-03 10:29:09 -07:00
Aiden McClelland
9f47a34b11 appmgr: logs for adding certs 2021-03-03 10:29:09 -07:00
Matt Hill
531dec936d markdown support for prebaked wizard component notes 2021-03-03 10:29:09 -07:00
Aiden McClelland
1d7684f4d4 appmgr: fsync fullchain 2021-03-03 10:29:09 -07:00
Aiden McClelland
cfacbcabd3 appmgr: make down for 0.2.9 more resilient 2021-03-03 10:29:09 -07:00
Keagan McClelland
4fcdf5f832 fixes issue with action 2021-03-03 10:29:09 -07:00
Keagan McClelland
2189c5643d fixes warning rendering 2021-03-03 10:29:09 -07:00
Aiden McClelland
aada5755de appmgr: sync lan services after tor reload 2021-03-03 10:29:09 -07:00
Keagan McClelland
60d31163c5 remove redundant imports 2021-03-03 10:29:09 -07:00
Aiden McClelland
fd6a1897c8 appmgr: fix portable 2021-03-03 10:29:09 -07:00
Aiden McClelland
62e0f742ba appmgr: create app metadata dir explicitly 2021-03-03 10:29:09 -07:00
Keagan McClelland
c42ff81a38 avoid unnecessary appmgr startup invocation 2021-03-03 10:29:09 -07:00
Keagan McClelland
cc49a73954 fix avahi-daemon edge case 2021-03-03 10:29:09 -07:00
Keagan McClelland
29a4506a40 fixes edge case where upgrade to 0.2.9 would not correctly set up lan 2021-03-03 10:29:09 -07:00
Keagan McClelland
efa60bf4ab fixes clap shit 2021-03-03 10:29:09 -07:00
Matt Hill
1c2fd192df dont block OS update for absent release notes 2021-03-03 10:29:09 -07:00
Matt Hill
0a9349bbc1 change to rocket 2021-03-03 10:29:09 -07:00
Keagan McClelland
653961da64 batches all lan addresses together
removes dbg

fixes clap docs

use actual log

removes service level enabling and disabling of lan

adds reset endpoint

reset lan on install/uninstall
2021-03-03 10:29:09 -07:00
Matt Hill
6585d91816 refresh LAN and more 2021-03-03 10:29:09 -07:00
Aiden McClelland
3e3097945f appmgr: actions override entrypoint 2021-03-03 10:29:09 -07:00
Aiden McClelland
c0f5f09767 appmgr: put linux syscall behind flag 2021-03-03 10:29:09 -07:00
Aiden McClelland
1c8889a60c appmgr: feature flag for avahi 2021-03-03 10:29:09 -07:00
Aiden McClelland
218bae3b46 appmgr: fix CA paths 2021-03-03 10:29:09 -07:00
Matt Hill
92c297648c remove lan stuff 2021-03-03 10:29:09 -07:00
Matt Hill
68eccdb63c fix launching UI from list page 2021-03-03 10:29:09 -07:00
Aiden McClelland
ee1c66d0c2 appmgr: bugfix: use fullchain cert 2021-03-03 10:29:09 -07:00
Keagan McClelland
c52f75c9e3 adds lanEnabled, and unconditionally returns lan address 2021-03-03 10:29:09 -07:00
Matt Hill
b46c75e391 optional properties 2021-03-03 10:29:09 -07:00
Keagan McClelland
7fb8f88c8d pins yajrc to git 2021-03-03 10:29:09 -07:00
Keagan McClelland
c83baec363 Appmgr/feature/lan (#209)
* WIP: Lan with blocking

* stuff

* appmgr: non-ui lan services

* dbus linker errors

* appmgr: allocate on stack

* dns resolves finally

* cleanup

* appmgr: generate ssl if missing

* appmgr: remove -p for purge

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2021-03-03 10:29:09 -07:00
Aiden McClelland
882cfde5f3 appmgr: add warning to actions 2021-03-03 10:29:09 -07:00
Aiden McClelland
53720130b3 appmgr: add support for actions 2021-03-03 10:29:09 -07:00
Matt Hill
7c321bbf6b better conditional for go to service 2021-03-03 10:29:09 -07:00
kn0wmad
bd060670e4 Update lan.page.ts
Fixes broken links
2021-03-03 10:29:09 -07:00
adamgoth
7ff538a526 ui: remove redundant conditional 2021-03-03 10:29:09 -07:00
adamgoth
3c74f3d46e ui: add go to service button on marketplace listing 2021-03-03 10:29:09 -07:00
Keagan McClelland
54ae7f82d6 adds action listings to AIS response 2021-03-03 10:29:09 -07:00
Keagan McClelland
39867478d0 fixed conflicts 2021-03-03 10:29:09 -07:00
Keagan McClelland
8e2642a741 actually render json 2021-03-03 10:29:09 -07:00
Keagan McClelland
a4f7d53a6b finishes lan support for the agent 2021-03-03 10:29:09 -07:00
Aaron Greenspan
397236c68e ui: concatObValues + comments 2021-03-03 10:29:09 -07:00
Matt Hill
8ce43d808e start alerts 2021-03-03 10:29:09 -07:00
Aaron Greenspan
e1200c2991 ui: fix distinctUntilChanged() 2021-03-03 10:29:09 -07:00
Aaron Greenspan
0937c81e46 ui: mocks off 2021-03-03 10:29:09 -07:00
Aaron Greenspan
02ab63da81 UI/feature/actions (#195)
* ui: actions page

* rework actions page

* add warning to Actions

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
2021-03-03 10:29:09 -07:00
Aaron Greenspan
5cf7d1ff88 UI/feature/enable disable lan (#192)
* ui: skip startup notifications in mocks

* ui: enable-disable lan toggle in ui

* ui: remove this.lanAddress for this.app.lanAddress
2021-03-03 10:29:09 -07:00
Aaron Greenspan
a20970fa17 ui: conditionally render lan if ui 2021-03-03 10:29:09 -07:00
Aaron Greenspan
30dd62285b Update agent/src/Handler/V0.hs
Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>
2021-03-03 10:29:09 -07:00
Aaron Greenspan
3065323e79 agent: adds lan address to server specs 2021-03-03 10:29:09 -07:00
Aaron Greenspan
e1a6a3d9ed ui: fix lan launchable 2021-03-03 10:29:09 -07:00
Aiden McClelland
c0e08df221 appmgr: ignore AlreadyExists error 2021-03-03 10:29:09 -07:00
Aiden McClelland
108213f920 trim hostname 2021-03-03 10:29:09 -07:00
Keagan McClelland
a8e229821f pin appmgr 0.2.9 2021-03-03 10:29:09 -07:00
Aaron Greenspan
a6b7d657a0 ui: 0.2.8 ~> 0.2.9 2021-03-03 10:29:09 -07:00
Keagan McClelland
77b8d0b2a0 updates agent 0.2.9 metadata 2021-03-03 10:29:09 -07:00
Aiden McClelland
9503f754ad fix stupid fucking comment that keagan won't let go 2021-03-03 10:29:09 -07:00
adamgoth
540868220d ui: use ionic positioning instead of custom CSS 2021-03-03 10:29:09 -07:00
adamgoth
dd8037fda1 ui: copy button for tor address spec 2021-03-03 10:29:09 -07:00
Aiden McClelland
6f09738b49 appmgr: write nginx conf when writing tor conf (#177)
* appmgr: write nginx conf when writing tor conf

* appmgr: fix hardcoded certs

* appmgr: add down for 0.2.9
2021-03-03 10:29:09 -07:00
Aaron Greenspan
808fff4187 ui: remove dead imports, fix mock type 2021-03-03 10:29:09 -07:00
Aaron Greenspan
a9735fd777 ui: adds agent logs page in server show 2021-03-03 10:29:09 -07:00
Aaron Greenspan
327c79350e ui: os-welcome 029 2021-03-03 10:29:09 -07:00
Keagan McClelland
44def3be85 release notes plumbing 2021-03-03 10:29:09 -07:00
Keagan McClelland
18df87b8f5 adds log fetch feature to agent 2021-03-03 10:29:09 -07:00
Matt Hill
97a85d6e01 0.2.9 welcome message 2021-03-03 10:29:09 -07:00
Aiden McClelland
3d4930acb4 don't delete lock file 2021-03-03 10:29:09 -07:00
Aaron Greenspan
58468dd53f ui: remove action button on wizard while loading 2021-03-03 10:29:09 -07:00
Matt Hill
50a2be243a implement base ui for LAN services 2021-03-03 10:29:09 -07:00
Aaron Greenspan
0d7b087665 ui: remove release notes modal 2021-03-03 10:29:09 -07:00
Aaron Greenspan
0e87cce8de ui: moves release notes to AAS 2021-03-03 10:29:09 -07:00
Aaron Greenspan
537f2d91b8 ui: rename dev-notes to notes 2021-03-03 10:29:09 -07:00
Aaron Greenspan
79604182c8 ui: add embassy os release notes 2021-03-03 10:29:09 -07:00
Aaron Greenspan
68faa17ab6 ui: fix emver check in startup alerts notifier 2021-03-03 10:29:09 -07:00
Lucy Cifferello
13a6d7f0c7 add local file for better image resolution 2021-02-26 16:09:09 -07:00
182 changed files with 7607 additions and 2632 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.DS_Store
/*.img /*.img
/buster.zip /buster.zip
/product_key /product_key

179
BuildGuide.md Normal file
View File

@@ -0,0 +1,179 @@
##### 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/
```
1. Re-make the agent
```
make agent
```
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 :(

View File

@@ -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>

View File

@@ -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)

View File

@@ -1,28 +1,30 @@
# EmbassyOS # EmbassyOS
[![Version](https://img.shields.io/github/v/tag/Start9Labs/embassy-os?color=success)](https://github.com/Start9Labs/embassy-os/releases) [![Version](https://img.shields.io/github/v/tag/Start9Labs/embassy-os?color=success)](https://github.com/Start9Labs/embassy-os/releases)
[![community](https://img.shields.io/badge/community-matrix-yellow)](https://matrix.to/#/#community:matrix.start9labs.com)
[![community](https://img.shields.io/badge/community-telegram-informational)](https://t.me/start9_labs) [![community](https://img.shields.io/badge/community-telegram-informational)](https://t.me/start9_labs)
[![support](https://img.shields.io/badge/support-docs-important)](https://docs.start9labs.com) [![support](https://img.shields.io/badge/support-docs-important)](https://docs.start9labs.com)
[![developer](https://img.shields.io/badge/developer-matrix-blueviolet)](https://matrix.to/#/#community-dev:matrix.start9labs.com) [![developer](https://img.shields.io/badge/developer-matrix-blueviolet)](https://matrix.to/#/#community-dev:matrix.start9labs.com)
[![website](https://img.shields.io/website?down_color=lightgrey&down_message=offline&up_color=success&up_message=online&url=https%3A%2F%2Fstart9labs.com)](https://start9labs.com) [![website](https://img.shields.io/website?down_color=lightgrey&down_message=offline&up_color=green&up_message=online&url=https%3A%2F%2Fstart9labs.com)](https://start9labs.com)
[![mastodon](https://img.shields.io/mastodon/follow/000000001?domain=https%3A%2F%2Fmastodon.start9labs.com&label=Follow&style=social)](http://mastodon.start9labs.com)
[![twitter](https://img.shields.io/twitter/follow/start9labs?label=Follow)](https://twitter.com/start9labs) [![twitter](https://img.shields.io/twitter/follow/start9labs?label=Follow)](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.
![EmbassyOS image](https://sesoodan.sirv.com/eos.png?w=600) <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
View File

@@ -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_*

View File

@@ -0,0 +1,521 @@
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.16
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.13::0.2.14
./migrations/0.2.14::0.2.15
./migrations/0.2.15::0.2.16
./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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,7 @@
/v0/specs SpecsR GET /v0/specs SpecsR GET
/v0/metrics MetricsR GET /v0/metrics MetricsR GET
/v0/logs LogsR GET
/v0/sshKeys SshKeysR GET POST /v0/sshKeys SshKeysR GET POST
/v0/sshKeys/#Text SshKeyByFingerprintR DELETE /v0/sshKeys/#Text SshKeyByFingerprintR DELETE
/v0/password PasswordR PATCH /v0/password PasswordR PATCH
@@ -38,6 +39,9 @@
/v0/apps/#AppId/backup/stop StopBackupR POST /v0/apps/#AppId/backup/stop StopBackupR POST
/v0/apps/#AppId/backup/restore RestoreBackupR POST /v0/apps/#AppId/backup/restore RestoreBackupR POST
/v0/apps/#AppId/autoconfig/#AppId AutoconfigureR POST /v0/apps/#AppId/autoconfig/#AppId AutoconfigureR POST
/v0/apps/#AppId/actions ActionR POST
/v0/network/lan/reset ResetLanR POST
/v0/disks DisksR GET /v0/disks DisksR GET
/v0/disks/eject EjectR POST /v0/disks/eject EjectR POST

View File

@@ -1,9 +1,9 @@
# Values formatted like "_env:YESOD_ENV_VAR_NAME:default_value" can be overridden by the specified environment variable. # Values formatted like "_env:YESOD_ENV_VAR_NAME:default_value" can be overridden by the specified environment variable.
# See https://github.com/yesodweb/yesod/wiki/Configuration#overriding-configuration-values-with-environment-variables # See https://github.com/yesodweb/yesod/wiki/Configuration#overriding-configuration-values-with-environment-variables
static-dir: "_env:YESOD_STATIC_DIR:static" static-dir: "_env:YESOD_STATIC_DIR:static"
host: "_env:YESOD_HOST:*4" # any IPv4 host host: "_env:YESOD_HOST:*4" # any IPv4 host
port: 5959 # NB: The port `yesod devel` uses is distinct from this value. Set the `yesod devel` port from the command line. port: 5959 # NB: The port `yesod devel` uses is distinct from this value. Set the `yesod devel` port from the command line.
ip-from-header: "_env:YESOD_IP_FROM_HEADER:false" ip-from-header: "_env:YESOD_IP_FROM_HEADER:false"
detailed-logging: "_env:DETAILED_LOGGING:false" detailed-logging: "_env:DETAILED_LOGGING:false"
@@ -33,6 +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.8" app-mgr-version-spec: "=0.2.16"
#analytics: UA-YOURCODE #analytics: UA-YOURCODE

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -1,116 +1,117 @@
name: ambassador-agent name: ambassador-agent
version: 0.2.8 version: 0.2.16
default-extensions: default-extensions:
- NoImplicitPrelude - NoImplicitPrelude
- BlockArguments - BlockArguments
- ConstraintKinds - ConstraintKinds
- DataKinds - DataKinds
- DeriveAnyClass - DeriveAnyClass
- DeriveFunctor - DeriveFunctor
- DeriveGeneric - DeriveGeneric
- DerivingStrategies - DerivingStrategies
- EmptyCase - EmptyCase
- FlexibleContexts - FlexibleContexts
- FlexibleInstances - FlexibleInstances
- GADTs - GADTs
- GeneralizedNewtypeDeriving - GeneralizedNewtypeDeriving
- InstanceSigs - InstanceSigs
- KindSignatures - KindSignatures
- LambdaCase - LambdaCase
- MultiParamTypeClasses - MultiParamTypeClasses
- MultiWayIf - MultiWayIf
- NamedFieldPuns - NamedFieldPuns
- NumericUnderscores - NumericUnderscores
- OverloadedStrings - OverloadedStrings
- PolyKinds - PolyKinds
- RankNTypes - RankNTypes
- StandaloneDeriving - StandaloneDeriving
- StandaloneKindSignatures - StandaloneKindSignatures
- TupleSections - TupleSections
- TypeApplications - TypeApplications
- TypeFamilies - TypeFamilies
- TypeOperators - TypeOperators
dependencies: dependencies:
- base >=4.9.1.0 && <5 - base >=4.9.1.0 && <5
- aeson - aeson
- aeson-flatten - aeson-flatten
- attoparsec - attoparsec
- bytestring - bytestring
- casing - casing
- comonad - comonad
- conduit - conduit
- conduit-extra - conduit-extra
- connection - connection
- containers - containers
- cryptonite - cryptonite
- cryptonite-conduit - cryptonite-conduit
- data-default - data-default
- directory - directory
- errors - errors
- exceptions - exceptions
- exinst - exinst
- fast-logger - fast-logger
- file-embed - file-embed
- filelock - filelock
- filepath - filepath
- fused-effects - fused-effects
- fused-effects-th - fused-effects-th
- git-embed - git-embed
- http-api-data - http-api-data
- http-client - http-client
- http-client-tls - http-client-tls
- http-conduit - http-conduit
- http-types - http-types
- interpolate - interpolate
- iso8601-time - iso8601-time
- lens - json-rpc
- lens-aeson - lens
- lifted-async - lens-aeson
- lifted-base - lifted-async
- memory - lifted-base
- mime-types - memory
- monad-control - mime-types
- monad-logger - monad-control
- network - monad-logger
- persistent - network
- persistent-sqlite - persistent
- persistent-template - persistent-sqlite
- process - persistent-template
- process-extras - process
- protolude - process-extras
- resourcet - protolude
- regex-compat # TODO: trim this dep - resourcet
- shell-conduit - regex-compat # TODO: trim this dep
- singletons - shell-conduit
- stm - singletons
- streaming - stm
- streaming-bytestring - streaming
- streaming-conduit - streaming-bytestring
- streaming-utils - streaming-conduit
- tar-conduit - streaming-utils
- template-haskell - tar-conduit
- text >=0.11 && <2.0 - template-haskell
- time - text >=0.11 && <2.0
- transformers - time
- transformers-base - transformers
- typed-process - transformers-base
- unix - typed-process
- unliftio # TODO: trim this dep - unix
- unliftio-core # TODO: trim this dep - unliftio # TODO: trim this dep
- unordered-containers - unliftio-core # TODO: trim this dep
- uuid - unordered-containers
- wai - uuid
- wai-cors - wai
- wai-extra - wai-cors
- warp - wai-extra
- yaml - warp
- yesod - yaml
- yesod-auth - yesod
- yesod-core - yesod-auth
- yesod-form - yesod-core
- yesod-persistent - yesod-form
- yesod-persistent
flags: flags:
library-only: library-only:
@@ -128,56 +129,57 @@ flags:
library: library:
source-dirs: src source-dirs: src
when: when:
- condition: (flag(dev)) || (flag(library-only)) - condition: (flag(dev)) || (flag(library-only))
then: then:
cpp-options: -DDEVELOPMENT cpp-options: -DDEVELOPMENT
ghc-options: ghc-options:
- -Wall - -Wall
- -Wunused-packages - -Wunused-packages
- -fwarn-tabs - -fwarn-tabs
- -O0 - -O0
- -fdefer-typed-holes - -fdefer-typed-holes
else: else:
ghc-options: ghc-options:
- -Wall - -Wall
- -Wunused-packages - -Wunused-packages
- -fwarn-tabs - -fwarn-tabs
- -O2 - -O2
- -fdefer-typed-holes - -fdefer-typed-holes
- condition: (flag(disable-auth)) - condition: (flag(disable-auth))
cpp-options: -DDISABLE_AUTH cpp-options: -DDISABLE_AUTH
tests: tests:
agent-test: agent-test:
source-dirs: test source-dirs: test
main: Main.hs main: Main.hs
ghc-options: ghc-options:
- -Wall - -Wall
- -fdefer-typed-holes - -fdefer-typed-holes
dependencies: dependencies:
- ambassador-agent - ambassador-agent
- hspec >=2.0.0 - hspec >=2.0.0
- hspec-expectations - hspec-expectations
- hedgehog - hedgehog
- yesod-test - yesod-test
- random - random
when: when:
- condition: false - condition: false
other-modules: Paths_ambassador_agent other-modules: Paths_ambassador_agent
executables: executables:
agent: agent:
source-dirs: app source-dirs: app
main: main.hs main: main.hs
ghc-options: ghc-options:
- -Wall - -Wall
- -threaded - -threaded
- -rtsopts - -rtsopts
- -with-rtsopts=-N - -with-rtsopts=-N
- -fdefer-typed-holes - -fdefer-typed-holes
dependencies: dependencies:
- ambassador-agent - ambassador-agent
when: when:
- buildable: false - buildable: false
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/*

View File

@@ -54,21 +54,21 @@ import Yesod.Persist.Core
import Constants import Constants
import qualified Daemon.AppNotifications as AppNotifications import qualified Daemon.AppNotifications as AppNotifications
import Daemon.RefreshProcDev import Daemon.RefreshProcDev
import qualified Daemon.SslRenew as SSLRenew
import Daemon.TorHealth
import Daemon.ZeroConf import Daemon.ZeroConf
import Foundation import Foundation
import Lib.Algebra.State.RegistryUrl import Lib.Algebra.State.RegistryUrl
import Lib.Background
import Lib.Database import Lib.Database
import Lib.External.Metrics.ProcDev import Lib.External.Metrics.ProcDev
import Lib.SelfUpdate import Lib.SelfUpdate
import Lib.Sound import Lib.Sound
import Lib.SystemPaths import Lib.SystemPaths
import Lib.Tor ( newTorManager )
import Lib.WebServer import Lib.WebServer
import Model import Model
import Settings import Settings
import Lib.Background
import qualified Daemon.SslRenew as SSLRenew
import Lib.Tor (newTorManager)
import Daemon.TorHealth
appMain :: IO () appMain :: IO ()
appMain = do appMain = do
@@ -118,6 +118,7 @@ makeFoundation appSettings = do
def <- getDefaultProcDevMetrics def <- getDefaultProcDevMetrics
appProcDevMomentCache <- newIORef (now, mempty, def) appProcDevMomentCache <- newIORef (now, mempty, def)
appLastTorRestart <- newIORef now appLastTorRestart <- newIORef now
appLanThread <- forkIO (sleep 10) >>= newMVar
-- We need a log function to create a connection pool. We need a connection -- We need a log function to create a connection pool. We need a connection
-- pool to create our foundation. And we need our foundation to get a -- pool to create our foundation. And we need our foundation to get a

View File

@@ -18,6 +18,9 @@ import Lib.ProductKey
import Lib.SystemPaths import Lib.SystemPaths
import Settings import Settings
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Control.Carrier.Lift
import Lib.Error
start9AgentServicePrefix :: IsString a => a start9AgentServicePrefix :: IsString a => a
start9AgentServicePrefix = "start9-" start9AgentServicePrefix = "start9-"
@@ -53,4 +56,10 @@ publishAgentToAvahi = do
"_http._tcp" "_http._tcp"
agentPort agentPort
lift Avahi.reload lift Avahi.reload
lift $ threadDelay 10_000_000
tid <- asks appLanThread >>= liftIO . takeMVar
liftIO $ killThread tid
tid' <- liftIO $ forkIO (runM . void . runExceptT @S9Error $ AppMgr2.runAppMgrCliC AppMgr2.lanEnable)
asks appLanThread >>= liftIO . flip putMVar tid'

View File

@@ -75,6 +75,7 @@ data AgentCtx = AgentCtx
, appBackgroundJobs :: TVar JobCache , appBackgroundJobs :: TVar JobCache
, appIconTags :: TVar (HM.HashMap AppId (Digest MD5)) , appIconTags :: TVar (HM.HashMap AppId (Digest MD5))
, appLastTorRestart :: IORef UTCTime , appLastTorRestart :: IORef UTCTime
, appLanThread :: MVar ThreadId
} }
setWebProcessThreadId :: ThreadId -> AgentCtx -> IO () setWebProcessThreadId :: ThreadId -> AgentCtx -> IO ()
@@ -185,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

View File

@@ -7,78 +7,82 @@
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
module Handler.Apps where module Handler.Apps where
import Startlude hiding ( modify import Startlude hiding ( Reader
, execState
, asks , asks
, Reader
, runReader
, catchError , catchError
, forkFinally
, empty , empty
, execState
, forkFinally
, modify
, runReader
) )
import Control.Carrier.Reader
import Control.Carrier.Error.Church import Control.Carrier.Error.Church
import Control.Carrier.Lift import Control.Carrier.Lift
import Control.Carrier.Reader
import qualified Control.Concurrent.Async.Lifted import qualified Control.Concurrent.Async.Lifted
as LAsync as LAsync
import qualified Control.Concurrent.Lifted as Lifted import qualified Control.Concurrent.Lifted as Lifted
import qualified Control.Exception.Lifted as Lifted
import Control.Concurrent.STM.TVar import Control.Concurrent.STM.TVar
import Control.Effect.Empty hiding ( guard ) import Control.Effect.Empty hiding ( guard )
import Control.Effect.Labelled ( HasLabelled import Control.Effect.Labelled ( HasLabelled
, Labelled , Labelled
, runLabelled , runLabelled
) )
import qualified Control.Exception.Lifted as Lifted
import Control.Lens hiding ( (??) ) import Control.Lens hiding ( (??) )
import Control.Monad.Logger import Control.Monad.Logger
import Control.Monad.Trans.Control ( MonadBaseControl ) import Control.Monad.Trans.Control ( MonadBaseControl )
import Crypto.Hash
import Data.Aeson import Data.Aeson
import Data.Aeson.Lens import Data.Aeson.Lens
import Data.Aeson.Types ( parseMaybe )
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import Data.IORef
import qualified Data.HashMap.Lazy as HML import qualified Data.HashMap.Lazy as HML
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import Data.IORef
import qualified Data.List.NonEmpty as NE import qualified Data.List.NonEmpty as NE
import Data.Singletons import Data.Singletons
import Data.Singletons.Prelude.Bool ( SBool(..) import Data.Singletons.Prelude.Bool ( If
, If , SBool(..)
) )
import Data.Singletons.Prelude.List ( Elem ) import Data.Singletons.Prelude.List ( Elem )
import qualified Data.Text as Text
import Database.Persist import Database.Persist
import Database.Persist.Sql ( ConnectionPool ) import Database.Persist.Sql ( ConnectionPool )
import Database.Persist.Sqlite ( runSqlPool ) import Database.Persist.Sqlite ( runSqlPool )
import Exinst import Exinst
import Network.HTTP.Types import Network.HTTP.Types
import qualified Network.JSONRPC as JSONRPC
import Yesod.Core.Content import Yesod.Core.Content
import Yesod.Core.Json
import Yesod.Core.Handler hiding ( cached ) import Yesod.Core.Handler hiding ( cached )
import Yesod.Core.Json
import Yesod.Core.Types ( JSONResponse(..) ) import Yesod.Core.Types ( JSONResponse(..) )
import Yesod.Persist.Core import Yesod.Persist.Core
import Foundation import Foundation
import Handler.Backups import Handler.Backups
import Handler.Icons import Handler.Icons
import Handler.Network
import Handler.Types.Apps import Handler.Types.Apps
import Handler.Util import Handler.Util
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2 import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Lib.Algebra.State.RegistryUrl import Lib.Algebra.State.RegistryUrl
import Lib.Background import Lib.Background
import Lib.Error import Lib.Error
import qualified Lib.External.AppManifest as AppManifest
import qualified Lib.External.AppMgr as AppMgr import qualified Lib.External.AppMgr as AppMgr
import qualified Lib.External.Registry as Reg import qualified Lib.External.Registry as Reg
import qualified Lib.External.AppManifest as AppManifest
import Lib.IconCache import Lib.IconCache
import qualified Lib.Notifications as Notifications import qualified Lib.Notifications as Notifications
import Lib.SystemPaths import Lib.SystemPaths
import Lib.TyFam.ConditionalData import Lib.TyFam.ConditionalData
import Lib.Types.Core import Lib.Types.Core
import Lib.Types.Emver import Lib.Types.Emver
import Lib.Types.NetAddress
import Lib.Types.ServerApp import Lib.Types.ServerApp
import Model import Model
import Settings import Settings
import Crypto.Hash
pureLog :: Show a => a -> Handler a pureLog :: Show a => a -> Handler a
pureLog = liftA2 (*>) ($logInfo . show) pure pureLog = liftA2 (*>) ($logInfo . show) pure
@@ -105,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)))
)
) )
) )
) )
@@ -118,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
@@ -144,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 =
@@ -173,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 =
@@ -203,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
@@ -244,18 +256,30 @@ getInstalledAppsLogic = do
, appInstalledPreviewStatus = AppStatusTmp Installing , appInstalledPreviewStatus = AppStatusTmp Installing
, appInstalledPreviewVersionInstalled = storeAppVersionInfoVersion , appInstalledPreviewVersionInstalled = storeAppVersionInfoVersion
, appInstalledPreviewTorAddress = Nothing , appInstalledPreviewTorAddress = Nothing
, appInstalledPreviewUi = False , appInstalledPreviewLanAddress = Nothing
, appInstalledPreviewTorUi = False
, appInstalledPreviewLanUi = False
} }
installedPreviews = flip installedPreviews = flip
HML.mapWithKey HML.mapWithKey
remapped remapped
\appId (s, v, AppMgr2.InfoRes {..}) -> AppInstalledPreview \appId (s, v, AppMgr2.InfoRes {..}) ->
{ appInstalledPreviewBase = AppBase appId infoResTitle (iconUrl appId v) let
, appInstalledPreviewStatus = s mLanAddress = do -- Maybe
, appInstalledPreviewVersionInstalled = v addrBase <- infoResTorAddress
, appInstalledPreviewTorAddress = infoResTorAddress let
, appInstalledPreviewUi = AppManifest.uiAvailable infoResManifest lanConfs = mapMaybe AppManifest.portMapEntryLan
} $ AppManifest.appManifestPortMapping infoResManifest
guard (not . null $ lanConfs)
pure $ LanAddress . (".onion" `Text.replace` ".local") . unTorAddress $ addrBase
in AppInstalledPreview { appInstalledPreviewBase = AppBase appId infoResTitle (iconUrl appId v)
, appInstalledPreviewStatus = s
, appInstalledPreviewVersionInstalled = v
, appInstalledPreviewTorAddress = infoResTorAddress
, appInstalledPreviewLanAddress = mLanAddress
, appInstalledPreviewTorUi = AppManifest.torUiAvailable infoResManifest
, appInstalledPreviewLanUi = AppManifest.lanUiAvailable infoResManifest
}
pure $ HML.elems $ HML.union installingPreviews installedPreviews pure $ HML.elems $ HML.union installingPreviews installedPreviews
@@ -281,18 +305,25 @@ 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
, appInstalledFullLastBackup = backupTime , appInstalledFullLastBackup = backupTime
, appInstalledFullTorAddress = Nothing , appInstalledFullTorAddress = Nothing
, appInstalledFullLanAddress = Nothing
, appInstalledFullTorUi = False
, appInstalledFullLanUi = False
, appInstalledFullConfiguredRequirements = [] , appInstalledFullConfiguredRequirements = []
, appInstalledFullUninstallAlert = Nothing , appInstalledFullUninstallAlert = Nothing
, appInstalledFullRestoreAlert = Nothing , appInstalledFullRestoreAlert = Nothing
, appInstalledFullStartAlert = Nothing
, appInstalledFullActions = []
} }
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)
@@ -306,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
@@ -316,18 +347,32 @@ getInstalledAppByIdLogic appId = do
(HM.lookup depId installCache $> AppStatusTmp Installing) (HM.lookup depId installCache $> AppStatusTmp Installing)
<|> (view _1 <$> HM.lookup depId remapped) <|> (view _1 <$> HM.lookup depId remapped)
pure $ dependencyInfoToDependencyRequirement (AsInstalled STrue) (base, depStatus, depInfo) pure $ dependencyInfoToDependencyRequirement (AsInstalled STrue) (base, depStatus, depInfo)
manifest <- lift $ LAsync.wait manifest' manifest <- (lift $ LAsync.wait manifest') >>= \case
Nothing -> throwError $ NotFoundE "manifest" (show appId)
Just x -> pure x
instructions <- lift $ LAsync.wait instructions' instructions <- lift $ LAsync.wait instructions'
backupTime <- lift $ LAsync.wait backupTime' backupTime <- lift $ LAsync.wait backupTime'
let lanAddress = do
addrBase <- infoResTorAddress
let lanConfs = mapMaybe AppManifest.portMapEntryLan $ AppManifest.appManifestPortMapping manifest
guard (not . null $ lanConfs)
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
, appInstalledFullLastBackup = backupTime , appInstalledFullLastBackup = backupTime
, appInstalledFullTorAddress = infoResTorAddress , appInstalledFullTorAddress = infoResTorAddress
, appInstalledFullLanAddress = lanAddress
, appInstalledFullTorUi = AppManifest.torUiAvailable manifest
, appInstalledFullLanUi = AppManifest.lanUiAvailable manifest
, appInstalledFullConfiguredRequirements = HM.elems requirements , appInstalledFullConfiguredRequirements = HM.elems requirements
, appInstalledFullUninstallAlert = manifest >>= AppManifest.appManifestUninstallAlert , appInstalledFullUninstallAlert = AppManifest.appManifestUninstallAlert manifest
, appInstalledFullRestoreAlert = manifest >>= AppManifest.appManifestRestoreAlert , appInstalledFullRestoreAlert = AppManifest.appManifestRestoreAlert manifest
, appInstalledFullStartAlert = AppManifest.appManifestStartAlert manifest
, appInstalledFullActions = AppManifest.appManifestActions manifest
} }
runMaybeT (installing <|> installed) `orThrowM` NotFoundE "appId" (show appId) runMaybeT (installing <|> installed) `orThrowM` NotFoundE "appId" (show appId)
@@ -343,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
@@ -361,7 +407,9 @@ postUninstallAppLogic appId dryrun = do
breakageIds <- HM.keys . AppMgr2.unBreakageMap <$> AppMgr2.remove flags appId breakageIds <- HM.keys . AppMgr2.unBreakageMap <$> AppMgr2.remove flags appId
bs <- pure (traverse (hydrate $ (AppMgr2.infoResTitle &&& AppMgr2.infoResVersion) <$> serverApps) breakageIds) bs <- pure (traverse (hydrate $ (AppMgr2.infoResTitle &&& AppMgr2.infoResVersion) <$> serverApps) breakageIds)
`orThrowM` InternalE "Reported app breakage for app that isn't installed, contact support" `orThrowM` InternalE "Reported app breakage for app that isn't installed, contact support"
when (not $ coerce dryrun) $ clearIcon appId when (not $ coerce dryrun) $ do
clearIcon appId
postResetLanLogic
pure $ WithBreakages bs () pure $ WithBreakages bs ()
type InstallResponse :: Bool -> Type type InstallResponse :: Bool -> Type
@@ -378,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
@@ -465,6 +514,7 @@ postInstallNewAppLogic appId appVersion dryrun = do
(void $ Notifications.emit k infoResVersion (Notifications.RestartFailed e)) (void $ Notifications.emit k infoResVersion (Notifications.RestartFailed e))
pool pool
) )
postResetLanLogic
postStartServerAppR :: AppId -> Handler () postStartServerAppR :: AppId -> Handler ()
@@ -630,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)
@@ -769,3 +819,21 @@ dependencyInfoToDependencyRequirement asInstalled (base, status, AppMgr2.Depende
let appDependencyRequirementReasonOptional = dependencyInfoReasonOptional let appDependencyRequirementReasonOptional = dependencyInfoReasonOptional
appDependencyRequirementDefault = dependencyInfoRequired appDependencyRequirementDefault = dependencyInfoRequired
in AppDependencyRequirement { .. } in AppDependencyRequirement { .. }
postActionR :: AppId -> Handler (JSONResponse JSONRPC.Response)
postActionR appId = do
req <- requireCheckJsonBody
fmap JSONResponse . intoHandler $ postActionLogic appId req
postActionLogic :: (Has (Error S9Error) sig m, Has AppMgr2.AppMgr sig m)
=> AppId
-> JSONRPC.Request
-> m JSONRPC.Response
postActionLogic appId (JSONRPC.Request { getReqMethod, getReqId }) = do
hm <- AppMgr2.action appId getReqMethod
case (HM.lookup "result" hm, HM.lookup "error" hm >>= parseMaybe parseJSON) of
(Just v , _ ) -> pure (JSONRPC.Response JSONRPC.V2 v getReqId)
(_ , Just e ) -> pure (JSONRPC.ResponseError JSONRPC.V2 e getReqId)
(Nothing, Nothing) -> throwError
$ AppMgrParseE "action" (decodeUtf8 . LBS.toStrict $ encode (Object hm)) "Invalid JSONRPC Response"
postActionLogic _ r = throwError $ InvalidRequestE (toJSON r) "Invalid JSONRPC Request"

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,36 @@
module Handler.Network where
import Startlude hiding ( Reader
, ask
, asks
, runReader
)
import Control.Carrier.Lift ( runM )
import Control.Effect.Error
import Lib.Error
import Yesod.Core ( getYesod )
import Control.Carrier.Reader ( runReader )
import Control.Effect.Labelled ( runLabelled )
import Control.Effect.Reader.Labelled
import Foundation
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Lib.Types.Core
postResetLanR :: Handler ()
postResetLanR = do
ctx <- getYesod
runM . handleS9ErrC . runReader (appLanThread ctx) . runLabelled @"lanThread" $ postResetLanLogic
postResetLanLogic :: (MonadIO m, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m, Has (Error S9Error) sig m)
=> m ()
postResetLanLogic = do
threadVar <- ask @"lanThread"
mtid <- liftIO . tryTakeMVar $ threadVar
case mtid of
Nothing -> throwError $ TemporarilyForbiddenE (AppId "LAN") "reset" "being reset"
Just tid -> liftIO $ do
killThread tid
newTid <- forkIO (void . runM . runExceptT @S9Error . AppMgr2.runAppMgrCliC $ AppMgr2.lanEnable)
putMVar threadVar newTid

View File

@@ -28,6 +28,9 @@ import Lib.SystemPaths hiding ( (</>) )
import Lib.Tor import Lib.Tor
import Settings import Settings
import Control.Carrier.Lift ( runM ) import Control.Carrier.Lift ( runM )
import System.Process
import qualified UnliftIO
import System.FileLock
getVersionR :: Handler AppVersionRes getVersionR :: Handler AppVersionRes
getVersionR = pure . AppVersionRes $ agentVersion getVersionR = pure . AppVersionRes $ agentVersion
@@ -35,8 +38,7 @@ getVersionR = pure . AppVersionRes $ agentVersion
getVersionLatestR :: Handler VersionLatestRes getVersionLatestR :: Handler VersionLatestRes
getVersionLatestR = handleS9ErrT $ do getVersionLatestR = handleS9ErrT $ do
s <- getsYesod appSettings s <- getsYesod appSettings
v <- interp s $ Reg.getLatestAgentVersion uncurry VersionLatestRes <$> interp s Reg.getLatestAgentVersion
pure $ VersionLatestRes v
where interp s = ExceptT . liftIO . runError . injectFilesystemBaseFromContext s . runRegistryUrlIOC where interp s = ExceptT . liftIO . runError . injectFilesystemBaseFromContext s . runRegistryUrlIOC
@@ -48,6 +50,8 @@ getSpecsR = handleS9ErrT $ do
specsDisk <- fmap show . metricDiskSize <$> getDfMetrics specsDisk <- fmap show . metricDiskSize <$> getDfMetrics
specsNetworkId <- lift . runM . injectFilesystemBaseFromContext settings $ getStart9AgentHostname specsNetworkId <- lift . runM . injectFilesystemBaseFromContext settings $ getStart9AgentHostname
specsTorAddress <- lift . runM . injectFilesystemBaseFromContext settings $ getAgentHiddenServiceUrl specsTorAddress <- lift . runM . injectFilesystemBaseFromContext settings $ getAgentHiddenServiceUrl
specsLanAddress <-
fmap (<> ".local") . lift . runM . injectFilesystemBaseFromContext settings $ getStart9AgentHostname
let specsAgentVersion = agentVersion let specsAgentVersion = agentVersion
returnJsonEncoding SpecsRes { .. } returnJsonEncoding SpecsRes { .. }
@@ -69,3 +73,9 @@ patchServerR = do
getGitR :: Handler Text getGitR :: Handler Text
getGitR = pure $embedGitRevision getGitR = pure $embedGitRevision
getLogsR :: Handler (JSONResponse [Text])
getLogsR = do
let debugLock = "/root/agent/tmp/debug.lock"
UnliftIO.bracket (liftIO $ lockFile debugLock Exclusive) (liftIO . unlockFile) $ const $ do
liftIO $ callCommand "journalctl -u agent --since \"1 hour ago\" > /root/agent/tmp/debug.log"
liftIO $ JSONResponse . lines <$> readFile "/root/agent/tmp/debug.log"

View File

@@ -9,6 +9,7 @@ import Data.Aeson
import Data.Aeson.Flatten import Data.Aeson.Flatten
import Data.Singletons import Data.Singletons
import qualified Lib.External.AppManifest as Manifest
import Lib.TyFam.ConditionalData import Lib.TyFam.ConditionalData
import Lib.Types.Core import Lib.Types.Core
import Lib.Types.Emver import Lib.Types.Emver
@@ -45,7 +46,9 @@ data AppInstalledPreview = AppInstalledPreview
, appInstalledPreviewStatus :: AppStatus , appInstalledPreviewStatus :: AppStatus
, appInstalledPreviewVersionInstalled :: Version , appInstalledPreviewVersionInstalled :: Version
, appInstalledPreviewTorAddress :: Maybe TorAddress , appInstalledPreviewTorAddress :: Maybe TorAddress
, appInstalledPreviewUi :: Bool , appInstalledPreviewLanAddress :: Maybe LanAddress
, appInstalledPreviewTorUi :: Bool
, appInstalledPreviewLanUi :: Bool
} }
deriving (Eq, Show) deriving (Eq, Show)
instance ToJSON AppInstalledPreview where instance ToJSON AppInstalledPreview where
@@ -53,7 +56,9 @@ instance ToJSON AppInstalledPreview where
[ "status" .= appInstalledPreviewStatus [ "status" .= appInstalledPreviewStatus
, "versionInstalled" .= appInstalledPreviewVersionInstalled , "versionInstalled" .= appInstalledPreviewVersionInstalled
, "torAddress" .= (unTorAddress <$> appInstalledPreviewTorAddress) , "torAddress" .= (unTorAddress <$> appInstalledPreviewTorAddress)
, "ui" .= appInstalledPreviewUi , "lanAddress" .= (unLanAddress <$> appInstalledPreviewLanAddress)
, "torUi" .= appInstalledPreviewTorUi
, "lanUi" .= appInstalledPreviewLanUi
] ]
data InstallNewAppReq = InstallNewAppReq data InstallNewAppReq = InstallNewAppReq
@@ -69,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
@@ -83,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
@@ -126,14 +135,21 @@ 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
, appInstalledFullLanAddress :: Maybe LanAddress
, appInstalledFullTorUi :: Bool
, appInstalledFullLanUi :: Bool
, appInstalledFullInstructions :: Maybe Text , appInstalledFullInstructions :: Maybe Text
, appInstalledFullLastBackup :: Maybe UTCTime , appInstalledFullLastBackup :: Maybe UTCTime
, appInstalledFullConfiguredRequirements :: [Stripped AppDependencyRequirement] , appInstalledFullConfiguredRequirements :: [Stripped AppDependencyRequirement]
, appInstalledFullUninstallAlert :: Maybe Text , appInstalledFullUninstallAlert :: Maybe Text
, appInstalledFullRestoreAlert :: Maybe Text , appInstalledFullRestoreAlert :: Maybe Text
, appInstalledFullStartAlert :: Maybe Text
, appInstalledFullActions :: [Manifest.Action]
} }
instance ToJSON AppInstalledFull where instance ToJSON AppInstalledFull where
toJSON AppInstalledFull {..} = object toJSON AppInstalledFull {..} = object
@@ -141,13 +157,20 @@ instance ToJSON AppInstalledFull where
, "lastBackup" .= appInstalledFullLastBackup , "lastBackup" .= appInstalledFullLastBackup
, "configuredRequirements" .= appInstalledFullConfiguredRequirements , "configuredRequirements" .= appInstalledFullConfiguredRequirements
, "torAddress" .= (unTorAddress <$> appInstalledFullTorAddress) , "torAddress" .= (unTorAddress <$> appInstalledFullTorAddress)
, "lanAddress" .= (unLanAddress <$> appInstalledFullLanAddress)
, "torUi" .= appInstalledFullTorUi
, "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
, "uninstallAlert" .= appInstalledFullUninstallAlert , "uninstallAlert" .= appInstalledFullUninstallAlert
, "restoreAlert" .= appInstalledFullRestoreAlert , "restoreAlert" .= appInstalledFullRestoreAlert
, "startAlert" .= appInstalledFullStartAlert
, "actions" .= appInstalledFullActions
] ]
data AppVersionInfo = AppVersionInfo data AppVersionInfo = AppVersionInfo

View File

@@ -15,11 +15,13 @@ import Lib.Types.Emver
import Model import Model
data VersionLatestRes = VersionLatestRes data VersionLatestRes = VersionLatestRes
{ versionLatestVersion :: Version { versionLatestVersion :: Version
, versionLatestReleaseNotes :: Maybe Text
} }
deriving (Eq, Show) deriving (Eq, Show)
instance ToJSON VersionLatestRes where instance ToJSON VersionLatestRes where
toJSON VersionLatestRes {..} = object $ ["versionLatest" .= versionLatestVersion] toJSON VersionLatestRes {..} =
object $ ["versionLatest" .= versionLatestVersion, "releaseNotes" .= versionLatestReleaseNotes]
instance ToTypedContent VersionLatestRes where instance ToTypedContent VersionLatestRes where
toTypedContent = toTypedContent . toJSON toTypedContent = toTypedContent . toJSON
instance ToContent VersionLatestRes where instance ToContent VersionLatestRes where
@@ -31,14 +33,15 @@ data ServerRes = ServerRes
, serverStatus :: Maybe AppStatus , serverStatus :: Maybe AppStatus
, serverStatusAt :: UTCTime , serverStatusAt :: UTCTime
, serverVersionInstalled :: Version , serverVersionInstalled :: Version
, serverNotifications :: [ Entity Notification ] , serverNotifications :: [Entity Notification]
, serverWifi :: WifiList , serverWifi :: WifiList
, serverSsh :: [ SshKeyFingerprint ] , serverSsh :: [SshKeyFingerprint]
, serverAlternativeRegistryUrl :: Maybe Text , serverAlternativeRegistryUrl :: Maybe Text
, serverSpecs :: SpecsRes , serverSpecs :: SpecsRes
, serverWelcomeAck :: Bool , serverWelcomeAck :: Bool
, serverAutoCheckUpdates :: Bool , serverAutoCheckUpdates :: Bool
} deriving (Eq, Show) }
deriving (Eq, Show)
type JsonEncoding a = Encoding type JsonEncoding a = Encoding
jsonEncode :: (Monad m, ToJSON a) => a -> m (JsonEncoding a) jsonEncode :: (Monad m, ToJSON a) => a -> m (JsonEncoding a)

View File

@@ -16,6 +16,7 @@ data SpecsRes = SpecsRes
, specsNetworkId :: Text , specsNetworkId :: Text
, specsAgentVersion :: Version , specsAgentVersion :: Version
, specsTorAddress :: Text , specsTorAddress :: Text
, specsLanAddress :: Text
} }
deriving (Eq, Show) deriving (Eq, Show)
@@ -23,6 +24,7 @@ instance ToJSON SpecsRes where
toJSON SpecsRes {..} = object toJSON SpecsRes {..} = object
[ "EmbassyOS Version" .= specsAgentVersion [ "EmbassyOS Version" .= specsAgentVersion
, "Tor Address" .= specsTorAddress , "Tor Address" .= specsTorAddress
, "LAN Address" .= specsLanAddress
, "Network ID" .= specsNetworkId , "Network ID" .= specsNetworkId
, "CPU" .= specsCPU , "CPU" .= specsCPU
, "Memory" .= specsMem , "Memory" .= specsMem
@@ -33,6 +35,7 @@ instance ToJSON SpecsRes where
. fold . fold
$ [ "EmbassyOS Version" .= specsAgentVersion $ [ "EmbassyOS Version" .= specsAgentVersion
, "Tor Address" .= specsTorAddress , "Tor Address" .= specsTorAddress
, "LAN Address" .= specsLanAddress
, "Network ID" .= specsNetworkId , "Network ID" .= specsNetworkId
, "CPU" .= specsCPU , "CPU" .= specsCPU
, "Memory" .= specsMem , "Memory" .= specsMem

View File

@@ -93,6 +93,7 @@ getSpecs settings = do
specsDisk <- fmap show . metricDiskSize <$> getDfMetrics specsDisk <- fmap show . metricDiskSize <$> getDfMetrics
specsNetworkId <- runM $ injectFilesystemBaseFromContext settings getStart9AgentHostname specsNetworkId <- runM $ injectFilesystemBaseFromContext settings getStart9AgentHostname
specsTorAddress <- runM $ injectFilesystemBaseFromContext settings getAgentHiddenServiceUrl specsTorAddress <- runM $ injectFilesystemBaseFromContext settings getAgentHiddenServiceUrl
specsLanAddress <- fmap (<> ".local") . runM $ injectFilesystemBaseFromContext settings getStart9AgentHostname
let specsAgentVersion = agentVersion let specsAgentVersion = agentVersion
pure $ SpecsRes { .. } pure $ SpecsRes { .. }

View File

@@ -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,30 +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 Lib.Algebra.Domain.AppMgr.TH
import Lib.Error
import Lib.External.AppManifest
import Lib.TyFam.ConditionalData
import Lib.Types.Core ( AppId(..)
, AppContainerStatus(..)
)
import Lib.Types.NetAddress
import Lib.Types.Emver
import Control.Monad.Trans.Class ( MonadTrans )
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.Base ( MonadBase(..) )
import Control.Monad.Fail ( MonadFail(fail) ) import Control.Monad.Fail ( MonadFail(fail) )
import Control.Monad.Trans.Resource ( MonadResource(..) ) import Control.Monad.Trans.Class ( MonadTrans )
import Control.Monad.Trans.Control ( defaultLiftBaseWith import Control.Monad.Trans.Control ( MonadBaseControl(..)
, defaultRestoreM
, MonadTransControl(..) , MonadTransControl(..)
, MonadBaseControl(..) , defaultLiftBaseWith
, defaultRestoreM
) )
import Control.Monad.Trans.Resource ( MonadResource(..) )
import qualified Data.ByteString.Char8 as C8 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.Types
import Lib.Error
import qualified Lib.External.AppManifest as Manifest
import Lib.TyFam.ConditionalData
import Lib.Types.Core ( AppContainerStatus(..)
, AppId(..)
)
import Lib.Types.Emver
import Lib.Types.NetAddress
import System.Process
import System.Process.Typed
type InfoRes :: Either OnlyInfoFlag [IncludeInfoFlag] -> Type type InfoRes :: Either OnlyInfoFlag [IncludeInfoFlag] -> Type
@@ -66,7 +66,7 @@ data InfoRes a = InfoRes
(Either_ (DefaultEqSym1 'OnlyDependencies) (ElemSym1 'IncludeDependencies) a) (Either_ (DefaultEqSym1 'OnlyDependencies) (ElemSym1 'IncludeDependencies) a)
(HM.HashMap AppId DependencyInfo) (HM.HashMap AppId DependencyInfo)
, infoResManifest , infoResManifest
:: Include (Either_ (DefaultEqSym1 'OnlyManifest) (ElemSym1 'IncludeManifest) a) AppManifest :: Include (Either_ (DefaultEqSym1 'OnlyManifest) (ElemSym1 'IncludeManifest) a) Manifest.AppManifest
, infoResStatus :: Include (Either_ (DefaultEqSym1 'OnlyStatus) (ElemSym1 'IncludeStatus) a) AppContainerStatus , infoResStatus :: Include (Either_ (DefaultEqSym1 'OnlyStatus) (ElemSym1 'IncludeStatus) a) AppContainerStatus
} }
instance SingI (a :: Either OnlyInfoFlag [IncludeInfoFlag]) => FromJSON (InfoRes a) where instance SingI (a :: Either OnlyInfoFlag [IncludeInfoFlag]) => FromJSON (InfoRes a) where
@@ -270,6 +270,8 @@ data AppMgr (m :: Type -> Type) k where
-- Tor ::_ -- Tor ::_
Update ::DryRun -> AppId -> Maybe VersionRange -> AppMgr m BreakageMap Update ::DryRun -> AppId -> Maybe VersionRange -> AppMgr m BreakageMap
-- Verify ::_ -- Verify ::_
LanEnable ::AppMgr m ()
Action ::AppId -> Text -> AppMgr m (HM.HashMap Text Value)
makeSmartConstructors ''AppMgr makeSmartConstructors ''AppMgr
newtype AppMgrCliC m a = AppMgrCliC { runAppMgrCliC :: m a } newtype AppMgrCliC m a = AppMgrCliC { runAppMgrCliC :: m a }
@@ -368,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"]
@@ -421,6 +426,16 @@ instance (Has (Error S9Error) sig m, Algebra sig m, MonadIO m) => Algebra (AppMg
ExitFailure 6 -> ExitFailure 6 ->
throwError $ NotFoundE "appId@version" ([i|#{appId}#{maybe "" (('@':) . show) version}|]) throwError $ NotFoundE "appId@version" ([i|#{appId}#{maybe "" (('@':) . show) version}|])
ExitFailure n -> throwError $ AppMgrE (toS $ String.unwords args) n ExitFailure n -> throwError $ AppMgrE (toS $ String.unwords args) n
(L LanEnable ) -> liftIO $ callProcess "appmgr" ["lan", "enable"] $> ctx
(L (Action appId action)) -> do
let args = ["actions", show appId, toS action]
(ec, out) <- readProcessInheritStderr "appmgr" args ""
case ec of
ExitSuccess -> case eitherDecodeStrict out of
Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e
Right x -> pure $ ctx $> x
ExitFailure 6 -> throwError $ NotFoundE "appId" (show appId)
ExitFailure n -> throwError $ AppMgrE (toS $ String.unwords args) n
R other -> AppMgrCliC $ alg (runAppMgrCliC . hdl) other ctx R other -> AppMgrCliC $ alg (runAppMgrCliC . hdl) other ctx
where where
versionSpec :: (IsString a, Semigroup a, ConvertText String a) => Maybe VersionRange -> a -> a versionSpec :: (IsString a, Semigroup a, ConvertText String a) => Maybe VersionRange -> a -> a

View File

@@ -9,12 +9,12 @@ import Data.Aeson
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import qualified Data.Yaml as Yaml import qualified Data.Yaml as Yaml
import Control.Monad.Fail ( MonadFail(fail) )
import Lib.Error import Lib.Error
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.Emver.Orphans ( ) import Lib.Types.Emver.Orphans ( )
import Control.Monad.Fail ( MonadFail(fail) )
data ImageType = ImageTypeTar data ImageType = ImageTypeTar
deriving (Eq, Show) deriving (Eq, Show)
@@ -47,14 +47,43 @@ instance FromJSON AssetMapping where
assetMappingOverwrite <- o .: "overwrite" assetMappingOverwrite <- o .: "overwrite"
pure $ AssetMapping { .. } pure $ AssetMapping { .. }
data Action = Action
{ actionId :: Text
, actionName :: Text
, actionDescription :: Text
, actionWarning :: Maybe Text
, actionAllowedStatuses :: [AppContainerStatus]
}
deriving Show
instance FromJSON Action where
parseJSON = withObject "AppAction" $ \o -> do
actionId <- o .: "id"
actionName <- o .: "name"
actionDescription <- o .: "description"
actionWarning <- o .:? "warning"
actionAllowedStatuses <- o .: "allowed-statuses"
pure Action { .. }
instance ToJSON Action where
toJSON Action {..} =
object
$ [ "id" .= actionId
, "name" .= actionName
, "description" .= actionDescription
, "allowedStatuses" .= actionAllowedStatuses
]
<> maybeToList (("warning" .=) <$> actionWarning)
data AppManifest where 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
, appManifestPortMapping :: HM.HashMap Word16 Word16 , appManifestPortMapping :: [PortMapEntry]
, appManifestImageType :: ImageType , appManifestImageType :: ImageType
, appManifestMount :: FilePath , appManifestMount :: FilePath
, appManifestAssets :: [AssetMapping] , appManifestAssets :: [AssetMapping]
@@ -62,20 +91,32 @@ data AppManifest where
, appManifestDependencies :: HM.HashMap AppId VersionRange , appManifestDependencies :: HM.HashMap AppId VersionRange
, appManifestUninstallAlert :: Maybe Text , appManifestUninstallAlert :: Maybe Text
, appManifestRestoreAlert :: Maybe Text , appManifestRestoreAlert :: Maybe Text
, appManifestStartAlert :: Maybe Text
, appManifestActions :: [Action]
} -> AppManifest } -> AppManifest
deriving instance Show AppManifest
uiAvailable :: AppManifest -> Bool torUiAvailable :: AppManifest -> Bool
uiAvailable AppManifest {..} = isJust $ HM.lookup 80 appManifestPortMapping torUiAvailable AppManifest {..} = any (== 80) $ portMapEntryTor <$> appManifestPortMapping
lanUiAvailable :: AppManifest -> Bool
lanUiAvailable AppManifest {..} = any id $ fmap portMapEntryLan appManifestPortMapping <&> \case
Just Standard -> True
Just (Custom 443) -> True
Just (Custom 80 ) -> True
_ -> False
instance FromJSON AppManifest where instance FromJSON AppManifest where
parseJSON = withObject "App Manifest " $ \o -> do parseJSON = withObject "App Manifest " $ \o -> do
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"
appManifestPortMapping <- o .: "ports" >>= fmap HM.fromList . traverse parsePortMapping appManifestPortMapping <- o .: "ports"
appManifestImageType <- o .: "image" >>= (.: "type") appManifestImageType <- o .: "image" >>= (.: "type")
appManifestMount <- o .: "mount" appManifestMount <- o .: "mount"
appManifestAssets <- o .: "assets" >>= traverse parseJSON appManifestAssets <- o .: "assets" >>= traverse parseJSON
@@ -83,13 +124,34 @@ instance FromJSON AppManifest where
appManifestDependencies <- o .:? "dependencies" .!= HM.empty >>= traverse parseDepInfo appManifestDependencies <- o .:? "dependencies" .!= HM.empty >>= traverse parseDepInfo
appManifestUninstallAlert <- o .:? "uninstall-alert" appManifestUninstallAlert <- o .:? "uninstall-alert"
appManifestRestoreAlert <- o .:? "restore-alert" appManifestRestoreAlert <- o .:? "restore-alert"
appManifestStartAlert <- o .:? "start-alert"
appManifestActions <- o .: "actions"
pure $ AppManifest { .. } pure $ AppManifest { .. }
where where parseDepInfo = withObject "Dep Info" $ (.: "version")
parsePortMapping = withObject "Port Mapping" $ \o -> liftA2 (,) (o .: "tor") (o .: "internal")
parseDepInfo = withObject "Dep Info" $ (.: "version")
getAppManifest :: (MonadIO m, HasFilesystemBase sig m) => AppId -> S9ErrT m (Maybe AppManifest) getAppManifest :: (MonadIO m, HasFilesystemBase sig m) => AppId -> S9ErrT m (Maybe AppManifest)
getAppManifest appId = do getAppManifest appId = do
base <- ask @"filesystemBase" base <- ask @"filesystemBase"
ExceptT $ first (ManifestParseE appId) <$> liftIO ExceptT $ first (ManifestParseE appId) <$> liftIO
(Yaml.decodeFileEither . toS $ (appMgrAppPath appId <> "manifest.yaml") `relativeTo` base) (Yaml.decodeFileEither . toS $ (appMgrAppPath appId <> "manifest.yaml") `relativeTo` base)
data LanConfiguration = Standard | Custom Word16 deriving (Eq, Show)
instance FromJSON LanConfiguration where
parseJSON = liftA2 (<|>) standard custom
where
standard =
withText "Standard Lan" \t -> if t == "standard" then pure Standard else fail "Not Standard Lan Conf"
custom = withObject "Custom Lan" $ \o -> do
Custom <$> (o .: "custom" >>= (.: "port"))
data PortMapEntry = PortMapEntry
{ portMapEntryInternal :: Word16
, portMapEntryTor :: Word16
, portMapEntryLan :: Maybe LanConfiguration
}
deriving (Eq, Show)
instance FromJSON PortMapEntry where
parseJSON = withObject "Port Map Entry" $ \o -> do
portMapEntryInternal <- o .: "internal"
portMapEntryTor <- o .: "tor"
portMapEntryLan <- o .:? "lan"
pure PortMapEntry { .. }

View File

@@ -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
@@ -150,12 +159,13 @@ getAppVersionForSpec appId spec = do
v <- o .: "version" v <- o .: "version"
pure v pure v
getLatestAgentVersion :: (Has RegistryUrl sig m, Has (Error S9Error) sig m, MonadIO m) => m Version getLatestAgentVersion :: (Has RegistryUrl sig m, Has (Error S9Error) sig m, MonadIO m) => m (Version, Maybe Text)
getLatestAgentVersion = do getLatestAgentVersion = do
val <- registryRequest agentVersionPath val <- registryRequest agentVersionPath
parseOrThrow agentVersionPath val $ withObject "version response" $ \o -> do parseOrThrow agentVersionPath val $ withObject "version response" $ \o -> do
v <- o .: "version" v <- o .: "version"
pure v rn <- o .:? "release-notes"
pure (v, rn)
where agentVersionPath = "sys/version/agent" where agentVersionPath = "sys/version/agent"
getLatestAgentVersionForSpec :: (Has RegistryUrl sig m, Has (Lift IO) sig m, Has (Error S9Error) sig m) getLatestAgentVersionForSpec :: (Has RegistryUrl sig m, Has (Lift IO) sig m, Has (Error S9Error) sig m)

View File

@@ -5,7 +5,9 @@
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
module Lib.SelfUpdate where module Lib.SelfUpdate where
import Startlude hiding ( runReader ) import Startlude hiding ( handle
, runReader
)
import Control.Carrier.Error.Either import Control.Carrier.Error.Either
import Control.Lens import Control.Lens
@@ -29,6 +31,7 @@ import Lib.SystemPaths
import Lib.Types.Emver import Lib.Types.Emver
import Lib.WebServer import Lib.WebServer
import Settings import Settings
import UnliftIO.Exception ( handle )
youngAgentPort :: Word16 youngAgentPort :: Word16
youngAgentPort = 5960 youngAgentPort = 5960
@@ -191,18 +194,21 @@ runSyncOps syncOps = do
pure res pure res
synchronizeSystemState :: AgentCtx -> Version -> IO () synchronizeSystemState :: AgentCtx -> Version -> IO ()
synchronizeSystemState ctx _version = handle @SomeException cleanup $ flip runReaderT ctx $ do synchronizeSystemState ctx _version = handle @_ @SomeException cleanup $ flip runReaderT ctx $ do
(restartsAndRuns, mTid) <- case synchronizer of (restartsAndRuns, mTid) <- case synchronizer of
Synchronizer { synchronizerOperations } -> flip runStateT Nothing $ for synchronizerOperations $ \syncOp -> do Synchronizer { synchronizerOperations } -> flip runStateT Nothing $ for synchronizerOperations $ \syncOp -> do
shouldRun <- lift $ syncOpShouldRun syncOp shouldRun <- lift $ syncOpShouldRun syncOp
putStrLn @Text [i|Sync Op "#{syncOpName syncOp}" should run: #{shouldRun}|] putStrLn @Text [i|Sync Op "#{syncOpName syncOp}" should run: #{shouldRun}|]
when shouldRun $ do when shouldRun $ do
whenM (isNothing <$> get) $ do tid <- get >>= \case
tid <- liftIO . forkIO . forever $ playSong 300 updateInProgress *> threadDelay 20_000_000 Nothing -> do
put (Just tid) tid <- liftIO . forkIO . forever $ playSong 300 updateInProgress *> threadDelay 20_000_000
put (Just tid)
pure tid
Just tid -> pure tid
putStrLn @Text [i|Running Sync Op: #{syncOpName syncOp}|] putStrLn @Text [i|Running Sync Op: #{syncOpName syncOp}|]
setUpdate True setUpdate True
lift $ syncOpRun syncOp lift $ handle @_ @SomeException (\e -> lift $ killThread tid *> cleanup e) $ syncOpRun syncOp
pure $ (syncOpRequiresReboot syncOp, shouldRun) pure $ (syncOpRequiresReboot syncOp, shouldRun)
case mTid of case mTid of
Nothing -> pure () Nothing -> pure ()
@@ -222,5 +228,6 @@ synchronizeSystemState ctx _version = handle @SomeException cleanup $ flip runRe
void $ try @SomeException Sound.stop void $ try @SomeException Sound.stop
void $ try @SomeException Sound.unexport void $ try @SomeException Sound.unexport
let e' = InternalE $ show e let e' = InternalE $ show e
setUpdate False
flip runReaderT ctx $ cantFail $ failUpdate e' flip runReaderT ctx $ cantFail $ failUpdate e'

View File

@@ -11,9 +11,9 @@ import Startlude hiding ( check
import qualified Startlude.ByteStream as ByteStream import qualified Startlude.ByteStream as ByteStream
import qualified Startlude.ByteStream.Char8 as ByteStream import qualified Startlude.ByteStream.Char8 as ByteStream
import Control.Carrier.Lift ( runM )
import qualified Control.Effect.Reader.Labelled import qualified Control.Effect.Reader.Labelled
as Fused as Fused
import Control.Carrier.Lift ( runM )
import Control.Monad.Trans.Reader ( mapReaderT ) import Control.Monad.Trans.Reader ( mapReaderT )
import Control.Monad.Trans.Resource import Control.Monad.Trans.Resource
import Data.Attoparsec.Text import Data.Attoparsec.Text
@@ -21,51 +21,57 @@ import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Char8 as B8
import qualified Data.Conduit as Conduit import qualified Data.Conduit as Conduit
import qualified Data.Conduit.Combinators as Conduit import qualified Data.Conduit.Combinators as Conduit
import qualified Data.Conduit.Tar as Conduit
import Data.Conduit.Shell hiding ( arch import Data.Conduit.Shell hiding ( arch
, patch
, stream
, hostname , hostname
, patch
, split
, stream
) )
import qualified Data.Conduit.Tar as Conduit
import Data.FileEmbed import Data.FileEmbed
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import Data.IORef import Data.IORef
import Data.String.Interpolate.IsString import Data.String.Interpolate.IsString
import qualified Data.Yaml as Yaml import qualified Data.Yaml as Yaml
import Exinst import Exinst
import System.FilePath ( splitPath import qualified Streaming.Conduit as Conduit
import qualified Streaming.Prelude as Stream
import qualified Streaming.Zip as Stream
import System.Directory
import System.FilePath ( (</>)
, joinPath , joinPath
, (</>) , splitPath
) )
import System.FilePath.Posix ( takeDirectory ) import System.FilePath.Posix ( takeDirectory )
import System.Directory
import System.IO.Error import System.IO.Error
import System.Posix.Files import System.Posix.Files
import System.Process ( callCommand ) import System.Process ( callCommand )
import qualified Streaming.Prelude as Stream
import qualified Streaming.Conduit as Conduit
import qualified Streaming.Zip as Stream
import Constants import Constants
import Control.Effect.Error hiding ( run )
import Control.Effect.Labelled ( runLabelled )
import Daemon.ZeroConf ( getStart9AgentHostname )
import Data.ByteString.Char8 ( split )
import qualified Data.ByteString.Char8 as C8
import Data.Conduit.List ( consume )
import qualified Data.Text as T
import Foundation import Foundation
import Handler.Network
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Lib.ClientManifest import Lib.ClientManifest
import Lib.Error import Lib.Error
import qualified Lib.External.AppMgr as AppMgr import qualified Lib.External.AppMgr as AppMgr
import Lib.External.Registry import Lib.External.Registry
import Lib.Sound import Lib.Sound
import Lib.Ssl import Lib.Ssl
import Lib.Tor
import Lib.Types.Core
import Lib.Types.NetAddress
import Lib.Types.Emver
import Lib.SystemCtl import Lib.SystemCtl
import Lib.SystemPaths hiding ( (</>) ) import Lib.SystemPaths hiding ( (</>) )
import Lib.Tor
import Lib.Types.Core
import Lib.Types.Emver
import Lib.Types.NetAddress
import Settings import Settings
import Util.File import Util.File
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Daemon.ZeroConf ( getStart9AgentHostname )
import qualified Data.Text as T
import Control.Effect.Error hiding ( run )
data Synchronizer = Synchronizer data Synchronizer = Synchronizer
@@ -96,15 +102,16 @@ 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_8 synchronizer = sync_0_2_16
{-# INLINE synchronizer #-} {-# INLINE synchronizer #-}
sync_0_2_8 :: Synchronizer sync_0_2_16 :: Synchronizer
sync_0_2_8 = Synchronizer sync_0_2_16 = Synchronizer
"0.2.8" "0.2.16"
[ syncCreateAgentTmp [ syncCreateAgentTmp
, syncCreateSshDir , syncCreateSshDir
, syncRemoveAvahiSystemdDependency , syncRemoveAvahiSystemdDependency
, syncInstallLibAvahi
, syncInstallAppMgr , syncInstallAppMgr
, syncFullUpgrade , syncFullUpgrade
, sync32BitKernel , sync32BitKernel
@@ -113,6 +120,7 @@ sync_0_2_8 = Synchronizer
, syncInstallDuplicity , syncInstallDuplicity
, syncInstallExfatFuse , syncInstallExfatFuse
, syncInstallExfatUtils , syncInstallExfatUtils
, syncUpgradeTor
, syncInstallAmbassadorUI , syncInstallAmbassadorUI
, syncOpenHttpPorts , syncOpenHttpPorts
, syncUpgradeLifeline , syncUpgradeLifeline
@@ -122,6 +130,8 @@ sync_0_2_8 = Synchronizer
, syncConvertEcdsaCerts , syncConvertEcdsaCerts
, syncRestarterService , syncRestarterService
, syncInstallEject , syncInstallEject
, syncDropCertificateUniqueness
, syncRemoveDefaultNginxCfg
] ]
syncCreateAgentTmp :: SyncOp syncCreateAgentTmp :: SyncOp
@@ -170,7 +180,7 @@ syncFullUpgrade = SyncOp "Full Upgrade" check migrate True
Just (Done _ (KernelVersion (Version av) _)) -> if av < (4, 19, 118, 0) then pure True else pure False Just (Done _ (KernelVersion (Version av) _)) -> if av < (4, 19, 118, 0) then pure True else pure False
_ -> pure False _ -> pure False
migrate = liftIO . run $ do migrate = liftIO . run $ do
shell "apt-get update" shell "apt-get update --allow-releaseinfo-change"
shell "apt-get full-upgrade -y" shell "apt-get full-upgrade -y"
sync32BitKernel :: SyncOp sync32BitKernel :: SyncOp
@@ -195,7 +205,7 @@ syncInstallNginx = SyncOp "Install Nginx" check migrate False
where where
check = liftIO . run $ fmap isNothing (shell [i|which nginx || true|] $| conduit await) check = liftIO . run $ fmap isNothing (shell [i|which nginx || true|] $| conduit await)
migrate = liftIO . run $ do migrate = liftIO . run $ do
shell "apt-get update" shell "apt-get update --allow-releaseinfo-change"
shell "apt-get install nginx -y" shell "apt-get install nginx -y"
syncInstallEject :: SyncOp syncInstallEject :: SyncOp
@@ -203,7 +213,7 @@ syncInstallEject = SyncOp "Install Eject" check migrate False
where where
check = liftIO . run $ fmap isNothing (shell [i|which eject || true|] $| conduit await) check = liftIO . run $ fmap isNothing (shell [i|which eject || true|] $| conduit await)
migrate = liftIO . run $ do migrate = liftIO . run $ do
shell "apt-get update" shell "apt-get update --allow-releaseinfo-change"
shell "apt-get install eject -y" shell "apt-get install eject -y"
syncInstallDuplicity :: SyncOp syncInstallDuplicity :: SyncOp
@@ -211,7 +221,7 @@ syncInstallDuplicity = SyncOp "Install duplicity" check migrate False
where where
check = liftIO . run $ fmap isNothing (shell [i|which duplicity || true|] $| conduit await) check = liftIO . run $ fmap isNothing (shell [i|which duplicity || true|] $| conduit await)
migrate = liftIO . run $ do migrate = liftIO . run $ do
shell "apt-get update" shell "apt-get update --allow-releaseinfo-change"
shell "apt-get install -y duplicity" shell "apt-get install -y duplicity"
syncInstallExfatFuse :: SyncOp syncInstallExfatFuse :: SyncOp
@@ -224,7 +234,7 @@ syncInstallExfatFuse = SyncOp "Install exfat-fuse" check migrate False
ProcessException _ (ExitFailure 1) -> pure True ProcessException _ (ExitFailure 1) -> pure True
_ -> throwIO e _ -> throwIO e
migrate = liftIO . run $ do migrate = liftIO . run $ do
shell "apt-get update" shell "apt-get update --allow-releaseinfo-change"
shell "apt-get install -y exfat-fuse" shell "apt-get install -y exfat-fuse"
syncInstallExfatUtils :: SyncOp syncInstallExfatUtils :: SyncOp
@@ -237,9 +247,22 @@ syncInstallExfatUtils = SyncOp "Install exfat-utils" check migrate False
ProcessException _ (ExitFailure 1) -> pure True ProcessException _ (ExitFailure 1) -> pure True
_ -> throwIO e _ -> throwIO e
migrate = liftIO . run $ do migrate = liftIO . run $ do
shell "apt-get update" shell "apt-get update --allow-releaseinfo-change"
shell "apt-get install -y exfat-utils" shell "apt-get install -y exfat-utils"
syncInstallLibAvahi :: SyncOp
syncInstallLibAvahi = SyncOp "Install libavahi-client" check migrate False
where
check =
liftIO
$ (run (shell [i|dpkg -l|] $| shell [i|grep libavahi-client3|] $| conduit await) $> False)
`catch` \(e :: ProcessException) -> case e of
ProcessException _ (ExitFailure 1) -> pure True
_ -> throwIO e
migrate = liftIO . run $ do
shell "apt-get update --allow-releaseinfo-change"
shell "apt-get install -y libavahi-client3"
syncWriteConf :: Text -> ByteString -> SystemPath -> SyncOp syncWriteConf :: Text -> ByteString -> SystemPath -> SyncOp
syncWriteConf name contents' confLocation = SyncOp [i|Write #{name} Conf|] check migrate False syncWriteConf name contents' confLocation = SyncOp [i|Write #{name} Conf|] check migrate False
where where
@@ -420,9 +443,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
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
@@ -480,7 +505,7 @@ replaceDerivativeCerts :: (HasFilesystemBase sig m, Fused.Has (Error S9Error) si
replaceDerivativeCerts = do replaceDerivativeCerts = do
sid <- getStart9AgentHostname sid <- getStart9AgentHostname
let hostname = sid <> ".local" let hostname = sid <> ".local"
tor <- getAgentHiddenServiceUrl torAddr <- getAgentHiddenServiceUrl
caKeyPath <- toS <$> getAbsoluteLocationFor rootCaKeyPath caKeyPath <- toS <$> getAbsoluteLocationFor rootCaKeyPath
caConfPath <- toS <$> getAbsoluteLocationFor rootCaOpenSslConfPath caConfPath <- toS <$> getAbsoluteLocationFor rootCaOpenSslConfPath
@@ -531,7 +556,7 @@ replaceDerivativeCerts = do
, duration = 365 , duration = 365
} }
hostname hostname
tor torAddr
liftIO $ do liftIO $ do
putStrLn @Text "openssl logs" putStrLn @Text "openssl logs"
putStrLn @Text "exit code: " putStrLn @Text "exit code: "
@@ -563,6 +588,67 @@ syncRestarterService = SyncOp "Install Restarter Service" check migrate True
liftIO $ callCommand "systemctl enable restarter.service" liftIO $ callCommand "systemctl enable restarter.service"
liftIO $ callCommand "systemctl enable restarter.timer" liftIO $ callCommand "systemctl enable restarter.timer"
syncUpgradeTor :: SyncOp
syncUpgradeTor = SyncOp "Install Latest Tor" check migrate False
where
check = run $ do
mTorVersion <- (shell "dpkg -s tor" $| shell "grep '^Version'" $| shell "cut -d ' ' -f2" $| conduit await)
let torVersion = case mTorVersion of
Nothing -> panic "invalid output from dpkg, can't read tor version"
Just x -> x
pure $ compareTorVersions torVersion "0.3.5.15-1" == LT
migrate = liftIO . run $ do
shell "apt-get update --allow-releaseinfo-change"
availVersions <-
(shell "apt-cache madison tor" $| shell "cut -d '|' -f2" $| shell "xargs" $| conduit consume)
latest <- case lastMay $ sortBy compareTorVersions availVersions of
Nothing -> throwIO $ ErrorCall "No available versions of tor"
Just x -> pure x
shell $ "apt-get install -y tor=" <> if "0.3.5.15-1" `elem` availVersions
then "0.3.5.15-1"
else (C8.unpack latest)
compareTorVersions :: ByteString -> ByteString -> Ordering
compareTorVersions a b =
let a' = (traverse (readMaybe @Int . decodeUtf8) . (split '.' <=< split '-') $ a)
b' = (traverse (readMaybe @Int . decodeUtf8) . (split '.' <=< split '-') $ b)
in case liftA2 compare a' b' of
Nothing -> panic "invalid tor version string"
Just x -> x
syncDropCertificateUniqueness :: SyncOp
syncDropCertificateUniqueness = SyncOp "Eliminate OpenSSL unique_subject=yes" check migrate False
where
uni = "unique_subject = no\n"
check = do
base <- asks $ appFilesystemBase . appSettings
contentsRoot <-
liftIO
$ (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
base <- asks $ appFilesystemBase . appSettings
liftIO $ BS.writeFile (toS $ (rootCaDirectory <> "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

View File

@@ -7,9 +7,7 @@ import Network.HTTP.Client
import Network.Connection import Network.Connection
import Lib.SystemPaths import Lib.SystemPaths
import Network.HTTP.Client.TLS ( mkManagerSettings import Network.HTTP.Client.TLS ( mkManagerSettings )
, newTlsManagerWith
)
import Data.Default import Data.Default
getAgentHiddenServiceUrl :: (HasFilesystemBase sig m, MonadIO m) => m Text getAgentHiddenServiceUrl :: (HasFilesystemBase sig m, MonadIO m) => m Text

View File

@@ -7,6 +7,10 @@ newtype TorAddress = TorAddress { unTorAddress :: Text } deriving (Eq)
instance Show TorAddress where instance Show TorAddress where
show = toS . unTorAddress show = toS . unTorAddress
newtype LanAddress = LanAddress { unLanAddress :: Text } deriving (Eq)
instance Show LanAddress where
show = toS . unLanAddress
newtype LanIp = LanIp { unLanIp :: Text } deriving (Eq) newtype LanIp = LanIp { unLanIp :: Text } deriving (Eq)
instance Show LanIp where instance Show LanIp where
show = toS . unLanIp show = toS . unLanIp

View File

@@ -45,6 +45,7 @@ import Handler.Backups
import Handler.Hosts import Handler.Hosts
import Handler.Icons import Handler.Icons
import Handler.Login import Handler.Login
import Handler.Network
import Handler.Notifications import Handler.Notifications
import Handler.PasswordUpdate import Handler.PasswordUpdate
import Handler.PowerOff import Handler.PowerOff

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -66,12 +66,65 @@ assets:
hidden-service-version: v3 hidden-service-version: v3
|] |]
mastodon330Manifest :: ByteString
mastodon330Manifest = [i|
---
id: mastodon
version: 3.3.0.1
title: Mastodon
description:
short: "A free, open-source social network server."
long: "Mastodon is a free, open-source social network server based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub)!"
release-notes: Added an acation to reset the admin password
install-alert: "After starting mastodon for the first time, it can take a long time (several minutes) to be ready.\nPlease be patient. On future starts of the service, it will be faster, but still takes longer than other services.\nMake sure to sign up for a user before giving out your link. The first user to sign up is set as the admin user.\n"
uninstall-alert: ~
restore-alert: ~
start-alert: "It may take several minutes after startup for this service to be ready for use.\n"
has-instructions: true
os-version-required: ">=0.2.8"
os-version-recommended: ">=0.2.8"
ports:
- internal: 80
tor: 80
lan: standard
- internal: 443
tor: 443
lan:
custom:
port: 443
- internal: 3000
tor: 3000
lan: ~
- internal: 4000
tor: 4000
lan: ~
image:
type: tar
shm-size-mb: ~
mount: /root/persistence
public: ~
shared: ~
assets: []
hidden-service-version: v3
dependencies: {}
actions:
- id: reset-admin-password
name: Reset Admin Password
description: This action will reset your admin password to a random value
allowed-statuses:
- RUNNING
command:
- docker_entrypoint.sh
- reset_admin_password.sh
|]
spec :: Spec spec :: Spec
spec = do spec = do
describe "parsing app manifest ports" $ do describe "parsing app manifest ports" $ do
it "should yield true for cups 0.2.3" $ do it "should parse mastodon 3.3.0" $ do
res <- decodeThrow @IO @(AppManifest 0) cups023Manifest res <- decodeThrow @IO @AppManifest mastodon330Manifest
uiAvailable res `shouldBe` True print res
it "should yield false for cups 0.2.3 Mod" $ do lanUiAvailable res `shouldBe` True
res <- decodeThrow @IO @(AppManifest 0) cups023ManifestModNoUI torUiAvailable res `shouldBe` True
uiAvailable res `shouldBe` False

1
appmgr/.gitignore vendored
View File

@@ -1,2 +1,3 @@
/target /target
**/*.rs.bk **/*.rs.bk
.DS_Store

1072
appmgr/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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.8" version = "0.2.16"
[lib] [lib]
name = "appmgrlib" name = "appmgrlib"
@@ -13,23 +13,29 @@ name = "appmgr"
path = "src/main.rs" path = "src/main.rs"
[features] [features]
default = [] avahi = ["avahi-sys"]
default = ["avahi"]
portable = [] portable = []
production = [] production = []
[dependencies] [dependencies]
argonautica = "0.2.0"
async-trait = "0.1.42" async-trait = "0.1.42"
avahi-sys = { git = "https://github.com/Start9Labs/avahi-sys", branch = "feature/dynamic-linking", features = [
"dynamic",
], optional = true }
base32 = "0.4.0" base32 = "0.4.0"
clap = "2.33" clap = "2.33"
ctrlc = "3.1.7"
ed25519-dalek = "1.0.1" ed25519-dalek = "1.0.1"
emver = { version = "0.1.0", features = ["serde"] } emver = { version = "0.1.0", features = ["serde"] }
failure = "0.1.8" failure = "0.1.8"
file-lock = "1.1" file-lock = "1.1"
futures = "0.3.8" futures = "0.3.8"
git-version = "0.3.4" git-version = "0.3.4"
http = "0.2.3"
itertools = "0.9.0" itertools = "0.9.0"
lazy_static = "1.4" lazy_static = "1.4"
libc = "0.2.86"
linear-map = { version = "1.2", features = ["serde_impl"] } linear-map = { version = "1.2", features = ["serde_impl"] }
log = "0.4.11" log = "0.4.11"
nix = "0.19.1" nix = "0.19.1"
@@ -41,6 +47,8 @@ rand = "0.7.3"
regex = "1.4.2" regex = "1.4.2"
reqwest = { version = "0.10.9", features = ["stream", "json"] } reqwest = { version = "0.10.9", features = ["stream", "json"] }
rpassword = "5.0.0" rpassword = "5.0.0"
rust-argon2 = "0.8.3"
scopeguard = "1.1" # because avahi-sys fucks your shit up
serde = { version = "1.0.118", features = ["derive", "rc"] } serde = { version = "1.0.118", features = ["derive", "rc"] }
serde_cbor = "0.11.1" serde_cbor = "0.11.1"
serde_json = "1.0.59" serde_json = "1.0.59"
@@ -49,3 +57,4 @@ simple-logging = "2.0"
tokio = { version = "0.3.5", features = ["full"] } tokio = { version = "0.3.5", features = ["full"] }
tokio-compat-02 = "0.1.2" tokio-compat-02 = "0.1.2"
tokio-tar = { version = "0.3.0", git = "https://github.com/dr-bonez/tokio-tar.git", rev = "1ba710f3" } tokio-tar = { version = "0.3.0", git = "https://github.com/dr-bonez/tokio-tar.git", rev = "1ba710f3" }
yajrc = { version = "0.1.0", git = "https://github.com/dr-bonez/yajrc", rev = "c2952a4a21c50f7be6f8003afa37ee77deb66d56" }

View File

@@ -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

View File

@@ -3,7 +3,12 @@
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 ..
rust-arm-builder sh -c "(cd appmgr && cargo build --release)" rust-arm-builder sh -c "(cd appmgr && cargo build)"

15
appmgr/build-portable.sh Executable file
View 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

View File

@@ -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 ..

116
appmgr/src/actions.rs Normal file
View File

@@ -0,0 +1,116 @@
use std::os::unix::process::ExitStatusExt;
use std::process::Stdio;
use linear_map::set::LinearSet;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, Error as IoError};
use yajrc::RpcError;
use crate::apps::DockerStatus;
pub const STATUS_NOT_ALLOWED: i32 = -2;
pub const INVALID_COMMAND: i32 = -3;
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Action {
pub id: String,
pub name: String,
pub description: String,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub warning: Option<String>,
pub allowed_statuses: LinearSet<DockerStatus>,
pub command: Vec<String>,
}
async fn tee<R: AsyncRead + Unpin, W: AsyncWrite + Unpin>(
mut r: R,
mut w: W,
) -> Result<Vec<u8>, IoError> {
let mut res = Vec::new();
let mut buf = vec![0; 2048];
let mut bytes;
while {
bytes = r.read(&mut buf).await?;
bytes != 0
} {
res.extend_from_slice(&buf[..bytes]);
w.write_all(&buf[..bytes]).await?;
}
w.flush().await?;
Ok(res)
}
impl Action {
pub async fn perform(&self, app_id: &str) -> Result<String, RpcError> {
let man = crate::apps::manifest(app_id)
.await
.map_err(failure::Error::from)
.map_err(failure::Error::compat)?;
let status = crate::apps::status(app_id, true)
.await
.map_err(failure::Error::from)
.map_err(failure::Error::compat)?
.status;
if !self.allowed_statuses.contains(&status) {
return Err(RpcError {
code: STATUS_NOT_ALLOWED,
message: format!(
"{} is in status {:?} which is not allowed by {}",
app_id, status, self.id
),
data: None,
});
}
let mut cmd = if status == DockerStatus::Running {
let mut cmd = tokio::process::Command::new("docker");
cmd.arg("exec").arg(&app_id).args(&self.command);
cmd
} else {
let mut cmd = tokio::process::Command::new("docker");
let entrypoint = self.command.get(0).ok_or_else(|| RpcError {
code: INVALID_COMMAND,
message: "Command Cannot Be Empty".to_owned(),
data: None,
})?;
cmd.arg("run")
.arg("--rm")
.arg("--name")
.arg(format!("{}_{}", app_id, self.id))
.arg("--mount")
.arg(format!(
"type=bind,src={}/{},dst={}",
crate::VOLUMES,
app_id,
man.mount.display()
))
.arg("--entrypoint")
.arg(entrypoint)
.arg(format!("start9/{}", app_id))
.args(&self.command[1..]);
// TODO: 0.3.0: net, tor, shm
cmd
};
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd.spawn()?;
let (stdout, stderr) = futures::try_join!(
tee(child.stdout.take().unwrap(), tokio::io::sink()),
tee(child.stderr.take().unwrap(), tokio::io::sink())
)?;
let status = child.wait().await?;
if status.success() {
String::from_utf8(stdout).map_err(From::from)
} else {
Err(RpcError {
code: status
.code()
.unwrap_or_else(|| status.signal().unwrap_or(0) + 128),
message: String::from_utf8(stderr)?,
data: None,
})
}
}
}

View File

@@ -1,14 +1,17 @@
use std::os::unix::process::ExitStatusExt;
use std::path::Path; use std::path::Path;
use argonautica::{Hasher, Verifier}; use argon2::Config;
use emver::Version; use emver::Version;
use futures::try_join; use futures::try_join;
use futures::TryStreamExt; use futures::TryStreamExt;
use rand::Rng;
use serde::Serialize; 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;
@@ -46,10 +49,7 @@ pub async fn create_backup<P: AsRef<Path>>(
let mut hash = String::new(); let mut hash = String::new();
f.read_to_string(&mut hash).await?; f.read_to_string(&mut hash).await?;
crate::ensure_code!( crate::ensure_code!(
Verifier::new() argon2::verify_encoded(&hash, password.as_bytes())
.with_password(password)
.with_hash(hash)
.verify()
.with_code(crate::error::INVALID_BACKUP_PASSWORD)?, .with_code(crate::error::INVALID_BACKUP_PASSWORD)?,
crate::error::INVALID_BACKUP_PASSWORD, crate::error::INVALID_BACKUP_PASSWORD,
"Invalid Backup Decryption Password" "Invalid Backup Decryption Password"
@@ -58,10 +58,8 @@ pub async fn create_backup<P: AsRef<Path>>(
{ {
// save password // save password
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
let salt = rand::thread_rng().gen::<[u8; 32]>();
let mut hasher = Hasher::default(); let hash = argon2::hash_encoded(password.as_bytes(), &salt, &Config::default()).unwrap(); // this is safe because apparently the API was poorly designed
hasher.opt_out_of_secret_key(true);
let hash = hasher.with_password(password).hash().no_code()?;
let mut f = tokio::fs::File::create(pw_path).await?; let mut f = tokio::fs::File::create(pw_path).await?;
f.write_all(hash.as_bytes()).await?; f.write_all(hash.as_bytes()).await?;
f.flush().await?; f.flush().await?;
@@ -160,10 +158,7 @@ pub async fn restore_backup<P: AsRef<Path>>(
let mut hash = String::new(); let mut hash = String::new();
f.read_to_string(&mut hash).await?; f.read_to_string(&mut hash).await?;
crate::ensure_code!( crate::ensure_code!(
Verifier::new() argon2::verify_encoded(&hash, password.as_bytes())
.with_password(password)
.with_hash(hash)
.verify()
.with_code(crate::error::INVALID_BACKUP_PASSWORD)?, .with_code(crate::error::INVALID_BACKUP_PASSWORD)?,
crate::error::INVALID_BACKUP_PASSWORD, crate::error::INVALID_BACKUP_PASSWORD,
"Invalid Backup Decryption Password" "Invalid Backup Decryption Password"
@@ -231,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(())
} }

View File

@@ -0,0 +1,10 @@
[req]
default_bits = 4096
default_md = sha256
distinguished_name = req_distinguished_name
prompt = no
[req_distinguished_name]
CN = {hostname}.local
O = Start9 Labs
OU = Embassy

View File

@@ -1864,6 +1864,9 @@ mod test {
hidden_service_version: crate::tor::HiddenServiceVersion::V3, hidden_service_version: crate::tor::HiddenServiceVersion::V3,
dependencies: deps, dependencies: deps,
extra: LinearMap::new(), extra: LinearMap::new(),
install_alert: None,
restore_alert: None,
uninstall_alert: None,
}) })
.unwrap(); .unwrap();
let config = spec let config = spec

View File

@@ -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?;

View File

@@ -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);

View File

@@ -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(())
} }

View File

@@ -256,6 +256,19 @@ pub async fn install_v0<R: AsyncRead + Unpin + Send + Sync>(
"Package Name Does Not Match Expected" "Package Name Does Not Match Expected"
); );
} }
log::info!(
"Creating metadata directory: {}/apps/{}",
crate::PERSISTENCE_DIR,
manifest.id
);
let app_dir = PersistencePath::from_ref("apps").join(&manifest.id);
let app_dir_path = app_dir.path();
if app_dir_path.exists() {
tokio::fs::remove_dir_all(&app_dir_path).await?;
}
tokio::fs::create_dir_all(&app_dir_path).await?;
let (ip, tor_addr, tor_key) = crate::tor::set_svc( let (ip, tor_addr, tor_key) = crate::tor::set_svc(
&manifest.id, &manifest.id,
crate::tor::NewService { crate::tor::NewService {
@@ -270,12 +283,6 @@ pub async fn install_v0<R: AsyncRead + Unpin + Send + Sync>(
log::info!("Creating volume {}/{}.", crate::VOLUMES, manifest.id); log::info!("Creating volume {}/{}.", crate::VOLUMES, manifest.id);
tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id)).await?; tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id)).await?;
let app_dir = PersistencePath::from_ref("apps").join(&manifest.id);
let app_dir_path = app_dir.path();
if app_dir_path.exists() {
tokio::fs::remove_dir_all(&app_dir_path).await?;
}
tokio::fs::create_dir_all(&app_dir_path).await?;
let _lock = app_dir.lock(true).await?; let _lock = app_dir.lock(true).await?;
log::info!("Saving manifest."); log::info!("Saving manifest.");
let mut manifest_out = app_dir.join("manifest.yaml").write(None).await?; let mut manifest_out = app_dir.join("manifest.yaml").write(None).await?;
@@ -554,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?,
}
} }
} }

93
appmgr/src/lan.rs Normal file
View File

@@ -0,0 +1,93 @@
use crate::Error;
use avahi_sys;
use futures::future::pending;
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct AppId {
pub un_app_id: String,
}
pub async fn enable_lan() -> Result<(), Error> {
unsafe {
let app_list = crate::apps::list_info().await?;
let simple_poll = avahi_sys::avahi_simple_poll_new();
let poll = avahi_sys::avahi_simple_poll_get(simple_poll);
let mut stack_err = 0;
let err_c: *mut i32 = &mut stack_err;
let avahi_client = avahi_sys::avahi_client_new(
poll,
avahi_sys::AvahiClientFlags::AVAHI_CLIENT_NO_FAIL,
None,
std::ptr::null_mut(),
err_c,
);
let group =
avahi_sys::avahi_entry_group_new(avahi_client, Some(noop), std::ptr::null_mut());
let hostname_raw = avahi_sys::avahi_client_get_host_name_fqdn(avahi_client);
let hostname_bytes = std::ffi::CStr::from_ptr(hostname_raw).to_bytes_with_nul();
const HOSTNAME_LEN: usize = 1 + 15 + 1 + 5; // leading byte, main address, dot, "local"
debug_assert_eq!(hostname_bytes.len(), HOSTNAME_LEN);
let mut hostname_buf = [0; HOSTNAME_LEN + 1];
hostname_buf[1..].copy_from_slice(hostname_bytes);
// assume fixed length prefix on hostname due to local address
hostname_buf[0] = 15; // set the prefix length to 15 for the main address
hostname_buf[16] = 5; // set the prefix length to 5 for "local"
for (app_id, app_info) in app_list {
let man = crate::apps::manifest(&app_id).await?;
if man
.ports
.iter()
.filter(|p| p.lan.is_some())
.next()
.is_none()
{
continue;
}
let tor_address = if let Some(addr) = app_info.tor_address {
addr
} else {
continue;
};
let lan_address = tor_address
.strip_suffix(".onion")
.ok_or_else(|| failure::format_err!("Invalid Tor Address: {:?}", tor_address))?
.to_owned()
+ ".local";
let lan_address_ptr = std::ffi::CString::new(lan_address)
.expect("Could not cast lan address to c string");
let _ = avahi_sys::avahi_entry_group_add_record(
group,
avahi_sys::AVAHI_IF_UNSPEC,
avahi_sys::AVAHI_PROTO_UNSPEC,
avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_USE_MULTICAST
| avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_ALLOW_MULTIPLE,
lan_address_ptr.as_ptr(),
avahi_sys::AVAHI_DNS_CLASS_IN as u16,
avahi_sys::AVAHI_DNS_TYPE_CNAME as u16,
avahi_sys::AVAHI_DEFAULT_TTL,
hostname_buf.as_ptr().cast(),
hostname_buf.len(),
);
log::info!("Published {:?}", lan_address_ptr);
}
avahi_sys::avahi_entry_group_commit(group);
ctrlc::set_handler(move || {
// please the borrow checker with the below semantics
// avahi_sys::avahi_entry_group_free(group);
// avahi_sys::avahi_client_free(avahi_client);
// drop(Box::from_raw(err_c));
std::process::exit(0);
})
.expect("Error setting signal handler");
}
pending().await
}
unsafe extern "C" fn noop(
_group: *mut avahi_sys::AvahiEntryGroup,
_state: avahi_sys::AvahiEntryGroupState,
_userdata: *mut core::ffi::c_void,
) {
}

View File

@@ -20,6 +20,7 @@ lazy_static::lazy_static! {
pub static ref QUIET: tokio::sync::RwLock<bool> = tokio::sync::RwLock::new(!std::env::var("APPMGR_QUIET").map(|a| a == "0").unwrap_or(true)); pub static ref QUIET: tokio::sync::RwLock<bool> = tokio::sync::RwLock::new(!std::env::var("APPMGR_QUIET").map(|a| a == "0").unwrap_or(true));
} }
pub mod actions;
pub mod apps; pub mod apps;
pub mod backup; pub mod backup;
pub mod config; pub mod config;
@@ -30,6 +31,8 @@ pub mod error;
pub mod index; pub mod index;
pub mod inspect; pub mod inspect;
pub mod install; pub mod install;
#[cfg(feature = "avahi")]
pub mod lan;
pub mod logs; pub mod logs;
pub mod manifest; pub mod manifest;
pub mod pack; pub mod pack;

View File

@@ -162,6 +162,17 @@ async fn inner_main() -> Result<(), Error> {
), ),
); );
#[cfg(feature = "avahi")]
#[allow(unused_mut)]
let mut app = app.subcommand(
SubCommand::with_name("lan")
.about("Configures LAN services")
.subcommand(
SubCommand::with_name("enable")
.about("Publishes the LAN addresses for all services"),
),
);
#[cfg(not(feature = "portable"))] #[cfg(not(feature = "portable"))]
let mut app = app let mut app = app
.subcommand( .subcommand(
@@ -399,7 +410,6 @@ async fn inner_main() -> Result<(), Error> {
.about("Removes an installed app") .about("Removes an installed app")
.arg( .arg(
Arg::with_name("purge") Arg::with_name("purge")
.short("p")
.long("purge") .long("purge")
.help("Deletes all application data"), .help("Deletes all application data"),
) )
@@ -820,6 +830,16 @@ async fn inner_main() -> Result<(), Error> {
) )
.subcommand( .subcommand(
SubCommand::with_name("repair-app-status").about("Restarts crashed apps"), // TODO: remove SubCommand::with_name("repair-app-status").about("Restarts crashed apps"), // TODO: remove
)
.subcommand(
SubCommand::with_name("actions")
.about("Perform an action for a service")
.arg(
Arg::with_name("SERVICE")
.help("ID of the service to perform an action on")
.required(true),
)
.arg(Arg::with_name("ACTION").help("ID of the action to perform")),
); );
let matches = app.clone().get_matches(); let matches = app.clone().get_matches();
@@ -1178,6 +1198,15 @@ async fn inner_main() -> Result<(), Error> {
std::process::exit(1); std::process::exit(1);
} }
}, },
#[cfg(feature = "avahi")]
#[cfg(not(feature = "portable"))]
("lan", Some(sub_m)) => match sub_m.subcommand() {
("enable", _) => crate::lan::enable_lan().await?,
_ => {
println!("{}", sub_m.usage());
std::process::exit(1);
}
},
#[cfg(not(feature = "portable"))] #[cfg(not(feature = "portable"))]
("info", Some(sub_m)) => { ("info", Some(sub_m)) => {
let name = sub_m.value_of("ID").unwrap(); let name = sub_m.value_of("ID").unwrap();
@@ -1547,6 +1576,34 @@ async fn inner_main() -> Result<(), Error> {
("repair-app-status", _) => { ("repair-app-status", _) => {
control::repair_app_status().await?; control::repair_app_status().await?;
} }
#[cfg(not(feature = "portable"))]
("actions", Some(sub_m)) => {
use yajrc::{GenericRpcMethod, RpcResponse};
let man = apps::manifest(sub_m.value_of("SERVICE").unwrap()).await?;
let action_id = sub_m.value_of("ACTION").unwrap();
println!(
"{}",
serde_json::to_string(&RpcResponse::<GenericRpcMethod>::from_result(
man.actions
.iter()
.filter(|a| &a.id == &action_id)
.next()
.ok_or_else(|| {
failure::format_err!(
"action {} does not exist for {}",
action_id,
man.id
)
})
.with_code(error::NOT_FOUND)?
.perform(&man.id)
.await
.map(serde_json::Value::String)
))
.with_code(error::SERDE_ERROR)?
)
}
("pack", Some(sub_m)) => { ("pack", Some(sub_m)) => {
pack( pack(
sub_m.value_of("PATH").unwrap(), sub_m.value_of("PATH").unwrap(),

View File

@@ -2,6 +2,7 @@ use std::path::PathBuf;
use linear_map::LinearMap; use linear_map::LinearMap;
use crate::actions::Action;
use crate::dependencies::Dependencies; use crate::dependencies::Dependencies;
use crate::tor::HiddenServiceVersion; use crate::tor::HiddenServiceVersion;
use crate::tor::PortMapping; use crate::tor::PortMapping;
@@ -43,6 +44,8 @@ pub struct ManifestV0 {
#[serde(default)] #[serde(default)]
pub restore_alert: Option<String>, pub restore_alert: Option<String>,
#[serde(default)] #[serde(default)]
pub start_alert: Option<String>,
#[serde(default)]
pub has_instructions: bool, pub has_instructions: bool,
#[serde(default = "emver::VersionRange::any")] #[serde(default = "emver::VersionRange::any")]
pub os_version_required: emver::VersionRange, pub os_version_required: emver::VersionRange,
@@ -63,6 +66,8 @@ pub struct ManifestV0 {
pub hidden_service_version: HiddenServiceVersion, pub hidden_service_version: HiddenServiceVersion,
#[serde(default)] #[serde(default)]
pub dependencies: Dependencies, pub dependencies: Dependencies,
#[serde(default)]
pub actions: Vec<Action>,
#[serde(flatten)] #[serde(flatten)]
pub extra: LinearMap<String, serde_yaml::Value>, pub extra: LinearMap<String, serde_yaml::Value>,
} }

View File

@@ -0,0 +1,28 @@
map $http_x_forwarded_proto $real_proto {{
ext+onions ext+onions;
ext+onion ext+onion;
https https;
http http;
default $scheme;
}}
server {{
listen 443 ssl;
server_name {hostname}.local;
ssl_certificate /root/appmgr/apps/{app_id}/cert-local.fullchain.crt.pem;
ssl_certificate_key /root/appmgr/apps/{app_id}/cert-local.key.pem;
location / {{
proxy_pass http://{app_ip}:{internal_port}/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $real_proto;
client_max_body_size 0;
proxy_request_buffering off;
proxy_buffering off;
}}
}}
server {{
listen 80;
server_name {hostname}.local;
return 301 https://$host$request_uri;
}}

View File

@@ -0,0 +1,11 @@
server {{
listen {port};
server_name {hostname}.local;
location / {{
proxy_pass http://{app_ip}:{internal_port}/;
proxy_set_header Host $host;
client_max_body_size 0;
proxy_request_buffering off;
proxy_buffering off;
}}
}}

View File

@@ -217,6 +217,13 @@ pub async fn verify(path: &str) -> Result<(), failure::Error> {
if let Some(shared) = &manifest.shared { if let Some(shared) = &manifest.shared {
validate_path(shared)?; validate_path(shared)?;
} }
for action in &manifest.actions {
ensure!(
!action.command.is_empty(),
"Command Cannot Be Empty: {}",
action.id
);
}
log::info!("Opening config spec from archive."); log::info!("Opening config spec from archive.");
let config_spec = entries let config_spec = entries
.next() .next()

View File

@@ -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")

View File

@@ -8,17 +8,62 @@ use failure::ResultExt as _;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use crate::util::{PersistencePath, YamlUpdateHandle}; use crate::util::{Invoke, PersistencePath, YamlUpdateHandle};
use crate::{Error, ResultExt as _}; use crate::{Error, ResultExt as _};
#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] #[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum LanOptions {
Standard,
Custom { port: u16 },
}
#[derive(Debug, Clone, Copy, serde::Serialize)]
pub struct PortMapping { pub struct PortMapping {
pub internal: u16, pub internal: u16,
pub tor: u16, pub tor: u16,
pub lan: Option<LanOptions>, // only for http interfaces
}
impl<'de> serde::de::Deserialize<'de> for PortMapping {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
pub struct PortMappingIF {
pub internal: u16,
pub tor: u16,
#[serde(default, deserialize_with = "deserialize_some")]
pub lan: Option<Option<LanOptions>>,
}
fn deserialize_some<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
T: serde::de::Deserialize<'de>,
D: serde::de::Deserializer<'de>,
{
serde::de::Deserialize::deserialize(deserializer).map(Some)
}
let input_format: PortMappingIF = serde::de::Deserialize::deserialize(deserializer)?;
Ok(PortMapping {
internal: input_format.internal,
tor: input_format.tor,
lan: if let Some(lan) = input_format.lan {
lan
} else if input_format.tor == 80 {
Some(LanOptions::Standard)
} else {
None
},
})
}
} }
pub const ETC_TOR_RC: &'static str = "/etc/tor/torrc"; pub const ETC_TOR_RC: &'static str = "/etc/tor/torrc";
pub const HIDDEN_SERVICE_DIR_ROOT: &'static str = "/var/lib/tor"; pub const HIDDEN_SERVICE_DIR_ROOT: &'static str = "/var/lib/tor";
pub const ETC_HOSTNAME: &'static str = "/etc/hostname";
pub const ETC_NGINX_SERVICES_CONF: &'static str = "/etc/nginx/sites-available/start9-services.conf";
#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)] #[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
@@ -179,6 +224,161 @@ pub async fn write_services(hidden_services: &ServicesMap) -> Result<(), Error>
Ok(()) Ok(())
} }
pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Error> {
let mut f = tokio::fs::File::create(ETC_NGINX_SERVICES_CONF).await?;
for (app_id, service) in &hidden_services.map {
let hostname = tokio::fs::read_to_string(
Path::new(HIDDEN_SERVICE_DIR_ROOT)
.join(format!("app-{}", app_id))
.join("hostname"),
)
.await
.with_context(|e| format!("{}/app-{}/hostname: {}", HIDDEN_SERVICE_DIR_ROOT, app_id, e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
let hostname_str = hostname
.trim()
.strip_suffix(".onion")
.ok_or_else(|| failure::format_err!("invalid tor hostname"))
.no_code()?;
for mapping in &service.ports {
match &mapping.lan {
Some(LanOptions::Standard) => {
log::info!("Writing LAN certificates for {}", app_id);
let base_path = PersistencePath::from_ref("apps").join(&app_id);
let key_path = base_path.join("cert-local.key.pem").path();
let conf_path = base_path.join("cert-local.csr.conf").path();
let req_path = base_path.join("cert-local.csr").path();
let cert_path = base_path.join("cert-local.crt.pem").path();
let fullchain_path = base_path.join("cert-local.fullchain.crt.pem");
if !fullchain_path.exists().await
|| tokio::fs::metadata(&key_path).await.is_err()
{
let mut fullchain_file = fullchain_path.write(None).await?;
tokio::process::Command::new("openssl")
.arg("ecparam")
.arg("-genkey")
.arg("-name")
.arg("prime256v1")
.arg("-noout")
.arg("-out")
.arg(&key_path)
.invoke("OpenSSL GenKey")
.await?;
tokio::fs::write(
&conf_path,
format!(
include_str!("cert-local.csr.conf.template"),
hostname = hostname_str
),
)
.await?;
tokio::process::Command::new("openssl")
.arg("req")
.arg("-config")
.arg(&conf_path)
.arg("-key")
.arg(&key_path)
.arg("-new")
.arg("-addext")
.arg(format!(
"subjectAltName=DNS:{hostname}.local",
hostname = hostname_str
))
.arg("-out")
.arg(&req_path)
.invoke("OpenSSL Req")
.await?;
tokio::process::Command::new("openssl")
.arg("ca")
.arg("-batch")
.arg("-config")
.arg("/root/agent/ca/intermediate/openssl.conf")
.arg("-rand_serial")
.arg("-keyfile")
.arg("/root/agent/ca/intermediate/private/embassy-int-ca.key.pem")
.arg("-cert")
.arg("/root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem")
.arg("-extensions")
.arg("server_cert")
.arg("-days")
.arg("365")
.arg("-notext")
.arg("-in")
.arg(&req_path)
.arg("-out")
.arg(&cert_path)
.invoke("OpenSSL CA")
.await?;
log::info!("Writing fullchain to: {}", fullchain_path.path().display());
tokio::io::copy(
&mut tokio::fs::File::open(&cert_path).await?,
&mut *fullchain_file,
)
.await?;
tokio::io::copy(
&mut tokio::fs::File::open(
"/root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem",
)
.await
.with_context(|e| {
format!(
"{}: /root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem",
e
)
})
.with_code(crate::error::FILESYSTEM_ERROR)?,
&mut *fullchain_file,
)
.await?;
tokio::io::copy(
&mut tokio::fs::File::open(
"/root/agent/ca/certs/embassy-root-ca.cert.pem",
)
.await
.with_context(|e| {
format!("{}: /root/agent/ca/certs/embassy-root-ca.cert.pem", e)
})
.with_code(crate::error::FILESYSTEM_ERROR)?,
&mut *fullchain_file,
)
.await?;
fullchain_file.commit().await?;
log::info!("{} written successfully", fullchain_path.path().display());
}
f.write_all(
format!(
include_str!("nginx-standard.conf.template"),
hostname = hostname_str,
app_ip = service.ip,
internal_port = mapping.internal,
app_id = app_id,
)
.as_bytes(),
)
.await?;
f.sync_all().await?;
}
Some(LanOptions::Custom { port }) => {
f.write_all(
format!(
include_str!("nginx.conf.template"),
hostname = hostname_str,
app_ip = service.ip,
port = port,
internal_port = mapping.internal,
)
.as_bytes(),
)
.await?
}
None => (),
}
}
}
Ok(())
}
pub async fn read_tor_address(name: &str, timeout: Option<Duration>) -> Result<String, Error> { pub async fn read_tor_address(name: &str, timeout: Option<Duration>) -> Result<String, Error> {
log::info!("Retrieving Tor hidden service address for {}.", name); log::info!("Retrieving Tor hidden service address for {}.", name);
let addr_path = Path::new(HIDDEN_SERVICE_DIR_ROOT) let addr_path = Path::new(HIDDEN_SERVICE_DIR_ROOT)
@@ -287,8 +487,8 @@ pub async fn set_svc(
Err(e) Err(e)
} }
})?; })?;
#[cfg(target_os = "linux")]
nix::unistd::sync(); nix::unistd::sync();
hidden_services.commit().await?;
log::info!("Reloading Tor."); log::info!("Reloading Tor.");
let svc_exit = std::process::Command::new("service") let svc_exit = std::process::Command::new("service")
.args(&["tor", "reload"]) .args(&["tor", "reload"])
@@ -302,19 +502,32 @@ pub async fn set_svc(
.or_else(|| { svc_exit.signal().map(|a| 128 + a) }) .or_else(|| { svc_exit.signal().map(|a| 128 + a) })
.unwrap_or(0) .unwrap_or(0)
); );
Ok(( let addr = if is_listening {
ip, Some(read_tor_address(name, Some(Duration::from_secs(30))).await?)
if is_listening { } else {
Some(read_tor_address(name, Some(Duration::from_secs(30))).await?) None
} else { };
None let key = if is_listening {
}, Some(read_tor_key(name, ver, Some(Duration::from_secs(30))).await?)
if is_listening { } else {
Some(read_tor_key(name, ver, Some(Duration::from_secs(30))).await?) None
} else { };
None write_lan_services(&hidden_services).await?;
}, log::info!("Reloading Nginx.");
)) 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)
);
hidden_services.commit().await?;
Ok((ip, addr, key))
} }
pub async fn rm_svc(name: &str) -> Result<(), Error> { pub async fn rm_svc(name: &str) -> Result<(), Error> {
@@ -333,7 +546,6 @@ pub async fn rm_svc(name: &str) -> Result<(), Error> {
} }
log::info!("Removing Tor hidden service {} from {}.", name, ETC_TOR_RC); log::info!("Removing Tor hidden service {} from {}.", name, ETC_TOR_RC);
write_services(&hidden_services).await?; write_services(&hidden_services).await?;
hidden_services.commit().await?;
log::info!("Reloading Tor."); log::info!("Reloading Tor.");
let svc_exit = std::process::Command::new("service") let svc_exit = std::process::Command::new("service")
.args(&["tor", "reload"]) .args(&["tor", "reload"])
@@ -344,6 +556,21 @@ pub async fn rm_svc(name: &str) -> Result<(), Error> {
"Failed to Reload Tor: {}", "Failed to Reload Tor: {}",
svc_exit.code().unwrap_or(0) svc_exit.code().unwrap_or(0)
); );
write_lan_services(&hidden_services).await?;
log::info!("Reloading Nginx.");
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)
);
hidden_services.commit().await?;
Ok(()) Ok(())
} }

View File

@@ -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)]
@@ -137,6 +145,7 @@ impl PersistenceFile {
if let Some(mut file) = self.file.take() { if let Some(mut file) = self.file.take() {
file.flush().await?; file.flush().await?;
file.shutdown().await?; file.shutdown().await?;
file.sync_all().await?;
drop(file); drop(file);
} }
if let Some(path) = self.needs_commit.take() { if let Some(path) = self.needs_commit.take() {
@@ -156,10 +165,6 @@ impl PersistenceFile {
.await .await
.with_context(|e| format!("{}.lock: {}", path.path().display(), e)) .with_context(|e| format!("{}.lock: {}", path.path().display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?; .with_code(crate::error::FILESYSTEM_ERROR)?;
tokio::fs::remove_file(format!("{}.lock", path.path().display()))
.await
.with_context(|e| format!("{}.lock: {}", path.path().display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
} }
Ok(()) Ok(())

View File

@@ -24,8 +24,17 @@ mod v0_2_5;
mod v0_2_6; mod v0_2_6;
mod v0_2_7; mod v0_2_7;
mod v0_2_8; mod v0_2_8;
mod v0_2_9;
pub use v0_2_8::Version as Current; mod v0_2_10;
mod v0_2_11;
mod v0_2_12;
mod v0_2_13;
mod v0_2_14;
mod v0_2_15;
mod v0_2_16;
pub use v0_2_16::Version as Current;
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)] #[serde(untagged)]
@@ -46,6 +55,14 @@ enum Version {
V0_2_6(Wrapper<v0_2_6::Version>), V0_2_6(Wrapper<v0_2_6::Version>),
V0_2_7(Wrapper<v0_2_7::Version>), V0_2_7(Wrapper<v0_2_7::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_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>),
V0_2_14(Wrapper<v0_2_14::Version>),
V0_2_15(Wrapper<v0_2_15::Version>),
V0_2_16(Wrapper<v0_2_16::Version>),
Other(emver::Version), Other(emver::Version),
} }
@@ -156,6 +173,14 @@ pub async fn init() -> Result<(), failure::Error> {
Version::V0_2_6(v) => v.0.migrate_to(&Current::new()).await?, Version::V0_2_6(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_7(v) => v.0.migrate_to(&Current::new()).await?, Version::V0_2_7(v) => v.0.migrate_to(&Current::new()).await?,
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_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::V0_2_14(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_15(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_16(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?
} }
@@ -172,7 +197,8 @@ pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error>
.collect(); .collect();
let url = format!("{}/appmgr?spec={}", &*crate::SYS_REGISTRY_URL, req_str); let url = format!("{}/appmgr?spec={}", &*crate::SYS_REGISTRY_URL, req_str);
log::info!("Fetching new version from {}", url); log::info!("Fetching new version from {}", url);
let response = reqwest::get(&url).compat() let response = reqwest::get(&url)
.compat()
.await .await
.with_code(crate::error::NETWORK_ERROR)? .with_code(crate::error::NETWORK_ERROR)?
.error_for_status() .error_for_status()
@@ -244,6 +270,14 @@ pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error>
Version::V0_2_6(v) => Current::new().migrate_to(&v.0).await?, Version::V0_2_6(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_7(v) => Current::new().migrate_to(&v.0).await?, Version::V0_2_7(v) => Current::new().migrate_to(&v.0).await?,
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_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::V0_2_14(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_15(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_16(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?
}; };

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_2_10: emver::Version = emver::Version::new(0, 2, 10, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_9::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_10
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View 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(())
}
}

View 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(())
}
}

View 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(())
}
}

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_2_14: emver::Version = emver::Version::new(0, 2, 14, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_13::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_14
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_2_15: emver::Version = emver::Version::new(0, 2, 15, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_14::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_15
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,21 @@
use super::*;
const V0_2_16: emver::Version = emver::Version::new(0, 2, 16, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_15::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_16
}
async fn up(&self) -> Result<(), Error> {
Ok(())
}
async fn down(&self) -> Result<(), Error> {
Ok(())
}
}

View File

@@ -0,0 +1,75 @@
use std::os::unix::process::ExitStatusExt;
use super::*;
const V0_2_9: emver::Version = emver::Version::new(0, 2, 9, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_8::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_9
}
async fn up(&self) -> Result<(), Error> {
crate::tor::write_lan_services(
&crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?,
)
.await?;
tokio::fs::os::unix::symlink(
crate::tor::ETC_NGINX_SERVICES_CONF,
"/etc/nginx/sites-enabled/start9-services.conf",
)
.await
.or_else(|e| {
if e.kind() == std::io::ErrorKind::AlreadyExists {
Ok(())
} else {
Err(e)
}
})?;
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> {
tokio::fs::remove_file("/etc/nginx/sites-enabled/start9-services.conf")
.await
.or_else(|e| match e {
e if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
e => Err(e),
})?;
tokio::fs::remove_file(crate::tor::ETC_NGINX_SERVICES_CONF)
.await
.or_else(|e| match e {
e if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
e => Err(e),
})?;
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(())
}
}

BIN
assets/Embassy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
assets/Marketplace.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
assets/ServiceDetails.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
assets/ServicesRunning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
assets/eos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

@@ -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}"

View File

@@ -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

View File

@@ -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
View 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"

View File

@@ -3,6 +3,7 @@ set -e
echo "turn off mocks" echo "turn off mocks"
echo "$( jq '.useMocks = false' use-mocks.json )" > use-mocks.json 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" echo "FILTER: rm -rf www"
rm -rf www rm -rf www

View File

@@ -1,6 +1,6 @@
manifest-version: 0 manifest-version: 0
app-id: start9-ambassador app-id: start9-ambassador
app-version: 0.2.8 app-version: 0.2.16
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/

1415
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "embassy-ui", "name": "embassy-ui",
"version": "0.2.8", "version": "0.2.16",
"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",
@@ -36,7 +36,7 @@
"json-pointer": "^0.6.1", "json-pointer": "^0.6.1",
"jsonpointerx": "^1.0.30", "jsonpointerx": "^1.0.30",
"jsontokens": "^3.0.0", "jsontokens": "^3.0.0",
"marked": "^1.2.0", "marked": "^2.0.0",
"rxjs": "^6.6.3", "rxjs": "^6.6.3",
"uuid": "^8.3.1", "uuid": "^8.3.1",
"zone.js": "^0.11.2" "zone.js": "^0.11.2"
@@ -53,7 +53,7 @@
"@types/marked": "^1.1.0", "@types/marked": "^1.1.0",
"@types/node": "^14.11.10", "@types/node": "^14.11.10",
"@types/uuid": "^8.0.0", "@types/uuid": "^8.0.0",
"node-html-parser": "^2.0.0", "node-html-parser": "2.0.0",
"ts-node": "^9.1.0", "ts-node": "^9.1.0",
"tslint": "^6.1.0", "tslint": "^6.1.0",
"typescript": "4.0.5" "typescript": "4.0.5"

View File

@@ -298,11 +298,12 @@ 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 if (spec === undefined) return true
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)])
} }

View File

@@ -70,7 +70,6 @@
<ion-icon name="arrow-forward"></ion-icon> <ion-icon name="arrow-forward"></ion-icon>
<ion-icon name="arrow-up"></ion-icon> <ion-icon name="arrow-up"></ion-icon>
<ion-icon name="bookmark-outline"></ion-icon> <ion-icon name="bookmark-outline"></ion-icon>
<ion-icon name="cart-outline"></ion-icon>
<ion-icon name="chevron-down"></ion-icon> <ion-icon name="chevron-down"></ion-icon>
<ion-icon name="chevron-up"></ion-icon> <ion-icon name="chevron-up"></ion-icon>
<ion-icon name="close"></ion-icon> <ion-icon name="close"></ion-icon>
@@ -86,6 +85,7 @@
<ion-icon name="eye-off-outline"></ion-icon> <ion-icon name="eye-off-outline"></ion-icon>
<ion-icon name="eye-outline"></ion-icon> <ion-icon name="eye-outline"></ion-icon>
<ion-icon name="file-tray-stacked-outline"></ion-icon> <ion-icon name="file-tray-stacked-outline"></ion-icon>
<ion-icon name="flash-outline"></ion-icon>
<ion-icon name="grid-outline"></ion-icon> <ion-icon name="grid-outline"></ion-icon>
<ion-icon name="help-circle-outline"></ion-icon> <ion-icon name="help-circle-outline"></ion-icon>
<ion-icon name="home-outline"></ion-icon> <ion-icon name="home-outline"></ion-icon>
@@ -93,13 +93,14 @@
<ion-icon name="list-outline"></ion-icon> <ion-icon name="list-outline"></ion-icon>
<ion-icon name="newspaper-outline"></ion-icon> <ion-icon name="newspaper-outline"></ion-icon>
<ion-icon name="notifications-outline"></ion-icon> <ion-icon name="notifications-outline"></ion-icon>
<ion-icon name="rocket-outline"></ion-icon>
<ion-icon name="power"></ion-icon> <ion-icon name="power"></ion-icon>
<ion-icon name="pulse"></ion-icon> <ion-icon name="pulse"></ion-icon>
<ion-icon name="qr-code-outline"></ion-icon> <ion-icon name="qr-code-outline"></ion-icon>
<ion-icon name="globe-outline"></ion-icon>
<ion-icon name="reload-outline"></ion-icon> <ion-icon name="reload-outline"></ion-icon>
<ion-icon name="refresh-outline"></ion-icon> <ion-icon name="refresh-outline"></ion-icon>
<ion-icon name="save-outline"></ion-icon> <ion-icon name="save-outline"></ion-icon>
<ion-icon name="storefront-outline"></ion-icon>
<ion-icon name="terminal-outline"></ion-icon> <ion-icon name="terminal-outline"></ion-icon>
<ion-icon name="trash-outline"></ion-icon> <ion-icon name="trash-outline"></ion-icon>
<ion-icon name="warning-outline"></ion-icon> <ion-icon name="warning-outline"></ion-icon>

View File

@@ -42,7 +42,7 @@ export class AppComponent {
{ {
title: 'Marketplace', title: 'Marketplace',
url: '/services/marketplace', url: '/services/marketplace',
icon: 'cart-outline', icon: 'storefront-outline',
}, },
{ {
title: 'Notifications', title: 'Notifications',

View File

@@ -11,7 +11,7 @@
<ion-slides *ngIf="!($error$ | async)" id="slide-show" style="--bullet-background: white" pager="false"> <ion-slides *ngIf="!($error$ | async)" id="slide-show" style="--bullet-background: white" pager="false">
<ion-slide *ngFor="let slide of params.slideDefinitions"> <ion-slide *ngFor="let slide of params.slideDefinitions">
<dependencies #components *ngIf="slide.selector === 'dependencies'" [params]="slide.params"></dependencies> <dependencies #components *ngIf="slide.selector === 'dependencies'" [params]="slide.params"></dependencies>
<developer-notes #components *ngIf="slide.selector === 'developer-notes'" [params]="slide.params"></developer-notes> <notes #components *ngIf="slide.selector === 'notes'" [params]="slide.params"></notes>
<dependents #components *ngIf="slide.selector === 'dependents'" [params]="slide.params" [finished]="finished"></dependents> <dependents #components *ngIf="slide.selector === 'dependents'" [params]="slide.params" [finished]="finished"></dependents>
<complete #components *ngIf="slide.selector === 'complete'" [params]="slide.params" [finished]="finished"></complete> <complete #components *ngIf="slide.selector === 'complete'" [params]="slide.params" [finished]="finished"></complete>
</ion-slide> </ion-slide>
@@ -43,8 +43,8 @@
<ion-text *ngIf="cancel.text as t">{{t}}</ion-text> <ion-text *ngIf="cancel.text as t">{{t}}</ion-text>
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon> <ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
</ion-button> </ion-button>
<ion-button slot="end" *ngIf="currentSlideDef.nextButton as nextButton" (click)="finished({})" [disabled]="$anythingLoading$ | async" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="nextButton.length > 16">{{nextButton}}</ion-text></ion-button> <ion-button slot="end" *ngIf="!($anythingLoading$ | async) && currentSlideDef.nextButton as nextButton" (click)="finished({})" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="nextButton.length > 16">{{nextButton}}</ion-text></ion-button>
<ion-button slot="end" *ngIf="currentSlideDef.finishButton as finishButton" (click)="finished({ final: true })" [disabled]="$anythingLoading$ | async" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="finishButton.length > 16">{{finishButton}}</ion-text></ion-button> <ion-button slot="end" *ngIf="!($anythingLoading$ | async) && currentSlideDef.finishButton as finishButton" (click)="finished({ final: true })" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="finishButton.length > 16">{{finishButton}}</ion-text></ion-button>
</ng-container> </ng-container>
<ng-container *ngIf="$error$ | async"> <ng-container *ngIf="$error$ | async">
<ion-button slot="start" (click)="finished({ final: true })" style="text-transform: capitalize; font-weight: bolder;" color="danger">Dismiss</ion-button> <ion-button slot="start" (click)="finished({ final: true })" style="text-transform: capitalize; font-weight: bolder;" color="danger">Dismiss</ion-button>

View File

@@ -7,7 +7,7 @@ import { SharingModule } from 'src/app/modules/sharing.module'
import { DependenciesComponentModule } from './dependencies/dependencies.component.module' import { DependenciesComponentModule } from './dependencies/dependencies.component.module'
import { DependentsComponentModule } from './dependents/dependents.component.module' import { DependentsComponentModule } from './dependents/dependents.component.module'
import { CompleteComponentModule } from './complete/complete.component.module' import { CompleteComponentModule } from './complete/complete.component.module'
import { DeveloperNotesComponentModule } from './developer-notes/developer-notes.component.module' import { NotesComponentModule } from './notes/notes.component.module'
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -21,7 +21,7 @@ import { DeveloperNotesComponentModule } from './developer-notes/developer-notes
DependenciesComponentModule, DependenciesComponentModule,
DependentsComponentModule, DependentsComponentModule,
CompleteComponentModule, CompleteComponentModule,
DeveloperNotesComponentModule, NotesComponentModule,
], ],
exports: [InstallWizardComponent], exports: [InstallWizardComponent],
}) })

View File

@@ -47,6 +47,7 @@
font-size: small; font-size: small;
border-width: 0px 0px 1px 0px; border-width: 0px 0px 1px 0px;
border-color: #393b40; border-color: #393b40;
text-align: left;
} }
@media (min-width:500px) { @media (min-width:500px) {
@@ -57,6 +58,7 @@
font-size: medium; font-size: medium;
border-width: 0px 0px 1px 0px; border-width: 0px 0px 1px 0px;
border-color: #393b40; border-color: #393b40;
text-align: left;
} }
} }

View File

@@ -1,13 +1,13 @@
import { Component, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core' import { Component, Input, NgZone, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'
import { IonContent, IonSlides, ModalController } from '@ionic/angular' import { IonContent, IonSlides, ModalController } from '@ionic/angular'
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs' import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'
import { map } from 'rxjs/operators' import { map } from 'rxjs/operators'
import { Cleanup } from 'src/app/util/cleanup' import { Cleanup } from 'src/app/util/cleanup'
import { capitalizeFirstLetter } from 'src/app/util/misc.util' import { capitalizeFirstLetter, pauseFor } from 'src/app/util/misc.util'
import { CompleteComponent } from './complete/complete.component' import { CompleteComponent } from './complete/complete.component'
import { DependenciesComponent } from './dependencies/dependencies.component' import { DependenciesComponent } from './dependencies/dependencies.component'
import { DependentsComponent } from './dependents/dependents.component' import { DependentsComponent } from './dependents/dependents.component'
import { DeveloperNotesComponent } from './developer-notes/developer-notes.component' import { NotesComponent } from './notes/notes.component'
import { Colorable, Loadable } from './loadable' import { Colorable, Loadable } from './loadable'
import { WizardAction } from './wizard-types' import { WizardAction } from './wizard-types'
@@ -50,7 +50,7 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
$currentColor$: BehaviorSubject<string> = new BehaviorSubject('medium') $currentColor$: BehaviorSubject<string> = new BehaviorSubject('medium')
$error$ = new BehaviorSubject(undefined) $error$ = new BehaviorSubject(undefined)
constructor (private readonly modalController: ModalController) { super() } constructor (private readonly modalController: ModalController, private readonly zone: NgZone) { super() }
ngOnInit () { } ngOnInit () { }
ngAfterViewInit () { ngAfterViewInit () {
@@ -80,15 +80,15 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
private async slide () { private async slide () {
if (this.slideComponents[this.slideIndex + 1] === undefined) { return this.finished({ final: true }) } if (this.slideComponents[this.slideIndex + 1] === undefined) { return this.finished({ final: true }) }
this.slideIndex += 1 this.zone.run(async () => {
this.currentSlide.load() this.slideComponents[this.slideIndex + 1].load()
await this.slideContainer.lockSwipes(false) await pauseFor(50)
await Promise.all([ this.slideIndex += 1
this.contentContainer.scrollToTop(), await this.slideContainer.lockSwipes(false)
this.slideContainer.slideNext(500), await this.contentContainer.scrollToTop()
]) await this.slideContainer.slideNext(500)
await this.slideContainer.lockSwipes(true) await this.slideContainer.lockSwipes(true)
this.slideContainer.update() })
} }
} }
@@ -114,8 +114,8 @@ export type SlideDefinition = SlideCommon & (
selector: 'complete', selector: 'complete',
params: CompleteComponent['params'] params: CompleteComponent['params']
} | { } | {
selector: 'developer-notes', selector: 'notes',
params: DeveloperNotesComponent['params'] params: NotesComponent['params']
} }
) )

View File

@@ -2,11 +2,9 @@
<div style="margin-top: 25px;"> <div style="margin-top: 25px;">
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;"> <div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
<ion-label [color]="$color$ | async" style="font-size: xx-large; font-weight: bold;"> <ion-label [color]="$color$ | async" style="font-size: xx-large; font-weight: bold;">
Warning {{params.title}}
</ion-label> </ion-label>
</div> </div>
<div class="long-message"> <div class="long-message" [innerHTML]="params.notes | markdown"></div>
{{params.developerNotes}}
</div>
</div> </div>
</div> </div>

View File

@@ -1,13 +1,13 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { DeveloperNotesComponent } from './developer-notes.component' import { NotesComponent } from './notes.component'
import { IonicModule } from '@ionic/angular' import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module' import { SharingModule } from 'src/app/modules/sharing.module'
@NgModule({ @NgModule({
declarations: [ declarations: [
DeveloperNotesComponent, NotesComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@@ -15,6 +15,6 @@ import { SharingModule } from 'src/app/modules/sharing.module'
RouterModule.forChild([]), RouterModule.forChild([]),
SharingModule, SharingModule,
], ],
exports: [DeveloperNotesComponent], exports: [NotesComponent],
}) })
export class DeveloperNotesComponentModule { } export class NotesComponentModule { }

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