Compare commits

...

28 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
54 changed files with 3568 additions and 178 deletions

View File

@@ -90,14 +90,18 @@
cat /proc/cpuinfo | grep Hardware cat /proc/cpuinfo | grep Hardware
``` ```
1. If your "Hardware" is [BCM2711](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711/README.md) then: 1. If your "Hardware" is [BCM2711](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711/README.md) then:
1. Change `C compiler flags` to `-marm -mcpu=cortex-a72` in the GHC settings: 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.2/lib/ghc-8.10.2/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 1. To prevent gcc errors we delete the `setup-exe-src` folder
``` ```
rm -rf ~/.stack/setup-exe-src/ rm -rf ~/.stack/setup-exe-src/
``` ```
1. Re-make the agent
```
make agent
```
6. Install requirements for step 7 6. Install requirements for step 7
1. Install NVM 1. Install NVM
@@ -145,7 +149,7 @@
cd ~/embassy-os cd ~/embassy-os
make make
#Depending from your hadware this can take 1-2h+ #Depending on your hardware this can take 1-2h+
#Wait for the "DONE!" message and take note of your product_key #Wait for the "DONE!" message and take note of your product_key
exit exit
``` ```

View File

@@ -13,10 +13,10 @@
EmbassyOS is a mass-market, graphical operating system designed to facilitate the discovery, installation, configuration, private self-hosting, and reliable operation of open-source software services and applications. It aims to eliminate trust and custodianship from personal computing. EmbassyOS is a mass-market, graphical operating system designed to facilitate the discovery, installation, configuration, private self-hosting, and reliable operation of open-source software services and applications. It aims to eliminate trust and custodianship from personal computing.
<img src="eos.png" width="100%"> <img src="assets/eos.png" width="100%">
## :warning: 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.
@@ -24,7 +24,7 @@ There are multiple ways to obtain and begin using EmbassyOS.
### :moneybag: 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.
### :hammer_and_wrench: 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.
@@ -33,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).
### :hammer_and_wrench: Build EmbassyOS from Source
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 ## :heart: Contributing
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). 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

@@ -33,5 +33,5 @@ database:
database: "start9_agent.sqlite3" database: "start9_agent.sqlite3"
poolsize: "_env:YESOD_SQLITE_POOLSIZE:10" poolsize: "_env:YESOD_SQLITE_POOLSIZE:10"
app-mgr-version-spec: "=0.2.12" 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

@@ -1,5 +1,5 @@
name: ambassador-agent name: ambassador-agent
version: 0.2.12 version: 0.2.16
default-extensions: default-extensions:
- NoImplicitPrelude - NoImplicitPrelude
@@ -182,3 +182,4 @@ executables:
condition: flag(library-only) condition: flag(library-only)
- condition: false - condition: false
other-modules: Paths_ambassador_agent other-modules: Paths_ambassador_agent
extra-source-files: ./migrations/*

View File

@@ -154,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 =
@@ -183,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 =
@@ -213,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
@@ -303,6 +305,8 @@ getInstalledAppByIdLogic appId = do
backupTime <- lift $ LAsync.wait backupTime' backupTime <- lift $ LAsync.wait backupTime'
hoistMaybe $ HM.lookup appId installCache <&> \(StoreApp {..}, StoreAppVersionInfo {..}) -> AppInstalledFull hoistMaybe $ HM.lookup appId installCache <&> \(StoreApp {..}, StoreAppVersionInfo {..}) -> AppInstalledFull
{ appInstalledFullBase = AppBase appId storeAppTitle (iconUrl appId storeAppVersionInfoVersion) { appInstalledFullBase = AppBase appId storeAppTitle (iconUrl appId storeAppVersionInfoVersion)
, appInstalledFullLicenseName = Nothing
, appInstalledFullLicenseLink = Nothing
, appInstalledFullStatus = AppStatusTmp Installing , appInstalledFullStatus = AppStatusTmp Installing
, appInstalledFullVersionInstalled = storeAppVersionInfoVersion , appInstalledFullVersionInstalled = storeAppVersionInfoVersion
, appInstalledFullInstructions = Nothing , appInstalledFullInstructions = Nothing
@@ -319,7 +323,7 @@ getInstalledAppByIdLogic appId = do
} }
serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|] serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|]
let remapped = remapAppMgrInfo jobCache serverApps let remapped = remapAppMgrInfo jobCache serverApps
appManifestFetchCached <- cached Reg.getAppManifest appManifestFetchCached <- cached Reg.getAppIndex
let let
installed = do installed = do
(status, version, AppMgr2.InfoRes {..}) <- hoistMaybe (HM.lookup appId remapped) (status, version, AppMgr2.InfoRes {..}) <- hoistMaybe (HM.lookup appId remapped)
@@ -333,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
@@ -354,6 +358,8 @@ getInstalledAppByIdLogic appId = do
guard (not . null $ lanConfs) guard (not . null $ lanConfs)
pure $ LanAddress . (".onion" `Text.replace` ".local") . unTorAddress $ addrBase pure $ LanAddress . (".onion" `Text.replace` ".local") . unTorAddress $ addrBase
pure AppInstalledFull { appInstalledFullBase = AppBase appId infoResTitle (iconUrl appId version) pure AppInstalledFull { appInstalledFullBase = AppBase appId infoResTitle (iconUrl appId version)
, appInstalledFullLicenseName = AppManifest.appManifestLicenseName manifest
, appInstalledFullLicenseLink = AppManifest.appManifestLicenseLink manifest
, appInstalledFullStatus = status , appInstalledFullStatus = status
, appInstalledFullVersionInstalled = version , appInstalledFullVersionInstalled = version
, appInstalledFullInstructions = instructions , appInstalledFullInstructions = instructions
@@ -674,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)

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

@@ -74,6 +74,8 @@ instance FromJSON InstallNewAppReq where
data AppAvailableFull = AppAvailableFull data AppAvailableFull = AppAvailableFull
{ appAvailableFullBase :: AppBase { appAvailableFullBase :: AppBase
, appAvailableFullLicenseName :: Maybe Text
, appAvailableFullLicenseLink :: Maybe Text
, appAvailableFullInstallInfo :: Maybe (Version, AppStatus) , appAvailableFullInstallInfo :: Maybe (Version, AppStatus)
, appAvailableFullVersionLatest :: Version , appAvailableFullVersionLatest :: Version
, appAvailableFullDescriptionShort :: Text , appAvailableFullDescriptionShort :: Text
@@ -88,7 +90,9 @@ instance ToJSON AppAvailableFull where
toJSON AppAvailableFull {..} = mergeTo toJSON AppAvailableFull {..} = mergeTo
(toJSON appAvailableFullBase) (toJSON appAvailableFullBase)
(object (object
[ "versionInstalled" .= fmap fst appAvailableFullInstallInfo [ "licenseName" .= appAvailableFullLicenseName
, "licenseLink" .= appAvailableFullLicenseLink
, "versionInstalled" .= fmap fst appAvailableFullInstallInfo
, "status" .= fmap snd appAvailableFullInstallInfo , "status" .= fmap snd appAvailableFullInstallInfo
, "versionLatest" .= appAvailableFullVersionLatest , "versionLatest" .= appAvailableFullVersionLatest
, "descriptionShort" .= appAvailableFullDescriptionShort , "descriptionShort" .= appAvailableFullDescriptionShort
@@ -131,6 +135,8 @@ instance ToJSON (AppDependencyRequirement Keep) where
-- mute violations downstream of version for installing apps -- mute violations downstream of version for installing apps
data AppInstalledFull = AppInstalledFull data AppInstalledFull = AppInstalledFull
{ appInstalledFullBase :: AppBase { appInstalledFullBase :: AppBase
, appInstalledFullLicenseName :: Maybe Text
, appInstalledFullLicenseLink :: Maybe Text
, appInstalledFullStatus :: AppStatus , appInstalledFullStatus :: AppStatus
, appInstalledFullVersionInstalled :: Version , appInstalledFullVersionInstalled :: Version
, appInstalledFullTorAddress :: Maybe TorAddress , appInstalledFullTorAddress :: Maybe TorAddress
@@ -156,6 +162,8 @@ instance ToJSON AppInstalledFull where
, "lanUi" .= appInstalledFullLanUi , "lanUi" .= appInstalledFullLanUi
, "id" .= appBaseId appInstalledFullBase , "id" .= appBaseId appInstalledFullBase
, "title" .= appBaseTitle appInstalledFullBase , "title" .= appBaseTitle appInstalledFullBase
, "licenseName" .= appInstalledFullLicenseName
, "licenseLink" .= appInstalledFullLicenseLink
, "iconURL" .= appBaseIconUrl appInstalledFullBase , "iconURL" .= appBaseIconUrl appInstalledFullBase
, "versionInstalled" .= appInstalledFullVersionInstalled , "versionInstalled" .= appInstalledFullVersionInstalled
, "status" .= appInstalledFullStatus , "status" .= appInstalledFullStatus

View File

@@ -78,6 +78,8 @@ data AppManifest where
AppManifest ::{ appManifestId :: AppId AppManifest ::{ appManifestId :: AppId
, appManifestVersion :: Version , appManifestVersion :: Version
, appManifestTitle :: Text , appManifestTitle :: Text
, appManifestLicenseName :: Maybe Text
, appManifestLicenseLink :: Maybe Text
, appManifestDescShort :: Text , appManifestDescShort :: Text
, appManifestDescLong :: Text , appManifestDescLong :: Text
, appManifestReleaseNotes :: Text , appManifestReleaseNotes :: Text
@@ -109,6 +111,8 @@ instance FromJSON AppManifest where
appManifestId <- o .: "id" appManifestId <- o .: "id"
appManifestVersion <- o .: "version" appManifestVersion <- o .: "version"
appManifestTitle <- o .: "title" appManifestTitle <- o .: "title"
appManifestLicenseName <- o .:? "license-info" >>= traverse (.: "license")
appManifestLicenseLink <- o .:? "license-info" >>= traverse (.: "url")
appManifestDescShort <- o .: "description" >>= (.: "short") appManifestDescShort <- o .: "description" >>= (.: "short")
appManifestDescLong <- o .: "description" >>= (.: "long") appManifestDescLong <- o .: "description" >>= (.: "long")
appManifestReleaseNotes <- o .: "release-notes" appManifestReleaseNotes <- o .: "release-notes"

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

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

@@ -24,6 +24,7 @@ import qualified Data.Conduit.Combinators as Conduit
import Data.Conduit.Shell hiding ( arch import Data.Conduit.Shell hiding ( arch
, hostname , hostname
, patch , patch
, split
, stream , stream
) )
import qualified Data.Conduit.Tar as Conduit import qualified Data.Conduit.Tar as Conduit
@@ -50,6 +51,9 @@ import Constants
import Control.Effect.Error hiding ( run ) import Control.Effect.Error hiding ( run )
import Control.Effect.Labelled ( runLabelled ) import Control.Effect.Labelled ( runLabelled )
import Daemon.ZeroConf ( getStart9AgentHostname ) 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 qualified Data.Text as T
import Foundation import Foundation
import Handler.Network import Handler.Network
@@ -98,12 +102,12 @@ parseKernelVersion = do
pure $ KernelVersion (Version (major', minor', patch', 0)) arch pure $ KernelVersion (Version (major', minor', patch', 0)) arch
synchronizer :: Synchronizer synchronizer :: Synchronizer
synchronizer = sync_0_2_12 synchronizer = sync_0_2_16
{-# INLINE synchronizer #-} {-# INLINE synchronizer #-}
sync_0_2_12 :: Synchronizer sync_0_2_16 :: Synchronizer
sync_0_2_12 = Synchronizer sync_0_2_16 = Synchronizer
"0.2.12" "0.2.16"
[ syncCreateAgentTmp [ syncCreateAgentTmp
, syncCreateSshDir , syncCreateSshDir
, syncRemoveAvahiSystemdDependency , syncRemoveAvahiSystemdDependency
@@ -176,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
@@ -201,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
@@ -209,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
@@ -217,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
@@ -230,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
@@ -243,7 +247,7 @@ 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
@@ -256,7 +260,7 @@ syncInstallLibAvahi = SyncOp "Install libavahi-client" 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 libavahi-client3" shell "apt-get install -y libavahi-client3"
syncWriteConf :: Text -> ByteString -> SystemPath -> SyncOp syncWriteConf :: Text -> ByteString -> SystemPath -> SyncOp
@@ -585,19 +589,32 @@ syncRestarterService = SyncOp "Install Restarter Service" check migrate True
liftIO $ callCommand "systemctl enable restarter.timer" liftIO $ callCommand "systemctl enable restarter.timer"
syncUpgradeTor :: SyncOp syncUpgradeTor :: SyncOp
syncUpgradeTor = SyncOp "Install Tor 0.3.5.14-1" check migrate False syncUpgradeTor = SyncOp "Install Latest Tor" check migrate False
where where
check = check = run $ do
liftIO mTorVersion <- (shell "dpkg -s tor" $| shell "grep '^Version'" $| shell "cut -d ' ' -f2" $| conduit await)
$ ( run (shell [i|dpkg -l|] $| shell [i|grep tor|] $| shell [i|grep 0.3.5.14-1|] $| conduit await) let torVersion = case mTorVersion of
$> False Nothing -> panic "invalid output from dpkg, can't read tor version"
) Just x -> x
`catch` \(e :: ProcessException) -> case e of pure $ compareTorVersions torVersion "0.3.5.15-1" == LT
ProcessException _ (ExitFailure 1) -> pure True
_ -> 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 tor=0.3.5.14-1" 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
syncDropCertificateUniqueness = SyncOp "Eliminate OpenSSL unique_subject=yes" check migrate False syncDropCertificateUniqueness = SyncOp "Eliminate OpenSSL unique_subject=yes" check migrate False

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

4
appmgr/Cargo.lock generated
View File

@@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.14.1" version = "0.14.1"
@@ -41,7 +43,7 @@ checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
[[package]] [[package]]
name = "appmgr" name = "appmgr"
version = "0.2.12" version = "0.2.16"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"avahi-sys", "avahi-sys",

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.12" version = "0.2.16"
[lib] [lib]
name = "appmgrlib" name = "appmgrlib"
@@ -20,7 +20,9 @@ production = []
[dependencies] [dependencies]
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 } 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" ctrlc = "3.1.7"

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
@@ -7,4 +19,4 @@
4. Config Spec violation 4. Config Spec violation
5. Config Rules violation 5. Config Rules violation
6. Requested value does not exist 6. Requested value does not exist
7. Invalid Backup Password 7. Invalid Backup Password

View File

@@ -29,8 +29,12 @@ mod v0_2_9;
mod v0_2_10; mod v0_2_10;
mod v0_2_11; mod v0_2_11;
mod v0_2_12; mod v0_2_12;
mod v0_2_13;
mod v0_2_14;
mod v0_2_15;
mod v0_2_16;
pub use v0_2_12::Version as Current; pub use v0_2_16::Version as Current;
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)] #[serde(untagged)]
@@ -55,6 +59,10 @@ enum Version {
V0_2_10(Wrapper<v0_2_10::Version>), V0_2_10(Wrapper<v0_2_10::Version>),
V0_2_11(Wrapper<v0_2_11::Version>), V0_2_11(Wrapper<v0_2_11::Version>),
V0_2_12(Wrapper<v0_2_12::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),
} }
@@ -169,6 +177,10 @@ pub async fn init() -> Result<(), failure::Error> {
Version::V0_2_10(v) => v.0.migrate_to(&Current::new()).await?, Version::V0_2_10(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_11(v) => v.0.migrate_to(&Current::new()).await?, Version::V0_2_11(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_12(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?
} }
@@ -262,6 +274,10 @@ pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error>
Version::V0_2_10(v) => Current::new().migrate_to(&v.0).await?, Version::V0_2_10(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_11(v) => Current::new().migrate_to(&v.0).await?, Version::V0_2_11(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_12(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_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(())
}
}

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

BIN
eos.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 657 KiB

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

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

8
ui/package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "embassy-ui", "name": "embassy-ui",
"version": "0.2.12", "version": "0.2.16",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -322,6 +322,12 @@
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true "dev": true
}, },
"typescript": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.5.tgz",
"integrity": "sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==",
"dev": true
},
"webpack-sources": { "webpack-sources": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.0.1.tgz", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.0.1.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "embassy-ui", "name": "embassy-ui",
"version": "0.2.12", "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",
@@ -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,6 +298,7 @@ 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()
if (spec === undefined) return true
let allKeys: Set<string> let allKeys: Set<string>
if (spec.type === 'union') { if (spec.type === 'union') {
let unionSpec = spec as ValueSpecOf<'union'> let unionSpec = spec as ValueSpecOf<'union'>
@@ -482,4 +483,4 @@ export function displayUniqueBy(uniqueBy: UniqueBy, spec: ValueSpecObject | Valu
} }
}).join(' or ') }).join(' or ')
} }
} }

View File

@@ -1,7 +1,7 @@
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar>
<ion-title > <ion-title >
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.12!</ion-label> <ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.16!</ion-label>
</ion-title> </ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
@@ -10,18 +10,7 @@
<div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%"> <div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
<h2>Highlights</h2> <h2>Highlights</h2>
<div class="main-content"> <div class="main-content">
<p>This release includes several bugfixes to resolve:</p> <p>This release fixes the occasional error of "'apt-get update' returned a failure exit code: 100"</p>
<ol>
<li>Faster file upload/download ability</li>
<li>More robust support for .local addresses</li>
<li>Refreshing error messages during configuration changes</li>
<li>Starting services with uninstalled optional dependencies</li>
<li>Uninstalling services with optional dependencies</li>
<li>Redirecting to HTTPS when navigating to LAN address</li>
<li>Displaying warning messages during concurrent upgrades of dependent services</li>
<li>Allowing larger file uploads</li>
<li>Patching a security fix for Tor</li>
</ol>
</div> </div>
<div class="close-button"> <div class="close-button">

View File

@@ -18,9 +18,11 @@ export interface AppAvailablePreview extends BaseApp {
} }
export type AppAvailableFull = export type AppAvailableFull =
AppAvailablePreview & AppAvailablePreview & {
{ descriptionLong: string descriptionLong: string
versions: string[] versions: string[]
licenseName?: string // @TODO required for 0.3.0
licenseLink?: string // @TODO required for 0.3.0
} & } &
AppAvailableVersionSpecificInfo AppAvailableVersionSpecificInfo
@@ -45,6 +47,8 @@ export interface AppInstalledPreview extends BaseApp {
} }
export interface AppInstalledFull extends AppInstalledPreview { export interface AppInstalledFull extends AppInstalledPreview {
licenseName?: string // @TODO required for 0.3.0
licenseLink?: string // @TODO required for 0.3.0
instructions: string | null instructions: string | null
lastBackup: string | null lastBackup: string | null
configuredRequirements: AppDependency[] | null // null if not yet configured configuredRequirements: AppDependency[] | null // null if not yet configured

View File

@@ -21,6 +21,26 @@
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text> <ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item> </ion-item>
<ion-card *ngIf="v1Status.status === 'instructions'" [href]="'https://start9.com/eos-' + v1Status.version" target="_blank" class="instructions-card">
<ion-card-header>
<ion-card-subtitle>Coming Soon...</ion-card-subtitle>
<ion-card-title>EmbassyOS Version {{ v1Status.version }}</ion-card-title>
</ion-card-header>
<ion-card-content>
<b>Get ready. View the update instructions.</b>
</ion-card-content>
</ion-card>
<ion-card *ngIf="v1Status.status === 'available'" [href]="'https://start9.com/eos-' + v1Status.version" target="_blank" class="available-card">
<ion-card-header>
<ion-card-subtitle>Now Available...</ion-card-subtitle>
<ion-card-title>EmbassyOS Version {{ v1Status.version }}</ion-card-title>
</ion-card-header>
<ion-card-content>
<b>View the update instructions.</b>
</ion-card-content>
</ion-card>
<ion-card *ngFor="let app of apps" style="margin: 10px 10px;" [routerLink]="[app.id]"> <ion-card *ngFor="let app of apps" style="margin: 10px 10px;" [routerLink]="[app.id]">
<ion-item style="--inner-border-width: 0 0 .4px 0; --border-color: #525252;" *ngIf="{ installing: (app.subject.status | async) === 'INSTALLING', installComparison: app.subject | compareInstalledAndLatest | async } as l"> <ion-item style="--inner-border-width: 0 0 .4px 0; --border-color: #525252;" *ngIf="{ installing: (app.subject.status | async) === 'INSTALLING', installComparison: app.subject | compareInstalledAndLatest | async } as l">
<ion-avatar style="margin-top: 8px;" slot="start"> <ion-avatar style="margin-top: 8px;" slot="start">

View File

@@ -3,4 +3,14 @@
font-style: italic; font-style: italic;
font-family: 'Open Sans'; font-family: 'Open Sans';
padding: 1px 0px 1.5px 0px; padding: 1px 0px 1.5px 0px;
}
.instructions-card {
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-medium) 150%);
margin: 16px 10px;
}
.available-card {
--background: linear-gradient(45deg, #101010 16%, var(--ion-color-danger) 150%);
margin: 16px 10px;
} }

View File

@@ -8,6 +8,7 @@ import { Subscription, BehaviorSubject, combineLatest } from 'rxjs'
import { take } from 'rxjs/operators' import { take } from 'rxjs/operators'
import { markAsLoadingDuringP } from 'src/app/services/loader.service' import { markAsLoadingDuringP } from 'src/app/services/loader.service'
import { OsUpdateService } from 'src/app/services/os-update.service' import { OsUpdateService } from 'src/app/services/os-update.service'
import { V1Status } from 'src/app/services/api/api-types'
@Component({ @Component({
selector: 'app-available-list', selector: 'app-available-list',
@@ -20,6 +21,7 @@ export class AppAvailableListPage {
installedAppDeltaSubscription: Subscription installedAppDeltaSubscription: Subscription
apps: PropertySubjectId<AppAvailablePreview>[] = [] apps: PropertySubjectId<AppAvailablePreview>[] = []
appsInstalled: PropertySubjectId<AppInstalledPreview>[] = [] appsInstalled: PropertySubjectId<AppInstalledPreview>[] = []
v1Status: V1Status = { status: 'nothing', version: '' }
constructor ( constructor (
private readonly apiService: ApiService, private readonly apiService: ApiService,
@@ -35,6 +37,7 @@ export class AppAvailableListPage {
markAsLoadingDuringP(this.$loading$, Promise.all([ markAsLoadingDuringP(this.$loading$, Promise.all([
this.getApps(), this.getApps(),
this.checkV1Status(),
this.osUpdateService.checkWhenNotAvailable$().toPromise(), // checks for an os update, banner component renders conditionally this.osUpdateService.checkWhenNotAvailable$().toPromise(), // checks for an os update, banner component renders conditionally
pauseFor(600), pauseFor(600),
])) ]))
@@ -44,6 +47,14 @@ export class AppAvailableListPage {
this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id)) this.appModel.getContents().forEach(appInstalled => this.mergeInstalledProps(appInstalled.id))
} }
async checkV1Status () {
try {
this.v1Status = await this.apiService.checkV1Status()
} catch (e) {
console.error(e)
}
}
mergeInstalledProps (appInstalledId: string) { mergeInstalledProps (appInstalledId: string) {
const appAvailable = this.apps.find(app => app.id === appInstalledId) const appAvailable = this.apps.find(app => app.id === appInstalledId)
if (!appAvailable) return if (!appAvailable) return

View File

@@ -17,6 +17,8 @@
versionInstalled: $app$.versionInstalled | async, versionInstalled: $app$.versionInstalled | async,
versionViewing: $app$.versionViewing | async, versionViewing: $app$.versionViewing | async,
descriptionLong: $app$.descriptionLong | async, descriptionLong: $app$.descriptionLong | async,
licenseName: $app$.licenseName | async,
licenseLink: $app$.licenseLink | async,
serviceRequirements: $app$.serviceRequirements | async, serviceRequirements: $app$.serviceRequirements | async,
iconURL: $app$.iconURL | async, iconURL: $app$.iconURL | async,
releaseNotes: $app$.releaseNotes | async releaseNotes: $app$.releaseNotes | async
@@ -112,9 +114,14 @@
<dependency-list [$loading$]="$dependenciesLoading$" [hostApp]="$app$ | peekProperties" [dependencies]="vars.serviceRequirements"></dependency-list> <dependency-list [$loading$]="$dependenciesLoading$" [hostApp]="$app$ | peekProperties" [dependencies]="vars.serviceRequirements"></dependency-list>
</ng-container> </ng-container>
<ion-item-divider></ion-item-divider> <ion-item-divider></ion-item-divider>
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
<ion-icon slot="start" name="newspaper-outline"></ion-icon>
<ion-label>License</ion-label>
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
</ion-item>
<ion-item lines="none" button (click)="presentAlertVersions()"> <ion-item lines="none" button (click)="presentAlertVersions()">
<ion-icon color="medium" slot="start" name="file-tray-stacked-outline"></ion-icon> <ion-icon slot="start" name="file-tray-stacked-outline"></ion-icon>
<ion-label color="medium">Other versions</ion-label> <ion-label>Other versions</ion-label>
</ion-item> </ion-item>
</ion-item-group> </ion-item-group>
</ng-container> </ng-container>

View File

@@ -15,6 +15,8 @@
torAddress: app.torAddress | async, torAddress: app.torAddress | async,
status: app.status | async, status: app.status | async,
versionInstalled: app.versionInstalled | async, versionInstalled: app.versionInstalled | async,
licenseName: app.licenseName | async,
licenseLink: app.licenseLink | async,
configuredRequirements: app.configuredRequirements | async, configuredRequirements: app.configuredRequirements | async,
lastBackup: app.lastBackup | async, lastBackup: app.lastBackup | async,
hasFetchedFull: app.hasFetchedFull | async, hasFetchedFull: app.hasFetchedFull | async,
@@ -157,6 +159,12 @@
<ion-icon slot="start" name="storefront-outline" color="primary"></ion-icon> <ion-icon slot="start" name="storefront-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label> <ion-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label>
</ion-item> </ion-item>
<!-- license -->
<ion-item *ngIf="vars.licenseLink" lines="none" button [href]="vars.licenseLink" target="_blank">
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">License</ion-text></ion-label>
<ion-note slot="end">{{ vars.licenseName }}</ion-note>
</ion-item>
<!-- dependencies --> <!-- dependencies -->
<ng-container *ngIf="vars.configuredRequirements && vars.configuredRequirements.length"> <ng-container *ngIf="vars.configuredRequirements && vars.configuredRequirements.length">

View File

@@ -37,3 +37,7 @@ export interface ApiAppConfig {
export type Unit = { never?: never; } // hack for the unit typ export type Unit = { never?: never; } // hack for the unit typ
export type V1Status = {
status: 'nothing' | 'instructions' | 'available'
version: string
}

View File

@@ -2,7 +2,7 @@ import { Rules } from '../../models/app-model'
import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types' import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalledFull, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
import { S9Notification, SSHFingerprint, ServerMetrics, DiskInfo } from '../../models/server-model' import { S9Notification, SSHFingerprint, ServerMetrics, DiskInfo } from '../../models/server-model'
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull, ApiAppInstalledPreview } from './api-types' import { Unit, ApiServer, ApiAppInstalledFull, ApiAppConfig, ApiAppAvailableFull, ApiAppInstalledPreview, V1Status } from './api-types'
import { AppMetrics, AppMetricsVersioned } from 'src/app/util/metrics.util' import { AppMetrics, AppMetricsVersioned } from 'src/app/util/metrics.util'
import { ConfigSpec } from 'src/app/app-config/config-types' import { ConfigSpec } from 'src/app/app-config/config-types'
@@ -64,6 +64,7 @@ export abstract class ApiService {
abstract ejectExternalDisk (logicalName: string): Promise<Unit> abstract ejectExternalDisk (logicalName: string): Promise<Unit>
abstract serviceAction (appId: string, serviceAction: ServiceAction): Promise<ReqRes.ServiceActionResponse> abstract serviceAction (appId: string, serviceAction: ServiceAction): Promise<ReqRes.ServiceActionResponse>
abstract refreshLAN (): Promise<Unit> abstract refreshLAN (): Promise<Unit>
abstract checkV1Status (): Promise<V1Status>
} }
export function isRpcFailure<Error, Result> (arg: { error: Error } | { result: Result }): arg is { error: Error } { export function isRpcFailure<Error, Result> (arg: { error: Error } | { result: Result }): arg is { error: Error } {

View File

@@ -4,7 +4,7 @@ import { AppModel, AppStatus } from '../../models/app-model'
import { AppAvailablePreview, AppAvailableFull, AppInstalledFull, AppInstalledPreview, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types' import { AppAvailablePreview, AppAvailableFull, AppInstalledFull, AppInstalledPreview, DependentBreakage, AppAvailableVersionSpecificInfo, ServiceAction } from '../../models/app-types'
import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model' import { S9Notification, SSHFingerprint, ServerModel, DiskInfo } from '../../models/server-model'
import { ApiService, ReqRes } from './api.service' import { ApiService, ReqRes } from './api.service'
import { ApiAppInstalledPreview, ApiServer, Unit } from './api-types' import { ApiAppInstalledPreview, ApiServer, Unit, V1Status } from './api-types'
import { HttpErrorResponse } from '@angular/common/http' import { HttpErrorResponse } from '@angular/common/http'
import { isUnauthorized } from 'src/app/util/web.util' import { isUnauthorized } from 'src/app/util/web.util'
import { Replace } from 'src/app/util/types.util' import { Replace } from 'src/app/util/types.util'
@@ -17,7 +17,7 @@ import { ConfigService } from '../config.service'
@Injectable() @Injectable()
export class LiveApiService extends ApiService { export class LiveApiService extends ApiService {
constructor( constructor (
private readonly http: HttpService, private readonly http: HttpService,
// TODO remove app + server model from here. updates to state should be done in a separate class wrapping ApiService + App/ServerModel // TODO remove app + server model from here. updates to state should be done in a separate class wrapping ApiService + App/ServerModel
private readonly appModel: AppModel, private readonly appModel: AppModel,
@@ -25,40 +25,40 @@ export class LiveApiService extends ApiService {
private readonly config: ConfigService, private readonly config: ConfigService,
) { super() } ) { super() }
testConnection(url: string): Promise<true> { testConnection (url: string): Promise<true> {
return this.http.raw.get(url).pipe(mapTo(true as true), catchError(e => catchHttpStatusError(e))).toPromise() return this.http.raw.get(url).pipe(mapTo(true as true), catchError(e => catchHttpStatusError(e))).toPromise()
} }
// Used to check whether password or key is valid. If so, it will be used implicitly by all other calls. // Used to check whether password or key is valid. If so, it will be used implicitly by all other calls.
async getCheckAuth(): Promise<Unit> { async getCheckAuth (): Promise<Unit> {
return this.http.serverRequest<Unit>({ method: Method.GET, url: '/authenticate' }, { version: '' }) return this.http.serverRequest<Unit>({ method: Method.GET, url: '/authenticate' }, { version: '' })
} }
async postLogin(password: string): Promise<Unit> { async postLogin (password: string): Promise<Unit> {
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/login', data: { password } }, { version: '' }) return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/login', data: { password } }, { version: '' })
} }
async postLogout(): Promise<Unit> { async postLogout (): Promise<Unit> {
return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/logout' }, { version: '' }).then(() => { this.authenticatedRequestsEnabled = false; return {} }) return this.http.serverRequest<Unit>({ method: Method.POST, url: '/auth/logout' }, { version: '' }).then(() => { this.authenticatedRequestsEnabled = false; return { } })
} }
async getServer(timeout?: number): Promise<ApiServer> { async getServer (timeout?: number): Promise<ApiServer> {
return this.authRequest<ReqRes.GetServerRes>({ method: Method.GET, url: '/', readTimeout: timeout }) return this.authRequest<ReqRes.GetServerRes>({ method: Method.GET, url: '/', readTimeout: timeout })
} }
async acknowledgeOSWelcome(version: string): Promise<Unit> { async acknowledgeOSWelcome (version: string): Promise<Unit> {
return this.authRequest<Unit>({ method: Method.POST, url: `/welcome/${version}` }) return this.authRequest<Unit>({ method: Method.POST, url: `/welcome/${version}` })
} }
async getVersionLatest(): Promise<ReqRes.GetVersionLatestRes> { async getVersionLatest (): Promise<ReqRes.GetVersionLatestRes> {
return this.authRequest<ReqRes.GetVersionLatestRes>({ method: Method.GET, url: '/versionLatest' }, { version: '' }) return this.authRequest<ReqRes.GetVersionLatestRes>({ method: Method.GET, url: '/versionLatest' }, { version: '' })
} }
async getServerMetrics(): Promise<ReqRes.GetServerMetricsRes> { async getServerMetrics (): Promise<ReqRes.GetServerMetricsRes> {
return this.authRequest<ReqRes.GetServerMetricsRes>({ method: Method.GET, url: `/metrics` }) return this.authRequest<ReqRes.GetServerMetricsRes>({ method: Method.GET, url: `/metrics` })
} }
async getNotifications(page: number, perPage: number): Promise<S9Notification[]> { async getNotifications (page: number, perPage: number): Promise<S9Notification[]> {
const params: ReqRes.GetNotificationsReq = { const params: ReqRes.GetNotificationsReq = {
page: String(page), page: String(page),
perPage: String(perPage), perPage: String(perPage),
@@ -66,27 +66,27 @@ export class LiveApiService extends ApiService {
return this.authRequest<ReqRes.GetNotificationsRes>({ method: Method.GET, url: `/notifications`, params }) return this.authRequest<ReqRes.GetNotificationsRes>({ method: Method.GET, url: `/notifications`, params })
} }
async deleteNotification(id: string): Promise<Unit> { async deleteNotification (id: string): Promise<Unit> {
return this.authRequest({ method: Method.DELETE, url: `/notifications/${id}` }) return this.authRequest({ method: Method.DELETE, url: `/notifications/${id}` })
} }
async getExternalDisks(): Promise<DiskInfo[]> { async getExternalDisks (): Promise<DiskInfo[]> {
return this.authRequest<ReqRes.GetExternalDisksRes>({ method: Method.GET, url: `/disks` }) return this.authRequest<ReqRes.GetExternalDisksRes>({ method: Method.GET, url: `/disks` })
} }
// TODO: EJECT-DISKS // TODO: EJECT-DISKS
async ejectExternalDisk(logicalName: string): Promise<Unit> { async ejectExternalDisk (logicalName: string): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/disks/eject`, data: { logicalName } }) return this.authRequest({ method: Method.POST, url: `/disks/eject`, data: { logicalName } })
} }
async updateAgent(version: string): Promise<Unit> { async updateAgent (version: string): Promise<Unit> {
const data: ReqRes.PostUpdateAgentReq = { const data: ReqRes.PostUpdateAgentReq = {
version: `=${version}`, version: `=${version}`,
} }
return this.authRequest({ method: Method.POST, url: '/update', data }) return this.authRequest({ method: Method.POST, url: '/update', data })
} }
async getAvailableAppVersionSpecificInfo(appId: string, versionSpec: string): Promise<AppAvailableVersionSpecificInfo> { async getAvailableAppVersionSpecificInfo (appId: string, versionSpec: string): Promise<AppAvailableVersionSpecificInfo> {
return this return this
.authRequest<Replace<ReqRes.GetAppAvailableVersionInfoRes, 'versionViewing', 'version'>>({ method: Method.GET, url: `/apps/${appId}/store/${versionSpec}` }) .authRequest<Replace<ReqRes.GetAppAvailableVersionInfoRes, 'versionViewing', 'version'>>({ method: Method.GET, url: `/apps/${appId}/store/${versionSpec}` })
.then(res => ({ ...res, versionViewing: res.version })) .then(res => ({ ...res, versionViewing: res.version }))
@@ -96,7 +96,7 @@ export class LiveApiService extends ApiService {
}) })
} }
async getAvailableApps(): Promise<AppAvailablePreview[]> { async getAvailableApps (): Promise<AppAvailablePreview[]> {
const res = await this.authRequest<ReqRes.GetAppsAvailableRes>({ method: Method.GET, url: '/apps/store' }) const res = await this.authRequest<ReqRes.GetAppsAvailableRes>({ method: Method.GET, url: '/apps/store' })
return res.map(a => { return res.map(a => {
const latestVersionTimestamp = new Date(a.latestVersionTimestamp) const latestVersionTimestamp = new Date(a.latestVersionTimestamp)
@@ -105,7 +105,7 @@ export class LiveApiService extends ApiService {
}) })
} }
async getAvailableApp(appId: string): Promise<AppAvailableFull> { async getAvailableApp (appId: string): Promise<AppAvailableFull> {
return this.authRequest<ReqRes.GetAppAvailableRes>({ method: Method.GET, url: `/apps/${appId}/store` }) return this.authRequest<ReqRes.GetAppAvailableRes>({ method: Method.GET, url: `/apps/${appId}/store` })
.then(res => { .then(res => {
return { return {
@@ -115,7 +115,7 @@ export class LiveApiService extends ApiService {
}) })
} }
async getInstalledApp(appId: string): Promise<AppInstalledFull> { async getInstalledApp (appId: string): Promise<AppInstalledFull> {
return this.authRequest<ReqRes.GetAppInstalledRes>({ method: Method.GET, url: `/apps/${appId}/installed` }) return this.authRequest<ReqRes.GetAppInstalledRes>({ method: Method.GET, url: `/apps/${appId}/installed` })
.then(app => { .then(app => {
return { return {
@@ -127,7 +127,7 @@ export class LiveApiService extends ApiService {
}) })
} }
async getInstalledApps(): Promise<AppInstalledPreview[]> { async getInstalledApps (): Promise<AppInstalledPreview[]> {
return this.authRequest<ReqRes.GetAppsInstalledRes>({ method: Method.GET, url: `/apps/installed` }) return this.authRequest<ReqRes.GetAppsInstalledRes>({ method: Method.GET, url: `/apps/installed` })
.then(apps => { .then(apps => {
return apps.map(app => { return apps.map(app => {
@@ -140,24 +140,24 @@ export class LiveApiService extends ApiService {
}) })
} }
async getAppConfig(appId: string): Promise<ReqRes.GetAppConfigRes> { async getAppConfig (appId: string): Promise<ReqRes.GetAppConfigRes> {
return this.authRequest<ReqRes.GetAppConfigRes>({ method: Method.GET, url: `/apps/${appId}/config` }) return this.authRequest<ReqRes.GetAppConfigRes>({ method: Method.GET, url: `/apps/${appId}/config` })
} }
async getAppLogs(appId: string, params: ReqRes.GetAppLogsReq = {}): Promise<string[]> { async getAppLogs (appId: string, params: ReqRes.GetAppLogsReq = { }): Promise<string[]> {
return this.authRequest<ReqRes.GetAppLogsRes>({ method: Method.GET, url: `/apps/${appId}/logs`, params: params as any }) return this.authRequest<ReqRes.GetAppLogsRes>({ method: Method.GET, url: `/apps/${appId}/logs`, params: params as any })
} }
async getServerLogs(): Promise<string[]> { async getServerLogs (): Promise<string[]> {
return this.authRequest<ReqRes.GetServerLogsRes>({ method: Method.GET, url: `/logs` }) return this.authRequest<ReqRes.GetServerLogsRes>({ method: Method.GET, url: `/logs` })
} }
async getAppMetrics(appId: string): Promise<AppMetrics> { async getAppMetrics (appId: string): Promise<AppMetrics> {
return this.authRequest<ReqRes.GetAppMetricsRes | string>({ method: Method.GET, url: `/apps/${appId}/metrics` }) return this.authRequest<ReqRes.GetAppMetricsRes | string>({ method: Method.GET, url: `/apps/${appId}/metrics` })
.then(parseMetricsPermissive) .then(parseMetricsPermissive)
} }
async installApp(appId: string, version: string, dryRun: boolean = false): Promise<AppInstalledFull & { breakages: DependentBreakage[] }> { async installApp (appId: string, version: string, dryRun: boolean = false): Promise<AppInstalledFull & { breakages: DependentBreakage[] }> {
const data: ReqRes.PostInstallAppReq = { const data: ReqRes.PostInstallAppReq = {
version, version,
} }
@@ -172,94 +172,94 @@ export class LiveApiService extends ApiService {
}) })
} }
async uninstallApp(appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> { async uninstallApp (appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/uninstall${dryRunParam(dryRun, true)}`, readTimeout: 60000 }) return this.authRequest({ method: Method.POST, url: `/apps/${appId}/uninstall${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
} }
async startApp(appId: string): Promise<Unit> { async startApp (appId: string): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/start`, readTimeout: 60000 }) return this.authRequest({ method: Method.POST, url: `/apps/${appId}/start`, readTimeout: 60000 })
.then(() => this.appModel.update({ id: appId, status: AppStatus.RUNNING })) .then(() => this.appModel.update({ id: appId, status: AppStatus.RUNNING }))
.then(() => ({})) .then(() => ({ }))
} }
async stopApp(appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> { async stopApp (appId: string, dryRun: boolean = false): Promise<{ breakages: DependentBreakage[] }> {
const res = await this.authRequest<{ breakages: DependentBreakage[] }>({ method: Method.POST, url: `/apps/${appId}/stop${dryRunParam(dryRun, true)}`, readTimeout: 60000 }) const res = await this.authRequest<{ breakages: DependentBreakage[] }>({ method: Method.POST, url: `/apps/${appId}/stop${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
if (!dryRun) this.appModel.update({ id: appId, status: AppStatus.STOPPING }, modulateTime(new Date(), 5, 'seconds')) if (!dryRun) this.appModel.update({ id: appId, status: AppStatus.STOPPING }, modulateTime(new Date(), 5, 'seconds'))
return res return res
} }
async restartApp(appId: string): Promise<Unit> { async restartApp (appId: string): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/restart`, readTimeout: 60000 }) return this.authRequest({ method: Method.POST, url: `/apps/${appId}/restart`, readTimeout: 60000 })
.then(() => ({} as any)) .then(() => ({ } as any))
} }
async createAppBackup(appId: string, logicalname: string, password?: string): Promise<Unit> { async createAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit> {
const data: ReqRes.PostAppBackupCreateReq = { const data: ReqRes.PostAppBackupCreateReq = {
password: password || undefined, password: password || undefined,
logicalname, logicalname,
} }
return this.authRequest<ReqRes.PostAppBackupCreateRes>({ method: Method.POST, url: `/apps/${appId}/backup`, data, readTimeout: 60000 }) return this.authRequest<ReqRes.PostAppBackupCreateRes>({ method: Method.POST, url: `/apps/${appId}/backup`, data, readTimeout: 60000 })
.then(() => this.appModel.update({ id: appId, status: AppStatus.CREATING_BACKUP })) .then(() => this.appModel.update({ id: appId, status: AppStatus.CREATING_BACKUP }))
.then(() => ({})) .then(() => ({ }))
} }
async stopAppBackup(appId: string): Promise<Unit> { async stopAppBackup (appId: string): Promise<Unit> {
return this.authRequest<ReqRes.PostAppBackupStopRes>({ method: Method.POST, url: `/apps/${appId}/backup/stop`, readTimeout: 60000 }) return this.authRequest<ReqRes.PostAppBackupStopRes>({ method: Method.POST, url: `/apps/${appId}/backup/stop`, readTimeout: 60000 })
.then(() => this.appModel.update({ id: appId, status: AppStatus.STOPPED })) .then(() => this.appModel.update({ id: appId, status: AppStatus.STOPPED }))
.then(() => ({})) .then(() => ({ }))
} }
async restoreAppBackup(appId: string, logicalname: string, password?: string): Promise<Unit> { async restoreAppBackup (appId: string, logicalname: string, password?: string): Promise<Unit> {
const data: ReqRes.PostAppBackupRestoreReq = { const data: ReqRes.PostAppBackupRestoreReq = {
password: password || undefined, password: password || undefined,
logicalname, logicalname,
} }
return this.authRequest<ReqRes.PostAppBackupRestoreRes>({ method: Method.POST, url: `/apps/${appId}/backup/restore`, data, readTimeout: 60000 }) return this.authRequest<ReqRes.PostAppBackupRestoreRes>({ method: Method.POST, url: `/apps/${appId}/backup/restore`, data, readTimeout: 60000 })
.then(() => this.appModel.update({ id: appId, status: AppStatus.RESTORING_BACKUP })) .then(() => this.appModel.update({ id: appId, status: AppStatus.RESTORING_BACKUP }))
.then(() => ({})) .then(() => ({ }))
} }
async patchAppConfig(app: AppInstalledPreview, config: object, dryRun = false): Promise<{ breakages: DependentBreakage[] }> { async patchAppConfig (app: AppInstalledPreview, config: object, dryRun = false): Promise<{ breakages: DependentBreakage[] }> {
const data: ReqRes.PatchAppConfigReq = { const data: ReqRes.PatchAppConfigReq = {
config, config,
} }
return this.authRequest({ method: Method.PATCH, url: `/apps/${app.id}/config${dryRunParam(dryRun, true)}`, data, readTimeout: 60000 }) return this.authRequest({ method: Method.PATCH, url: `/apps/${app.id}/config${dryRunParam(dryRun, true)}`, data, readTimeout: 60000 })
} }
async postConfigureDependency(dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }> { async postConfigureDependency (dependencyId: string, dependentId: string, dryRun?: boolean): Promise<{ config: object, breakages: DependentBreakage[] }> {
return this.authRequest({ method: Method.POST, url: `/apps/${dependencyId}/autoconfig/${dependentId}${dryRunParam(dryRun, true)}`, readTimeout: 60000 }) return this.authRequest({ method: Method.POST, url: `/apps/${dependencyId}/autoconfig/${dependentId}${dryRunParam(dryRun, true)}`, readTimeout: 60000 })
} }
async patchServerConfig(attr: string, value: any): Promise<Unit> { async patchServerConfig (attr: string, value: any): Promise<Unit> {
const data: ReqRes.PatchServerConfigReq = { const data: ReqRes.PatchServerConfigReq = {
value, value,
} }
return this.authRequest({ method: Method.PATCH, url: `/${attr}`, data, readTimeout: 60000 }) return this.authRequest({ method: Method.PATCH, url: `/${attr}`, data, readTimeout: 60000 })
.then(() => this.serverModel.update({ [attr]: value })) .then(() => this.serverModel.update({ [attr]: value }))
.then(() => ({})) .then(() => ({ }))
} }
async wipeAppData(app: AppInstalledPreview): Promise<Unit> { async wipeAppData (app: AppInstalledPreview): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/apps/${app.id}/wipe`, readTimeout: 60000 }).then((res) => { return this.authRequest({ method: Method.POST, url: `/apps/${app.id}/wipe`, readTimeout: 60000 }).then((res) => {
this.appModel.update({ id: app.id, status: AppStatus.NEEDS_CONFIG }) this.appModel.update({ id: app.id, status: AppStatus.NEEDS_CONFIG })
return res return res
}) })
} }
async toggleAppLAN(appId: string, toggle: 'enable' | 'disable'): Promise<Unit> { async toggleAppLAN (appId: string, toggle: 'enable' | 'disable'): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/lan/${toggle}` }) return this.authRequest({ method: Method.POST, url: `/apps/${appId}/lan/${toggle}` })
} }
async addSSHKey(sshKey: string): Promise<Unit> { async addSSHKey (sshKey: string): Promise<Unit> {
const data: ReqRes.PostAddSSHKeyReq = { const data: ReqRes.PostAddSSHKeyReq = {
sshKey, sshKey,
} }
const fingerprint = await this.authRequest<ReqRes.PostAddSSHKeyRes>({ method: Method.POST, url: `/sshKeys`, data }) const fingerprint = await this.authRequest<ReqRes.PostAddSSHKeyRes>({ method: Method.POST, url: `/sshKeys`, data })
this.serverModel.update({ ssh: [...this.serverModel.peek().ssh, fingerprint] }) this.serverModel.update({ ssh: [...this.serverModel.peek().ssh, fingerprint] })
return {} return { }
} }
async addWifi(ssid: string, password: string, country: string, connect: boolean): Promise<Unit> { async addWifi (ssid: string, password: string, country: string, connect: boolean): Promise<Unit> {
const data: ReqRes.PostAddWifiReq = { const data: ReqRes.PostAddWifiReq = {
ssid, ssid,
password, password,
@@ -269,30 +269,30 @@ export class LiveApiService extends ApiService {
return this.authRequest({ method: Method.POST, url: `/wifi`, data }) return this.authRequest({ method: Method.POST, url: `/wifi`, data })
} }
async connectWifi(ssid: string): Promise<Unit> { async connectWifi (ssid: string): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: encodeURI(`/wifi/${ssid}`) }) return this.authRequest({ method: Method.POST, url: encodeURI(`/wifi/${ssid}`) })
} }
async deleteWifi(ssid: string): Promise<Unit> { async deleteWifi (ssid: string): Promise<Unit> {
return this.authRequest({ method: Method.DELETE, url: encodeURI(`/wifi/${ssid}`) }) return this.authRequest({ method: Method.DELETE, url: encodeURI(`/wifi/${ssid}`) })
} }
async deleteSSHKey(fingerprint: SSHFingerprint): Promise<Unit> { async deleteSSHKey (fingerprint: SSHFingerprint): Promise<Unit> {
await this.authRequest({ method: Method.DELETE, url: `/sshKeys/${fingerprint.hash}` }) await this.authRequest({ method: Method.DELETE, url: `/sshKeys/${fingerprint.hash}` })
const ssh = this.serverModel.peek().ssh const ssh = this.serverModel.peek().ssh
this.serverModel.update({ ssh: ssh.filter(s => s !== fingerprint) }) this.serverModel.update({ ssh: ssh.filter(s => s !== fingerprint) })
return {} return { }
} }
async restartServer(): Promise<Unit> { async restartServer (): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: '/restart', readTimeout: 60000 }) return this.authRequest({ method: Method.POST, url: '/restart', readTimeout: 60000 })
} }
async shutdownServer(): Promise<Unit> { async shutdownServer (): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: '/shutdown', readTimeout: 60000 }) return this.authRequest({ method: Method.POST, url: '/shutdown', readTimeout: 60000 })
} }
async serviceAction(appId: string, s: ServiceAction): Promise<ReqRes.ServiceActionResponse> { async serviceAction (appId: string, s: ServiceAction): Promise<ReqRes.ServiceActionResponse> {
const data: ReqRes.ServiceActionRequest = { const data: ReqRes.ServiceActionRequest = {
jsonrpc: '2.0', jsonrpc: '2.0',
id: uuid.v4(), id: uuid.v4(),
@@ -301,11 +301,15 @@ export class LiveApiService extends ApiService {
return this.authRequest({ method: Method.POST, url: `/apps/${appId}/actions`, data, readTimeout: 300000 }) return this.authRequest({ method: Method.POST, url: `/apps/${appId}/actions`, data, readTimeout: 300000 })
} }
async refreshLAN(): Promise<Unit> { async refreshLAN (): Promise<Unit> {
return this.authRequest({ method: Method.POST, url: '/network/lan/reset' }) return this.authRequest({ method: Method.POST, url: '/network/lan/reset' })
} }
private async authRequest<T>(opts: HttpOptions, overrides: Partial<{ version: string }> = {}): Promise<T> { async checkV1Status (): Promise<V1Status> {
return this.http.request({ method: Method.GET, url: 'https://registry.start9labs.com/sys/status' })
}
private async authRequest<T> (opts: HttpOptions, overrides: Partial<{ version: string }> = { }): Promise<T> {
if (!this.authenticatedRequestsEnabled) throw new Error(`Authenticated requests are not enabled. Do you need to login?`) if (!this.authenticatedRequestsEnabled) throw new Error(`Authenticated requests are not enabled. Do you need to login?`)
opts.withCredentials = true opts.withCredentials = true
@@ -324,7 +328,7 @@ const dryRunParam = (dryRun: boolean, first: boolean) => {
return first ? `?dryrun` : `&dryrun` return first ? `?dryrun` : `&dryrun`
} }
function catchHttpStatusError(error: HttpErrorResponse): Observable<true> { function catchHttpStatusError (error: HttpErrorResponse): Observable<true> {
if (error.error instanceof ErrorEvent) { if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly. // A client-side or network error occurred. Handle it accordingly.
return throwError('Not Connected') return throwError('Not Connected')

View File

@@ -4,7 +4,7 @@ import { AppAvailablePreview, AppAvailableFull, AppInstalledPreview, AppInstalle
import { S9Notification, SSHFingerprint, ServerStatus, ServerModel, DiskInfo } from '../../models/server-model' import { S9Notification, SSHFingerprint, ServerStatus, ServerModel, DiskInfo } from '../../models/server-model'
import { pauseFor } from '../../util/misc.util' import { pauseFor } from '../../util/misc.util'
import { ApiService, ReqRes } from './api.service' import { ApiService, ReqRes } from './api.service'
import { ApiAppInstalledFull, ApiAppInstalledPreview, ApiServer, Unit as EmptyResponse, Unit } from './api-types' import { ApiAppInstalledFull, ApiAppInstalledPreview, ApiServer, Unit as EmptyResponse, Unit, V1Status } from './api-types'
import { AppMetrics, AppMetricsVersioned, parseMetricsPermissive } from 'src/app/util/metrics.util' import { AppMetrics, AppMetricsVersioned, parseMetricsPermissive } from 'src/app/util/metrics.util'
import { mockApiAppAvailableFull, mockApiAppAvailableVersionInfo, mockApiAppInstalledFull, mockAppDependentBreakages, toInstalledPreview } from './mock-app-fixures' import { mockApiAppAvailableFull, mockApiAppAvailableVersionInfo, mockApiAppInstalledFull, mockAppDependentBreakages, toInstalledPreview } from './mock-app-fixures'
import { ConfigService } from '../config.service' import { ConfigService } from '../config.service'
@@ -273,12 +273,19 @@ export class MockApiService extends ApiService {
return mockRefreshLAN() return mockRefreshLAN()
} }
async checkV1Status (): Promise<V1Status> {
return {
status: 'instructions',
version: '1.0.0',
}
}
private hasUI (app: ApiAppInstalledPreview): boolean { private hasUI (app: ApiAppInstalledPreview): boolean {
return app.lanUi || app.torUi return app.lanUi || app.torUi
} }
private isLaunchable (app: ApiAppInstalledPreview): boolean { private isLaunchable (app: ApiAppInstalledPreview): boolean {
return !this.config.isConsulate && return !this.config.isConsulate &&
app.status === AppStatus.RUNNING && app.status === AppStatus.RUNNING &&
( (
(app.torAddress && app.torUi && this.config.isTor()) || (app.torAddress && app.torUi && this.config.isTor()) ||
@@ -355,7 +362,7 @@ async function mockGetServerLogs (): Promise<ReqRes.GetServerLogsRes> {
async function mockGetAppMetrics (): Promise<ReqRes.GetAppMetricsRes> { async function mockGetAppMetrics (): Promise<ReqRes.GetAppMetricsRes> {
await pauseFor(1000) await pauseFor(1000)
return mockApiAppMetricsV1 return mockApiAppMetricsV1 as ReqRes.GetAppMetricsRes
} }
async function mockGetAvailableAppVersionInfo (): Promise<ReqRes.GetAppAvailableVersionInfoRes> { async function mockGetAvailableAppVersionInfo (): Promise<ReqRes.GetAppAvailableVersionInfoRes> {
@@ -492,8 +499,8 @@ const mockApiNotifications: ReqRes.GetNotificationsRes = [
const mockApiServer: () => ReqRes.GetServerRes = () => ({ const mockApiServer: () => ReqRes.GetServerRes = () => ({
serverId: 'start9-mockxyzab', serverId: 'start9-mockxyzab',
name: 'Embassy:12345678', name: 'Embassy:12345678',
versionInstalled: '0.2.12', versionInstalled: '0.2.16',
versionLatest: '0.2.13', versionLatest: '0.2.16',
status: ServerStatus.RUNNING, status: ServerStatus.RUNNING,
alternativeRegistryUrl: 'beta-registry.start9labs.com', alternativeRegistryUrl: 'beta-registry.start9labs.com',
welcomeAck: true, welcomeAck: true,

View File

@@ -54,6 +54,8 @@ export const bitcoinI: ApiAppInstalledFull = {
versionInstalled: '0.18.1', versionInstalled: '0.18.1',
lanAddress: undefined, lanAddress: undefined,
title: 'Bitcoin Core', title: 'Bitcoin Core',
licenseName: 'MIT',
licenseLink: 'https://github.com/bitcoin/bitcoin/blob/master/COPYING',
torAddress: '4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpbi7utijcltosqemad.onion', torAddress: '4acth47i6kxnvkewtm6q7ib2s3ufpo5sqbsnzjpbi7utijcltosqemad.onion',
startAlert: 'Bitcoind could take a loooooong time to start. Please be patient.', startAlert: 'Bitcoind could take a loooooong time to start. Please be patient.',
status: AppStatus.STOPPED, status: AppStatus.STOPPED,
@@ -147,6 +149,8 @@ export const bitcoinA: AppAvailableFull = {
id: 'bitcoind', id: 'bitcoind',
versionLatest: '0.19.1.1', versionLatest: '0.19.1.1',
versionInstalled: '0.19.0', versionInstalled: '0.19.0',
licenseName: 'MIT',
licenseLink: 'https://github.com/bitcoin/bitcoin/blob/master/COPYING',
status: AppStatus.UNKNOWN, status: AppStatus.UNKNOWN,
title: 'Bitcoin Core', title: 'Bitcoin Core',
descriptionShort: 'Bitcoin is an innovative payment network and new kind of money.', descriptionShort: 'Bitcoin is an innovative payment network and new kind of money.',

View File

@@ -5,6 +5,7 @@ import { WizardBaker } from '../components/install-wizard/prebaked-wizards'
import { OSWelcomePage } from '../modals/os-welcome/os-welcome.page' import { OSWelcomePage } from '../modals/os-welcome/os-welcome.page'
import { S9Server } from '../models/server-model' import { S9Server } from '../models/server-model'
import { displayEmver } from '../pipes/emver.pipe' import { displayEmver } from '../pipes/emver.pipe'
import { V1Status } from './api/api-types'
import { ApiService, ReqRes } from './api/api.service' import { ApiService, ReqRes } from './api/api.service'
import { ConfigService } from './config.service' import { ConfigService } from './config.service'
import { Emver } from './emver.service' import { Emver } from './emver.service'
@@ -36,6 +37,13 @@ export class StartupAlertsNotifier {
display: vl => this.displayOsUpdateCheck(vl), display: vl => this.displayOsUpdateCheck(vl),
hasRun: this.config.skipStartupAlerts, hasRun: this.config.skipStartupAlerts,
} }
const v1StatusUpdate: Check<V1Status> = {
name: 'v1Status',
shouldRun: s => this.shouldRunOsUpdateCheck(s),
check: () => this.v1StatusCheck(),
display: s => this.displayV1Check(s),
hasRun: this.config.skipStartupAlerts,
}
const apps: Check<boolean> = { const apps: Check<boolean> = {
name: 'apps', name: 'apps',
shouldRun: s => this.shouldRunAppsCheck(s), shouldRun: s => this.shouldRunAppsCheck(s),
@@ -43,7 +51,7 @@ export class StartupAlertsNotifier {
display: () => this.displayAppsCheck(), display: () => this.displayAppsCheck(),
hasRun: this.config.skipStartupAlerts, hasRun: this.config.skipStartupAlerts,
} }
this.checks = [welcome, osUpdate, apps] this.checks = [welcome, osUpdate, v1StatusUpdate, apps]
} }
// This takes our three checks and filters down to those that should run. // This takes our three checks and filters down to those that should run.
@@ -85,6 +93,10 @@ export class StartupAlertsNotifier {
return server.autoCheckUpdates return server.autoCheckUpdates
} }
private async v1StatusCheck (): Promise<V1Status> {
return this.apiService.checkV1Status()
}
private async osUpdateCheck (s: Readonly<S9Server>): Promise<ReqRes.GetVersionLatestRes | undefined> { private async osUpdateCheck (s: Readonly<S9Server>): Promise<ReqRes.GetVersionLatestRes | undefined> {
const res = await this.apiService.getVersionLatest() const res = await this.apiService.getVersionLatest()
return this.osUpdateService.updateIsAvailable(s.versionInstalled, res) ? res : undefined return this.osUpdateService.updateIsAvailable(s.versionInstalled, res) ? res : undefined
@@ -131,6 +143,33 @@ export class StartupAlertsNotifier {
return true return true
} }
private async displayV1Check (s: V1Status): Promise<boolean> {
return new Promise(async resolve => {
if (s.status !== 'available') return resolve(true)
const alert = await this.alertCtrl.create({
backdropDismiss: true,
header: `EmbassyOS ${s.version} Now Available!`,
message: `Version ${s.version} introduces SSD support and a whole lot more.`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
handler: () => resolve(true),
},
{
text: 'View Instructions',
handler: () => {
window.open(`https://start9.com/eos-${s.version}`, '_blank')
resolve(false)
},
},
],
})
await alert.present()
})
}
private async displayAppsCheck (): Promise<boolean> { private async displayAppsCheck (): Promise<boolean> {
return new Promise(async resolve => { return new Promise(async resolve => {
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({