mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11b007a31d | ||
|
|
5b8f27e53e | ||
|
|
9f4523676f | ||
|
|
bc5163d800 | ||
|
|
9b7fe03c19 | ||
|
|
9a2aaa08b8 | ||
|
|
8c87e6653c | ||
|
|
1c3b16e870 | ||
|
|
276085f084 | ||
|
|
52fc992090 | ||
|
|
af46a375a9 | ||
|
|
74a559eade | ||
|
|
f12d97122a | ||
|
|
ba9b3519de | ||
|
|
43e89df652 | ||
|
|
7bdc109bd4 | ||
|
|
ac5dec476d | ||
|
|
1f56be3cbf | ||
|
|
ed46ddbf44 | ||
|
|
2973c316a8 | ||
|
|
7e7a9dc140 | ||
|
|
b95686282d | ||
|
|
09f858d28d | ||
|
|
424afb3d1c | ||
|
|
a056f6d318 | ||
|
|
5339b23ea6 | ||
|
|
bd61510c24 | ||
|
|
91557c39e5 | ||
|
|
894fa21002 | ||
|
|
d611c69b0c | ||
|
|
d430986403 | ||
|
|
c8aafbdbc9 | ||
|
|
2e3e1401f5 | ||
|
|
deb0b1e561 | ||
|
|
daf701a76c | ||
|
|
43035e7271 | ||
|
|
e37db33d62 | ||
|
|
adab9e7fca | ||
|
|
a21bd91460 | ||
|
|
acc2722586 | ||
|
|
d8d6541b11 | ||
|
|
1a7d40afa9 | ||
|
|
b152a93dd8 | ||
|
|
ae90b70348 | ||
|
|
21f982e9a6 | ||
|
|
4100d4ca97 | ||
|
|
f5ae93c999 | ||
|
|
2f5ad4d82b | ||
|
|
6e2a332bcd | ||
|
|
4a2e496e8a | ||
|
|
e69a936fb8 | ||
|
|
d9894d4082 | ||
|
|
6b3fa54551 | ||
|
|
9f47a34b11 | ||
|
|
531dec936d | ||
|
|
1d7684f4d4 | ||
|
|
cfacbcabd3 | ||
|
|
4fcdf5f832 | ||
|
|
2189c5643d | ||
|
|
aada5755de | ||
|
|
60d31163c5 | ||
|
|
fd6a1897c8 | ||
|
|
62e0f742ba | ||
|
|
c42ff81a38 | ||
|
|
cc49a73954 | ||
|
|
29a4506a40 | ||
|
|
efa60bf4ab | ||
|
|
1c2fd192df | ||
|
|
0a9349bbc1 | ||
|
|
653961da64 | ||
|
|
6585d91816 | ||
|
|
3e3097945f | ||
|
|
c0f5f09767 | ||
|
|
1c8889a60c | ||
|
|
218bae3b46 | ||
|
|
92c297648c | ||
|
|
68eccdb63c | ||
|
|
ee1c66d0c2 | ||
|
|
c52f75c9e3 | ||
|
|
b46c75e391 | ||
|
|
7fb8f88c8d | ||
|
|
c83baec363 | ||
|
|
882cfde5f3 | ||
|
|
53720130b3 | ||
|
|
7c321bbf6b | ||
|
|
bd060670e4 | ||
|
|
7ff538a526 | ||
|
|
3c74f3d46e | ||
|
|
54ae7f82d6 | ||
|
|
39867478d0 | ||
|
|
8e2642a741 | ||
|
|
a4f7d53a6b | ||
|
|
397236c68e | ||
|
|
8ce43d808e | ||
|
|
e1200c2991 | ||
|
|
0937c81e46 | ||
|
|
02ab63da81 | ||
|
|
5cf7d1ff88 | ||
|
|
a20970fa17 | ||
|
|
30dd62285b | ||
|
|
3065323e79 | ||
|
|
e1a6a3d9ed | ||
|
|
c0e08df221 | ||
|
|
108213f920 | ||
|
|
a8e229821f | ||
|
|
a6b7d657a0 | ||
|
|
77b8d0b2a0 | ||
|
|
9503f754ad | ||
|
|
540868220d | ||
|
|
dd8037fda1 | ||
|
|
6f09738b49 | ||
|
|
808fff4187 | ||
|
|
a9735fd777 | ||
|
|
327c79350e | ||
|
|
44def3be85 | ||
|
|
18df87b8f5 | ||
|
|
97a85d6e01 | ||
|
|
3d4930acb4 | ||
|
|
58468dd53f | ||
|
|
50a2be243a | ||
|
|
0d7b087665 | ||
|
|
0e87cce8de | ||
|
|
537f2d91b8 | ||
|
|
79604182c8 | ||
|
|
68faa17ab6 | ||
|
|
13a6d7f0c7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.DS_Store
|
||||
/*.img
|
||||
/buster.zip
|
||||
/product_key
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
## ⚠️ 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.
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
/v0/specs SpecsR GET
|
||||
/v0/metrics MetricsR GET
|
||||
|
||||
/v0/logs LogsR GET
|
||||
/v0/sshKeys SshKeysR GET POST
|
||||
/v0/sshKeys/#Text SshKeyByFingerprintR DELETE
|
||||
/v0/password PasswordR PATCH
|
||||
@@ -38,6 +39,9 @@
|
||||
/v0/apps/#AppId/backup/stop StopBackupR POST
|
||||
/v0/apps/#AppId/backup/restore RestoreBackupR POST
|
||||
/v0/apps/#AppId/autoconfig/#AppId AutoconfigureR POST
|
||||
/v0/apps/#AppId/actions ActionR POST
|
||||
|
||||
/v0/network/lan/reset ResetLanR POST
|
||||
|
||||
/v0/disks DisksR GET
|
||||
/v0/disks/eject EjectR POST
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Values formatted like "_env:YESOD_ENV_VAR_NAME:default_value" can be overridden by the specified environment variable.
|
||||
# See https://github.com/yesodweb/yesod/wiki/Configuration#overriding-configuration-values-with-environment-variables
|
||||
|
||||
static-dir: "_env:YESOD_STATIC_DIR:static"
|
||||
host: "_env:YESOD_HOST:*4" # any IPv4 host
|
||||
port: 5959 # NB: The port `yesod devel` uses is distinct from this value. Set the `yesod devel` port from the command line.
|
||||
static-dir: "_env:YESOD_STATIC_DIR:static"
|
||||
host: "_env:YESOD_HOST:*4" # any IPv4 host
|
||||
port: 5959 # NB: The port `yesod devel` uses is distinct from this value. Set the `yesod devel` port from the command line.
|
||||
ip-from-header: "_env:YESOD_IP_FROM_HEADER:false"
|
||||
detailed-logging: "_env:DETAILED_LOGGING:false"
|
||||
|
||||
@@ -33,6 +33,5 @@ database:
|
||||
database: "start9_agent.sqlite3"
|
||||
poolsize: "_env:YESOD_SQLITE_POOLSIZE:10"
|
||||
|
||||
app-mgr-version-spec: "=0.2.8"
|
||||
|
||||
app-mgr-version-spec: "=0.2.11"
|
||||
#analytics: UA-YOURCODE
|
||||
|
||||
1
agent/migrations/0.2.10::0.2.11
Normal file
1
agent/migrations/0.2.10::0.2.11
Normal file
@@ -0,0 +1 @@
|
||||
SELECT TRUE;
|
||||
1
agent/migrations/0.2.8::0.2.9
Normal file
1
agent/migrations/0.2.8::0.2.9
Normal file
@@ -0,0 +1 @@
|
||||
SELECT TRUE;
|
||||
1
agent/migrations/0.2.9::0.2.10
Normal file
1
agent/migrations/0.2.9::0.2.10
Normal file
@@ -0,0 +1 @@
|
||||
SELECT TRUE;
|
||||
@@ -1,116 +1,117 @@
|
||||
name: ambassador-agent
|
||||
version: 0.2.8
|
||||
version: 0.2.11
|
||||
|
||||
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
|
||||
- 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
|
||||
|
||||
dependencies:
|
||||
- base >=4.9.1.0 && <5
|
||||
- aeson
|
||||
- aeson-flatten
|
||||
- attoparsec
|
||||
- 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
|
||||
- lens
|
||||
- lens-aeson
|
||||
- lifted-async
|
||||
- lifted-base
|
||||
- memory
|
||||
- mime-types
|
||||
- monad-control
|
||||
- monad-logger
|
||||
- network
|
||||
- persistent
|
||||
- persistent-sqlite
|
||||
- persistent-template
|
||||
- process
|
||||
- process-extras
|
||||
- protolude
|
||||
- resourcet
|
||||
- regex-compat # TODO: trim this dep
|
||||
- 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 # TODO: trim this dep
|
||||
- unliftio-core # TODO: trim this dep
|
||||
- unordered-containers
|
||||
- uuid
|
||||
- wai
|
||||
- wai-cors
|
||||
- wai-extra
|
||||
- warp
|
||||
- yaml
|
||||
- yesod
|
||||
- yesod-auth
|
||||
- yesod-core
|
||||
- yesod-form
|
||||
- yesod-persistent
|
||||
- base >=4.9.1.0 && <5
|
||||
- aeson
|
||||
- aeson-flatten
|
||||
- attoparsec
|
||||
- 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
|
||||
- resourcet
|
||||
- regex-compat # TODO: trim this dep
|
||||
- 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 # TODO: trim this dep
|
||||
- unliftio-core # TODO: trim this dep
|
||||
- unordered-containers
|
||||
- uuid
|
||||
- wai
|
||||
- wai-cors
|
||||
- wai-extra
|
||||
- warp
|
||||
- yaml
|
||||
- yesod
|
||||
- yesod-auth
|
||||
- yesod-core
|
||||
- yesod-form
|
||||
- yesod-persistent
|
||||
|
||||
flags:
|
||||
library-only:
|
||||
@@ -128,56 +129,56 @@ flags:
|
||||
library:
|
||||
source-dirs: src
|
||||
when:
|
||||
- condition: (flag(dev)) || (flag(library-only))
|
||||
then:
|
||||
cpp-options: -DDEVELOPMENT
|
||||
ghc-options:
|
||||
- -Wall
|
||||
- -Wunused-packages
|
||||
- -fwarn-tabs
|
||||
- -O0
|
||||
- -fdefer-typed-holes
|
||||
else:
|
||||
ghc-options:
|
||||
- -Wall
|
||||
- -Wunused-packages
|
||||
- -fwarn-tabs
|
||||
- -O2
|
||||
- -fdefer-typed-holes
|
||||
- condition: (flag(disable-auth))
|
||||
cpp-options: -DDISABLE_AUTH
|
||||
- condition: (flag(dev)) || (flag(library-only))
|
||||
then:
|
||||
cpp-options: -DDEVELOPMENT
|
||||
ghc-options:
|
||||
- -Wall
|
||||
- -Wunused-packages
|
||||
- -fwarn-tabs
|
||||
- -O0
|
||||
- -fdefer-typed-holes
|
||||
else:
|
||||
ghc-options:
|
||||
- -Wall
|
||||
- -Wunused-packages
|
||||
- -fwarn-tabs
|
||||
- -O2
|
||||
- -fdefer-typed-holes
|
||||
- condition: (flag(disable-auth))
|
||||
cpp-options: -DDISABLE_AUTH
|
||||
tests:
|
||||
agent-test:
|
||||
source-dirs: test
|
||||
main: Main.hs
|
||||
ghc-options:
|
||||
- -Wall
|
||||
- -fdefer-typed-holes
|
||||
- -Wall
|
||||
- -fdefer-typed-holes
|
||||
dependencies:
|
||||
- ambassador-agent
|
||||
- hspec >=2.0.0
|
||||
- hspec-expectations
|
||||
- hedgehog
|
||||
- yesod-test
|
||||
- random
|
||||
- ambassador-agent
|
||||
- hspec >=2.0.0
|
||||
- hspec-expectations
|
||||
- hedgehog
|
||||
- yesod-test
|
||||
- random
|
||||
when:
|
||||
- condition: false
|
||||
other-modules: Paths_ambassador_agent
|
||||
- condition: false
|
||||
other-modules: Paths_ambassador_agent
|
||||
|
||||
executables:
|
||||
agent:
|
||||
source-dirs: app
|
||||
main: main.hs
|
||||
ghc-options:
|
||||
- -Wall
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
- -with-rtsopts=-N
|
||||
- -fdefer-typed-holes
|
||||
- -Wall
|
||||
- -threaded
|
||||
- -rtsopts
|
||||
- -with-rtsopts=-N
|
||||
- -fdefer-typed-holes
|
||||
dependencies:
|
||||
- ambassador-agent
|
||||
- ambassador-agent
|
||||
when:
|
||||
- buildable: false
|
||||
condition: flag(library-only)
|
||||
- condition: false
|
||||
other-modules: Paths_ambassador_agent
|
||||
- buildable: false
|
||||
condition: flag(library-only)
|
||||
- condition: false
|
||||
other-modules: Paths_ambassador_agent
|
||||
|
||||
@@ -54,21 +54,21 @@ import Yesod.Persist.Core
|
||||
import Constants
|
||||
import qualified Daemon.AppNotifications as AppNotifications
|
||||
import Daemon.RefreshProcDev
|
||||
import qualified Daemon.SslRenew as SSLRenew
|
||||
import Daemon.TorHealth
|
||||
import Daemon.ZeroConf
|
||||
import Foundation
|
||||
import Lib.Algebra.State.RegistryUrl
|
||||
import Lib.Background
|
||||
import Lib.Database
|
||||
import Lib.External.Metrics.ProcDev
|
||||
import Lib.SelfUpdate
|
||||
import Lib.Sound
|
||||
import Lib.SystemPaths
|
||||
import Lib.Tor ( newTorManager )
|
||||
import Lib.WebServer
|
||||
import Model
|
||||
import Settings
|
||||
import Lib.Background
|
||||
import qualified Daemon.SslRenew as SSLRenew
|
||||
import Lib.Tor (newTorManager)
|
||||
import Daemon.TorHealth
|
||||
|
||||
appMain :: IO ()
|
||||
appMain = do
|
||||
@@ -118,6 +118,7 @@ makeFoundation appSettings = do
|
||||
def <- getDefaultProcDevMetrics
|
||||
appProcDevMomentCache <- newIORef (now, mempty, def)
|
||||
appLastTorRestart <- newIORef now
|
||||
appLanThread <- forkIO (sleep 10) >>= newMVar
|
||||
|
||||
-- We need a log function to create a connection pool. We need a connection
|
||||
-- pool to create our foundation. And we need our foundation to get a
|
||||
|
||||
@@ -18,6 +18,9 @@ import Lib.ProductKey
|
||||
import Lib.SystemPaths
|
||||
|
||||
import Settings
|
||||
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
||||
import Control.Carrier.Lift
|
||||
import Lib.Error
|
||||
|
||||
start9AgentServicePrefix :: IsString a => a
|
||||
start9AgentServicePrefix = "start9-"
|
||||
@@ -53,4 +56,10 @@ publishAgentToAvahi = do
|
||||
"_http._tcp"
|
||||
agentPort
|
||||
lift Avahi.reload
|
||||
lift $ threadDelay 10_000_000
|
||||
tid <- asks appLanThread >>= liftIO . takeMVar
|
||||
liftIO $ killThread tid
|
||||
tid' <- liftIO $ forkIO (runM . void . runExceptT @S9Error $ AppMgr2.runAppMgrCliC AppMgr2.lanEnable)
|
||||
asks appLanThread >>= liftIO . flip putMVar tid'
|
||||
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ data AgentCtx = AgentCtx
|
||||
, appBackgroundJobs :: TVar JobCache
|
||||
, appIconTags :: TVar (HM.HashMap AppId (Digest MD5))
|
||||
, appLastTorRestart :: IORef UTCTime
|
||||
, appLanThread :: MVar ThreadId
|
||||
}
|
||||
|
||||
setWebProcessThreadId :: ThreadId -> AgentCtx -> IO ()
|
||||
@@ -185,6 +186,8 @@ cutoffDuringUpdate m = do
|
||||
path <- asks $ pathInfo . reqWaiRequest . handlerRequest
|
||||
case path of
|
||||
[v] | v == "v" <> (show . major $ agentVersion) -> m
|
||||
[auth] | auth == "auth" -> m
|
||||
(_:ssh:_) | ssh == "sshKeys" -> m
|
||||
_ -> handleS9ErrT $ throwE UpdateInProgressE
|
||||
Nothing -> m
|
||||
|
||||
|
||||
@@ -7,78 +7,82 @@
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
module Handler.Apps where
|
||||
|
||||
import Startlude hiding ( modify
|
||||
, execState
|
||||
import Startlude hiding ( Reader
|
||||
, asks
|
||||
, Reader
|
||||
, runReader
|
||||
, catchError
|
||||
, forkFinally
|
||||
, empty
|
||||
, execState
|
||||
, forkFinally
|
||||
, modify
|
||||
, runReader
|
||||
)
|
||||
|
||||
import Control.Carrier.Reader
|
||||
import Control.Carrier.Error.Church
|
||||
import Control.Carrier.Lift
|
||||
import Control.Carrier.Reader
|
||||
import qualified Control.Concurrent.Async.Lifted
|
||||
as LAsync
|
||||
import qualified Control.Concurrent.Lifted as Lifted
|
||||
import qualified Control.Exception.Lifted as Lifted
|
||||
import Control.Concurrent.STM.TVar
|
||||
import Control.Effect.Empty hiding ( guard )
|
||||
import Control.Effect.Labelled ( HasLabelled
|
||||
, Labelled
|
||||
, runLabelled
|
||||
)
|
||||
import qualified Control.Exception.Lifted as Lifted
|
||||
import Control.Lens hiding ( (??) )
|
||||
import Control.Monad.Logger
|
||||
import Control.Monad.Trans.Control ( MonadBaseControl )
|
||||
import Crypto.Hash
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Lens
|
||||
import Data.Aeson.Types ( parseMaybe )
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import Data.IORef
|
||||
import qualified Data.HashMap.Lazy as HML
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import Data.IORef
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
import Data.Singletons
|
||||
import Data.Singletons.Prelude.Bool ( SBool(..)
|
||||
, If
|
||||
import Data.Singletons.Prelude.Bool ( If
|
||||
, SBool(..)
|
||||
)
|
||||
import Data.Singletons.Prelude.List ( Elem )
|
||||
|
||||
import qualified Data.Text as Text
|
||||
import Database.Persist
|
||||
import Database.Persist.Sql ( ConnectionPool )
|
||||
import Database.Persist.Sqlite ( runSqlPool )
|
||||
import Exinst
|
||||
import Network.HTTP.Types
|
||||
import qualified Network.JSONRPC as JSONRPC
|
||||
import Yesod.Core.Content
|
||||
import Yesod.Core.Json
|
||||
import Yesod.Core.Handler hiding ( cached )
|
||||
import Yesod.Core.Json
|
||||
import Yesod.Core.Types ( JSONResponse(..) )
|
||||
import Yesod.Persist.Core
|
||||
|
||||
import Foundation
|
||||
import Handler.Backups
|
||||
import Handler.Icons
|
||||
import Handler.Network
|
||||
import Handler.Types.Apps
|
||||
import Handler.Util
|
||||
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
||||
import Lib.Algebra.State.RegistryUrl
|
||||
import Lib.Background
|
||||
import Lib.Error
|
||||
import qualified Lib.External.AppManifest as AppManifest
|
||||
import qualified Lib.External.AppMgr as AppMgr
|
||||
import qualified Lib.External.Registry as Reg
|
||||
import qualified Lib.External.AppManifest as AppManifest
|
||||
import Lib.IconCache
|
||||
import qualified Lib.Notifications as Notifications
|
||||
import Lib.SystemPaths
|
||||
import Lib.TyFam.ConditionalData
|
||||
import Lib.Types.Core
|
||||
import Lib.Types.Emver
|
||||
import Lib.Types.NetAddress
|
||||
import Lib.Types.ServerApp
|
||||
import Model
|
||||
import Settings
|
||||
import Crypto.Hash
|
||||
|
||||
pureLog :: Show a => a -> Handler a
|
||||
pureLog = liftA2 (*>) ($logInfo . show) pure
|
||||
@@ -105,7 +109,11 @@ type AllEffects m
|
||||
( Labelled
|
||||
"databaseConnection"
|
||||
(ReaderT ConnectionPool)
|
||||
(ReaderT AgentCtx (ErrorC S9Error (LiftC m)))
|
||||
( Labelled
|
||||
"lanThread"
|
||||
(ReaderT (MVar ThreadId))
|
||||
(ReaderT AgentCtx (ErrorC S9Error (LiftC m)))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -118,6 +126,8 @@ intoHandler m = do
|
||||
runM
|
||||
. handleS9ErrC
|
||||
. flip runReaderT ctx
|
||||
. flip runReaderT (appLanThread ctx)
|
||||
. runLabelled @"lanThread"
|
||||
. flip runReaderT (appConnPool ctx)
|
||||
. runLabelled @"databaseConnection"
|
||||
. flip runReaderT fsbase
|
||||
@@ -244,18 +254,30 @@ getInstalledAppsLogic = do
|
||||
, appInstalledPreviewStatus = AppStatusTmp Installing
|
||||
, appInstalledPreviewVersionInstalled = storeAppVersionInfoVersion
|
||||
, appInstalledPreviewTorAddress = Nothing
|
||||
, appInstalledPreviewUi = False
|
||||
, appInstalledPreviewLanAddress = Nothing
|
||||
, appInstalledPreviewTorUi = False
|
||||
, appInstalledPreviewLanUi = False
|
||||
}
|
||||
installedPreviews = flip
|
||||
HML.mapWithKey
|
||||
remapped
|
||||
\appId (s, v, AppMgr2.InfoRes {..}) -> AppInstalledPreview
|
||||
{ appInstalledPreviewBase = AppBase appId infoResTitle (iconUrl appId v)
|
||||
, appInstalledPreviewStatus = s
|
||||
, appInstalledPreviewVersionInstalled = v
|
||||
, appInstalledPreviewTorAddress = infoResTorAddress
|
||||
, appInstalledPreviewUi = AppManifest.uiAvailable infoResManifest
|
||||
}
|
||||
\appId (s, v, AppMgr2.InfoRes {..}) ->
|
||||
let
|
||||
mLanAddress = do -- Maybe
|
||||
addrBase <- infoResTorAddress
|
||||
let
|
||||
lanConfs = mapMaybe AppManifest.portMapEntryLan
|
||||
$ AppManifest.appManifestPortMapping infoResManifest
|
||||
guard (not . null $ lanConfs)
|
||||
pure $ LanAddress . (".onion" `Text.replace` ".local") . unTorAddress $ addrBase
|
||||
in AppInstalledPreview { appInstalledPreviewBase = AppBase appId infoResTitle (iconUrl appId v)
|
||||
, appInstalledPreviewStatus = s
|
||||
, appInstalledPreviewVersionInstalled = v
|
||||
, appInstalledPreviewTorAddress = infoResTorAddress
|
||||
, appInstalledPreviewLanAddress = mLanAddress
|
||||
, appInstalledPreviewTorUi = AppManifest.torUiAvailable infoResManifest
|
||||
, appInstalledPreviewLanUi = AppManifest.lanUiAvailable infoResManifest
|
||||
}
|
||||
|
||||
pure $ HML.elems $ HML.union installingPreviews installedPreviews
|
||||
|
||||
@@ -286,9 +308,14 @@ getInstalledAppByIdLogic appId = do
|
||||
, appInstalledFullInstructions = Nothing
|
||||
, appInstalledFullLastBackup = backupTime
|
||||
, appInstalledFullTorAddress = Nothing
|
||||
, appInstalledFullLanAddress = Nothing
|
||||
, appInstalledFullTorUi = False
|
||||
, appInstalledFullLanUi = False
|
||||
, appInstalledFullConfiguredRequirements = []
|
||||
, appInstalledFullUninstallAlert = Nothing
|
||||
, appInstalledFullRestoreAlert = Nothing
|
||||
, appInstalledFullStartAlert = Nothing
|
||||
, appInstalledFullActions = []
|
||||
}
|
||||
serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|]
|
||||
let remapped = remapAppMgrInfo jobCache serverApps
|
||||
@@ -316,18 +343,30 @@ getInstalledAppByIdLogic appId = do
|
||||
(HM.lookup depId installCache $> AppStatusTmp Installing)
|
||||
<|> (view _1 <$> HM.lookup depId remapped)
|
||||
pure $ dependencyInfoToDependencyRequirement (AsInstalled STrue) (base, depStatus, depInfo)
|
||||
manifest <- lift $ LAsync.wait manifest'
|
||||
manifest <- (lift $ LAsync.wait manifest') >>= \case
|
||||
Nothing -> throwError $ NotFoundE "manifest" (show appId)
|
||||
Just x -> pure x
|
||||
instructions <- lift $ LAsync.wait instructions'
|
||||
backupTime <- lift $ LAsync.wait backupTime'
|
||||
let lanAddress = do
|
||||
addrBase <- infoResTorAddress
|
||||
let lanConfs = mapMaybe AppManifest.portMapEntryLan $ AppManifest.appManifestPortMapping manifest
|
||||
guard (not . null $ lanConfs)
|
||||
pure $ LanAddress . (".onion" `Text.replace` ".local") . unTorAddress $ addrBase
|
||||
pure AppInstalledFull { appInstalledFullBase = AppBase appId infoResTitle (iconUrl appId version)
|
||||
, appInstalledFullStatus = status
|
||||
, appInstalledFullVersionInstalled = version
|
||||
, appInstalledFullInstructions = instructions
|
||||
, appInstalledFullLastBackup = backupTime
|
||||
, appInstalledFullTorAddress = infoResTorAddress
|
||||
, appInstalledFullLanAddress = lanAddress
|
||||
, appInstalledFullTorUi = AppManifest.torUiAvailable manifest
|
||||
, appInstalledFullLanUi = AppManifest.lanUiAvailable manifest
|
||||
, appInstalledFullConfiguredRequirements = HM.elems requirements
|
||||
, appInstalledFullUninstallAlert = manifest >>= AppManifest.appManifestUninstallAlert
|
||||
, appInstalledFullRestoreAlert = manifest >>= AppManifest.appManifestRestoreAlert
|
||||
, appInstalledFullUninstallAlert = AppManifest.appManifestUninstallAlert manifest
|
||||
, appInstalledFullRestoreAlert = AppManifest.appManifestRestoreAlert manifest
|
||||
, appInstalledFullStartAlert = AppManifest.appManifestStartAlert manifest
|
||||
, appInstalledFullActions = AppManifest.appManifestActions manifest
|
||||
}
|
||||
runMaybeT (installing <|> installed) `orThrowM` NotFoundE "appId" (show appId)
|
||||
|
||||
@@ -343,6 +382,7 @@ postUninstallAppLogic :: ( HasFilesystemBase sig m
|
||||
, MonadIO m
|
||||
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
|
||||
, HasLabelled "iconTagCache" (Reader (TVar (HM.HashMap AppId (Digest MD5)))) sig m
|
||||
, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m
|
||||
)
|
||||
=> AppId
|
||||
-> AppMgr2.DryRun
|
||||
@@ -361,7 +401,9 @@ postUninstallAppLogic appId dryrun = do
|
||||
breakageIds <- HM.keys . AppMgr2.unBreakageMap <$> AppMgr2.remove flags appId
|
||||
bs <- pure (traverse (hydrate $ (AppMgr2.infoResTitle &&& AppMgr2.infoResVersion) <$> serverApps) breakageIds)
|
||||
`orThrowM` InternalE "Reported app breakage for app that isn't installed, contact support"
|
||||
when (not $ coerce dryrun) $ clearIcon appId
|
||||
when (not $ coerce dryrun) $ do
|
||||
clearIcon appId
|
||||
postResetLanLogic
|
||||
pure $ WithBreakages bs ()
|
||||
|
||||
type InstallResponse :: Bool -> Type
|
||||
@@ -378,6 +420,7 @@ postInstallNewAppR appId = do
|
||||
|
||||
postInstallNewAppLogic :: forall sig m a
|
||||
. ( Has (Reader AgentCtx) sig m
|
||||
, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m
|
||||
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
|
||||
, HasLabelled "iconTagCache" (Reader (TVar (HM.HashMap AppId (Digest MD5)))) sig m
|
||||
, Has (Error S9Error) sig m
|
||||
@@ -465,6 +508,7 @@ postInstallNewAppLogic appId appVersion dryrun = do
|
||||
(void $ Notifications.emit k infoResVersion (Notifications.RestartFailed e))
|
||||
pool
|
||||
)
|
||||
postResetLanLogic
|
||||
|
||||
|
||||
postStartServerAppR :: AppId -> Handler ()
|
||||
@@ -769,3 +813,21 @@ dependencyInfoToDependencyRequirement asInstalled (base, status, AppMgr2.Depende
|
||||
let appDependencyRequirementReasonOptional = dependencyInfoReasonOptional
|
||||
appDependencyRequirementDefault = dependencyInfoRequired
|
||||
in AppDependencyRequirement { .. }
|
||||
|
||||
postActionR :: AppId -> Handler (JSONResponse JSONRPC.Response)
|
||||
postActionR appId = do
|
||||
req <- requireCheckJsonBody
|
||||
fmap JSONResponse . intoHandler $ postActionLogic appId req
|
||||
|
||||
postActionLogic :: (Has (Error S9Error) sig m, Has AppMgr2.AppMgr sig m)
|
||||
=> AppId
|
||||
-> JSONRPC.Request
|
||||
-> m JSONRPC.Response
|
||||
postActionLogic appId (JSONRPC.Request { getReqMethod, getReqId }) = do
|
||||
hm <- AppMgr2.action appId getReqMethod
|
||||
case (HM.lookup "result" hm, HM.lookup "error" hm >>= parseMaybe parseJSON) of
|
||||
(Just v , _ ) -> pure (JSONRPC.Response JSONRPC.V2 v getReqId)
|
||||
(_ , Just e ) -> pure (JSONRPC.ResponseError JSONRPC.V2 e getReqId)
|
||||
(Nothing, Nothing) -> throwError
|
||||
$ AppMgrParseE "action" (decodeUtf8 . LBS.toStrict $ encode (Object hm)) "Invalid JSONRPC Response"
|
||||
postActionLogic _ r = throwError $ InvalidRequestE (toJSON r) "Invalid JSONRPC Request"
|
||||
|
||||
@@ -7,11 +7,11 @@ import Startlude hiding ( Reader
|
||||
, runReader
|
||||
)
|
||||
|
||||
import Control.Effect.Labelled hiding ( Handler )
|
||||
import Control.Effect.Reader.Labelled
|
||||
import Control.Carrier.Error.Church
|
||||
import Control.Carrier.Lift
|
||||
import Control.Carrier.Reader ( runReader )
|
||||
import Control.Effect.Labelled hiding ( Handler )
|
||||
import Control.Effect.Reader.Labelled
|
||||
import Data.Aeson
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import Data.UUID.V4
|
||||
@@ -20,8 +20,13 @@ import Yesod.Auth
|
||||
import Yesod.Core
|
||||
import Yesod.Core.Types
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Exinst
|
||||
import Foundation
|
||||
import Handler.Network
|
||||
import Handler.Util
|
||||
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
||||
import Lib.Background
|
||||
import Lib.Error
|
||||
import qualified Lib.External.AppMgr as AppMgr
|
||||
import qualified Lib.Notifications as Notifications
|
||||
@@ -29,10 +34,6 @@ import Lib.Password
|
||||
import Lib.Types.Core
|
||||
import Lib.Types.Emver
|
||||
import Model
|
||||
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
||||
import Lib.Background
|
||||
import Control.Concurrent.STM
|
||||
import Exinst
|
||||
|
||||
|
||||
data CreateBackupReq = CreateBackupReq
|
||||
@@ -58,8 +59,9 @@ instance FromJSON RestoreBackupReq where
|
||||
pure RestoreBackupReq { .. }
|
||||
|
||||
data EjectDiskReq = EjectDiskReq
|
||||
{ ejectDiskLogicalName :: Text
|
||||
} deriving (Eq, Show)
|
||||
{ ejectDiskLogicalName :: Text
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
instance FromJSON EjectDiskReq where
|
||||
parseJSON = withObject "Eject Disk Req" $ \o -> do
|
||||
ejectDiskLogicalName <- o .: "logicalName"
|
||||
@@ -100,6 +102,8 @@ postRestoreBackupR appId = disableEndpointOnFailedUpdate $ do
|
||||
& runReader appConnPool
|
||||
& runLabelled @"backgroundJobCache"
|
||||
& runReader appBackgroundJobs
|
||||
& runLabelled @"lanThread"
|
||||
& runReader appLanThread
|
||||
& handleS9ErrC
|
||||
& runM
|
||||
|
||||
@@ -173,6 +177,7 @@ stopBackupLogic appId = do
|
||||
|
||||
restoreBackupLogic :: ( HasLabelled "backgroundJobCache" (Reader (TVar JobCache)) sig m
|
||||
, HasLabelled "databaseConnection" (Reader ConnectionPool) sig m
|
||||
, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m
|
||||
, Has (Error S9Error) sig m
|
||||
, Has AppMgr2.AppMgr sig m
|
||||
, MonadIO m
|
||||
@@ -181,10 +186,11 @@ restoreBackupLogic :: ( HasLabelled "backgroundJobCache" (Reader (TVar JobCache)
|
||||
-> RestoreBackupReq
|
||||
-> m ()
|
||||
restoreBackupLogic appId RestoreBackupReq {..} = do
|
||||
jobCache <- ask @"backgroundJobCache"
|
||||
db <- ask @"databaseConnection"
|
||||
version <- fmap AppMgr2.infoResVersion $ AppMgr2.info [AppMgr2.flags| |] appId `orThrowM` NotFoundE "appId"
|
||||
(show appId)
|
||||
lanThread <- ask @"lanThread"
|
||||
jobCache <- ask @"backgroundJobCache"
|
||||
db <- ask @"databaseConnection"
|
||||
version <- fmap AppMgr2.infoResVersion $ AppMgr2.info [AppMgr2.flags| |] appId `orThrowM` NotFoundE "appId"
|
||||
(show appId)
|
||||
res <- liftIO . atomically $ do
|
||||
(JobCache jobs) <- readTVar jobCache
|
||||
case HM.lookup appId jobs of
|
||||
@@ -206,10 +212,13 @@ restoreBackupLogic appId RestoreBackupReq {..} = do
|
||||
let notif = case appmgrRes of
|
||||
Left e -> Notifications.RestoreFailed e
|
||||
Right _ -> Notifications.RestoreSucceeded
|
||||
resetRes <- runExceptT @S9Error $ runReader lanThread . runLabelled @"lanThread" $ postResetLanLogic
|
||||
case resetRes of
|
||||
Left _ -> pure () -- temporarily forbidden is the only possible thing here so ignore it
|
||||
Right () -> pure ()
|
||||
flip runSqlPool db $ void $ Notifications.emit appId version notif
|
||||
liftIO . atomically $ modifyTVar jobCache (insertJob appId Restore tid)
|
||||
|
||||
|
||||
listDisksLogic :: (Has (Error S9Error) sig m, MonadIO m) => m [AppMgr.DiskInfo]
|
||||
listDisksLogic = runExceptT AppMgr.diskShow >>= liftEither
|
||||
|
||||
|
||||
36
agent/src/Handler/Network.hs
Normal file
36
agent/src/Handler/Network.hs
Normal file
@@ -0,0 +1,36 @@
|
||||
module Handler.Network where
|
||||
|
||||
import Startlude hiding ( Reader
|
||||
, ask
|
||||
, asks
|
||||
, runReader
|
||||
)
|
||||
|
||||
import Control.Carrier.Lift ( runM )
|
||||
import Control.Effect.Error
|
||||
import Lib.Error
|
||||
import Yesod.Core ( getYesod )
|
||||
|
||||
import Control.Carrier.Reader ( runReader )
|
||||
import Control.Effect.Labelled ( runLabelled )
|
||||
import Control.Effect.Reader.Labelled
|
||||
import Foundation
|
||||
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
||||
import Lib.Types.Core
|
||||
|
||||
postResetLanR :: Handler ()
|
||||
postResetLanR = do
|
||||
ctx <- getYesod
|
||||
runM . handleS9ErrC . runReader (appLanThread ctx) . runLabelled @"lanThread" $ postResetLanLogic
|
||||
|
||||
postResetLanLogic :: (MonadIO m, HasLabelled "lanThread" (Reader (MVar ThreadId)) sig m, Has (Error S9Error) sig m)
|
||||
=> m ()
|
||||
postResetLanLogic = do
|
||||
threadVar <- ask @"lanThread"
|
||||
mtid <- liftIO . tryTakeMVar $ threadVar
|
||||
case mtid of
|
||||
Nothing -> throwError $ TemporarilyForbiddenE (AppId "LAN") "reset" "being reset"
|
||||
Just tid -> liftIO $ do
|
||||
killThread tid
|
||||
newTid <- forkIO (void . runM . runExceptT @S9Error . AppMgr2.runAppMgrCliC $ AppMgr2.lanEnable)
|
||||
putMVar threadVar newTid
|
||||
@@ -28,6 +28,9 @@ import Lib.SystemPaths hiding ( (</>) )
|
||||
import Lib.Tor
|
||||
import Settings
|
||||
import Control.Carrier.Lift ( runM )
|
||||
import System.Process
|
||||
import qualified UnliftIO
|
||||
import System.FileLock
|
||||
|
||||
getVersionR :: Handler AppVersionRes
|
||||
getVersionR = pure . AppVersionRes $ agentVersion
|
||||
@@ -35,8 +38,7 @@ getVersionR = pure . AppVersionRes $ agentVersion
|
||||
getVersionLatestR :: Handler VersionLatestRes
|
||||
getVersionLatestR = handleS9ErrT $ do
|
||||
s <- getsYesod appSettings
|
||||
v <- interp s $ Reg.getLatestAgentVersion
|
||||
pure $ VersionLatestRes v
|
||||
uncurry VersionLatestRes <$> interp s Reg.getLatestAgentVersion
|
||||
where interp s = ExceptT . liftIO . runError . injectFilesystemBaseFromContext s . runRegistryUrlIOC
|
||||
|
||||
|
||||
@@ -48,6 +50,8 @@ getSpecsR = handleS9ErrT $ do
|
||||
specsDisk <- fmap show . metricDiskSize <$> getDfMetrics
|
||||
specsNetworkId <- lift . runM . injectFilesystemBaseFromContext settings $ getStart9AgentHostname
|
||||
specsTorAddress <- lift . runM . injectFilesystemBaseFromContext settings $ getAgentHiddenServiceUrl
|
||||
specsLanAddress <-
|
||||
fmap (<> ".local") . lift . runM . injectFilesystemBaseFromContext settings $ getStart9AgentHostname
|
||||
|
||||
let specsAgentVersion = agentVersion
|
||||
returnJsonEncoding SpecsRes { .. }
|
||||
@@ -69,3 +73,9 @@ patchServerR = do
|
||||
getGitR :: Handler Text
|
||||
getGitR = pure $embedGitRevision
|
||||
|
||||
getLogsR :: Handler (JSONResponse [Text])
|
||||
getLogsR = do
|
||||
let debugLock = "/root/agent/tmp/debug.lock"
|
||||
UnliftIO.bracket (liftIO $ lockFile debugLock Exclusive) (liftIO . unlockFile) $ const $ do
|
||||
liftIO $ callCommand "journalctl -u agent --since \"1 hour ago\" > /root/agent/tmp/debug.log"
|
||||
liftIO $ JSONResponse . lines <$> readFile "/root/agent/tmp/debug.log"
|
||||
|
||||
@@ -9,6 +9,7 @@ import Data.Aeson
|
||||
import Data.Aeson.Flatten
|
||||
import Data.Singletons
|
||||
|
||||
import qualified Lib.External.AppManifest as Manifest
|
||||
import Lib.TyFam.ConditionalData
|
||||
import Lib.Types.Core
|
||||
import Lib.Types.Emver
|
||||
@@ -45,7 +46,9 @@ data AppInstalledPreview = AppInstalledPreview
|
||||
, appInstalledPreviewStatus :: AppStatus
|
||||
, appInstalledPreviewVersionInstalled :: Version
|
||||
, appInstalledPreviewTorAddress :: Maybe TorAddress
|
||||
, appInstalledPreviewUi :: Bool
|
||||
, appInstalledPreviewLanAddress :: Maybe LanAddress
|
||||
, appInstalledPreviewTorUi :: Bool
|
||||
, appInstalledPreviewLanUi :: Bool
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
instance ToJSON AppInstalledPreview where
|
||||
@@ -53,7 +56,9 @@ instance ToJSON AppInstalledPreview where
|
||||
[ "status" .= appInstalledPreviewStatus
|
||||
, "versionInstalled" .= appInstalledPreviewVersionInstalled
|
||||
, "torAddress" .= (unTorAddress <$> appInstalledPreviewTorAddress)
|
||||
, "ui" .= appInstalledPreviewUi
|
||||
, "lanAddress" .= (unLanAddress <$> appInstalledPreviewLanAddress)
|
||||
, "torUi" .= appInstalledPreviewTorUi
|
||||
, "lanUi" .= appInstalledPreviewLanUi
|
||||
]
|
||||
|
||||
data InstallNewAppReq = InstallNewAppReq
|
||||
@@ -129,11 +134,16 @@ data AppInstalledFull = AppInstalledFull
|
||||
, appInstalledFullStatus :: AppStatus
|
||||
, appInstalledFullVersionInstalled :: Version
|
||||
, appInstalledFullTorAddress :: Maybe TorAddress
|
||||
, appInstalledFullLanAddress :: Maybe LanAddress
|
||||
, appInstalledFullTorUi :: Bool
|
||||
, appInstalledFullLanUi :: Bool
|
||||
, appInstalledFullInstructions :: Maybe Text
|
||||
, appInstalledFullLastBackup :: Maybe UTCTime
|
||||
, appInstalledFullConfiguredRequirements :: [Stripped AppDependencyRequirement]
|
||||
, appInstalledFullUninstallAlert :: Maybe Text
|
||||
, appInstalledFullRestoreAlert :: Maybe Text
|
||||
, appInstalledFullStartAlert :: Maybe Text
|
||||
, appInstalledFullActions :: [Manifest.Action]
|
||||
}
|
||||
instance ToJSON AppInstalledFull where
|
||||
toJSON AppInstalledFull {..} = object
|
||||
@@ -141,6 +151,9 @@ instance ToJSON AppInstalledFull where
|
||||
, "lastBackup" .= appInstalledFullLastBackup
|
||||
, "configuredRequirements" .= appInstalledFullConfiguredRequirements
|
||||
, "torAddress" .= (unTorAddress <$> appInstalledFullTorAddress)
|
||||
, "lanAddress" .= (unLanAddress <$> appInstalledFullLanAddress)
|
||||
, "torUi" .= appInstalledFullTorUi
|
||||
, "lanUi" .= appInstalledFullLanUi
|
||||
, "id" .= appBaseId appInstalledFullBase
|
||||
, "title" .= appBaseTitle appInstalledFullBase
|
||||
, "iconURL" .= appBaseIconUrl appInstalledFullBase
|
||||
@@ -148,6 +161,8 @@ instance ToJSON AppInstalledFull where
|
||||
, "status" .= appInstalledFullStatus
|
||||
, "uninstallAlert" .= appInstalledFullUninstallAlert
|
||||
, "restoreAlert" .= appInstalledFullRestoreAlert
|
||||
, "startAlert" .= appInstalledFullStartAlert
|
||||
, "actions" .= appInstalledFullActions
|
||||
]
|
||||
|
||||
data AppVersionInfo = AppVersionInfo
|
||||
|
||||
@@ -15,11 +15,13 @@ import Lib.Types.Emver
|
||||
import Model
|
||||
|
||||
data VersionLatestRes = VersionLatestRes
|
||||
{ versionLatestVersion :: Version
|
||||
{ versionLatestVersion :: Version
|
||||
, versionLatestReleaseNotes :: Maybe Text
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
instance ToJSON VersionLatestRes where
|
||||
toJSON VersionLatestRes {..} = object $ ["versionLatest" .= versionLatestVersion]
|
||||
toJSON VersionLatestRes {..} =
|
||||
object $ ["versionLatest" .= versionLatestVersion, "releaseNotes" .= versionLatestReleaseNotes]
|
||||
instance ToTypedContent VersionLatestRes where
|
||||
toTypedContent = toTypedContent . toJSON
|
||||
instance ToContent VersionLatestRes where
|
||||
@@ -31,14 +33,15 @@ data ServerRes = ServerRes
|
||||
, serverStatus :: Maybe AppStatus
|
||||
, serverStatusAt :: UTCTime
|
||||
, serverVersionInstalled :: Version
|
||||
, serverNotifications :: [ Entity Notification ]
|
||||
, serverNotifications :: [Entity Notification]
|
||||
, serverWifi :: WifiList
|
||||
, serverSsh :: [ SshKeyFingerprint ]
|
||||
, serverSsh :: [SshKeyFingerprint]
|
||||
, serverAlternativeRegistryUrl :: Maybe Text
|
||||
, serverSpecs :: SpecsRes
|
||||
, serverWelcomeAck :: Bool
|
||||
, serverAutoCheckUpdates :: Bool
|
||||
} deriving (Eq, Show)
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
type JsonEncoding a = Encoding
|
||||
jsonEncode :: (Monad m, ToJSON a) => a -> m (JsonEncoding a)
|
||||
|
||||
@@ -16,6 +16,7 @@ data SpecsRes = SpecsRes
|
||||
, specsNetworkId :: Text
|
||||
, specsAgentVersion :: Version
|
||||
, specsTorAddress :: Text
|
||||
, specsLanAddress :: Text
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -23,6 +24,7 @@ instance ToJSON SpecsRes where
|
||||
toJSON SpecsRes {..} = object
|
||||
[ "EmbassyOS Version" .= specsAgentVersion
|
||||
, "Tor Address" .= specsTorAddress
|
||||
, "LAN Address" .= specsLanAddress
|
||||
, "Network ID" .= specsNetworkId
|
||||
, "CPU" .= specsCPU
|
||||
, "Memory" .= specsMem
|
||||
@@ -33,6 +35,7 @@ instance ToJSON SpecsRes where
|
||||
. fold
|
||||
$ [ "EmbassyOS Version" .= specsAgentVersion
|
||||
, "Tor Address" .= specsTorAddress
|
||||
, "LAN Address" .= specsLanAddress
|
||||
, "Network ID" .= specsNetworkId
|
||||
, "CPU" .= specsCPU
|
||||
, "Memory" .= specsMem
|
||||
|
||||
@@ -93,6 +93,7 @@ getSpecs settings = do
|
||||
specsDisk <- fmap show . metricDiskSize <$> getDfMetrics
|
||||
specsNetworkId <- runM $ injectFilesystemBaseFromContext settings getStart9AgentHostname
|
||||
specsTorAddress <- runM $ injectFilesystemBaseFromContext settings getAgentHiddenServiceUrl
|
||||
specsLanAddress <- fmap (<> ".local") . runM $ injectFilesystemBaseFromContext settings getStart9AgentHostname
|
||||
|
||||
let specsAgentVersion = agentVersion
|
||||
pure $ SpecsRes { .. }
|
||||
|
||||
@@ -11,8 +11,7 @@ module Lib.Algebra.Domain.AppMgr
|
||||
( module Lib.Algebra.Domain.AppMgr
|
||||
, module Lib.Algebra.Domain.AppMgr.Types
|
||||
, module Lib.Algebra.Domain.AppMgr.TH
|
||||
)
|
||||
where
|
||||
) where
|
||||
|
||||
import Startlude
|
||||
|
||||
@@ -26,30 +25,31 @@ import Data.Singletons.Prelude hiding ( Error )
|
||||
import Data.Singletons.Prelude.Either
|
||||
import qualified Data.String as String
|
||||
|
||||
import Lib.Algebra.Domain.AppMgr.Types
|
||||
import Lib.Algebra.Domain.AppMgr.TH
|
||||
import Lib.Error
|
||||
import Lib.External.AppManifest
|
||||
import Lib.TyFam.ConditionalData
|
||||
import Lib.Types.Core ( AppId(..)
|
||||
, AppContainerStatus(..)
|
||||
)
|
||||
import Lib.Types.NetAddress
|
||||
import Lib.Types.Emver
|
||||
import Control.Monad.Trans.Class ( MonadTrans )
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import System.Process.Typed
|
||||
import Data.String.Interpolate.IsString
|
||||
( i )
|
||||
import Control.Monad.Base ( MonadBase(..) )
|
||||
import Control.Monad.Fail ( MonadFail(fail) )
|
||||
import Control.Monad.Trans.Resource ( MonadResource(..) )
|
||||
import Control.Monad.Trans.Control ( defaultLiftBaseWith
|
||||
, defaultRestoreM
|
||||
import Control.Monad.Trans.Class ( MonadTrans )
|
||||
import Control.Monad.Trans.Control ( MonadBaseControl(..)
|
||||
, MonadTransControl(..)
|
||||
, MonadBaseControl(..)
|
||||
, defaultLiftBaseWith
|
||||
, defaultRestoreM
|
||||
)
|
||||
import Control.Monad.Trans.Resource ( MonadResource(..) )
|
||||
import qualified Data.ByteString.Char8 as C8
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import Data.String.Interpolate.IsString
|
||||
( i )
|
||||
import Lib.Algebra.Domain.AppMgr.TH
|
||||
import Lib.Algebra.Domain.AppMgr.Types
|
||||
import Lib.Error
|
||||
import qualified Lib.External.AppManifest as Manifest
|
||||
import Lib.TyFam.ConditionalData
|
||||
import Lib.Types.Core ( AppContainerStatus(..)
|
||||
, AppId(..)
|
||||
)
|
||||
import Lib.Types.Emver
|
||||
import Lib.Types.NetAddress
|
||||
import System.Process
|
||||
import System.Process.Typed
|
||||
|
||||
|
||||
type InfoRes :: Either OnlyInfoFlag [IncludeInfoFlag] -> Type
|
||||
@@ -66,7 +66,7 @@ data InfoRes a = InfoRes
|
||||
(Either_ (DefaultEqSym1 'OnlyDependencies) (ElemSym1 'IncludeDependencies) a)
|
||||
(HM.HashMap AppId DependencyInfo)
|
||||
, infoResManifest
|
||||
:: Include (Either_ (DefaultEqSym1 'OnlyManifest) (ElemSym1 'IncludeManifest) a) AppManifest
|
||||
:: Include (Either_ (DefaultEqSym1 'OnlyManifest) (ElemSym1 'IncludeManifest) a) Manifest.AppManifest
|
||||
, infoResStatus :: Include (Either_ (DefaultEqSym1 'OnlyStatus) (ElemSym1 'IncludeStatus) a) AppContainerStatus
|
||||
}
|
||||
instance SingI (a :: Either OnlyInfoFlag [IncludeInfoFlag]) => FromJSON (InfoRes a) where
|
||||
@@ -270,6 +270,8 @@ data AppMgr (m :: Type -> Type) k where
|
||||
-- Tor ::_
|
||||
Update ::DryRun -> AppId -> Maybe VersionRange -> AppMgr m BreakageMap
|
||||
-- Verify ::_
|
||||
LanEnable ::AppMgr m ()
|
||||
Action ::AppId -> Text -> AppMgr m (HM.HashMap Text Value)
|
||||
makeSmartConstructors ''AppMgr
|
||||
|
||||
newtype AppMgrCliC m a = AppMgrCliC { runAppMgrCliC :: m a }
|
||||
@@ -368,13 +370,16 @@ instance (Has (Error S9Error) sig m, Algebra sig m, MonadIO m) => Algebra (AppMg
|
||||
(L (List (SRight flags))) -> do
|
||||
let renderedFlags = (genInclusiveFlag <$> fromSing flags) <> ["--json"]
|
||||
let args = "list" : renderedFlags
|
||||
(ec, out) <- readProcessInheritStderr "appmgr" args ""
|
||||
res <- case ec of
|
||||
ExitSuccess -> case withSingI flags $ eitherDecodeStrict out of
|
||||
Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e
|
||||
Right x -> pure x
|
||||
ExitFailure n -> throwError $ AppMgrE "list" n
|
||||
pure $ ctx $> res
|
||||
let runIt retryCount = do
|
||||
(ec, out) <- readProcessInheritStderr "appmgr" args ""
|
||||
case ec of
|
||||
ExitSuccess -> case withSingI flags $ eitherDecodeStrict out of
|
||||
Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e
|
||||
Right x -> pure $ ctx $> x
|
||||
ExitFailure 6 ->
|
||||
if retryCount > 0 then runIt (retryCount - 1) else throwError $ AppMgrE "list" 6
|
||||
ExitFailure n -> throwError $ AppMgrE "list" n
|
||||
runIt (1 :: Word) -- with 1 retry
|
||||
(L (Remove dryorpurge appId)) -> do
|
||||
let args = "remove" : case dryorpurge of
|
||||
Left (DryRun True) -> ["--dry-run", show appId, "--json"]
|
||||
@@ -421,6 +426,16 @@ instance (Has (Error S9Error) sig m, Algebra sig m, MonadIO m) => Algebra (AppMg
|
||||
ExitFailure 6 ->
|
||||
throwError $ NotFoundE "appId@version" ([i|#{appId}#{maybe "" (('@':) . show) version}|])
|
||||
ExitFailure n -> throwError $ AppMgrE (toS $ String.unwords args) n
|
||||
(L LanEnable ) -> liftIO $ callProcess "appmgr" ["lan", "enable"] $> ctx
|
||||
(L (Action appId action)) -> do
|
||||
let args = ["actions", show appId, toS action]
|
||||
(ec, out) <- readProcessInheritStderr "appmgr" args ""
|
||||
case ec of
|
||||
ExitSuccess -> case eitherDecodeStrict out of
|
||||
Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e
|
||||
Right x -> pure $ ctx $> x
|
||||
ExitFailure 6 -> throwError $ NotFoundE "appId" (show appId)
|
||||
ExitFailure n -> throwError $ AppMgrE (toS $ String.unwords args) n
|
||||
R other -> AppMgrCliC $ alg (runAppMgrCliC . hdl) other ctx
|
||||
where
|
||||
versionSpec :: (IsString a, Semigroup a, ConvertText String a) => Maybe VersionRange -> a -> a
|
||||
|
||||
74
agent/src/Lib/External/AppManifest.hs
vendored
74
agent/src/Lib/External/AppManifest.hs
vendored
@@ -9,12 +9,12 @@ import Data.Aeson
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.Yaml as Yaml
|
||||
|
||||
import Control.Monad.Fail ( MonadFail(fail) )
|
||||
import Lib.Error
|
||||
import Lib.SystemPaths
|
||||
import Lib.Types.Core
|
||||
import Lib.Types.Emver
|
||||
import Lib.Types.Emver.Orphans ( )
|
||||
import Control.Monad.Fail ( MonadFail(fail) )
|
||||
|
||||
data ImageType = ImageTypeTar
|
||||
deriving (Eq, Show)
|
||||
@@ -47,6 +47,33 @@ instance FromJSON AssetMapping where
|
||||
assetMappingOverwrite <- o .: "overwrite"
|
||||
pure $ AssetMapping { .. }
|
||||
|
||||
data Action = Action
|
||||
{ actionId :: Text
|
||||
, actionName :: Text
|
||||
, actionDescription :: Text
|
||||
, actionWarning :: Maybe Text
|
||||
, actionAllowedStatuses :: [AppContainerStatus]
|
||||
}
|
||||
deriving Show
|
||||
instance FromJSON Action where
|
||||
parseJSON = withObject "AppAction" $ \o -> do
|
||||
actionId <- o .: "id"
|
||||
actionName <- o .: "name"
|
||||
actionDescription <- o .: "description"
|
||||
actionWarning <- o .:? "warning"
|
||||
actionAllowedStatuses <- o .: "allowed-statuses"
|
||||
pure Action { .. }
|
||||
instance ToJSON Action where
|
||||
toJSON Action {..} =
|
||||
object
|
||||
$ [ "id" .= actionId
|
||||
, "name" .= actionName
|
||||
, "description" .= actionDescription
|
||||
, "allowedStatuses" .= actionAllowedStatuses
|
||||
]
|
||||
<> maybeToList (("warning" .=) <$> actionWarning)
|
||||
|
||||
|
||||
data AppManifest where
|
||||
AppManifest ::{ appManifestId :: AppId
|
||||
, appManifestVersion :: Version
|
||||
@@ -54,7 +81,7 @@ data AppManifest where
|
||||
, appManifestDescShort :: Text
|
||||
, appManifestDescLong :: Text
|
||||
, appManifestReleaseNotes :: Text
|
||||
, appManifestPortMapping :: HM.HashMap Word16 Word16
|
||||
, appManifestPortMapping :: [PortMapEntry]
|
||||
, appManifestImageType :: ImageType
|
||||
, appManifestMount :: FilePath
|
||||
, appManifestAssets :: [AssetMapping]
|
||||
@@ -62,10 +89,20 @@ data AppManifest where
|
||||
, appManifestDependencies :: HM.HashMap AppId VersionRange
|
||||
, appManifestUninstallAlert :: Maybe Text
|
||||
, appManifestRestoreAlert :: Maybe Text
|
||||
, appManifestStartAlert :: Maybe Text
|
||||
, appManifestActions :: [Action]
|
||||
} -> AppManifest
|
||||
deriving instance Show AppManifest
|
||||
|
||||
uiAvailable :: AppManifest -> Bool
|
||||
uiAvailable AppManifest {..} = isJust $ HM.lookup 80 appManifestPortMapping
|
||||
torUiAvailable :: AppManifest -> Bool
|
||||
torUiAvailable AppManifest {..} = any (== 80) $ portMapEntryTor <$> appManifestPortMapping
|
||||
|
||||
lanUiAvailable :: AppManifest -> Bool
|
||||
lanUiAvailable AppManifest {..} = any id $ fmap portMapEntryLan appManifestPortMapping <&> \case
|
||||
Just Standard -> True
|
||||
Just (Custom 443) -> True
|
||||
Just (Custom 80 ) -> True
|
||||
_ -> False
|
||||
|
||||
instance FromJSON AppManifest where
|
||||
parseJSON = withObject "App Manifest " $ \o -> do
|
||||
@@ -75,7 +112,7 @@ instance FromJSON AppManifest where
|
||||
appManifestDescShort <- o .: "description" >>= (.: "short")
|
||||
appManifestDescLong <- o .: "description" >>= (.: "long")
|
||||
appManifestReleaseNotes <- o .: "release-notes"
|
||||
appManifestPortMapping <- o .: "ports" >>= fmap HM.fromList . traverse parsePortMapping
|
||||
appManifestPortMapping <- o .: "ports"
|
||||
appManifestImageType <- o .: "image" >>= (.: "type")
|
||||
appManifestMount <- o .: "mount"
|
||||
appManifestAssets <- o .: "assets" >>= traverse parseJSON
|
||||
@@ -83,13 +120,34 @@ instance FromJSON AppManifest where
|
||||
appManifestDependencies <- o .:? "dependencies" .!= HM.empty >>= traverse parseDepInfo
|
||||
appManifestUninstallAlert <- o .:? "uninstall-alert"
|
||||
appManifestRestoreAlert <- o .:? "restore-alert"
|
||||
appManifestStartAlert <- o .:? "start-alert"
|
||||
appManifestActions <- o .: "actions"
|
||||
pure $ AppManifest { .. }
|
||||
where
|
||||
parsePortMapping = withObject "Port Mapping" $ \o -> liftA2 (,) (o .: "tor") (o .: "internal")
|
||||
parseDepInfo = withObject "Dep Info" $ (.: "version")
|
||||
where parseDepInfo = withObject "Dep Info" $ (.: "version")
|
||||
|
||||
getAppManifest :: (MonadIO m, HasFilesystemBase sig m) => AppId -> S9ErrT m (Maybe AppManifest)
|
||||
getAppManifest appId = do
|
||||
base <- ask @"filesystemBase"
|
||||
ExceptT $ first (ManifestParseE appId) <$> liftIO
|
||||
(Yaml.decodeFileEither . toS $ (appMgrAppPath appId <> "manifest.yaml") `relativeTo` base)
|
||||
|
||||
data LanConfiguration = Standard | Custom Word16 deriving (Eq, Show)
|
||||
instance FromJSON LanConfiguration where
|
||||
parseJSON = liftA2 (<|>) standard custom
|
||||
where
|
||||
standard =
|
||||
withText "Standard Lan" \t -> if t == "standard" then pure Standard else fail "Not Standard Lan Conf"
|
||||
custom = withObject "Custom Lan" $ \o -> do
|
||||
Custom <$> (o .: "custom" >>= (.: "port"))
|
||||
data PortMapEntry = PortMapEntry
|
||||
{ portMapEntryInternal :: Word16
|
||||
, portMapEntryTor :: Word16
|
||||
, portMapEntryLan :: Maybe LanConfiguration
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
instance FromJSON PortMapEntry where
|
||||
parseJSON = withObject "Port Map Entry" $ \o -> do
|
||||
portMapEntryInternal <- o .: "internal"
|
||||
portMapEntryTor <- o .: "tor"
|
||||
portMapEntryLan <- o .:? "lan"
|
||||
pure PortMapEntry { .. }
|
||||
|
||||
7
agent/src/Lib/External/Registry.hs
vendored
7
agent/src/Lib/External/Registry.hs
vendored
@@ -150,12 +150,13 @@ getAppVersionForSpec appId spec = do
|
||||
v <- o .: "version"
|
||||
pure v
|
||||
|
||||
getLatestAgentVersion :: (Has RegistryUrl sig m, Has (Error S9Error) sig m, MonadIO m) => m Version
|
||||
getLatestAgentVersion :: (Has RegistryUrl sig m, Has (Error S9Error) sig m, MonadIO m) => m (Version, Maybe Text)
|
||||
getLatestAgentVersion = do
|
||||
val <- registryRequest agentVersionPath
|
||||
parseOrThrow agentVersionPath val $ withObject "version response" $ \o -> do
|
||||
v <- o .: "version"
|
||||
pure v
|
||||
v <- o .: "version"
|
||||
rn <- o .:? "release-notes"
|
||||
pure (v, rn)
|
||||
where agentVersionPath = "sys/version/agent"
|
||||
|
||||
getLatestAgentVersionForSpec :: (Has RegistryUrl sig m, Has (Lift IO) sig m, Has (Error S9Error) sig m)
|
||||
|
||||
@@ -11,9 +11,9 @@ import Startlude hiding ( check
|
||||
import qualified Startlude.ByteStream as ByteStream
|
||||
import qualified Startlude.ByteStream.Char8 as ByteStream
|
||||
|
||||
import Control.Carrier.Lift ( runM )
|
||||
import qualified Control.Effect.Reader.Labelled
|
||||
as Fused
|
||||
import Control.Carrier.Lift ( runM )
|
||||
import Control.Monad.Trans.Reader ( mapReaderT )
|
||||
import Control.Monad.Trans.Resource
|
||||
import Data.Attoparsec.Text
|
||||
@@ -21,51 +21,53 @@ import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Char8 as B8
|
||||
import qualified Data.Conduit as Conduit
|
||||
import qualified Data.Conduit.Combinators as Conduit
|
||||
import qualified Data.Conduit.Tar as Conduit
|
||||
import Data.Conduit.Shell hiding ( arch
|
||||
, hostname
|
||||
, patch
|
||||
, stream
|
||||
, hostname
|
||||
)
|
||||
import qualified Data.Conduit.Tar as Conduit
|
||||
import Data.FileEmbed
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import Data.IORef
|
||||
import Data.String.Interpolate.IsString
|
||||
import qualified Data.Yaml as Yaml
|
||||
import Exinst
|
||||
import System.FilePath ( splitPath
|
||||
import qualified Streaming.Conduit as Conduit
|
||||
import qualified Streaming.Prelude as Stream
|
||||
import qualified Streaming.Zip as Stream
|
||||
import System.Directory
|
||||
import System.FilePath ( (</>)
|
||||
, joinPath
|
||||
, (</>)
|
||||
, splitPath
|
||||
)
|
||||
import System.FilePath.Posix ( takeDirectory )
|
||||
import System.Directory
|
||||
import System.IO.Error
|
||||
import System.Posix.Files
|
||||
import System.Process ( callCommand )
|
||||
import qualified Streaming.Prelude as Stream
|
||||
import qualified Streaming.Conduit as Conduit
|
||||
import qualified Streaming.Zip as Stream
|
||||
|
||||
import Constants
|
||||
import Control.Effect.Error hiding ( run )
|
||||
import Control.Effect.Labelled ( runLabelled )
|
||||
import Daemon.ZeroConf ( getStart9AgentHostname )
|
||||
import qualified Data.Text as T
|
||||
import Foundation
|
||||
import Handler.Network
|
||||
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
||||
import Lib.ClientManifest
|
||||
import Lib.Error
|
||||
import qualified Lib.External.AppMgr as AppMgr
|
||||
import Lib.External.Registry
|
||||
import Lib.Sound
|
||||
import Lib.Ssl
|
||||
import Lib.Tor
|
||||
import Lib.Types.Core
|
||||
import Lib.Types.NetAddress
|
||||
import Lib.Types.Emver
|
||||
import Lib.SystemCtl
|
||||
import Lib.SystemPaths hiding ( (</>) )
|
||||
import Lib.Tor
|
||||
import Lib.Types.Core
|
||||
import Lib.Types.Emver
|
||||
import Lib.Types.NetAddress
|
||||
import Settings
|
||||
import Util.File
|
||||
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
|
||||
import Daemon.ZeroConf ( getStart9AgentHostname )
|
||||
import qualified Data.Text as T
|
||||
import Control.Effect.Error hiding ( run )
|
||||
|
||||
|
||||
data Synchronizer = Synchronizer
|
||||
@@ -96,15 +98,16 @@ parseKernelVersion = do
|
||||
pure $ KernelVersion (Version (major', minor', patch', 0)) arch
|
||||
|
||||
synchronizer :: Synchronizer
|
||||
synchronizer = sync_0_2_8
|
||||
synchronizer = sync_0_2_11
|
||||
{-# INLINE synchronizer #-}
|
||||
|
||||
sync_0_2_8 :: Synchronizer
|
||||
sync_0_2_8 = Synchronizer
|
||||
"0.2.8"
|
||||
sync_0_2_11 :: Synchronizer
|
||||
sync_0_2_11 = Synchronizer
|
||||
"0.2.11"
|
||||
[ syncCreateAgentTmp
|
||||
, syncCreateSshDir
|
||||
, syncRemoveAvahiSystemdDependency
|
||||
, syncInstallLibAvahi
|
||||
, syncInstallAppMgr
|
||||
, syncFullUpgrade
|
||||
, sync32BitKernel
|
||||
@@ -113,6 +116,7 @@ sync_0_2_8 = Synchronizer
|
||||
, syncInstallDuplicity
|
||||
, syncInstallExfatFuse
|
||||
, syncInstallExfatUtils
|
||||
, syncUpgradeTor
|
||||
, syncInstallAmbassadorUI
|
||||
, syncOpenHttpPorts
|
||||
, syncUpgradeLifeline
|
||||
@@ -122,6 +126,8 @@ sync_0_2_8 = Synchronizer
|
||||
, syncConvertEcdsaCerts
|
||||
, syncRestarterService
|
||||
, syncInstallEject
|
||||
, syncDropCertificateUniqueness
|
||||
, syncRemoveDefaultNginxCfg
|
||||
]
|
||||
|
||||
syncCreateAgentTmp :: SyncOp
|
||||
@@ -240,6 +246,19 @@ syncInstallExfatUtils = SyncOp "Install exfat-utils" check migrate False
|
||||
shell "apt-get update"
|
||||
shell "apt-get install -y exfat-utils"
|
||||
|
||||
syncInstallLibAvahi :: SyncOp
|
||||
syncInstallLibAvahi = SyncOp "Install libavahi-client" check migrate False
|
||||
where
|
||||
check =
|
||||
liftIO
|
||||
$ (run (shell [i|dpkg -l|] $| shell [i|grep libavahi-client3|] $| conduit await) $> False)
|
||||
`catch` \(e :: ProcessException) -> case e of
|
||||
ProcessException _ (ExitFailure 1) -> pure True
|
||||
_ -> throwIO e
|
||||
migrate = liftIO . run $ do
|
||||
shell "apt-get update"
|
||||
shell "apt-get install -y libavahi-client3"
|
||||
|
||||
syncWriteConf :: Text -> ByteString -> SystemPath -> SyncOp
|
||||
syncWriteConf name contents' confLocation = SyncOp [i|Write #{name} Conf|] check migrate False
|
||||
where
|
||||
@@ -420,9 +439,11 @@ syncInstallAppMgr = SyncOp "Install AppMgr" check migrate False
|
||||
Left _ -> pure True
|
||||
Right v -> not . (v <||) <$> asks (appMgrVersionSpec . appSettings)
|
||||
migrate = fmap (either absurd id) . runExceptT . flip catchE failUpdate $ do
|
||||
lan <- asks appLanThread
|
||||
avs <- asks $ appMgrVersionSpec . appSettings
|
||||
av <- AppMgr.installNewAppMgr avs
|
||||
unless (av <|| avs) $ throwE $ AppMgrVersionE av avs
|
||||
flip runReaderT lan $ runLabelled @"lanThread" $ postResetLanLogic -- to accommodate 0.2.x -> 0.2.9 where previous appmgr didn't correctly set up lan
|
||||
|
||||
syncUpgradeLifeline :: SyncOp
|
||||
syncUpgradeLifeline = SyncOp "Upgrade Lifeline" check migrate False
|
||||
@@ -480,7 +501,7 @@ replaceDerivativeCerts :: (HasFilesystemBase sig m, Fused.Has (Error S9Error) si
|
||||
replaceDerivativeCerts = do
|
||||
sid <- getStart9AgentHostname
|
||||
let hostname = sid <> ".local"
|
||||
tor <- getAgentHiddenServiceUrl
|
||||
torAddr <- getAgentHiddenServiceUrl
|
||||
|
||||
caKeyPath <- toS <$> getAbsoluteLocationFor rootCaKeyPath
|
||||
caConfPath <- toS <$> getAbsoluteLocationFor rootCaOpenSslConfPath
|
||||
@@ -531,7 +552,7 @@ replaceDerivativeCerts = do
|
||||
, duration = 365
|
||||
}
|
||||
hostname
|
||||
tor
|
||||
torAddr
|
||||
liftIO $ do
|
||||
putStrLn @Text "openssl logs"
|
||||
putStrLn @Text "exit code: "
|
||||
@@ -563,6 +584,54 @@ syncRestarterService = SyncOp "Install Restarter Service" check migrate True
|
||||
liftIO $ callCommand "systemctl enable restarter.service"
|
||||
liftIO $ callCommand "systemctl enable restarter.timer"
|
||||
|
||||
syncUpgradeTor :: SyncOp
|
||||
syncUpgradeTor = SyncOp "Install Tor 0.3.5.14-1" check migrate False
|
||||
where
|
||||
check =
|
||||
liftIO
|
||||
$ ( run (shell [i|dpkg -l|] $| shell [i|grep tor|] $| shell [i|grep 0.3.5.14-1|] $| conduit await)
|
||||
$> False
|
||||
)
|
||||
`catch` \(e :: ProcessException) -> case e of
|
||||
ProcessException _ (ExitFailure 1) -> pure True
|
||||
_ -> throwIO e
|
||||
migrate = liftIO . run $ do
|
||||
shell "apt-get update"
|
||||
shell "apt-get install -y tor=0.3.5.14-1"
|
||||
|
||||
syncDropCertificateUniqueness :: SyncOp
|
||||
syncDropCertificateUniqueness = SyncOp "Eliminate OpenSSL unique_subject=yes" check migrate False
|
||||
where
|
||||
uni = "unique_subject = no\n"
|
||||
check = do
|
||||
base <- asks $ appFilesystemBase . appSettings
|
||||
contentsRoot <-
|
||||
liftIO
|
||||
$ (fmap Just . BS.readFile . toS $ (rootCaDirectory <> "index.txt.attr") `relativeTo` base)
|
||||
`catch` \(e :: IOException) -> if isDoesNotExistError e then pure Nothing else throwIO e
|
||||
contentsInt <-
|
||||
liftIO
|
||||
$ (fmap Just . BS.readFile . toS $ (intermediateCaDirectory <> "index.txt.attr") `relativeTo` base)
|
||||
`catch` \(e :: IOException) -> if isDoesNotExistError e then pure Nothing else throwIO e
|
||||
case (contentsRoot, contentsInt) of
|
||||
(Just root, Just int) -> pure $ uni /= root || uni /= int
|
||||
_ -> pure True
|
||||
migrate = do
|
||||
base <- asks $ appFilesystemBase . appSettings
|
||||
liftIO $ BS.writeFile (toS $ (rootCaDirectory <> "index.txt.attr") `relativeTo` base) uni
|
||||
liftIO $ BS.writeFile (toS $ (intermediateCaDirectory <> "index.txt.attr") `relativeTo` base) uni
|
||||
|
||||
syncRemoveDefaultNginxCfg :: SyncOp
|
||||
syncRemoveDefaultNginxCfg = SyncOp "Remove Default Nginx Configuration" check migrate False
|
||||
where
|
||||
check = do
|
||||
base <- asks $ appFilesystemBase . appSettings
|
||||
liftIO $ doesPathExist (toS $ nginxSitesEnabled "default" `relativeTo` base)
|
||||
migrate = do
|
||||
base <- asks $ appFilesystemBase . appSettings
|
||||
liftIO $ removeFileIfExists (toS $ nginxSitesEnabled "default" `relativeTo` base)
|
||||
liftIO $ systemCtl RestartService "nginx" $> ()
|
||||
|
||||
failUpdate :: S9Error -> ExceptT Void (ReaderT AgentCtx IO) ()
|
||||
failUpdate e = do
|
||||
ref <- asks appIsUpdateFailed
|
||||
|
||||
@@ -7,9 +7,7 @@ import Network.HTTP.Client
|
||||
import Network.Connection
|
||||
|
||||
import Lib.SystemPaths
|
||||
import Network.HTTP.Client.TLS ( mkManagerSettings
|
||||
, newTlsManagerWith
|
||||
)
|
||||
import Network.HTTP.Client.TLS ( mkManagerSettings )
|
||||
import Data.Default
|
||||
|
||||
getAgentHiddenServiceUrl :: (HasFilesystemBase sig m, MonadIO m) => m Text
|
||||
|
||||
@@ -7,6 +7,10 @@ newtype TorAddress = TorAddress { unTorAddress :: Text } deriving (Eq)
|
||||
instance Show TorAddress where
|
||||
show = toS . unTorAddress
|
||||
|
||||
newtype LanAddress = LanAddress { unLanAddress :: Text } deriving (Eq)
|
||||
instance Show LanAddress where
|
||||
show = toS . unLanAddress
|
||||
|
||||
newtype LanIp = LanIp { unLanIp :: Text } deriving (Eq)
|
||||
instance Show LanIp where
|
||||
show = toS . unLanIp
|
||||
|
||||
@@ -45,6 +45,7 @@ import Handler.Backups
|
||||
import Handler.Hosts
|
||||
import Handler.Icons
|
||||
import Handler.Login
|
||||
import Handler.Network
|
||||
import Handler.Notifications
|
||||
import Handler.PasswordUpdate
|
||||
import Handler.PowerOff
|
||||
|
||||
67
agent/test/Lib/External/AppManifestSpec.hs
vendored
67
agent/test/Lib/External/AppManifestSpec.hs
vendored
@@ -66,12 +66,65 @@ assets:
|
||||
hidden-service-version: v3
|
||||
|]
|
||||
|
||||
mastodon330Manifest :: ByteString
|
||||
mastodon330Manifest = [i|
|
||||
---
|
||||
id: mastodon
|
||||
version: 3.3.0.1
|
||||
title: Mastodon
|
||||
description:
|
||||
short: "A free, open-source social network server."
|
||||
long: "Mastodon is a free, open-source social network server based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub)!"
|
||||
release-notes: Added an acation to reset the admin password
|
||||
install-alert: "After starting mastodon for the first time, it can take a long time (several minutes) to be ready.\nPlease be patient. On future starts of the service, it will be faster, but still takes longer than other services.\nMake sure to sign up for a user before giving out your link. The first user to sign up is set as the admin user.\n"
|
||||
uninstall-alert: ~
|
||||
restore-alert: ~
|
||||
start-alert: "It may take several minutes after startup for this service to be ready for use.\n"
|
||||
has-instructions: true
|
||||
os-version-required: ">=0.2.8"
|
||||
os-version-recommended: ">=0.2.8"
|
||||
ports:
|
||||
- internal: 80
|
||||
tor: 80
|
||||
lan: standard
|
||||
- internal: 443
|
||||
tor: 443
|
||||
lan:
|
||||
custom:
|
||||
port: 443
|
||||
- internal: 3000
|
||||
tor: 3000
|
||||
lan: ~
|
||||
- internal: 4000
|
||||
tor: 4000
|
||||
lan: ~
|
||||
image:
|
||||
type: tar
|
||||
shm-size-mb: ~
|
||||
mount: /root/persistence
|
||||
public: ~
|
||||
shared: ~
|
||||
assets: []
|
||||
hidden-service-version: v3
|
||||
dependencies: {}
|
||||
actions:
|
||||
- id: reset-admin-password
|
||||
name: Reset Admin Password
|
||||
description: This action will reset your admin password to a random value
|
||||
allowed-statuses:
|
||||
- RUNNING
|
||||
command:
|
||||
- docker_entrypoint.sh
|
||||
- reset_admin_password.sh
|
||||
|]
|
||||
|
||||
|
||||
spec :: Spec
|
||||
spec = do
|
||||
describe "parsing app manifest ports" $ do
|
||||
it "should yield true for cups 0.2.3" $ do
|
||||
res <- decodeThrow @IO @(AppManifest 0) cups023Manifest
|
||||
uiAvailable res `shouldBe` True
|
||||
it "should yield false for cups 0.2.3 Mod" $ do
|
||||
res <- decodeThrow @IO @(AppManifest 0) cups023ManifestModNoUI
|
||||
uiAvailable res `shouldBe` False
|
||||
describe "parsing app manifest ports" $ do
|
||||
it "should parse mastodon 3.3.0" $ do
|
||||
res <- decodeThrow @IO @AppManifest mastodon330Manifest
|
||||
print res
|
||||
lanUiAvailable res `shouldBe` True
|
||||
torUiAvailable res `shouldBe` True
|
||||
|
||||
|
||||
1
appmgr/.gitignore
vendored
1
appmgr/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
.DS_Store
|
||||
1070
appmgr/Cargo.lock
generated
1070
appmgr/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
authors = ["Aiden McClelland <me@drbonez.dev>"]
|
||||
edition = "2018"
|
||||
name = "appmgr"
|
||||
version = "0.2.8"
|
||||
version = "0.2.11"
|
||||
|
||||
[lib]
|
||||
name = "appmgrlib"
|
||||
@@ -13,23 +13,27 @@ name = "appmgr"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
avahi = ["avahi-sys"]
|
||||
default = ["avahi"]
|
||||
portable = []
|
||||
production = []
|
||||
|
||||
[dependencies]
|
||||
argonautica = "0.2.0"
|
||||
async-trait = "0.1.42"
|
||||
avahi-sys = { git = "https://github.com/Start9Labs/avahi-sys", branch = "feature/dynamic-linking", features = ["dynamic"], optional = true }
|
||||
base32 = "0.4.0"
|
||||
clap = "2.33"
|
||||
ctrlc = "3.1.7"
|
||||
ed25519-dalek = "1.0.1"
|
||||
emver = { version = "0.1.0", features = ["serde"] }
|
||||
failure = "0.1.8"
|
||||
file-lock = "1.1"
|
||||
futures = "0.3.8"
|
||||
git-version = "0.3.4"
|
||||
http = "0.2.3"
|
||||
itertools = "0.9.0"
|
||||
lazy_static = "1.4"
|
||||
libc = "0.2.86"
|
||||
linear-map = { version = "1.2", features = ["serde_impl"] }
|
||||
log = "0.4.11"
|
||||
nix = "0.19.1"
|
||||
@@ -41,6 +45,8 @@ rand = "0.7.3"
|
||||
regex = "1.4.2"
|
||||
reqwest = { version = "0.10.9", features = ["stream", "json"] }
|
||||
rpassword = "5.0.0"
|
||||
rust-argon2 = "0.8.3"
|
||||
scopeguard = "1.1" # because avahi-sys fucks your shit up
|
||||
serde = { version = "1.0.118", features = ["derive", "rc"] }
|
||||
serde_cbor = "0.11.1"
|
||||
serde_json = "1.0.59"
|
||||
@@ -49,3 +55,4 @@ simple-logging = "2.0"
|
||||
tokio = { version = "0.3.5", features = ["full"] }
|
||||
tokio-compat-02 = "0.1.2"
|
||||
tokio-tar = { version = "0.3.0", git = "https://github.com/dr-bonez/tokio-tar.git", rev = "1ba710f3" }
|
||||
yajrc = { version = "0.1.0", git = "https://github.com/dr-bonez/yajrc", rev = "c2952a4a21c50f7be6f8003afa37ee77deb66d56" }
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
set -e
|
||||
shopt -s expand_aliases
|
||||
|
||||
if [ "$0" != "./build-dev.sh" ]; then
|
||||
>&2 echo "Must be run from appmgr directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest'
|
||||
|
||||
cd ..
|
||||
rust-arm-builder sh -c "(cd appmgr && cargo build --release)"
|
||||
rust-arm-builder sh -c "(cd appmgr && cargo build)"
|
||||
|
||||
15
appmgr/build-portable.sh
Executable file
15
appmgr/build-portable.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
shopt -s expand_aliases
|
||||
|
||||
if [ "$0" != "./build-portable.sh" ]; then
|
||||
>&2 echo "Must be run from appmgr directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
alias 'rust-musl-builder'='docker run --rm -it -v "$HOME"/.cargo/registry:/root/.cargo/registry -v "$(pwd)":/home/rust/src messense/rust-musl-cross:x86_64-musl'
|
||||
|
||||
cd ..
|
||||
rust-musl-builder sh -c "(cd appmgr && cargo build --release --target=x86_64-unknown-linux-musl --features=portable,production --no-default-features)"
|
||||
cd appmgr
|
||||
@@ -3,6 +3,11 @@
|
||||
set -e
|
||||
shopt -s expand_aliases
|
||||
|
||||
if [ "$0" != "./build-prod.sh" ]; then
|
||||
>&2 echo "Must be run from appmgr directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest'
|
||||
|
||||
cd ..
|
||||
|
||||
116
appmgr/src/actions.rs
Normal file
116
appmgr/src/actions.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
use std::process::Stdio;
|
||||
|
||||
use linear_map::set::LinearSet;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, Error as IoError};
|
||||
use yajrc::RpcError;
|
||||
|
||||
use crate::apps::DockerStatus;
|
||||
|
||||
pub const STATUS_NOT_ALLOWED: i32 = -2;
|
||||
pub const INVALID_COMMAND: i32 = -3;
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Action {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub warning: Option<String>,
|
||||
pub allowed_statuses: LinearSet<DockerStatus>,
|
||||
pub command: Vec<String>,
|
||||
}
|
||||
|
||||
async fn tee<R: AsyncRead + Unpin, W: AsyncWrite + Unpin>(
|
||||
mut r: R,
|
||||
mut w: W,
|
||||
) -> Result<Vec<u8>, IoError> {
|
||||
let mut res = Vec::new();
|
||||
let mut buf = vec![0; 2048];
|
||||
let mut bytes;
|
||||
while {
|
||||
bytes = r.read(&mut buf).await?;
|
||||
bytes != 0
|
||||
} {
|
||||
res.extend_from_slice(&buf[..bytes]);
|
||||
w.write_all(&buf[..bytes]).await?;
|
||||
}
|
||||
w.flush().await?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub async fn perform(&self, app_id: &str) -> Result<String, RpcError> {
|
||||
let man = crate::apps::manifest(app_id)
|
||||
.await
|
||||
.map_err(failure::Error::from)
|
||||
.map_err(failure::Error::compat)?;
|
||||
let status = crate::apps::status(app_id, true)
|
||||
.await
|
||||
.map_err(failure::Error::from)
|
||||
.map_err(failure::Error::compat)?
|
||||
.status;
|
||||
if !self.allowed_statuses.contains(&status) {
|
||||
return Err(RpcError {
|
||||
code: STATUS_NOT_ALLOWED,
|
||||
message: format!(
|
||||
"{} is in status {:?} which is not allowed by {}",
|
||||
app_id, status, self.id
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
let mut cmd = if status == DockerStatus::Running {
|
||||
let mut cmd = tokio::process::Command::new("docker");
|
||||
cmd.arg("exec").arg(&app_id).args(&self.command);
|
||||
cmd
|
||||
} else {
|
||||
let mut cmd = tokio::process::Command::new("docker");
|
||||
let entrypoint = self.command.get(0).ok_or_else(|| RpcError {
|
||||
code: INVALID_COMMAND,
|
||||
message: "Command Cannot Be Empty".to_owned(),
|
||||
data: None,
|
||||
})?;
|
||||
cmd.arg("run")
|
||||
.arg("--rm")
|
||||
.arg("--name")
|
||||
.arg(format!("{}_{}", app_id, self.id))
|
||||
.arg("--mount")
|
||||
.arg(format!(
|
||||
"type=bind,src={}/{},dst={}",
|
||||
crate::VOLUMES,
|
||||
app_id,
|
||||
man.mount.display()
|
||||
))
|
||||
.arg("--entrypoint")
|
||||
.arg(entrypoint)
|
||||
.arg(format!("start9/{}", app_id))
|
||||
.args(&self.command[1..]);
|
||||
// TODO: 0.3.0: net, tor, shm
|
||||
cmd
|
||||
};
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
let mut child = cmd.spawn()?;
|
||||
|
||||
let (stdout, stderr) = futures::try_join!(
|
||||
tee(child.stdout.take().unwrap(), tokio::io::sink()),
|
||||
tee(child.stderr.take().unwrap(), tokio::io::sink())
|
||||
)?;
|
||||
|
||||
let status = child.wait().await?;
|
||||
if status.success() {
|
||||
String::from_utf8(stdout).map_err(From::from)
|
||||
} else {
|
||||
Err(RpcError {
|
||||
code: status
|
||||
.code()
|
||||
.unwrap_or_else(|| status.signal().unwrap_or(0) + 128),
|
||||
message: String::from_utf8(stderr)?,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
use std::path::Path;
|
||||
|
||||
use argonautica::{Hasher, Verifier};
|
||||
use argon2::Config;
|
||||
use emver::Version;
|
||||
use futures::try_join;
|
||||
use futures::TryStreamExt;
|
||||
use rand::Rng;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::util::from_yaml_async_reader;
|
||||
use crate::util::to_yaml_async_writer;
|
||||
use crate::util::Invoke;
|
||||
use crate::util::PersistencePath;
|
||||
use crate::version::VersionT;
|
||||
use crate::Error;
|
||||
use crate::ResultExt;
|
||||
@@ -46,10 +49,7 @@ pub async fn create_backup<P: AsRef<Path>>(
|
||||
let mut hash = String::new();
|
||||
f.read_to_string(&mut hash).await?;
|
||||
crate::ensure_code!(
|
||||
Verifier::new()
|
||||
.with_password(password)
|
||||
.with_hash(hash)
|
||||
.verify()
|
||||
argon2::verify_encoded(&hash, password.as_bytes())
|
||||
.with_code(crate::error::INVALID_BACKUP_PASSWORD)?,
|
||||
crate::error::INVALID_BACKUP_PASSWORD,
|
||||
"Invalid Backup Decryption Password"
|
||||
@@ -58,10 +58,8 @@ pub async fn create_backup<P: AsRef<Path>>(
|
||||
{
|
||||
// save password
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
let mut hasher = Hasher::default();
|
||||
hasher.opt_out_of_secret_key(true);
|
||||
let hash = hasher.with_password(password).hash().no_code()?;
|
||||
let salt = rand::thread_rng().gen::<[u8; 32]>();
|
||||
let hash = argon2::hash_encoded(password.as_bytes(), &salt, &Config::default()).unwrap(); // this is safe because apparently the API was poorly designed
|
||||
let mut f = tokio::fs::File::create(pw_path).await?;
|
||||
f.write_all(hash.as_bytes()).await?;
|
||||
f.flush().await?;
|
||||
@@ -160,10 +158,7 @@ pub async fn restore_backup<P: AsRef<Path>>(
|
||||
let mut hash = String::new();
|
||||
f.read_to_string(&mut hash).await?;
|
||||
crate::ensure_code!(
|
||||
Verifier::new()
|
||||
.with_password(password)
|
||||
.with_hash(hash)
|
||||
.verify()
|
||||
argon2::verify_encoded(&hash, password.as_bytes())
|
||||
.with_code(crate::error::INVALID_BACKUP_PASSWORD)?,
|
||||
crate::error::INVALID_BACKUP_PASSWORD,
|
||||
"Invalid Backup Decryption Password"
|
||||
@@ -231,6 +226,28 @@ pub async fn restore_backup<P: AsRef<Path>>(
|
||||
}
|
||||
|
||||
crate::tor::restart().await?;
|
||||
// Delete the fullchain certificate, so it can be regenerated with the restored tor pubkey address
|
||||
PersistencePath::from_ref("apps")
|
||||
.join(&app_id)
|
||||
.join("cert-local.fullchain.crt.pem")
|
||||
.delete()
|
||||
.await?;
|
||||
crate::tor::write_lan_services(
|
||||
&crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?,
|
||||
)
|
||||
.await?;
|
||||
let svc_exit = std::process::Command::new("service")
|
||||
.args(&["nginx", "reload"])
|
||||
.status()?;
|
||||
crate::ensure_code!(
|
||||
svc_exit.success(),
|
||||
crate::error::GENERAL_ERROR,
|
||||
"Failed to Reload Nginx: {}",
|
||||
svc_exit
|
||||
.code()
|
||||
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||
.unwrap_or(0)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
10
appmgr/src/cert-local.csr.conf.template
Normal file
10
appmgr/src/cert-local.csr.conf.template
Normal file
@@ -0,0 +1,10 @@
|
||||
[req]
|
||||
default_bits = 4096
|
||||
default_md = sha256
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt = no
|
||||
|
||||
[req_distinguished_name]
|
||||
CN = {hostname}.local
|
||||
O = Start9 Labs
|
||||
OU = Embassy
|
||||
@@ -1864,6 +1864,9 @@ mod test {
|
||||
hidden_service_version: crate::tor::HiddenServiceVersion::V3,
|
||||
dependencies: deps,
|
||||
extra: LinearMap::new(),
|
||||
install_alert: None,
|
||||
restore_alert: None,
|
||||
uninstall_alert: None,
|
||||
})
|
||||
.unwrap();
|
||||
let config = spec
|
||||
|
||||
@@ -24,7 +24,6 @@ pub async fn start_app(name: &str, update_metadata: bool) -> Result<(), Error> {
|
||||
if status == crate::apps::DockerStatus::Stopped {
|
||||
if update_metadata {
|
||||
crate::config::configure(name, None, None, false).await?;
|
||||
crate::dependencies::update_shared(name).await?;
|
||||
crate::dependencies::update_binds(name).await?;
|
||||
}
|
||||
crate::apps::set_needs_restart(name, false).await?;
|
||||
|
||||
@@ -188,31 +188,6 @@ pub async fn auto_configure(
|
||||
crate::config::configure(dependency, Some(dependency_config), None, dry_run).await
|
||||
}
|
||||
|
||||
pub async fn update_shared(dependency_id: &str) -> Result<(), Error> {
|
||||
let dependency_manifest = crate::apps::manifest(dependency_id).await?;
|
||||
if let Some(shared) = dependency_manifest.shared {
|
||||
for dependent_id in &crate::apps::dependents(dependency_id, false).await? {
|
||||
let dependent_manifest = crate::apps::manifest(&dependent_id).await?;
|
||||
if dependent_manifest
|
||||
.dependencies
|
||||
.0
|
||||
.get(dependency_id)
|
||||
.ok_or_else(|| failure::format_err!("failed to index dependent: {}", dependent_id))?
|
||||
.mount_shared
|
||||
{
|
||||
tokio::fs::create_dir_all(
|
||||
Path::new(crate::VOLUMES)
|
||||
.join(dependency_id)
|
||||
.join(&shared)
|
||||
.join(&dependent_id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_binds(dependent_id: &str) -> Result<(), Error> {
|
||||
let dependent_manifest = crate::apps::manifest(dependent_id).await?;
|
||||
let dependency_manifests = futures::future::try_join_all(
|
||||
@@ -222,12 +197,19 @@ pub async fn update_binds(dependent_id: &str) -> Result<(), Error> {
|
||||
.into_iter()
|
||||
.filter(|(_, info)| info.mount_public || info.mount_shared)
|
||||
.map(|(id, info)| async {
|
||||
crate::apps::manifest(&id).await.map(|man| (id, info, man))
|
||||
Ok::<_, Error>(if crate::apps::list_info().await?.contains_key(&id) {
|
||||
let man = crate::apps::manifest(&id).await?;
|
||||
Some((id, info, man))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
// i just have a gut feeling this shouldn't be concurrent
|
||||
for (dependency_id, info, dependency_manifest) in dependency_manifests {
|
||||
for (dependency_id, info, dependency_manifest) in
|
||||
dependency_manifests.into_iter().filter_map(|a| a)
|
||||
{
|
||||
match (dependency_manifest.public, info.mount_public) {
|
||||
(Some(public), true) => {
|
||||
let public_path = Path::new(crate::VOLUMES).join(&dependency_id).join(public);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::path::Path;
|
||||
|
||||
use failure::ResultExt as _;
|
||||
use futures::future::try_join_all;
|
||||
|
||||
use crate::util::Invoke;
|
||||
use crate::Error;
|
||||
use crate::ResultExt;
|
||||
use crate::ResultExt as _;
|
||||
|
||||
pub const FSTAB: &'static str = "/etc/fstab";
|
||||
|
||||
@@ -153,6 +154,11 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
|
||||
dst: P1,
|
||||
read_only: bool,
|
||||
) -> Result<(), Error> {
|
||||
log::info!(
|
||||
"Binding {} to {}",
|
||||
src.as_ref().display(),
|
||||
dst.as_ref().display()
|
||||
);
|
||||
let is_mountpoint = tokio::process::Command::new("mountpoint")
|
||||
.arg(dst.as_ref())
|
||||
.stdout(std::process::Stdio::null())
|
||||
@@ -185,6 +191,7 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
|
||||
}
|
||||
|
||||
pub async fn unmount<P: AsRef<Path>>(mount_point: P) -> Result<(), Error> {
|
||||
log::info!("Unmounting {}.", mount_point.as_ref().display());
|
||||
let umount_output = tokio::process::Command::new("umount")
|
||||
.arg(mount_point.as_ref())
|
||||
.output()
|
||||
@@ -192,10 +199,14 @@ pub async fn unmount<P: AsRef<Path>>(mount_point: P) -> Result<(), Error> {
|
||||
crate::ensure_code!(
|
||||
umount_output.status.success(),
|
||||
crate::error::FILESYSTEM_ERROR,
|
||||
"Error Unmounting Drive: {}",
|
||||
"Error Unmounting Drive: {}: {}",
|
||||
mount_point.as_ref().display(),
|
||||
std::str::from_utf8(&umount_output.stderr).unwrap_or("Unknown Error")
|
||||
);
|
||||
tokio::fs::remove_dir_all(mount_point.as_ref()).await?;
|
||||
tokio::fs::remove_dir_all(mount_point.as_ref())
|
||||
.await
|
||||
.with_context(|e| format!("rm {}: {}", mount_point.as_ref().display(), e))
|
||||
.with_code(crate::error::FILESYSTEM_ERROR)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -256,6 +256,19 @@ pub async fn install_v0<R: AsyncRead + Unpin + Send + Sync>(
|
||||
"Package Name Does Not Match Expected"
|
||||
);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Creating metadata directory: {}/apps/{}",
|
||||
crate::PERSISTENCE_DIR,
|
||||
manifest.id
|
||||
);
|
||||
let app_dir = PersistencePath::from_ref("apps").join(&manifest.id);
|
||||
let app_dir_path = app_dir.path();
|
||||
if app_dir_path.exists() {
|
||||
tokio::fs::remove_dir_all(&app_dir_path).await?;
|
||||
}
|
||||
tokio::fs::create_dir_all(&app_dir_path).await?;
|
||||
|
||||
let (ip, tor_addr, tor_key) = crate::tor::set_svc(
|
||||
&manifest.id,
|
||||
crate::tor::NewService {
|
||||
@@ -270,12 +283,6 @@ pub async fn install_v0<R: AsyncRead + Unpin + Send + Sync>(
|
||||
log::info!("Creating volume {}/{}.", crate::VOLUMES, manifest.id);
|
||||
tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id)).await?;
|
||||
|
||||
let app_dir = PersistencePath::from_ref("apps").join(&manifest.id);
|
||||
let app_dir_path = app_dir.path();
|
||||
if app_dir_path.exists() {
|
||||
tokio::fs::remove_dir_all(&app_dir_path).await?;
|
||||
}
|
||||
tokio::fs::create_dir_all(&app_dir_path).await?;
|
||||
let _lock = app_dir.lock(true).await?;
|
||||
log::info!("Saving manifest.");
|
||||
let mut manifest_out = app_dir.join("manifest.yaml").write(None).await?;
|
||||
@@ -554,14 +561,17 @@ pub async fn install_v0<R: AsyncRead + Unpin + Send + Sync>(
|
||||
crate::config::configure(&manifest.id, Some(empty_config), None, false).await?;
|
||||
}
|
||||
}
|
||||
crate::dependencies::update_binds(&manifest.id).await?;
|
||||
for (dep_id, dep_info) in manifest.dependencies.0 {
|
||||
if dep_info.mount_shared
|
||||
&& crate::apps::list_info().await?.get(&dep_id).is_some()
|
||||
&& crate::apps::manifest(&dep_id).await?.shared.is_some()
|
||||
&& crate::apps::status(&dep_id, false).await?.status
|
||||
!= crate::apps::DockerStatus::Stopped
|
||||
{
|
||||
crate::apps::set_needs_restart(&dep_id, true).await?;
|
||||
match crate::apps::status(&dep_id, false).await?.status {
|
||||
crate::apps::DockerStatus::Stopped => (),
|
||||
crate::apps::DockerStatus::Running => crate::control::restart_app(&dep_id).await?,
|
||||
_ => crate::apps::set_needs_restart(&dep_id, true).await?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
93
appmgr/src/lan.rs
Normal file
93
appmgr/src/lan.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use crate::Error;
|
||||
use avahi_sys;
|
||||
use futures::future::pending;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct AppId {
|
||||
pub un_app_id: String,
|
||||
}
|
||||
|
||||
pub async fn enable_lan() -> Result<(), Error> {
|
||||
unsafe {
|
||||
let app_list = crate::apps::list_info().await?;
|
||||
|
||||
let simple_poll = avahi_sys::avahi_simple_poll_new();
|
||||
let poll = avahi_sys::avahi_simple_poll_get(simple_poll);
|
||||
let mut stack_err = 0;
|
||||
let err_c: *mut i32 = &mut stack_err;
|
||||
let avahi_client = avahi_sys::avahi_client_new(
|
||||
poll,
|
||||
avahi_sys::AvahiClientFlags::AVAHI_CLIENT_NO_FAIL,
|
||||
None,
|
||||
std::ptr::null_mut(),
|
||||
err_c,
|
||||
);
|
||||
let group =
|
||||
avahi_sys::avahi_entry_group_new(avahi_client, Some(noop), std::ptr::null_mut());
|
||||
let hostname_raw = avahi_sys::avahi_client_get_host_name_fqdn(avahi_client);
|
||||
let hostname_bytes = std::ffi::CStr::from_ptr(hostname_raw).to_bytes_with_nul();
|
||||
const HOSTNAME_LEN: usize = 1 + 15 + 1 + 5; // leading byte, main address, dot, "local"
|
||||
debug_assert_eq!(hostname_bytes.len(), HOSTNAME_LEN);
|
||||
let mut hostname_buf = [0; HOSTNAME_LEN + 1];
|
||||
hostname_buf[1..].copy_from_slice(hostname_bytes);
|
||||
// assume fixed length prefix on hostname due to local address
|
||||
hostname_buf[0] = 15; // set the prefix length to 15 for the main address
|
||||
hostname_buf[16] = 5; // set the prefix length to 5 for "local"
|
||||
|
||||
for (app_id, app_info) in app_list {
|
||||
let man = crate::apps::manifest(&app_id).await?;
|
||||
if man
|
||||
.ports
|
||||
.iter()
|
||||
.filter(|p| p.lan.is_some())
|
||||
.next()
|
||||
.is_none()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let tor_address = if let Some(addr) = app_info.tor_address {
|
||||
addr
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let lan_address = tor_address
|
||||
.strip_suffix(".onion")
|
||||
.ok_or_else(|| failure::format_err!("Invalid Tor Address: {:?}", tor_address))?
|
||||
.to_owned()
|
||||
+ ".local";
|
||||
let lan_address_ptr = std::ffi::CString::new(lan_address)
|
||||
.expect("Could not cast lan address to c string");
|
||||
let _ = avahi_sys::avahi_entry_group_add_record(
|
||||
group,
|
||||
avahi_sys::AVAHI_IF_UNSPEC,
|
||||
avahi_sys::AVAHI_PROTO_UNSPEC,
|
||||
avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_USE_MULTICAST
|
||||
| avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_ALLOW_MULTIPLE,
|
||||
lan_address_ptr.as_ptr(),
|
||||
avahi_sys::AVAHI_DNS_CLASS_IN as u16,
|
||||
avahi_sys::AVAHI_DNS_TYPE_CNAME as u16,
|
||||
avahi_sys::AVAHI_DEFAULT_TTL,
|
||||
hostname_buf.as_ptr().cast(),
|
||||
hostname_buf.len(),
|
||||
);
|
||||
log::info!("Published {:?}", lan_address_ptr);
|
||||
}
|
||||
avahi_sys::avahi_entry_group_commit(group);
|
||||
ctrlc::set_handler(move || {
|
||||
// please the borrow checker with the below semantics
|
||||
// avahi_sys::avahi_entry_group_free(group);
|
||||
// avahi_sys::avahi_client_free(avahi_client);
|
||||
// drop(Box::from_raw(err_c));
|
||||
std::process::exit(0);
|
||||
})
|
||||
.expect("Error setting signal handler");
|
||||
}
|
||||
pending().await
|
||||
}
|
||||
|
||||
unsafe extern "C" fn noop(
|
||||
_group: *mut avahi_sys::AvahiEntryGroup,
|
||||
_state: avahi_sys::AvahiEntryGroupState,
|
||||
_userdata: *mut core::ffi::c_void,
|
||||
) {
|
||||
}
|
||||
@@ -20,6 +20,7 @@ lazy_static::lazy_static! {
|
||||
pub static ref QUIET: tokio::sync::RwLock<bool> = tokio::sync::RwLock::new(!std::env::var("APPMGR_QUIET").map(|a| a == "0").unwrap_or(true));
|
||||
}
|
||||
|
||||
pub mod actions;
|
||||
pub mod apps;
|
||||
pub mod backup;
|
||||
pub mod config;
|
||||
@@ -30,6 +31,8 @@ pub mod error;
|
||||
pub mod index;
|
||||
pub mod inspect;
|
||||
pub mod install;
|
||||
#[cfg(feature = "avahi")]
|
||||
pub mod lan;
|
||||
pub mod logs;
|
||||
pub mod manifest;
|
||||
pub mod pack;
|
||||
|
||||
@@ -162,6 +162,17 @@ async fn inner_main() -> Result<(), Error> {
|
||||
),
|
||||
);
|
||||
|
||||
#[cfg(feature = "avahi")]
|
||||
#[allow(unused_mut)]
|
||||
let mut app = app.subcommand(
|
||||
SubCommand::with_name("lan")
|
||||
.about("Configures LAN services")
|
||||
.subcommand(
|
||||
SubCommand::with_name("enable")
|
||||
.about("Publishes the LAN addresses for all services"),
|
||||
),
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "portable"))]
|
||||
let mut app = app
|
||||
.subcommand(
|
||||
@@ -399,7 +410,6 @@ async fn inner_main() -> Result<(), Error> {
|
||||
.about("Removes an installed app")
|
||||
.arg(
|
||||
Arg::with_name("purge")
|
||||
.short("p")
|
||||
.long("purge")
|
||||
.help("Deletes all application data"),
|
||||
)
|
||||
@@ -820,6 +830,16 @@ async fn inner_main() -> Result<(), Error> {
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("repair-app-status").about("Restarts crashed apps"), // TODO: remove
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("actions")
|
||||
.about("Perform an action for a service")
|
||||
.arg(
|
||||
Arg::with_name("SERVICE")
|
||||
.help("ID of the service to perform an action on")
|
||||
.required(true),
|
||||
)
|
||||
.arg(Arg::with_name("ACTION").help("ID of the action to perform")),
|
||||
);
|
||||
|
||||
let matches = app.clone().get_matches();
|
||||
@@ -1178,6 +1198,15 @@ async fn inner_main() -> Result<(), Error> {
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
#[cfg(feature = "avahi")]
|
||||
#[cfg(not(feature = "portable"))]
|
||||
("lan", Some(sub_m)) => match sub_m.subcommand() {
|
||||
("enable", _) => crate::lan::enable_lan().await?,
|
||||
_ => {
|
||||
println!("{}", sub_m.usage());
|
||||
std::process::exit(1);
|
||||
}
|
||||
},
|
||||
#[cfg(not(feature = "portable"))]
|
||||
("info", Some(sub_m)) => {
|
||||
let name = sub_m.value_of("ID").unwrap();
|
||||
@@ -1547,6 +1576,34 @@ async fn inner_main() -> Result<(), Error> {
|
||||
("repair-app-status", _) => {
|
||||
control::repair_app_status().await?;
|
||||
}
|
||||
#[cfg(not(feature = "portable"))]
|
||||
("actions", Some(sub_m)) => {
|
||||
use yajrc::{GenericRpcMethod, RpcResponse};
|
||||
|
||||
let man = apps::manifest(sub_m.value_of("SERVICE").unwrap()).await?;
|
||||
let action_id = sub_m.value_of("ACTION").unwrap();
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&RpcResponse::<GenericRpcMethod>::from_result(
|
||||
man.actions
|
||||
.iter()
|
||||
.filter(|a| &a.id == &action_id)
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
failure::format_err!(
|
||||
"action {} does not exist for {}",
|
||||
action_id,
|
||||
man.id
|
||||
)
|
||||
})
|
||||
.with_code(error::NOT_FOUND)?
|
||||
.perform(&man.id)
|
||||
.await
|
||||
.map(serde_json::Value::String)
|
||||
))
|
||||
.with_code(error::SERDE_ERROR)?
|
||||
)
|
||||
}
|
||||
("pack", Some(sub_m)) => {
|
||||
pack(
|
||||
sub_m.value_of("PATH").unwrap(),
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use linear_map::LinearMap;
|
||||
|
||||
use crate::actions::Action;
|
||||
use crate::dependencies::Dependencies;
|
||||
use crate::tor::HiddenServiceVersion;
|
||||
use crate::tor::PortMapping;
|
||||
@@ -43,6 +44,8 @@ pub struct ManifestV0 {
|
||||
#[serde(default)]
|
||||
pub restore_alert: Option<String>,
|
||||
#[serde(default)]
|
||||
pub start_alert: Option<String>,
|
||||
#[serde(default)]
|
||||
pub has_instructions: bool,
|
||||
#[serde(default = "emver::VersionRange::any")]
|
||||
pub os_version_required: emver::VersionRange,
|
||||
@@ -63,6 +66,8 @@ pub struct ManifestV0 {
|
||||
pub hidden_service_version: HiddenServiceVersion,
|
||||
#[serde(default)]
|
||||
pub dependencies: Dependencies,
|
||||
#[serde(default)]
|
||||
pub actions: Vec<Action>,
|
||||
#[serde(flatten)]
|
||||
pub extra: LinearMap<String, serde_yaml::Value>,
|
||||
}
|
||||
|
||||
19
appmgr/src/nginx-standard.conf.template
Normal file
19
appmgr/src/nginx-standard.conf.template
Normal file
@@ -0,0 +1,19 @@
|
||||
server {{
|
||||
listen 443 ssl;
|
||||
server_name {hostname}.local;
|
||||
ssl_certificate /root/appmgr/apps/{app_id}/cert-local.fullchain.crt.pem;
|
||||
ssl_certificate_key /root/appmgr/apps/{app_id}/cert-local.key.pem;
|
||||
location / {{
|
||||
proxy_pass http://{app_ip}:{internal_port}/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
client_max_body_size 0;
|
||||
}}
|
||||
}}
|
||||
server {{
|
||||
listen 80;
|
||||
server_name {hostname}.local;
|
||||
return 301 https://$host$request_uri;
|
||||
}}
|
||||
9
appmgr/src/nginx.conf.template
Normal file
9
appmgr/src/nginx.conf.template
Normal file
@@ -0,0 +1,9 @@
|
||||
server {{
|
||||
listen {port};
|
||||
server_name {hostname}.local;
|
||||
location / {{
|
||||
proxy_pass http://{app_ip}:{internal_port}/;
|
||||
proxy_set_header Host $host;
|
||||
client_max_body_size 0;
|
||||
}}
|
||||
}}
|
||||
@@ -217,6 +217,13 @@ pub async fn verify(path: &str) -> Result<(), failure::Error> {
|
||||
if let Some(shared) = &manifest.shared {
|
||||
validate_path(shared)?;
|
||||
}
|
||||
for action in &manifest.actions {
|
||||
ensure!(
|
||||
!action.command.is_empty(),
|
||||
"Command Cannot Be Empty: {}",
|
||||
action.id
|
||||
);
|
||||
}
|
||||
log::info!("Opening config spec from archive.");
|
||||
let config_spec = entries
|
||||
.next()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::failure::ResultExt;
|
||||
use std::path::Path;
|
||||
|
||||
use linear_map::LinearMap;
|
||||
|
||||
use crate::dependencies::{DependencyError, TaggedDependencyError};
|
||||
use crate::Error;
|
||||
use crate::ResultExt as _;
|
||||
|
||||
pub async fn remove(
|
||||
name: &str,
|
||||
@@ -55,48 +57,79 @@ pub async fn remove(
|
||||
log::info!("Removing tor hidden service.");
|
||||
crate::tor::rm_svc(name).await?;
|
||||
log::info!("Removing app metadata.");
|
||||
tokio::fs::remove_dir_all(Path::new(crate::PERSISTENCE_DIR).join("apps").join(name))
|
||||
.await?;
|
||||
log::info!("Destroying mounted volume.");
|
||||
let metadata_path = Path::new(crate::PERSISTENCE_DIR).join("apps").join(name);
|
||||
tokio::fs::remove_dir_all(&metadata_path)
|
||||
.await
|
||||
.with_context(|e| format!("rm {}: {}", metadata_path.display(), e))
|
||||
.with_code(crate::error::FILESYSTEM_ERROR)?;
|
||||
log::info!("Unbinding shared filesystem.");
|
||||
for (dep, info) in manifest.dependencies.0.iter() {
|
||||
if info.mount_public {
|
||||
crate::disks::unmount(
|
||||
Path::new(crate::VOLUMES)
|
||||
.join(name)
|
||||
.join("start9")
|
||||
.join("public")
|
||||
.join(&dep),
|
||||
)
|
||||
.await?;
|
||||
let installed_apps = crate::apps::list_info().await?;
|
||||
for (dep, _) in manifest.dependencies.0.iter() {
|
||||
let path = Path::new(crate::VOLUMES)
|
||||
.join(name)
|
||||
.join("start9")
|
||||
.join("public")
|
||||
.join(&dep);
|
||||
if path.exists() {
|
||||
crate::disks::unmount(&path).await?;
|
||||
} else {
|
||||
log::warn!("{} does not exist, skipping...", path.display());
|
||||
}
|
||||
if info.mount_shared {
|
||||
if let Some(shared) = match crate::apps::manifest(dep).await {
|
||||
Ok(man) => man.shared,
|
||||
Err(e) => {
|
||||
log::error!("Failed to Fetch Dependency Manifest: {}", e);
|
||||
None
|
||||
}
|
||||
} {
|
||||
let path = Path::new(crate::VOLUMES)
|
||||
.join(name)
|
||||
.join("start9")
|
||||
.join("shared")
|
||||
.join(&dep);
|
||||
if path.exists() {
|
||||
crate::disks::unmount(&path).await?;
|
||||
}
|
||||
let path = Path::new(crate::VOLUMES)
|
||||
.join(name)
|
||||
.join("start9")
|
||||
.join("shared")
|
||||
.join(&dep);
|
||||
if path.exists() {
|
||||
crate::disks::unmount(&path).await?;
|
||||
} else {
|
||||
log::warn!("{} does not exist, skipping...", path.display());
|
||||
}
|
||||
if installed_apps.contains_key(dep) {
|
||||
let dep_man = crate::apps::manifest(dep).await?;
|
||||
if let Some(shared) = dep_man.shared {
|
||||
let path = Path::new(crate::VOLUMES).join(dep).join(&shared).join(name);
|
||||
if path.exists() {
|
||||
tokio::fs::remove_dir_all(
|
||||
Path::new(crate::VOLUMES).join(dep).join(&shared).join(name),
|
||||
)
|
||||
.await?;
|
||||
tokio::fs::remove_dir_all(&path)
|
||||
.await
|
||||
.with_context(|e| format!("rm {}: {}", path.display(), e))
|
||||
.with_code(crate::error::FILESYSTEM_ERROR)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!("{} is not installed, skipping...", dep);
|
||||
}
|
||||
}
|
||||
if manifest.public.is_some() || manifest.shared.is_some() {
|
||||
for dependent in crate::apps::dependents(name, false).await? {
|
||||
let path = Path::new(crate::VOLUMES)
|
||||
.join(&dependent)
|
||||
.join("start9")
|
||||
.join("public")
|
||||
.join(name);
|
||||
if path.exists() {
|
||||
crate::disks::unmount(&path).await?;
|
||||
} else {
|
||||
log::warn!("{} does not exist, skipping...", path.display());
|
||||
}
|
||||
let path = Path::new(crate::VOLUMES)
|
||||
.join(dependent)
|
||||
.join("start9")
|
||||
.join("shared")
|
||||
.join(name);
|
||||
if path.exists() {
|
||||
crate::disks::unmount(&path).await?;
|
||||
} else {
|
||||
log::warn!("{} does not exist, skipping...", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
tokio::fs::remove_dir_all(Path::new(crate::VOLUMES).join(name)).await?;
|
||||
log::info!("Destroying mounted volume.");
|
||||
let volume_path = Path::new(crate::VOLUMES).join(name);
|
||||
tokio::fs::remove_dir_all(&volume_path)
|
||||
.await
|
||||
.with_context(|e| format!("rm {}: {}", volume_path.display(), e))
|
||||
.with_code(crate::error::FILESYSTEM_ERROR)?;
|
||||
log::info!("Pruning unused docker images.");
|
||||
crate::ensure_code!(
|
||||
std::process::Command::new("docker")
|
||||
|
||||
@@ -8,17 +8,62 @@ use failure::ResultExt as _;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::util::{PersistencePath, YamlUpdateHandle};
|
||||
use crate::util::{Invoke, PersistencePath, YamlUpdateHandle};
|
||||
use crate::{Error, ResultExt as _};
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum LanOptions {
|
||||
Standard,
|
||||
Custom { port: u16 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize)]
|
||||
pub struct PortMapping {
|
||||
pub internal: u16,
|
||||
pub tor: u16,
|
||||
pub lan: Option<LanOptions>, // only for http interfaces
|
||||
}
|
||||
impl<'de> serde::de::Deserialize<'de> for PortMapping {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct PortMappingIF {
|
||||
pub internal: u16,
|
||||
pub tor: u16,
|
||||
#[serde(default, deserialize_with = "deserialize_some")]
|
||||
pub lan: Option<Option<LanOptions>>,
|
||||
}
|
||||
|
||||
fn deserialize_some<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
|
||||
where
|
||||
T: serde::de::Deserialize<'de>,
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
serde::de::Deserialize::deserialize(deserializer).map(Some)
|
||||
}
|
||||
|
||||
let input_format: PortMappingIF = serde::de::Deserialize::deserialize(deserializer)?;
|
||||
Ok(PortMapping {
|
||||
internal: input_format.internal,
|
||||
tor: input_format.tor,
|
||||
lan: if let Some(lan) = input_format.lan {
|
||||
lan
|
||||
} else if input_format.tor == 80 {
|
||||
Some(LanOptions::Standard)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub const ETC_TOR_RC: &'static str = "/etc/tor/torrc";
|
||||
pub const HIDDEN_SERVICE_DIR_ROOT: &'static str = "/var/lib/tor";
|
||||
pub const ETC_HOSTNAME: &'static str = "/etc/hostname";
|
||||
pub const ETC_NGINX_SERVICES_CONF: &'static str = "/etc/nginx/sites-available/start9-services.conf";
|
||||
|
||||
#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -179,6 +224,161 @@ pub async fn write_services(hidden_services: &ServicesMap) -> Result<(), Error>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Error> {
|
||||
let mut f = tokio::fs::File::create(ETC_NGINX_SERVICES_CONF).await?;
|
||||
for (app_id, service) in &hidden_services.map {
|
||||
let hostname = tokio::fs::read_to_string(
|
||||
Path::new(HIDDEN_SERVICE_DIR_ROOT)
|
||||
.join(format!("app-{}", app_id))
|
||||
.join("hostname"),
|
||||
)
|
||||
.await
|
||||
.with_context(|e| format!("{}/app-{}/hostname: {}", HIDDEN_SERVICE_DIR_ROOT, app_id, e))
|
||||
.with_code(crate::error::FILESYSTEM_ERROR)?;
|
||||
let hostname_str = hostname
|
||||
.trim()
|
||||
.strip_suffix(".onion")
|
||||
.ok_or_else(|| failure::format_err!("invalid tor hostname"))
|
||||
.no_code()?;
|
||||
for mapping in &service.ports {
|
||||
match &mapping.lan {
|
||||
Some(LanOptions::Standard) => {
|
||||
log::info!("Writing LAN certificates for {}", app_id);
|
||||
let base_path = PersistencePath::from_ref("apps").join(&app_id);
|
||||
let key_path = base_path.join("cert-local.key.pem").path();
|
||||
let conf_path = base_path.join("cert-local.csr.conf").path();
|
||||
let req_path = base_path.join("cert-local.csr").path();
|
||||
let cert_path = base_path.join("cert-local.crt.pem").path();
|
||||
let fullchain_path = base_path.join("cert-local.fullchain.crt.pem");
|
||||
if !fullchain_path.exists().await
|
||||
|| tokio::fs::metadata(&key_path).await.is_err()
|
||||
{
|
||||
let mut fullchain_file = fullchain_path.write(None).await?;
|
||||
tokio::process::Command::new("openssl")
|
||||
.arg("ecparam")
|
||||
.arg("-genkey")
|
||||
.arg("-name")
|
||||
.arg("prime256v1")
|
||||
.arg("-noout")
|
||||
.arg("-out")
|
||||
.arg(&key_path)
|
||||
.invoke("OpenSSL GenKey")
|
||||
.await?;
|
||||
tokio::fs::write(
|
||||
&conf_path,
|
||||
format!(
|
||||
include_str!("cert-local.csr.conf.template"),
|
||||
hostname = hostname_str
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
tokio::process::Command::new("openssl")
|
||||
.arg("req")
|
||||
.arg("-config")
|
||||
.arg(&conf_path)
|
||||
.arg("-key")
|
||||
.arg(&key_path)
|
||||
.arg("-new")
|
||||
.arg("-addext")
|
||||
.arg(format!(
|
||||
"subjectAltName=DNS:{hostname}.local",
|
||||
hostname = hostname_str
|
||||
))
|
||||
.arg("-out")
|
||||
.arg(&req_path)
|
||||
.invoke("OpenSSL Req")
|
||||
.await?;
|
||||
tokio::process::Command::new("openssl")
|
||||
.arg("ca")
|
||||
.arg("-batch")
|
||||
.arg("-config")
|
||||
.arg("/root/agent/ca/intermediate/openssl.conf")
|
||||
.arg("-rand_serial")
|
||||
.arg("-keyfile")
|
||||
.arg("/root/agent/ca/intermediate/private/embassy-int-ca.key.pem")
|
||||
.arg("-cert")
|
||||
.arg("/root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem")
|
||||
.arg("-extensions")
|
||||
.arg("server_cert")
|
||||
.arg("-days")
|
||||
.arg("365")
|
||||
.arg("-notext")
|
||||
.arg("-in")
|
||||
.arg(&req_path)
|
||||
.arg("-out")
|
||||
.arg(&cert_path)
|
||||
.invoke("OpenSSL CA")
|
||||
.await?;
|
||||
log::info!("Writing fullchain to: {}", fullchain_path.path().display());
|
||||
tokio::io::copy(
|
||||
&mut tokio::fs::File::open(&cert_path).await?,
|
||||
&mut *fullchain_file,
|
||||
)
|
||||
.await?;
|
||||
tokio::io::copy(
|
||||
&mut tokio::fs::File::open(
|
||||
"/root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem",
|
||||
)
|
||||
.await
|
||||
.with_context(|e| {
|
||||
format!(
|
||||
"{}: /root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem",
|
||||
e
|
||||
)
|
||||
})
|
||||
.with_code(crate::error::FILESYSTEM_ERROR)?,
|
||||
&mut *fullchain_file,
|
||||
)
|
||||
.await?;
|
||||
tokio::io::copy(
|
||||
&mut tokio::fs::File::open(
|
||||
"/root/agent/ca/certs/embassy-root-ca.cert.pem",
|
||||
)
|
||||
.await
|
||||
.with_context(|e| {
|
||||
format!("{}: /root/agent/ca/certs/embassy-root-ca.cert.pem", e)
|
||||
})
|
||||
.with_code(crate::error::FILESYSTEM_ERROR)?,
|
||||
&mut *fullchain_file,
|
||||
)
|
||||
.await?;
|
||||
fullchain_file.commit().await?;
|
||||
log::info!("{} written successfully", fullchain_path.path().display());
|
||||
}
|
||||
f.write_all(
|
||||
format!(
|
||||
include_str!("nginx-standard.conf.template"),
|
||||
hostname = hostname_str,
|
||||
app_ip = service.ip,
|
||||
internal_port = mapping.internal,
|
||||
app_id = app_id,
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
f.sync_all().await?;
|
||||
}
|
||||
Some(LanOptions::Custom { port }) => {
|
||||
f.write_all(
|
||||
format!(
|
||||
include_str!("nginx.conf.template"),
|
||||
hostname = hostname_str,
|
||||
app_ip = service.ip,
|
||||
port = port,
|
||||
internal_port = mapping.internal,
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn read_tor_address(name: &str, timeout: Option<Duration>) -> Result<String, Error> {
|
||||
log::info!("Retrieving Tor hidden service address for {}.", name);
|
||||
let addr_path = Path::new(HIDDEN_SERVICE_DIR_ROOT)
|
||||
@@ -287,8 +487,8 @@ pub async fn set_svc(
|
||||
Err(e)
|
||||
}
|
||||
})?;
|
||||
#[cfg(target_os = "linux")]
|
||||
nix::unistd::sync();
|
||||
hidden_services.commit().await?;
|
||||
log::info!("Reloading Tor.");
|
||||
let svc_exit = std::process::Command::new("service")
|
||||
.args(&["tor", "reload"])
|
||||
@@ -302,19 +502,32 @@ pub async fn set_svc(
|
||||
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||
.unwrap_or(0)
|
||||
);
|
||||
Ok((
|
||||
ip,
|
||||
if is_listening {
|
||||
Some(read_tor_address(name, Some(Duration::from_secs(30))).await?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
if is_listening {
|
||||
Some(read_tor_key(name, ver, Some(Duration::from_secs(30))).await?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
))
|
||||
let addr = if is_listening {
|
||||
Some(read_tor_address(name, Some(Duration::from_secs(30))).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let key = if is_listening {
|
||||
Some(read_tor_key(name, ver, Some(Duration::from_secs(30))).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
write_lan_services(&hidden_services).await?;
|
||||
log::info!("Reloading Nginx.");
|
||||
let svc_exit = std::process::Command::new("service")
|
||||
.args(&["nginx", "reload"])
|
||||
.status()?;
|
||||
crate::ensure_code!(
|
||||
svc_exit.success(),
|
||||
crate::error::GENERAL_ERROR,
|
||||
"Failed to Reload Nginx: {}",
|
||||
svc_exit
|
||||
.code()
|
||||
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||
.unwrap_or(0)
|
||||
);
|
||||
hidden_services.commit().await?;
|
||||
Ok((ip, addr, key))
|
||||
}
|
||||
|
||||
pub async fn rm_svc(name: &str) -> Result<(), Error> {
|
||||
@@ -333,7 +546,6 @@ pub async fn rm_svc(name: &str) -> Result<(), Error> {
|
||||
}
|
||||
log::info!("Removing Tor hidden service {} from {}.", name, ETC_TOR_RC);
|
||||
write_services(&hidden_services).await?;
|
||||
hidden_services.commit().await?;
|
||||
log::info!("Reloading Tor.");
|
||||
let svc_exit = std::process::Command::new("service")
|
||||
.args(&["tor", "reload"])
|
||||
@@ -344,6 +556,21 @@ pub async fn rm_svc(name: &str) -> Result<(), Error> {
|
||||
"Failed to Reload Tor: {}",
|
||||
svc_exit.code().unwrap_or(0)
|
||||
);
|
||||
write_lan_services(&hidden_services).await?;
|
||||
log::info!("Reloading Nginx.");
|
||||
let svc_exit = std::process::Command::new("service")
|
||||
.args(&["nginx", "reload"])
|
||||
.status()?;
|
||||
crate::ensure_code!(
|
||||
svc_exit.success(),
|
||||
crate::error::GENERAL_ERROR,
|
||||
"Failed to Reload Nginx: {}",
|
||||
svc_exit
|
||||
.code()
|
||||
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||
.unwrap_or(0)
|
||||
);
|
||||
hidden_services.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,14 @@ impl PersistencePath {
|
||||
pub async fn for_update(self) -> Result<UpdateHandle<ForRead>, Error> {
|
||||
UpdateHandle::new(self).await
|
||||
}
|
||||
|
||||
pub async fn delete(&self) -> Result<(), Error> {
|
||||
match tokio::fs::remove_file(self.path()).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(k) if k.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
e => e.with_code(crate::error::FILESYSTEM_ERROR),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -137,6 +145,7 @@ impl PersistenceFile {
|
||||
if let Some(mut file) = self.file.take() {
|
||||
file.flush().await?;
|
||||
file.shutdown().await?;
|
||||
file.sync_all().await?;
|
||||
drop(file);
|
||||
}
|
||||
if let Some(path) = self.needs_commit.take() {
|
||||
@@ -156,10 +165,6 @@ impl PersistenceFile {
|
||||
.await
|
||||
.with_context(|e| format!("{}.lock: {}", path.path().display(), e))
|
||||
.with_code(crate::error::FILESYSTEM_ERROR)?;
|
||||
tokio::fs::remove_file(format!("{}.lock", path.path().display()))
|
||||
.await
|
||||
.with_context(|e| format!("{}.lock: {}", path.path().display(), e))
|
||||
.with_code(crate::error::FILESYSTEM_ERROR)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -24,8 +24,12 @@ mod v0_2_5;
|
||||
mod v0_2_6;
|
||||
mod v0_2_7;
|
||||
mod v0_2_8;
|
||||
mod v0_2_9;
|
||||
|
||||
pub use v0_2_8::Version as Current;
|
||||
mod v0_2_10;
|
||||
mod v0_2_11;
|
||||
|
||||
pub use v0_2_11::Version as Current;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
@@ -46,6 +50,9 @@ enum Version {
|
||||
V0_2_6(Wrapper<v0_2_6::Version>),
|
||||
V0_2_7(Wrapper<v0_2_7::Version>),
|
||||
V0_2_8(Wrapper<v0_2_8::Version>),
|
||||
V0_2_9(Wrapper<v0_2_9::Version>),
|
||||
V0_2_10(Wrapper<v0_2_10::Version>),
|
||||
V0_2_11(Wrapper<v0_2_11::Version>),
|
||||
Other(emver::Version),
|
||||
}
|
||||
|
||||
@@ -156,6 +163,9 @@ pub async fn init() -> Result<(), failure::Error> {
|
||||
Version::V0_2_6(v) => v.0.migrate_to(&Current::new()).await?,
|
||||
Version::V0_2_7(v) => v.0.migrate_to(&Current::new()).await?,
|
||||
Version::V0_2_8(v) => v.0.migrate_to(&Current::new()).await?,
|
||||
Version::V0_2_9(v) => v.0.migrate_to(&Current::new()).await?,
|
||||
Version::V0_2_10(v) => v.0.migrate_to(&Current::new()).await?,
|
||||
Version::V0_2_11(v) => v.0.migrate_to(&Current::new()).await?,
|
||||
Version::Other(_) => (),
|
||||
// TODO find some way to automate this?
|
||||
}
|
||||
@@ -172,7 +182,8 @@ pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error>
|
||||
.collect();
|
||||
let url = format!("{}/appmgr?spec={}", &*crate::SYS_REGISTRY_URL, req_str);
|
||||
log::info!("Fetching new version from {}", url);
|
||||
let response = reqwest::get(&url).compat()
|
||||
let response = reqwest::get(&url)
|
||||
.compat()
|
||||
.await
|
||||
.with_code(crate::error::NETWORK_ERROR)?
|
||||
.error_for_status()
|
||||
@@ -244,6 +255,9 @@ pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error>
|
||||
Version::V0_2_6(v) => Current::new().migrate_to(&v.0).await?,
|
||||
Version::V0_2_7(v) => Current::new().migrate_to(&v.0).await?,
|
||||
Version::V0_2_8(v) => Current::new().migrate_to(&v.0).await?,
|
||||
Version::V0_2_9(v) => Current::new().migrate_to(&v.0).await?,
|
||||
Version::V0_2_10(v) => Current::new().migrate_to(&v.0).await?,
|
||||
Version::V0_2_11(v) => Current::new().migrate_to(&v.0).await?,
|
||||
Version::Other(_) => (),
|
||||
// TODO find some way to automate this?
|
||||
};
|
||||
|
||||
21
appmgr/src/version/v0_2_10.rs
Normal file
21
appmgr/src/version/v0_2_10.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use super::*;
|
||||
|
||||
const V0_2_10: emver::Version = emver::Version::new(0, 2, 10, 0);
|
||||
|
||||
pub struct Version;
|
||||
#[async_trait]
|
||||
impl VersionT for Version {
|
||||
type Previous = v0_2_9::Version;
|
||||
fn new() -> Self {
|
||||
Version
|
||||
}
|
||||
fn semver(&self) -> &'static emver::Version {
|
||||
&V0_2_10
|
||||
}
|
||||
async fn up(&self) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
async fn down(&self) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
38
appmgr/src/version/v0_2_11.rs
Normal file
38
appmgr/src/version/v0_2_11.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use super::*;
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
|
||||
const V0_2_11: emver::Version = emver::Version::new(0, 2, 11, 0);
|
||||
|
||||
pub struct Version;
|
||||
#[async_trait]
|
||||
impl VersionT for Version {
|
||||
type Previous = v0_2_10::Version;
|
||||
fn new() -> Self {
|
||||
Version
|
||||
}
|
||||
fn semver(&self) -> &'static emver::Version {
|
||||
&V0_2_11
|
||||
}
|
||||
async fn up(&self) -> Result<(), Error> {
|
||||
crate::tor::write_lan_services(
|
||||
&crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?,
|
||||
)
|
||||
.await?;
|
||||
let svc_exit = std::process::Command::new("service")
|
||||
.args(&["nginx", "reload"])
|
||||
.status()?;
|
||||
crate::ensure_code!(
|
||||
svc_exit.success(),
|
||||
crate::error::GENERAL_ERROR,
|
||||
"Failed to Reload Nginx: {}",
|
||||
svc_exit
|
||||
.code()
|
||||
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||
.unwrap_or(0)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
async fn down(&self) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
75
appmgr/src/version/v0_2_9.rs
Normal file
75
appmgr/src/version/v0_2_9.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
const V0_2_9: emver::Version = emver::Version::new(0, 2, 9, 0);
|
||||
|
||||
pub struct Version;
|
||||
#[async_trait]
|
||||
impl VersionT for Version {
|
||||
type Previous = v0_2_8::Version;
|
||||
fn new() -> Self {
|
||||
Version
|
||||
}
|
||||
fn semver(&self) -> &'static emver::Version {
|
||||
&V0_2_9
|
||||
}
|
||||
async fn up(&self) -> Result<(), Error> {
|
||||
crate::tor::write_lan_services(
|
||||
&crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?,
|
||||
)
|
||||
.await?;
|
||||
tokio::fs::os::unix::symlink(
|
||||
crate::tor::ETC_NGINX_SERVICES_CONF,
|
||||
"/etc/nginx/sites-enabled/start9-services.conf",
|
||||
)
|
||||
.await
|
||||
.or_else(|e| {
|
||||
if e.kind() == std::io::ErrorKind::AlreadyExists {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})?;
|
||||
let svc_exit = std::process::Command::new("service")
|
||||
.args(&["nginx", "reload"])
|
||||
.status()?;
|
||||
crate::ensure_code!(
|
||||
svc_exit.success(),
|
||||
crate::error::GENERAL_ERROR,
|
||||
"Failed to Reload Nginx: {}",
|
||||
svc_exit
|
||||
.code()
|
||||
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||
.unwrap_or(0)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
async fn down(&self) -> Result<(), Error> {
|
||||
tokio::fs::remove_file("/etc/nginx/sites-enabled/start9-services.conf")
|
||||
.await
|
||||
.or_else(|e| match e {
|
||||
e if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
tokio::fs::remove_file(crate::tor::ETC_NGINX_SERVICES_CONF)
|
||||
.await
|
||||
.or_else(|e| match e {
|
||||
e if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let svc_exit = std::process::Command::new("service")
|
||||
.args(&["nginx", "reload"])
|
||||
.status()?;
|
||||
crate::ensure_code!(
|
||||
svc_exit.success(),
|
||||
crate::error::GENERAL_ERROR,
|
||||
"Failed to Reload Nginx: {}",
|
||||
svc_exit
|
||||
.code()
|
||||
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
|
||||
.unwrap_or(0)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
manifest-version: 0
|
||||
app-id: start9-ambassador
|
||||
app-version: 0.2.8
|
||||
app-version: 0.2.11
|
||||
uri-rewrites:
|
||||
- =/api -> http://{{start9-ambassador}}:5959/authenticate
|
||||
- /api/ -> http://{{start9-ambassador}}:5959/
|
||||
|
||||
17421
ui/package-lock.json
generated
17421
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "embassy-ui",
|
||||
"version": "0.2.8",
|
||||
"version": "0.2.11",
|
||||
"description": "GUI for EmbassyOS",
|
||||
"author": "Start9 Labs",
|
||||
"homepage": "https://github.com/Start9Labs/embassy-ui",
|
||||
@@ -36,7 +36,7 @@
|
||||
"json-pointer": "^0.6.1",
|
||||
"jsonpointerx": "^1.0.30",
|
||||
"jsontokens": "^3.0.0",
|
||||
"marked": "^1.2.0",
|
||||
"marked": "^2.0.0",
|
||||
"rxjs": "^6.6.3",
|
||||
"uuid": "^8.3.1",
|
||||
"zone.js": "^0.11.2"
|
||||
|
||||
@@ -298,11 +298,11 @@ export class ConfigCursor<T extends ValueType> {
|
||||
const mappedCfg = this.mappedConfig()
|
||||
if (cfg && mappedCfg && typeof cfg === 'object' && typeof mappedCfg === 'object') {
|
||||
const spec = this.spec()
|
||||
let allKeys
|
||||
let allKeys: Set<string>
|
||||
if (spec.type === 'union') {
|
||||
let unionSpec = spec as ValueSpecOf<'union'>
|
||||
const labelForSelection = unionSpec.tag.id
|
||||
allKeys = new Set([...Object.keys(unionSpec.variants[cfg[labelForSelection]])])
|
||||
allKeys = new Set([labelForSelection, ...Object.keys(unionSpec.variants[cfg[labelForSelection]])])
|
||||
} else {
|
||||
allKeys = new Set([...Object.keys(cfg), ...Object.keys(mappedCfg)])
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
<ion-icon name="arrow-forward"></ion-icon>
|
||||
<ion-icon name="arrow-up"></ion-icon>
|
||||
<ion-icon name="bookmark-outline"></ion-icon>
|
||||
<ion-icon name="cart-outline"></ion-icon>
|
||||
<ion-icon name="chevron-down"></ion-icon>
|
||||
<ion-icon name="chevron-up"></ion-icon>
|
||||
<ion-icon name="close"></ion-icon>
|
||||
@@ -86,20 +85,22 @@
|
||||
<ion-icon name="eye-off-outline"></ion-icon>
|
||||
<ion-icon name="eye-outline"></ion-icon>
|
||||
<ion-icon name="file-tray-stacked-outline"></ion-icon>
|
||||
<ion-icon name="flash-outline"></ion-icon>
|
||||
<ion-icon name="grid-outline"></ion-icon>
|
||||
<ion-icon name="help-circle-outline"></ion-icon>
|
||||
<ion-icon name="home-outline"></ion-icon>
|
||||
<ion-icon name="information-circle-outline"></ion-icon>
|
||||
<ion-icon name="list-outline"></ion-icon>
|
||||
<ion-icon name="newspaper-outline"></ion-icon>
|
||||
<ion-icon name="notifications-outline"></ion-icon>
|
||||
<ion-icon name="notifications-outline"></ion-icon>
|
||||
<ion-icon name="rocket-outline"></ion-icon>
|
||||
<ion-icon name="power"></ion-icon>
|
||||
<ion-icon name="pulse"></ion-icon>
|
||||
<ion-icon name="qr-code-outline"></ion-icon>
|
||||
<ion-icon name="globe-outline"></ion-icon>
|
||||
<ion-icon name="reload-outline"></ion-icon>
|
||||
<ion-icon name="refresh-outline"></ion-icon>
|
||||
<ion-icon name="save-outline"></ion-icon>
|
||||
<ion-icon name="storefront-outline"></ion-icon>
|
||||
<ion-icon name="terminal-outline"></ion-icon>
|
||||
<ion-icon name="trash-outline"></ion-icon>
|
||||
<ion-icon name="warning-outline"></ion-icon>
|
||||
|
||||
@@ -42,7 +42,7 @@ export class AppComponent {
|
||||
{
|
||||
title: 'Marketplace',
|
||||
url: '/services/marketplace',
|
||||
icon: 'cart-outline',
|
||||
icon: 'storefront-outline',
|
||||
},
|
||||
{
|
||||
title: 'Notifications',
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<ion-slides *ngIf="!($error$ | async)" id="slide-show" style="--bullet-background: white" pager="false">
|
||||
<ion-slide *ngFor="let slide of params.slideDefinitions">
|
||||
<dependencies #components *ngIf="slide.selector === 'dependencies'" [params]="slide.params"></dependencies>
|
||||
<developer-notes #components *ngIf="slide.selector === 'developer-notes'" [params]="slide.params"></developer-notes>
|
||||
<notes #components *ngIf="slide.selector === 'notes'" [params]="slide.params"></notes>
|
||||
<dependents #components *ngIf="slide.selector === 'dependents'" [params]="slide.params" [finished]="finished"></dependents>
|
||||
<complete #components *ngIf="slide.selector === 'complete'" [params]="slide.params" [finished]="finished"></complete>
|
||||
</ion-slide>
|
||||
@@ -43,8 +43,8 @@
|
||||
<ion-text *ngIf="cancel.text as t">{{t}}</ion-text>
|
||||
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button slot="end" *ngIf="currentSlideDef.nextButton as nextButton" (click)="finished({})" [disabled]="$anythingLoading$ | async" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="nextButton.length > 16">{{nextButton}}</ion-text></ion-button>
|
||||
<ion-button slot="end" *ngIf="currentSlideDef.finishButton as finishButton" (click)="finished({ final: true })" [disabled]="$anythingLoading$ | async" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="finishButton.length > 16">{{finishButton}}</ion-text></ion-button>
|
||||
<ion-button slot="end" *ngIf="!($anythingLoading$ | async) && currentSlideDef.nextButton as nextButton" (click)="finished({})" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="nextButton.length > 16">{{nextButton}}</ion-text></ion-button>
|
||||
<ion-button slot="end" *ngIf="!($anythingLoading$ | async) && currentSlideDef.finishButton as finishButton" (click)="finished({ final: true })" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="finishButton.length > 16">{{finishButton}}</ion-text></ion-button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="$error$ | async">
|
||||
<ion-button slot="start" (click)="finished({ final: true })" style="text-transform: capitalize; font-weight: bolder;" color="danger">Dismiss</ion-button>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
import { DependenciesComponentModule } from './dependencies/dependencies.component.module'
|
||||
import { DependentsComponentModule } from './dependents/dependents.component.module'
|
||||
import { CompleteComponentModule } from './complete/complete.component.module'
|
||||
import { DeveloperNotesComponentModule } from './developer-notes/developer-notes.component.module'
|
||||
import { NotesComponentModule } from './notes/notes.component.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -21,7 +21,7 @@ import { DeveloperNotesComponentModule } from './developer-notes/developer-notes
|
||||
DependenciesComponentModule,
|
||||
DependentsComponentModule,
|
||||
CompleteComponentModule,
|
||||
DeveloperNotesComponentModule,
|
||||
NotesComponentModule,
|
||||
],
|
||||
exports: [InstallWizardComponent],
|
||||
})
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
font-size: small;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-color: #393b40;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media (min-width:500px) {
|
||||
@@ -57,6 +58,7 @@
|
||||
font-size: medium;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-color: #393b40;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Component, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'
|
||||
import { Component, Input, NgZone, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'
|
||||
import { IonContent, IonSlides, ModalController } from '@ionic/angular'
|
||||
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
|
||||
import { capitalizeFirstLetter, pauseFor } from 'src/app/util/misc.util'
|
||||
import { CompleteComponent } from './complete/complete.component'
|
||||
import { DependenciesComponent } from './dependencies/dependencies.component'
|
||||
import { DependentsComponent } from './dependents/dependents.component'
|
||||
import { DeveloperNotesComponent } from './developer-notes/developer-notes.component'
|
||||
import { NotesComponent } from './notes/notes.component'
|
||||
import { Colorable, Loadable } from './loadable'
|
||||
import { WizardAction } from './wizard-types'
|
||||
|
||||
@@ -50,7 +50,7 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
|
||||
$currentColor$: BehaviorSubject<string> = new BehaviorSubject('medium')
|
||||
$error$ = new BehaviorSubject(undefined)
|
||||
|
||||
constructor (private readonly modalController: ModalController) { super() }
|
||||
constructor (private readonly modalController: ModalController, private readonly zone: NgZone) { super() }
|
||||
ngOnInit () { }
|
||||
|
||||
ngAfterViewInit () {
|
||||
@@ -80,15 +80,15 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
|
||||
|
||||
private async slide () {
|
||||
if (this.slideComponents[this.slideIndex + 1] === undefined) { return this.finished({ final: true }) }
|
||||
this.slideIndex += 1
|
||||
this.currentSlide.load()
|
||||
await this.slideContainer.lockSwipes(false)
|
||||
await Promise.all([
|
||||
this.contentContainer.scrollToTop(),
|
||||
this.slideContainer.slideNext(500),
|
||||
])
|
||||
await this.slideContainer.lockSwipes(true)
|
||||
this.slideContainer.update()
|
||||
this.zone.run(async () => {
|
||||
this.slideComponents[this.slideIndex + 1].load()
|
||||
await pauseFor(50)
|
||||
this.slideIndex += 1
|
||||
await this.slideContainer.lockSwipes(false)
|
||||
await this.contentContainer.scrollToTop()
|
||||
await this.slideContainer.slideNext(500)
|
||||
await this.slideContainer.lockSwipes(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,8 +114,8 @@ export type SlideDefinition = SlideCommon & (
|
||||
selector: 'complete',
|
||||
params: CompleteComponent['params']
|
||||
} | {
|
||||
selector: 'developer-notes',
|
||||
params: DeveloperNotesComponent['params']
|
||||
selector: 'notes',
|
||||
params: NotesComponent['params']
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
<div style="margin-top: 25px;">
|
||||
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
|
||||
<ion-label [color]="$color$ | async" style="font-size: xx-large; font-weight: bold;">
|
||||
Warning
|
||||
{{params.title}}
|
||||
</ion-label>
|
||||
</div>
|
||||
<div class="long-message">
|
||||
{{params.developerNotes}}
|
||||
</div>
|
||||
<div class="long-message" [innerHTML]="params.notes | markdown"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,13 +1,13 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { DeveloperNotesComponent } from './developer-notes.component'
|
||||
import { NotesComponent } from './notes.component'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DeveloperNotesComponent,
|
||||
NotesComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -15,6 +15,6 @@ import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
RouterModule.forChild([]),
|
||||
SharingModule,
|
||||
],
|
||||
exports: [DeveloperNotesComponent],
|
||||
exports: [NotesComponent],
|
||||
})
|
||||
export class DeveloperNotesComponentModule { }
|
||||
export class NotesComponentModule { }
|
||||
@@ -4,24 +4,24 @@ import { Colorable, Loadable } from '../loadable'
|
||||
import { WizardAction } from '../wizard-types'
|
||||
|
||||
@Component({
|
||||
selector: 'developer-notes',
|
||||
templateUrl: './developer-notes.component.html',
|
||||
selector: 'notes',
|
||||
templateUrl: './notes.component.html',
|
||||
styleUrls: ['../install-wizard.component.scss'],
|
||||
})
|
||||
export class DeveloperNotesComponent implements OnInit, Loadable, Colorable {
|
||||
export class NotesComponent implements OnInit, Loadable, Colorable {
|
||||
@Input() params: {
|
||||
action: WizardAction
|
||||
developerNotes: string
|
||||
notes: string
|
||||
title: string
|
||||
titleColor: string
|
||||
}
|
||||
|
||||
$loading$ = new BehaviorSubject(false)
|
||||
$color$ = new BehaviorSubject('warning')
|
||||
$color$ = new BehaviorSubject('light')
|
||||
$cancel$ = new Subject<void>()
|
||||
|
||||
load () { }
|
||||
|
||||
constructor () { }
|
||||
ngOnInit () {
|
||||
console.log('Developer Notes', this.params)
|
||||
}
|
||||
ngOnInit () { this.$color$.next(this.params.titleColor) }
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AppModel, AppStatus } from 'src/app/models/app-model'
|
||||
import { OsUpdateService } from 'src/app/services/os-update.service'
|
||||
import { exists } from 'src/app/util/misc.util'
|
||||
import { AppDependency, DependentBreakage, AppInstalledPreview } from '../../models/app-types'
|
||||
import { ApiService } from '../../services/api/api.service'
|
||||
@@ -7,7 +8,11 @@ import { InstallWizardComponent, SlideDefinition, TopbarParams } from './install
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class WizardBaker {
|
||||
constructor (private readonly apiService: ApiService, private readonly appModel: AppModel) { }
|
||||
constructor (
|
||||
private readonly apiService: ApiService,
|
||||
private readonly updateService: OsUpdateService,
|
||||
private readonly appModel: AppModel
|
||||
) { }
|
||||
|
||||
install (values: {
|
||||
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
|
||||
@@ -23,8 +28,8 @@ export class WizardBaker {
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
installAlert ? { selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
||||
action, developerNotes: installAlert,
|
||||
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
||||
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
|
||||
}} : undefined,
|
||||
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Install', params: {
|
||||
action, title, version, serviceRequirements,
|
||||
@@ -52,8 +57,8 @@ export class WizardBaker {
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
installAlert ? { selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
||||
action, developerNotes: installAlert,
|
||||
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
||||
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
|
||||
}} : undefined,
|
||||
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Update', params: {
|
||||
action, title, version, serviceRequirements,
|
||||
@@ -70,6 +75,26 @@ export class WizardBaker {
|
||||
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
||||
}
|
||||
|
||||
updateOS (values: {
|
||||
version: string, releaseNotes: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
const { version, releaseNotes } = values
|
||||
|
||||
const action = 'update'
|
||||
const title = 'EmbassyOS'
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{ selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Update OS', params: {
|
||||
action, notes: releaseNotes || 'No release notes for this version', title: 'Release Notes', titleColor: 'dark',
|
||||
}},
|
||||
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
|
||||
action, verb: 'beginning update for', title, executeAction: () => this.updateService.updateEmbassyOS(version),
|
||||
}},
|
||||
]
|
||||
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
|
||||
}
|
||||
|
||||
downgrade (values: {
|
||||
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
|
||||
}): InstallWizardComponent['params'] {
|
||||
@@ -84,8 +109,8 @@ export class WizardBaker {
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
installAlert ? { selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
||||
action, developerNotes: installAlert,
|
||||
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
|
||||
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
|
||||
}} : undefined,
|
||||
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Downgrade', params: {
|
||||
action, title, version, serviceRequirements,
|
||||
@@ -115,8 +140,8 @@ export class WizardBaker {
|
||||
const toolbar: TopbarParams = { action, title, version }
|
||||
|
||||
const slideDefinitions: SlideDefinition[] = [
|
||||
{ selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Continue', params: {
|
||||
action, developerNotes: uninstallAlert || defaultUninstallationWarning(title) },
|
||||
{ selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Continue', params: {
|
||||
action, notes: uninstallAlert || defaultUninstallationWarning(title), title: 'Warning', titleColor: 'warning' },
|
||||
},
|
||||
{ selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Uninstall', params: {
|
||||
action, verb: 'uninstalling', title, fetchBreakages: () => this.apiService.uninstallApp(id, true).then( ({ breakages }) => breakages ),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ion-item button lines="none" *ngIf="updateAvailable$ | async as version" (click)="confirmUpdate(version)">
|
||||
<ion-item button lines="none" *ngIf="updateAvailable$ | async as res" (click)="confirmUpdate(res)">
|
||||
<ion-label>
|
||||
New EmbassyOS Version {{version | displayEmver}} Available!
|
||||
New EmbassyOS Version {{res.versionLatest | displayEmver}} Available!
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { OsUpdateService } from 'src/app/services/os-update.service'
|
||||
import { Observable } from 'rxjs'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { LoaderService } from 'src/app/services/loader.service'
|
||||
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { WizardBaker } from '../install-wizard/prebaked-wizards'
|
||||
import { wizardModal } from '../install-wizard/install-wizard.component'
|
||||
import { ReqRes } from 'src/app/services/api/api.service'
|
||||
|
||||
@Component({
|
||||
selector: 'update-os-banner',
|
||||
@@ -11,38 +12,24 @@ import { displayEmver } from 'src/app/pipes/emver.pipe'
|
||||
styleUrls: ['./update-os-banner.component.scss'],
|
||||
})
|
||||
export class UpdateOsBannerComponent {
|
||||
updateAvailable$: Observable<undefined | string>
|
||||
updateAvailable$: Observable<undefined | ReqRes.GetVersionLatestRes>
|
||||
constructor (
|
||||
private readonly osUpdateService: OsUpdateService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly loader: LoaderService,
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
) {
|
||||
this.updateAvailable$ = this.osUpdateService.watchForUpdateAvailable$()
|
||||
}
|
||||
|
||||
ngOnInit () { }
|
||||
|
||||
async confirmUpdate (versionLatest: string) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: `Update EmbassyOS`,
|
||||
message: `Update EmbassyOS to version ${displayEmver(versionLatest)}?`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Update',
|
||||
handler: () => this.update(versionLatest),
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async update (versionLatest: string) {
|
||||
return this.loader.displayDuringP(
|
||||
this.osUpdateService.updateEmbassyOS(versionLatest),
|
||||
async confirmUpdate (res: ReqRes.GetVersionLatestRes) {
|
||||
await wizardModal(
|
||||
this.modalCtrl,
|
||||
this.wizardBaker.updateOS({
|
||||
version: res.versionLatest,
|
||||
releaseNotes: res.releaseNotes,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,15 @@
|
||||
<ion-item-group>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-icon size="small" slot="start" *ngIf="!edited"
|
||||
style="margin-right: 15px; color: rgba(0,0,0,0); background: radial-gradient(#2a4e8970, #2a4e8970 35%, transparent 35%, transparent);"
|
||||
name="ellipse"></ion-icon>
|
||||
<ion-icon size="small" slot="start" *ngIf="edited" style="margin-right: 15px" color="primary" name="ellipse">
|
||||
</ion-icon>
|
||||
<ion-label>{{ spec.tag.name }}</ion-label>
|
||||
<ion-select slot="end" [interfaceOptions]="setSelectOptions()" placeholder="Select One" [(ngModel)]="value[spec.tag.id]" [selectedText]="spec.tag.variantNames[value[spec.tag.id]]" (ngModelChange)="handleUnionChange()">
|
||||
<ion-select slot="end" [interfaceOptions]="setSelectOptions()" placeholder="Select One"
|
||||
[(ngModel)]="value[spec.tag.id]" [selectedText]="spec.tag.variantNames[value[spec.tag.id]]"
|
||||
(ngModelChange)="handleUnionChange()">
|
||||
<ion-select-option *ngFor="let option of spec.variants | keyvalue: asIsOrder" [value]="option.key">
|
||||
{{ spec.tag.variantNames[option.key] }}
|
||||
<span *ngIf="option.key === spec.default"> (default)</span>
|
||||
@@ -28,4 +35,4 @@
|
||||
<object-config [cursor]="cursor" (onEdit)="handleObjectEdit()"></object-config>
|
||||
</ion-item-group>
|
||||
|
||||
</ion-content>
|
||||
</ion-content>
|
||||
@@ -19,6 +19,7 @@ export class AppConfigUnionPage {
|
||||
spec: ValueSpecUnion
|
||||
value: object
|
||||
error: string
|
||||
edited: boolean
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
@@ -28,6 +29,7 @@ export class AppConfigUnionPage {
|
||||
this.spec = this.cursor.spec()
|
||||
this.value = this.cursor.config()
|
||||
this.error = this.cursor.checkInvalid()
|
||||
this.edited = this.cursor.seekNext(this.spec.tag.id).isEdited()
|
||||
}
|
||||
|
||||
async dismiss () {
|
||||
@@ -37,6 +39,8 @@ export class AppConfigUnionPage {
|
||||
async handleUnionChange () {
|
||||
this.value = mapUnionSpec(this.spec, this.value)
|
||||
this.objectConfig.annotations = this.objectConfig.cursor.getAnnotations()
|
||||
this.error = this.cursor.checkInvalid()
|
||||
this.edited = this.cursor.seekNext(this.spec.tag.id).isEdited()
|
||||
}
|
||||
|
||||
setSelectOptions () {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppReleaseNotesPage } from './app-release-notes.page'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppReleaseNotesPage],
|
||||
})
|
||||
export class AppReleaseNotesPageModule { }
|
||||
@@ -1,14 +0,0 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
<ion-title style="font-size: 16px;">{{ version }} Release Notes</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div [innerHTML]="releaseNotes | markdown"></div>
|
||||
</ion-content>
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
|
||||
@Component({
|
||||
selector: 'app-release-notes',
|
||||
templateUrl: './app-release-notes.page.html',
|
||||
styleUrls: ['./app-release-notes.page.scss'],
|
||||
})
|
||||
export class AppReleaseNotesPage {
|
||||
@Input() releaseNotes: string
|
||||
@Input() version: string
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
) { }
|
||||
|
||||
dismiss () {
|
||||
this.modalCtrl.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,25 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title >
|
||||
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to {{ version }}!</ion-label>
|
||||
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.11!</ion-label>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
|
||||
<div>
|
||||
<h2>Highlights</h2>
|
||||
<p>
|
||||
0.2.8 is a small but important update designed to enhance awareness around potential pitfalls of using certain services.
|
||||
It introduces warnings for installing, uninstalling, backing up, and restoring backups of stateful services such as LND or c-lightning.
|
||||
Additionally, it draws a distinction between services that are designed to be launched inside the browser and those that are designed to run in the background
|
||||
</p>
|
||||
<p>
|
||||
0.2.8 also introduces automatic update checks. With this enabled, each time you visit your embassy you will be notified if new Embassy OS or service versions are available. This setting can be edited in your Embassy Config page.
|
||||
<ion-item lines="none" style="--border-radius: var(--icon-border-radius); margin-top: 15px">
|
||||
<ion-label>Auto Check for Updates</ion-label>
|
||||
<ion-toggle slot="end" [(ngModel)]="autoCheckUpdates"></ion-toggle>
|
||||
</ion-item>
|
||||
</p>
|
||||
|
||||
<div style="margin-top: 30px">
|
||||
<h5 style="color: var(--ion-color-danger)">Important</h5>
|
||||
<p>
|
||||
If you have LND or c-lightning installed, please update them to the latest versions.
|
||||
An oversight in Start9’s USB backups system has created a situation where <b>restoring</b> a LND or c-lightning backup could potentially result in permanent loss of channel funds.
|
||||
To be clear, <ion-text style="font-weight: 'bold';">DO NOT</ion-text> attempt to <b>restore</b> a LND or c-lightning backup until you have updated to the latest versions.
|
||||
</p>
|
||||
</div>
|
||||
<h2>Highlights</h2>
|
||||
<div class="main-content">
|
||||
<p>This release includes several bugfixes to resolve:</p>
|
||||
<ol>
|
||||
<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 class="close-button">
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
height: 100%;
|
||||
color: var(--ion-color-medium);
|
||||
}
|
||||
@@ -12,24 +12,17 @@ import { ConfigService } from 'src/app/services/config.service'
|
||||
export class OSWelcomePage {
|
||||
@Input() version: string
|
||||
|
||||
autoCheckUpdates = true
|
||||
|
||||
constructor (
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly config: ConfigService,
|
||||
) { }
|
||||
|
||||
async dismiss () {
|
||||
this.apiService
|
||||
.patchServerConfig('autoCheckUpdates', this.autoCheckUpdates)
|
||||
.then(() => this.serverModel.update({ autoCheckUpdates: this.autoCheckUpdates }))
|
||||
.then(() => this.apiService.acknowledgeOSWelcome(this.config.version))
|
||||
.catch(console.error)
|
||||
this.apiService.acknowledgeOSWelcome(this.config.version).catch(console.error)
|
||||
|
||||
// return false to skip subsequent alert modals (e.g. check for updates modals)
|
||||
// return true to show subsequent alert modals
|
||||
return this.modalCtrl.dismiss(this.autoCheckUpdates)
|
||||
return this.modalCtrl.dismiss(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ function emptyAppInstalledFull (): Omit<AppInstalledFull, keyof AppInstalledPrev
|
||||
lastBackup: null,
|
||||
configuredRequirements: null,
|
||||
hasFetchedFull: false,
|
||||
actions: [],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,21 +34,36 @@ export interface AppAvailableVersionSpecificInfo {
|
||||
// installed
|
||||
|
||||
export interface AppInstalledPreview extends BaseApp {
|
||||
lanAddress?: string
|
||||
torAddress: string
|
||||
versionInstalled: string
|
||||
ui: boolean
|
||||
lanUi: boolean
|
||||
torUi: boolean
|
||||
// FE state only
|
||||
hasUI: boolean
|
||||
launchable: boolean
|
||||
}
|
||||
|
||||
export interface AppInstalledFull extends AppInstalledPreview {
|
||||
instructions: string | null
|
||||
lastBackup: string | null
|
||||
configuredRequirements: AppDependency[] | null // null if not yet configured
|
||||
hasFetchedFull: boolean
|
||||
startAlert?: string
|
||||
uninstallAlert?: string
|
||||
restoreAlert?: string
|
||||
actions: Actions
|
||||
// FE state only
|
||||
hasFetchedFull: boolean
|
||||
}
|
||||
// dependencies
|
||||
|
||||
export type Actions = ServiceAction[]
|
||||
export interface ServiceAction {
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
warning?: string
|
||||
allowedStatuses: AppStatus[]
|
||||
}
|
||||
export interface AppDependency extends InstalledAppDependency {
|
||||
// explanation of why it *is* optional. null represents it is required.
|
||||
optional: string | null
|
||||
|
||||
@@ -115,7 +115,7 @@ export interface S9Server {
|
||||
name: string
|
||||
origin: string
|
||||
versionInstalled: string
|
||||
versionLatest: string | undefined
|
||||
versionLatest: string | undefined // not on the api as of 0.2.8
|
||||
status: ServerStatus
|
||||
badge: number
|
||||
alternativeRegistryUrl: string | null
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { AppActionsPage } from './app-actions.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AppActionsPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
QRComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppActionsPage],
|
||||
})
|
||||
export class AppActionsPageModule { }
|
||||
@@ -0,0 +1,41 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Actions</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding-top">
|
||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ng-container *ngIf="!($loading$ | async) && {
|
||||
title: app.title | async,
|
||||
versionInstalled: app.versionInstalled | async,
|
||||
status: app.status | async,
|
||||
actions: app.actions | async
|
||||
} as vars">
|
||||
<ion-item *ngIf="error" style="margin-bottom: 16px;">
|
||||
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
|
||||
</ion-item>
|
||||
|
||||
<!-- no metrics -->
|
||||
<ion-item *ngIf="!vars.actions.length">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>No Actions for {{ vars.title }} {{ vars.versionInstalled }}.</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<!-- actions -->
|
||||
<ion-item-group>
|
||||
<ion-item button *ngFor="let action of vars.actions" (click)="handleAction(action)" >
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2><ion-text color="primary">{{ action.name }}</ion-text><ion-icon *ngIf="!(action.allowedStatuses | includes: vars.status)" color="danger" name="close-outline"></ion-icon></h2>
|
||||
<p><ion-text color="dark">{{ action.description }}</ion-text></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
</ng-container>
|
||||
</ion-content>
|
||||
120
ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts
Normal file
120
ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ApiService, isRpcFailure, isRpcSuccess } from 'src/app/services/api/api.service'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { ModelPreload } from 'src/app/models/model-preload'
|
||||
import { LoaderService, markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||
import { ServiceAction, AppInstalledFull } from 'src/app/models/app-types'
|
||||
import { PropertySubject } from 'src/app/util/property-subject.util'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { AppStatus } from 'src/app/models/app-model'
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
|
||||
@Component({
|
||||
selector: 'app-actions',
|
||||
templateUrl: './app-actions.page.html',
|
||||
styleUrls: ['./app-actions.page.scss'],
|
||||
})
|
||||
export class AppActionsPage extends Cleanup {
|
||||
error = ''
|
||||
$loading$ = new BehaviorSubject(true)
|
||||
appId: string
|
||||
app: PropertySubject<AppInstalledFull>
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly apiService: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
private readonly preload: ModelPreload,
|
||||
private readonly loaderService: LoaderService,
|
||||
) { super() }
|
||||
|
||||
ngOnInit() {
|
||||
this.appId = this.route.snapshot.paramMap.get('appId')
|
||||
|
||||
markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId)).pipe(
|
||||
map(app => this.app = app),
|
||||
).subscribe({ error: e => this.error = e.message })
|
||||
}
|
||||
|
||||
async handleAction(action: ServiceAction) {
|
||||
if (action.allowedStatuses.includes(this.app.status.getValue())) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Confirm',
|
||||
message: `Are you sure you want to execute action "${action.name}"? ${action.warning ? action.warning : ""}`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Execute',
|
||||
handler: () => {
|
||||
this.executeAction(action)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
} else {
|
||||
const joinStatuses = (statuses: AppStatus[]) => {
|
||||
const last = statuses.pop()
|
||||
let s = statuses.join(', ')
|
||||
if (last) {
|
||||
if (statuses.length > 1) { // oxford comma
|
||||
s += ','
|
||||
}
|
||||
s += ` or ${last}`
|
||||
}
|
||||
return s
|
||||
}
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Forbidden',
|
||||
message: `Action "${action.name}" can only be executed when service is ${joinStatuses(action.allowedStatuses)}`,
|
||||
buttons: ['OK'],
|
||||
cssClass: 'alert-error-message',
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
}
|
||||
|
||||
private async executeAction(action: ServiceAction) {
|
||||
try {
|
||||
const res = await this.loaderService.displayDuringP(
|
||||
this.apiService.serviceAction(this.appId, action),
|
||||
)
|
||||
|
||||
if (isRpcFailure(res)) {
|
||||
this.presentAlertActionFail(res.error.code, res.error.message)
|
||||
}
|
||||
|
||||
if (isRpcSuccess(res)) {
|
||||
const successAlert = await this.alertCtrl.create({
|
||||
header: 'Execution Complete',
|
||||
message: res.result.split('\n').join('</br ></br />'),
|
||||
buttons: ['OK'],
|
||||
cssClass: 'alert-success-message',
|
||||
})
|
||||
return await successAlert.present()
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof HttpErrorResponse) {
|
||||
this.presentAlertActionFail(e.status, e.message)
|
||||
} else {
|
||||
this.presentAlertActionFail(-1, e.message || JSON.stringify(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertActionFail(code: number, message: string): Promise<void> {
|
||||
const failureAlert = await this.alertCtrl.create({
|
||||
header: 'Execution Failed',
|
||||
message: `Error code ${code}. ${message}`,
|
||||
buttons: ['OK'],
|
||||
cssClass: 'alert-error-message',
|
||||
})
|
||||
return await failureAlert.present()
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import { RecommendationButtonComponentModule } from 'src/app/components/recommen
|
||||
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
|
||||
import { ErrorMessageComponentModule } from 'src/app/components/error-message/error-message.component.module'
|
||||
import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module'
|
||||
import { AppReleaseNotesPageModule } from 'src/app/modals/app-release-notes/app-release-notes.module'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -35,7 +34,6 @@ const routes: Routes = [
|
||||
InstallWizardComponentModule,
|
||||
ErrorMessageComponentModule,
|
||||
InformationPopoverComponentModule,
|
||||
AppReleaseNotesPageModule,
|
||||
],
|
||||
declarations: [AppAvailableShowPage],
|
||||
})
|
||||
|
||||
@@ -53,13 +53,18 @@
|
||||
Install
|
||||
</ion-button>
|
||||
|
||||
<div *ngIf="vars.versionInstalled && vars.status !== 'INSTALLING' ">
|
||||
<ion-button *ngIf="installedStatus === 'installed-below'" class="main-action-button" expand="block" fill="outline" color="success" (click)="update('update')">
|
||||
Update to {{ vars.versionViewing | displayEmver }}
|
||||
</ion-button>
|
||||
<ion-button *ngIf="installedStatus === 'installed-above'" class="main-action-button" expand="block" fill="outline" color="warning" (click)="update('downgrade')">
|
||||
Downgrade to {{ vars.versionViewing | displayEmver }}
|
||||
<div *ngIf="vars.versionInstalled">
|
||||
<ion-button class="main-action-button" expand="block" fill="outline" [routerLink]="['/services', 'installed', vars.id]">
|
||||
Go to Service
|
||||
</ion-button>
|
||||
<div *ngIf="vars.status !== 'INSTALLING' ">
|
||||
<ion-button *ngIf="installedStatus === 'installed-below'" class="main-action-button" expand="block" fill="outline" color="success" (click)="update('update')">
|
||||
Update to {{ vars.versionViewing | displayEmver }}
|
||||
</ion-button>
|
||||
<ion-button *ngIf="installedStatus === 'installed-above'" class="main-action-button" expand="block" fill="outline" color="warning" (click)="update('downgrade')">
|
||||
Downgrade to {{ vars.versionViewing | displayEmver }}
|
||||
</ion-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ion-item-group>
|
||||
@@ -81,6 +86,14 @@
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
|
||||
<ion-item-divider style="color: var(--ion-color-dark); font-weight: bold;">New in {{ vars.versionViewing | displayEmver }}</ion-item-divider>
|
||||
<ion-item lines="none">
|
||||
<ion-label *ngIf="!($newVersionLoading$ | async)" style="display: flex; align-items: center; justify-content: space-between;" class="ion-text-wrap" >
|
||||
<div id='release-notes'color="dark" [innerHTML]="vars.releaseNotes | markdown"></div>
|
||||
</ion-label>
|
||||
<ion-spinner style="display: block; margin: auto;" name="lines" color="dark" *ngIf="$newVersionLoading$ | async"></ion-spinner>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider class="divider">Description</ion-item-divider>
|
||||
<ion-item lines="none">
|
||||
<ion-label class="ion-text-wrap">
|
||||
@@ -90,13 +103,6 @@
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Release Notes</ion-item-divider>
|
||||
<ion-item lines="none" button details="true" [disabled]="" (click)="presentModalReleaseNotes()" [disabled]="$newVersionLoading$ | async">
|
||||
<ion-icon slot="start" name="newspaper-outline" color="medium"></ion-icon>
|
||||
<ion-label *ngIf="!($newVersionLoading$ | async)"><ion-text color="medium">New in {{ vars.versionViewing | displayEmver }}</ion-text></ion-label>
|
||||
<ion-spinner style="display: block; margin: auto;" name="lines" color="dark" *ngIf="$newVersionLoading$ | async"></ion-spinner>
|
||||
</ion-item>
|
||||
|
||||
<ng-container *ngIf="(vars.serviceRequirements)?.length">
|
||||
<ion-item-divider class="divider">Service Dependencies
|
||||
<ion-button style="position: relative; right: 10px;" size="small" fill="clear" color="medium" (click)="presentPopover(serviceDependencyDefintion, $event)">
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
// .recommendation-container {
|
||||
// margin-top: 3px;
|
||||
// display: grid;
|
||||
// grid-template-columns: auto auto;
|
||||
// justify-content: start;
|
||||
// grid-column-gap: 5px;
|
||||
// align-items: center;
|
||||
// }
|
||||
|
||||
.recommendation-text {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
// @media (min-width:1000px) {
|
||||
// .recommendation-text {
|
||||
// font-size: small;
|
||||
// }
|
||||
// }
|
||||
|
||||
.recommendation-error {
|
||||
color: var(--ion-color-danger);
|
||||
}
|
||||
@@ -31,4 +16,9 @@
|
||||
font-size: medium;
|
||||
padding-left: 10px;
|
||||
font-weight: unset;
|
||||
}
|
||||
|
||||
#release-notes {
|
||||
overflow: auto;
|
||||
max-height: 160px;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, HostListener, NgZone } from '@angular/core'
|
||||
import { Component, NgZone } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AppAvailableFull, AppAvailableVersionSpecificInfo, AppDependency } from 'src/app/models/app-types'
|
||||
import { AppAvailableFull, AppAvailableVersionSpecificInfo } from 'src/app/models/app-types'
|
||||
import { ApiService } from 'src/app/services/api/api.service'
|
||||
import { AlertController, ModalController, NavController, PopoverController } from '@ionic/angular'
|
||||
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
|
||||
import { BehaviorSubject, from, merge, Observable, of, Subscription } from 'rxjs'
|
||||
import { BehaviorSubject, from, Observable, of } from 'rxjs'
|
||||
import { catchError, concatMap, filter, switchMap, tap } from 'rxjs/operators'
|
||||
import { Recommendation } from 'src/app/components/recommendation-button/recommendation-button.component'
|
||||
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
|
||||
@@ -13,8 +13,6 @@ import { AppModel } from 'src/app/models/app-model'
|
||||
import { initPropertySubject, peekProperties, PropertySubject } from 'src/app/util/property-subject.util'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
||||
import { pauseFor } from 'src/app/util/misc.util'
|
||||
import { AppReleaseNotesPage } from 'src/app/modals/app-release-notes/app-release-notes.page'
|
||||
import { Emver } from 'src/app/services/emver.service'
|
||||
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
||||
|
||||
@@ -40,8 +38,6 @@ export class AppAvailableShowPage extends Cleanup {
|
||||
|
||||
serviceDependencyDefintion = '<span style="font-style: italic">Service Dependencies</span> are other services that this service recommends or requires in order to run.'
|
||||
|
||||
showMoreReleaseNotes = false
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly apiService: ApiService,
|
||||
@@ -192,20 +188,6 @@ export class AppAvailableShowPage extends Cleanup {
|
||||
}
|
||||
}
|
||||
|
||||
async presentModalReleaseNotes () {
|
||||
const { releaseNotes, versionViewing } = peekProperties(this.$app$)
|
||||
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: AppReleaseNotesPage,
|
||||
componentProps: {
|
||||
releaseNotes,
|
||||
version: versionViewing,
|
||||
},
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
|
||||
private fetchRecommendation (): Observable<any> {
|
||||
this.recommendation = history.state && history.state.installationRecommendation
|
||||
|
||||
|
||||
@@ -22,26 +22,32 @@
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let app of apps" sizeXs="4" sizeSm="3" sizeMd="2" sizeLg="2">
|
||||
<ng-container *ngIf="{ tor: app.subject.torAddress | async, status: app.subject.status | async, ui: app.subject.ui | async, iconURL: app.subject.iconURL | async | iconParse, title: app.subject.title | async } as vars" >
|
||||
<ng-container *ngIf="{
|
||||
status: app.subject.status | async,
|
||||
hasUI: app.subject.hasUI | async,
|
||||
launchable: app.subject.launchable | async,
|
||||
iconURL: app.subject.iconURL | async | iconParse,
|
||||
title: app.subject.title | async
|
||||
} as vars">
|
||||
|
||||
<ion-card class="installed-card" [class.installed-card-on]="vars.status === 'RUNNING'" style="position:relative" [routerLink]="['/services', 'installed', app.id]">
|
||||
<div class="launch-container" *ngIf="vars.ui && !isConsulate">
|
||||
<div class="launch-button-triangle" (click)="launchUiTab(vars.tor, $event)" [class.disabled]="vars.status !== AppStatus.RUNNING || !isTor">
|
||||
<ion-icon class="launch-button-triangle-icon" name="globe-outline"></ion-icon>
|
||||
<ion-card class="installed-card" style="position:relative" [routerLink]="['/services', 'installed', app.id]">
|
||||
<div class="launch-container" *ngIf="vars.hasUI">
|
||||
<div class="launch-button-triangle" (click)="launchUiTab(app.id, $event)" [class.disabled]="!vars.launchable">
|
||||
<ion-icon name="rocket-outline"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img style="position: absolute" class="main-img" [src]="vars.iconURL" [alt]="app.subject.title | async" />
|
||||
<img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'green'" src="assets/img/running-bulb.png"/>
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'red'" src="assets/img/issue-bulb.png"/>
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'yellow'" src="assets/img/warning-bulb.png"/>
|
||||
<img class="bulb-off" *ngIf="vars.status | displayBulb: 'off'" src="assets/img/off-bulb.png"/>
|
||||
<ion-card-header>
|
||||
<status [appStatus]="vars.status" size="small"></status>
|
||||
<p>{{ vars.title }}</p>
|
||||
</ion-card-header>
|
||||
</ion-card>
|
||||
<img style="position: absolute" class="main-img" [src]="vars.iconURL" [alt]="vars.title" />
|
||||
<img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'green'" src="assets/img/running-bulb.png"/>
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'red'" src="assets/img/issue-bulb.png"/>
|
||||
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'yellow'" src="assets/img/warning-bulb.png"/>
|
||||
<img class="bulb-off" *ngIf="vars.status | displayBulb: 'off'" src="assets/img/off-bulb.png"/>
|
||||
<ion-card-header>
|
||||
<status [appStatus]="vars.status" size="small"></status>
|
||||
<p>{{ vars.title }}</p>
|
||||
</ion-card-header>
|
||||
</ion-card>
|
||||
</ng-container>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
@@ -53,7 +59,7 @@
|
||||
<p class="ion-text-wrap">Get started by installing your first service.</p>
|
||||
</div>
|
||||
<ion-button [routerLink]="['/services','marketplace']" style="width: 50%;" fill="outline">
|
||||
<ion-icon slot="start" name="cart-outline"></ion-icon>
|
||||
<ion-icon slot="start" name="storefront-outline"></ion-icon>
|
||||
Marketplace
|
||||
</ion-button>
|
||||
</div>
|
||||
|
||||
@@ -35,19 +35,15 @@ export class AppInstalledListPage extends Cleanup {
|
||||
segmentValue: 'services' | 'embassy' = 'services'
|
||||
|
||||
showCertDownload : boolean
|
||||
isConsulate: boolean
|
||||
isTor: boolean
|
||||
|
||||
constructor (
|
||||
private readonly serverModel: ServerModel,
|
||||
private readonly appModel: AppModel,
|
||||
private readonly preload: ModelPreload,
|
||||
private readonly syncDaemon: SyncDaemon,
|
||||
config: ConfigService,
|
||||
private readonly config: ConfigService,
|
||||
) {
|
||||
super()
|
||||
this.isConsulate = config.isConsulateAndroid || config.isConsulateIos
|
||||
this.isTor = config.isTor()
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
@@ -96,14 +92,21 @@ export class AppInstalledListPage extends Cleanup {
|
||||
this.error = e.message
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
async launchUiTab (address: string, event: Event) {
|
||||
async launchUiTab (id: string, event: Event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
address = address.startsWith('http') ? address : `http://${address}`
|
||||
return window.open(address, '_blank')
|
||||
|
||||
const app = this.apps.find(app => app.id === id).subject
|
||||
|
||||
let uiAddress: string
|
||||
if (this.config.isTor()) {
|
||||
uiAddress = `http://${app.torAddress.getValue()}`
|
||||
} else {
|
||||
uiAddress = `https://${app.lanAddress.getValue()}`
|
||||
}
|
||||
return window.open(uiAddress, '_blank')
|
||||
}
|
||||
|
||||
async doRefresh (event: any) {
|
||||
|
||||
@@ -20,10 +20,11 @@
|
||||
hasFetchedFull: app.hasFetchedFull | async,
|
||||
iconURL: app.iconURL | async,
|
||||
title: app.title | async,
|
||||
ui: app.ui | async
|
||||
hasUI: app.hasUI | async,
|
||||
launchable: app.launchable | async,
|
||||
lanAddress: app.lanAddress | async
|
||||
} as vars" class="ion-padding-bottom">
|
||||
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
|
||||
|
||||
<ng-container *ngIf="!($loading$ | async)">
|
||||
<ion-refresher *ngIf="app && app.id" slot="fixed" (ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
|
||||
@@ -53,62 +54,67 @@
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="no-cushion-item" lines=none style="margin-bottom: 10px">
|
||||
<ion-item class="no-cushion-item" lines=none style="margin-bottom: 10px;">
|
||||
<ion-label class="status-readout">
|
||||
<status size="bold-large" [appStatus]="vars.status"></status>
|
||||
<ion-button *ngIf="vars.status === AppStatus.NEEDS_CONFIG" expand="block" fill="outline" [routerLink]="['config']">
|
||||
<ion-button *ngIf="vars.status === AppStatus.NEEDS_CONFIG" expand="block" fill="outline" [routerLink]="['config']">
|
||||
Configure
|
||||
</ion-button>
|
||||
<ion-button *ngIf="[AppStatus.RUNNING, AppStatus.CRASHED, AppStatus.PAUSED, AppStatus.RESTARTING] | includes: vars.status" expand="block" fill="outline" (click)="stop()">
|
||||
<ion-button *ngIf="[AppStatus.RUNNING, AppStatus.CRASHED, AppStatus.PAUSED, AppStatus.RESTARTING] | includes: vars.status" expand="block" fill="outline" color="danger" (click)="stop()">
|
||||
Stop
|
||||
</ion-button>
|
||||
<ion-button *ngIf="vars.status === AppStatus.CREATING_BACKUP" expand="block" fill="outline" (click)="presentAlertStopBackup()">
|
||||
<ion-button *ngIf="vars.status === AppStatus.CREATING_BACKUP" expand="block" fill="outline" (click)="presentAlertStopBackup()">
|
||||
Stop Backup
|
||||
</ion-button>
|
||||
<ion-button *ngIf="vars.status === AppStatus.DEAD" expand="block" fill="outline" (click)="uninstall()">
|
||||
<ion-button *ngIf="vars.status === AppStatus.DEAD" expand="block" fill="outline" (click)="uninstall()">
|
||||
Force Uninstall
|
||||
</ion-button>
|
||||
<ion-button *ngIf="vars.status === AppStatus.BROKEN_DEPENDENCIES" expand="block" fill="outline" (click)="scrollToRequirements()">
|
||||
<ion-button *ngIf="vars.status === AppStatus.BROKEN_DEPENDENCIES" expand="block" fill="outline" (click)="scrollToRequirements()">
|
||||
Fix
|
||||
</ion-button>
|
||||
<ion-button *ngIf="vars.status === AppStatus.STOPPED" expand="block" fill="outline" (click)="start()">
|
||||
<ion-button *ngIf="vars.status === AppStatus.STOPPED" expand="block" fill="outline" color="success" (click)="tryStart()">
|
||||
Start
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="no-cushion-item" *ngIf="vars.ui && !isConsulate" lines="none">
|
||||
<ion-label style="margin-bottom: 10px; margin-top: 0px; display: flex; justify-content: left; align-items: center;" class="ion-text-wrap">
|
||||
<ion-button fill="clear" size="small" class="launch-explanation-button" (click)="presentLaunchPopover(vars.status, $event)">
|
||||
<ion-icon color="medium" name="information-circle-outline">
|
||||
</ion-icon>
|
||||
</ion-button>
|
||||
<ion-button [disabled]="vars.status !== 'RUNNING' || !isTor" class="launch-button" [class.launch-button-off]="vars.status !== 'RUNNING' || !isTor" (click)="launchUiTab()">
|
||||
<ion-icon style="position: absolute; z-index: 1; left: 0;" name="globe-outline"></ion-icon>
|
||||
<ion-text>LAUNCH</ion-text>
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-button size="small" *ngIf="vars.hasUI" [disabled]="!vars.launchable" class="launch-button" expand="block" (click)="launchUiTab()">
|
||||
Launch Web Interface
|
||||
<ion-icon slot="end" name="rocket-outline"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="app && app.id && vars.status !== 'INSTALLING'">
|
||||
<ion-item-group class="ion-padding-bottom">
|
||||
<ion-item-divider>Tor Address</ion-item-divider>
|
||||
<ion-item lines="none">
|
||||
<ion-label style="display: flex; justify-content: space-between; align-items: center;" class="ion-text-wrap">
|
||||
<p style="color: var(--ion-color-dark)">{{ vars.torAddress }}</p>
|
||||
<ion-button slot="end" fill="clear" (click)="copyTor()">
|
||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
<!-- addresses -->
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>Tor Address</h2>
|
||||
<p>{{ vars.torAddress }}</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" fill="clear" (click)="copyTor()">
|
||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
<ion-item lines="none">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h2>LAN Address</h2>
|
||||
<p *ngIf="!hideLAN">{{ vars.lanAddress }}</p>
|
||||
<p *ngIf="hideLAN"><ion-text color="warning">No LAN address for {{ vars.title }} {{ vars.versionInstalled }}</ion-text></p>
|
||||
</ion-label>
|
||||
<ion-button *ngIf="!hideLAN" slot="end" fill="clear" (click)="copyLAN()">
|
||||
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>Backups</ion-item-divider>
|
||||
<!-- backups -->
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<!-- create backup -->
|
||||
<ion-item button [disabled]="[AppStatus.RESTORING_BACKUP, AppStatus.CREATING_BACKUP, AppStatus.INSTALLING, AppStatus.RESTARTING, AppStatus.STOPPING] | includes: vars.status" (click)="presentModalBackup('create')">
|
||||
<ion-icon slot="start" name="save-outline" color="primary"></ion-icon>
|
||||
<ion-label style="display: flex; flex-direction: column;">
|
||||
<ion-text color="primary">Create new Backup</ion-text>
|
||||
<ion-text color="primary">Create Backup</ion-text>
|
||||
<ion-text color="medium" style="font-size: x-small">
|
||||
Last Backup: {{vars.lastBackup ? (vars.lastBackup | date: 'short') : 'never'}}
|
||||
</ion-text>
|
||||
@@ -120,12 +126,7 @@
|
||||
<ion-label><ion-text color="primary">Restore from Backup</ion-text></ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item-divider>General</ion-item-divider>
|
||||
<!-- instructions -->
|
||||
<ion-item button details="true" (click)="checkForUpdates()">
|
||||
<ion-icon slot="start" name="refresh-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text style="font-weight: 500;" color="primary">Check for Updates</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
<!-- instructions -->
|
||||
<ion-item [routerLink]="['instructions']">
|
||||
<ion-icon slot="start" name="list-outline" color="primary"></ion-icon>
|
||||
@@ -141,6 +142,11 @@
|
||||
<ion-icon slot="start" name="information-circle-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Properties</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- actions -->
|
||||
<ion-item [routerLink]="['actions']">
|
||||
<ion-icon slot="start" name="flash-outline" color="primary"></ion-icon>
|
||||
<ion-label><ion-text color="primary">Actions</ion-text></ion-label>
|
||||
</ion-item>
|
||||
<!-- logs -->
|
||||
<ion-item [disabled]="vars.status === AppStatus.INSTALLING" [routerLink]="['logs']">
|
||||
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
|
||||
@@ -148,7 +154,7 @@
|
||||
</ion-item>
|
||||
<!-- marketplace -->
|
||||
<ion-item lines="none" [routerLink]="['/services', 'marketplace', appId]">
|
||||
<ion-icon slot="start" name="cart-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-item>
|
||||
|
||||
|
||||
@@ -40,22 +40,11 @@
|
||||
}
|
||||
|
||||
.launch-button {
|
||||
width: 100%;
|
||||
padding: 0px 10px;
|
||||
--background: linear-gradient(200deg, rgb(70 193 255), rgb(70 193 255 / 45%));
|
||||
width: calc(100% - 32px);
|
||||
border-radius: 8px;
|
||||
--border-radius: 8px;
|
||||
}
|
||||
|
||||
.launch-button-off {
|
||||
--background: #383838;
|
||||
color: var(--ion-color-medium)
|
||||
}
|
||||
|
||||
.launch-explanation-button {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: -2px;
|
||||
--border-radius: 100px;
|
||||
--background: rgb(70 193 255 / 75%);
|
||||
--background-hover: rgb(70 193 255);
|
||||
--background-hover-opacity: 100%;
|
||||
--border-style: none;
|
||||
--color: white;
|
||||
--border-radius: 10px;
|
||||
margin: 12px 10px;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
|
||||
import { catchError, concatMap, filter, switchMap, tap } from 'rxjs/operators'
|
||||
import { Cleanup } from 'src/app/util/cleanup'
|
||||
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
|
||||
import { Emver } from 'src/app/services/emver.service'
|
||||
import { displayEmver } from 'src/app/pipes/emver.pipe'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
@Component({
|
||||
@@ -34,13 +32,10 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
appId: string
|
||||
AppStatus = AppStatus
|
||||
showInstructions = false
|
||||
isConsulate: boolean
|
||||
isTor: boolean
|
||||
|
||||
hideLAN: boolean
|
||||
|
||||
dependencyDefintion = () => `<span style="font-style: italic">Dependencies</span> are other services which must be installed, configured appropriately, and started in order to start ${this.app.title.getValue()}`
|
||||
launchDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. If a service does not have this button, you must access it using another interface, such as a mobile app, desktop app, or another service on the Embassy. Please view the instructions for a service for details on how to use it.</p>`
|
||||
launchOffDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. Get your service running in order to launch!</p>`
|
||||
launchLocalDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. Visit your Embassy at its Tor address to launch this service!</p>`
|
||||
|
||||
@ViewChild(IonContent) content: IonContent
|
||||
|
||||
@@ -56,12 +51,9 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
private readonly wizardBaker: WizardBaker,
|
||||
private readonly appModel: AppModel,
|
||||
private readonly popoverController: PopoverController,
|
||||
private readonly emver: Emver,
|
||||
config: ConfigService,
|
||||
private readonly config: ConfigService,
|
||||
) {
|
||||
super()
|
||||
this.isConsulate = config.isConsulateIos || config.isConsulateAndroid
|
||||
this.isTor = config.isTor()
|
||||
}
|
||||
|
||||
async ngOnInit () {
|
||||
@@ -70,8 +62,12 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
this.cleanup(
|
||||
markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId))
|
||||
.pipe(
|
||||
tap(app => this.app = app),
|
||||
concatMap(() => this.syncWhenDependencyInstalls()), //must be final in stack
|
||||
tap(app => {
|
||||
this.app = app
|
||||
const appP = peekProperties(this.app)
|
||||
this.hideLAN = !appP.lanAddress || (appP.id === 'mastodon' && appP.versionInstalled === '3.3.0') // @TODO delete this hack in 0.3.0
|
||||
}),
|
||||
concatMap(() => this.syncWhenDependencyInstalls()), // must be final in stack
|
||||
catchError(e => of(this.setError(e))),
|
||||
).subscribe(),
|
||||
)
|
||||
@@ -103,61 +99,15 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
}
|
||||
|
||||
async launchUiTab () {
|
||||
let uiAddress = this.app.torAddress.getValue()
|
||||
uiAddress = uiAddress.startsWith('http') ? uiAddress : `http://${uiAddress}`
|
||||
let uiAddress: string
|
||||
if (this.config.isTor()) {
|
||||
uiAddress = `http://${this.app.torAddress.getValue()}`
|
||||
} else {
|
||||
uiAddress = `https://${this.app.lanAddress.getValue()}`
|
||||
}
|
||||
return window.open(uiAddress, '_blank')
|
||||
}
|
||||
|
||||
async checkForUpdates () {
|
||||
const app = peekProperties(this.app)
|
||||
|
||||
this.loader.of({
|
||||
message: `Checking for updates...`,
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringAsync(
|
||||
async () => {
|
||||
const { versionLatest } = await this.apiService.getAvailableApp(this.appId)
|
||||
if (this.emver.compare(versionLatest, app.versionInstalled) === 1) {
|
||||
this.presentAlertUpdate(app, versionLatest)
|
||||
} else {
|
||||
this.presentAlertUpToDate()
|
||||
}
|
||||
},
|
||||
).catch(e => this.setError(e))
|
||||
}
|
||||
|
||||
async presentAlertUpdate (app: AppInstalledFull, versionLatest: string) {
|
||||
const alert = await this.alertCtrl.create({
|
||||
backdropDismiss: false,
|
||||
header: 'Update Available',
|
||||
message: `New version ${displayEmver(versionLatest)} found for ${app.title}.`,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'View in Store',
|
||||
cssClass: 'alert-success',
|
||||
handler: () => {
|
||||
this.navCtrl.navigateForward(['/services', 'marketplace', this.appId])
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async presentAlertUpToDate () {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Up To Date',
|
||||
message: `You are running the latest version of ${this.app.title.getValue()}!`,
|
||||
buttons: ['OK'],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
async copyTor () {
|
||||
const app = peekProperties(this.app)
|
||||
let message = ''
|
||||
@@ -172,6 +122,20 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async copyLAN () {
|
||||
const app = peekProperties(this.app)
|
||||
let message = ''
|
||||
await copyToClipboard(app.lanAddress).then(success => { message = success ? 'copied to clipboard!' : 'failed to copy' })
|
||||
|
||||
const toast = await this.toastCtrl.create({
|
||||
header: message,
|
||||
position: 'bottom',
|
||||
duration: 1000,
|
||||
cssClass: 'notification-toast',
|
||||
})
|
||||
await toast.present()
|
||||
}
|
||||
|
||||
async stop (): Promise<void> {
|
||||
const app = peekProperties(this.app)
|
||||
|
||||
@@ -200,15 +164,13 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
}).catch(e => this.setError(e))
|
||||
}
|
||||
|
||||
async start (): Promise<void> {
|
||||
async tryStart (): Promise<void> {
|
||||
const app = peekProperties(this.app)
|
||||
this.loader.of({
|
||||
message: `Starting ${app.title}...`,
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringP(
|
||||
this.apiService.startApp(this.appId),
|
||||
).catch(e => this.setError(e))
|
||||
if (app.startAlert) {
|
||||
this.presentAlertStart(app)
|
||||
} else {
|
||||
this.start(app)
|
||||
}
|
||||
}
|
||||
|
||||
async presentModalBackup (type: 'create' | 'restore') {
|
||||
@@ -275,18 +237,6 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
return this.navCtrl.navigateRoot('/services/installed')
|
||||
}
|
||||
|
||||
async presentLaunchPopover (status: AppStatus, ev: any) {
|
||||
let desc: string
|
||||
if (!this.isTor) {
|
||||
desc = this.launchLocalDefinition
|
||||
} else if (status !== AppStatus.RUNNING) {
|
||||
desc = this.launchOffDefinition
|
||||
} else {
|
||||
desc = this.launchDefinition
|
||||
}
|
||||
return this.presentPopover(desc, ev)
|
||||
}
|
||||
|
||||
async presentPopover (information: string, ev: any) {
|
||||
const popover = await this.popoverController.create({
|
||||
component: InformationPopoverComponent,
|
||||
@@ -301,8 +251,39 @@ export class AppInstalledShowPage extends Cleanup {
|
||||
return await popover.present()
|
||||
}
|
||||
|
||||
private setError (e: Error) {
|
||||
private async presentAlertStart (app: AppInstalledFull): Promise<void> {
|
||||
const alert = await this.alertCtrl.create({
|
||||
header: 'Warning',
|
||||
message: app.startAlert,
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
role: 'cancel',
|
||||
},
|
||||
{
|
||||
text: 'Start',
|
||||
handler: () => {
|
||||
this.start(app)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
await alert.present()
|
||||
}
|
||||
|
||||
private async start (app: AppInstalledFull): Promise<void> {
|
||||
this.loader.of({
|
||||
message: `Starting ${app.title}...`,
|
||||
spinner: 'lines',
|
||||
cssClass: 'loader',
|
||||
}).displayDuringP(
|
||||
this.apiService.startApp(this.appId),
|
||||
).catch(e => this.setError(e))
|
||||
}
|
||||
|
||||
private setError (e: Error): Observable<void> {
|
||||
this.$error$.next(e.message)
|
||||
return of()
|
||||
}
|
||||
|
||||
private clearError () {
|
||||
|
||||
@@ -2,10 +2,8 @@ import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Routes, RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
|
||||
import { AppInstructionsPage } from './app-instructions.page'
|
||||
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
|
||||
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
|
||||
import { SharingModule } from 'src/app/modules/sharing.module'
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -21,7 +19,6 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
PwaBackComponentModule,
|
||||
BadgeMenuComponentModule,
|
||||
SharingModule,
|
||||
],
|
||||
declarations: [AppInstructionsPage],
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
<pwa-back-button></pwa-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>Instructions</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<badge-menu-button></badge-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ export class AppInstructionsPage {
|
||||
error = ''
|
||||
app: AppInstalledFull = { } as any
|
||||
appId: string
|
||||
instructions: any
|
||||
|
||||
constructor (
|
||||
private readonly route: ActivatedRoute,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user