feat(log): colored logging using termcolor
This commit is contained in:
parent
d8aa65ea4d
commit
4f4a51ae8c
@ -19,6 +19,7 @@ library
|
|||||||
, System.Serverman
|
, System.Serverman
|
||||||
, System.Serverman.Utils
|
, System.Serverman.Utils
|
||||||
, System.Serverman.Action
|
, System.Serverman.Action
|
||||||
|
, System.Serverman.Log
|
||||||
|
|
||||||
, System.Serverman.Actions.Install
|
, System.Serverman.Actions.Install
|
||||||
, System.Serverman.Actions.Env
|
, System.Serverman.Actions.Env
|
||||||
@ -49,6 +50,8 @@ library
|
|||||||
, hint
|
, hint
|
||||||
, stack
|
, stack
|
||||||
, exceptions
|
, exceptions
|
||||||
|
, monad-loops
|
||||||
|
, termcolor
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|
||||||
executable serverman
|
executable serverman
|
||||||
@ -66,6 +69,7 @@ test-suite serverman-test
|
|||||||
main-is: Spec.hs
|
main-is: Spec.hs
|
||||||
build-depends: base
|
build-depends: base
|
||||||
, serverman
|
, serverman
|
||||||
|
, quickcheck
|
||||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
@ -30,5 +30,6 @@ module System.Serverman ( run
|
|||||||
|
|
||||||
run (Free (Remote addrs action next)) = mapM_ (\addr -> runRemotely addr (run action)) addrs >> run next
|
run (Free (Remote addrs action next)) = mapM_ (\addr -> runRemotely addr (run action)) addrs >> run next
|
||||||
|
|
||||||
run (Free (FetchRepository next)) = fetchRepo >> run next
|
run (Free (FetchRepository next)) = fetchRepo False >> run next
|
||||||
|
run (Free (UpdateRepository next)) = fetchRepo True >> run next
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ module System.Serverman.Action ( ActionF(..)
|
|||||||
, Action
|
, Action
|
||||||
, call
|
, call
|
||||||
, fetchRepository
|
, fetchRepository
|
||||||
|
, updateRepository
|
||||||
, start
|
, start
|
||||||
, stop
|
, stop
|
||||||
, install
|
, install
|
||||||
@ -33,6 +34,7 @@ module System.Serverman.Action ( ActionF(..)
|
|||||||
| Install Service x
|
| Install Service x
|
||||||
| Remote [Address] (Action ()) x
|
| Remote [Address] (Action ()) x
|
||||||
| FetchRepository x
|
| FetchRepository x
|
||||||
|
| UpdateRepository x
|
||||||
| Start Service x
|
| Start Service x
|
||||||
| Stop Service x
|
| Stop Service x
|
||||||
|
|
||||||
@ -44,6 +46,7 @@ module System.Serverman.Action ( ActionF(..)
|
|||||||
fmap f (DetectOS x) = DetectOS (f x)
|
fmap f (DetectOS x) = DetectOS (f x)
|
||||||
fmap f (Remote addr action x) = Remote addr action (f x)
|
fmap f (Remote addr action x) = Remote addr action (f x)
|
||||||
fmap f (FetchRepository x) = FetchRepository (f x)
|
fmap f (FetchRepository x) = FetchRepository (f x)
|
||||||
|
fmap f (UpdateRepository x) = UpdateRepository (f x)
|
||||||
|
|
||||||
type Action = Free ActionF
|
type Action = Free ActionF
|
||||||
|
|
||||||
@ -67,3 +70,6 @@ module System.Serverman.Action ( ActionF(..)
|
|||||||
|
|
||||||
fetchRepository :: Action ()
|
fetchRepository :: Action ()
|
||||||
fetchRepository = liftF $ FetchRepository ()
|
fetchRepository = liftF $ FetchRepository ()
|
||||||
|
|
||||||
|
updateRepository :: Action ()
|
||||||
|
updateRepository = liftF $ UpdateRepository ()
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
module System.Serverman.Actions.Call (callService) where
|
module System.Serverman.Actions.Call (callService) where
|
||||||
import System.Serverman.Types
|
import System.Serverman.Types
|
||||||
import System.Serverman.Utils
|
import System.Serverman.Utils
|
||||||
|
import System.Serverman.Log
|
||||||
import qualified System.Serverman.Actions.Repository
|
import qualified System.Serverman.Actions.Repository
|
||||||
import System.Serverman.Actions.Remote
|
import System.Serverman.Actions.Remote
|
||||||
|
|
||||||
@ -14,10 +15,13 @@ module System.Serverman.Actions.Call (callService) where
|
|||||||
import System.Posix.Env
|
import System.Posix.Env
|
||||||
import Data.List
|
import Data.List
|
||||||
import Stack.Package
|
import Stack.Package
|
||||||
|
import Data.Maybe
|
||||||
|
|
||||||
callService :: Service -> Maybe FilePath -> App ()
|
callService :: Service -> Maybe FilePath -> App ()
|
||||||
callService s@(Service { name, version }) remote = do
|
callService s@(Service { name, version }) remote = do
|
||||||
state@(AppState { repositoryURL }) <- get
|
done <- progress
|
||||||
|
|
||||||
|
state@(AppState { repositoryURL, helpArg }) <- get
|
||||||
put $ state { remoteMode = Nothing }
|
put $ state { remoteMode = Nothing }
|
||||||
|
|
||||||
dir <- liftIO $ getAppUserDataDirectory "serverman"
|
dir <- liftIO $ getAppUserDataDirectory "serverman"
|
||||||
@ -35,21 +39,39 @@ module System.Serverman.Actions.Call (callService) where
|
|||||||
|
|
||||||
(Right stackEnv) <- exec "stack" ["exec", "env", "--allow-different-user"] "" (Just path) True
|
(Right stackEnv) <- exec "stack" ["exec", "env", "--allow-different-user"] "" (Just path) True
|
||||||
(Right stackSourceEnv) <- exec "stack" ["exec", "env", "--allow-different-user"] "" (Just source) True
|
(Right stackSourceEnv) <- exec "stack" ["exec", "env", "--allow-different-user"] "" (Just source) True
|
||||||
|
|
||||||
let finalEnv = map (mergeEnv $ parseKeyValue stackSourceEnv '=') (parseKeyValue stackEnv '=')
|
let finalEnv = map (mergeEnv $ parseKeyValue stackSourceEnv '=') (parseKeyValue stackEnv '=')
|
||||||
|
|
||||||
backupEnv <- liftIO $ getEnvironment
|
backupEnv <- liftIO $ getEnvironment
|
||||||
liftIO $ setEnvironment finalEnv
|
liftIO $ setEnvironment finalEnv
|
||||||
|
|
||||||
func <- liftIO $ runInterpreter (interpreter include entry)
|
func <- liftIO $ runInterpreter (getCall include entry)
|
||||||
|
helpOutput <- liftIO $ runInterpreter (getHelp include entry)
|
||||||
|
|
||||||
case func of
|
done
|
||||||
Right fn -> handleRemote remote $ fn s
|
|
||||||
Left err -> liftIO $ do
|
|
||||||
putStrLn $ "error reading `call` from module " ++ entry
|
|
||||||
case err of
|
|
||||||
WontCompile errs -> mapM_ (putStrLn . errMsg) errs
|
|
||||||
|
|
||||||
x -> print x
|
if helpArg then
|
||||||
|
case helpOutput of
|
||||||
|
Right fn -> write =<< fn
|
||||||
|
Left e -> do
|
||||||
|
write $ "could not find a help entry for " ++ name
|
||||||
|
case e of
|
||||||
|
WontCompile errs -> mapM_ (write . errMsg) errs
|
||||||
|
|
||||||
|
GhcException ie -> err ie
|
||||||
|
UnknownError ie -> err ie
|
||||||
|
NotAllowed ie -> err ie
|
||||||
|
else
|
||||||
|
case func of
|
||||||
|
Right fn -> handleRemote remote $ fn s
|
||||||
|
Left e -> do
|
||||||
|
err $ "couldn't read `call` from module " ++ entry
|
||||||
|
case e of
|
||||||
|
WontCompile errs -> mapM_ (write . errMsg) errs
|
||||||
|
|
||||||
|
GhcException ie -> err ie
|
||||||
|
UnknownError ie -> err ie
|
||||||
|
NotAllowed ie -> err ie
|
||||||
|
|
||||||
liftIO $ setEnvironment backupEnv
|
liftIO $ setEnvironment backupEnv
|
||||||
|
|
||||||
@ -62,15 +84,22 @@ module System.Serverman.Actions.Call (callService) where
|
|||||||
handleRemote _ action = action
|
handleRemote _ action = action
|
||||||
|
|
||||||
mergeEnv other (key, value)
|
mergeEnv other (key, value)
|
||||||
| key `elem` ["GHC_PACKAGE_PATH", "HASKELL_PACKAGE_SANDBOXES"] =
|
| key `elem` ["GHC_PACKAGE_PATH", "HASKELL_PACKAGE_SANDBOXES", "PATH"] =
|
||||||
let (Just alt) = lookup key other
|
let (Just alt) = lookup key other
|
||||||
in (key, value ++ ":" ++ alt)
|
in (key, value ++ ":" ++ alt)
|
||||||
|
| key == "LD_PRELOAD" = (key, "")
|
||||||
| otherwise = (key, value)
|
| otherwise = (key, value)
|
||||||
|
|
||||||
interpreter :: [FilePath] -> FilePath -> Interpreter (Service -> App ())
|
getCall :: [FilePath] -> FilePath -> Interpreter (Service -> App ())
|
||||||
interpreter path entry = do
|
getCall path entry = do
|
||||||
set [searchPath := path]
|
set [searchPath := path]
|
||||||
loadModules [entry]
|
loadModules [entry]
|
||||||
setTopLevelModules ["Main"]
|
setTopLevelModules ["Main"]
|
||||||
interpret "call" (as :: Service -> App ())
|
interpret "call" (as :: Service -> App ())
|
||||||
|
|
||||||
|
getHelp :: [FilePath] -> FilePath -> Interpreter (App String)
|
||||||
|
getHelp path entry = do
|
||||||
|
set [searchPath := path]
|
||||||
|
loadModules [entry]
|
||||||
|
setTopLevelModules ["Main"]
|
||||||
|
interpret "help" (as :: App String)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
module System.Serverman.Actions.Env (OS(..), getOS, releaseToOS) where
|
module System.Serverman.Actions.Env (OS(..), getOS, releaseToOS) where
|
||||||
import System.Serverman.Utils
|
import System.Serverman.Utils
|
||||||
import System.Serverman.Types
|
import System.Serverman.Types
|
||||||
|
import System.Serverman.Log
|
||||||
|
|
||||||
import System.Process
|
import System.Process
|
||||||
import Data.List
|
import Data.List
|
||||||
@ -10,6 +11,8 @@ module System.Serverman.Actions.Env (OS(..), getOS, releaseToOS) where
|
|||||||
import Control.Monad.State
|
import Control.Monad.State
|
||||||
|
|
||||||
getOS = do
|
getOS = do
|
||||||
|
verbose "detecting os"
|
||||||
|
|
||||||
arch_release <- execute "cat" ["/etc/os-release"] "" False
|
arch_release <- execute "cat" ["/etc/os-release"] "" False
|
||||||
deb_release <- execute "cat" ["/etc/lsb-release"] "" False
|
deb_release <- execute "cat" ["/etc/lsb-release"] "" False
|
||||||
|
|
||||||
|
@ -4,10 +4,11 @@
|
|||||||
module System.Serverman.Actions.Install (installService) where
|
module System.Serverman.Actions.Install (installService) where
|
||||||
import System.Serverman.Action
|
import System.Serverman.Action
|
||||||
import System.Serverman.Utils
|
import System.Serverman.Utils
|
||||||
import System.Serverman.Services
|
import System.Serverman.Services hiding (info)
|
||||||
import System.Serverman.Actions.Env
|
import System.Serverman.Actions.Env
|
||||||
import System.Serverman.Actions.Repository
|
import System.Serverman.Actions.Repository
|
||||||
import System.Serverman.Types
|
import System.Serverman.Types
|
||||||
|
import System.Serverman.Log
|
||||||
|
|
||||||
import System.IO.Error
|
import System.IO.Error
|
||||||
import System.Process
|
import System.Process
|
||||||
@ -20,6 +21,7 @@ module System.Serverman.Actions.Install (installService) where
|
|||||||
|
|
||||||
installService :: Service -> App ()
|
installService :: Service -> App ()
|
||||||
installService s@(Service { dependencies, packages }) = do
|
installService s@(Service { dependencies, packages }) = do
|
||||||
|
done <- progress
|
||||||
(AppState { os }) <- get
|
(AppState { os }) <- get
|
||||||
|
|
||||||
deps <- catMaybes <$> mapM findService dependencies
|
deps <- catMaybes <$> mapM findService dependencies
|
||||||
@ -33,11 +35,11 @@ module System.Serverman.Actions.Install (installService) where
|
|||||||
|
|
||||||
process <- liftedAsync $ do
|
process <- liftedAsync $ do
|
||||||
result <- executeRoot (fst base) (snd base ++ pkg) "" True
|
result <- executeRoot (fst base) (snd base ++ pkg) "" True
|
||||||
|
done
|
||||||
|
|
||||||
case result of
|
case result of
|
||||||
Left err -> return ()
|
Left err -> return ()
|
||||||
Right _ -> do
|
Right _ -> info $ "installed " ++ show s
|
||||||
liftIO $ putStrLn $ "installed " ++ show s ++ "."
|
|
||||||
|
|
||||||
liftIO $ wait process
|
liftIO $ wait process
|
||||||
return ()
|
return ()
|
||||||
|
@ -6,11 +6,14 @@ module System.Serverman.Actions.Manage (startService, stopService) where
|
|||||||
import System.Serverman.Actions.Env
|
import System.Serverman.Actions.Env
|
||||||
import System.Serverman.Actions.Install
|
import System.Serverman.Actions.Install
|
||||||
import System.Serverman.Services
|
import System.Serverman.Services
|
||||||
|
import System.Serverman.Log
|
||||||
|
|
||||||
import Control.Monad.State hiding (liftIO)
|
import Control.Monad.State hiding (liftIO)
|
||||||
|
|
||||||
startService :: Service -> App ()
|
startService :: Service -> App ()
|
||||||
startService (Service { service }) = do
|
startService (Service { service }) = do
|
||||||
|
verbose $ "starting service " ++ service
|
||||||
|
|
||||||
(AppState { os }) <- get
|
(AppState { os }) <- get
|
||||||
case os of
|
case os of
|
||||||
_ -> do
|
_ -> do
|
||||||
@ -20,6 +23,8 @@ module System.Serverman.Actions.Manage (startService, stopService) where
|
|||||||
|
|
||||||
stopService :: Service -> App ()
|
stopService :: Service -> App ()
|
||||||
stopService (Service { service }) = do
|
stopService (Service { service }) = do
|
||||||
|
verbose $ "stopping service " ++ service
|
||||||
|
|
||||||
(AppState { os }) <- get
|
(AppState { os }) <- get
|
||||||
case os of
|
case os of
|
||||||
_ -> do
|
_ -> do
|
||||||
|
@ -12,9 +12,11 @@ module System.Serverman.Actions.Remote ( runRemotely
|
|||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Control.Monad.State hiding (liftIO)
|
import Control.Monad.State hiding (liftIO)
|
||||||
|
import Control.Concurrent
|
||||||
import Data.IORef
|
import Data.IORef
|
||||||
import Data.Either
|
import Data.Either
|
||||||
import Control.Concurrent
|
|
||||||
|
actionDelay = 1000000
|
||||||
|
|
||||||
runRemotely :: Address -> App r -> App ()
|
runRemotely :: Address -> App r -> App ()
|
||||||
runRemotely addr@(Address host port user) action = do
|
runRemotely addr@(Address host port user) action = do
|
||||||
@ -41,15 +43,13 @@ module System.Serverman.Actions.Remote ( runRemotely
|
|||||||
liftIO $ createDirectoryIfMissing True path
|
liftIO $ createDirectoryIfMissing True path
|
||||||
|
|
||||||
-- check if a connection to SSH server using public key is possible
|
-- check if a connection to SSH server using public key is possible
|
||||||
-- result <- execRemote servermanAddr (Just keyPath) Nothing "" "echo" [] "" Nothing False
|
|
||||||
execute "fusermount" ["-u", path] "" False
|
execute "fusermount" ["-u", path] "" False
|
||||||
result <- execute "sshfs" (p ++ noPassword ++ uid ++ options ++ ["-o", "IdentityFile=" ++ keyPath, smConnection ++ ":/", path]) "" True
|
result <- execute "sshfs" (p ++ noPassword ++ uid ++ options ++ ["-o", "IdentityFile=" ++ keyPath, smConnection ++ ":/", path]) "" False
|
||||||
|
|
||||||
liftIO $ threadDelay 500
|
|
||||||
|
|
||||||
case result of
|
case result of
|
||||||
Right _ -> do
|
Right _ -> do
|
||||||
state <- get
|
state <- get
|
||||||
|
liftIO $ threadDelay actionDelay
|
||||||
put $ state { remoteMode = Just (servermanAddr, keyPath) }
|
put $ state { remoteMode = Just (servermanAddr, keyPath) }
|
||||||
getOS
|
getOS
|
||||||
action
|
action
|
||||||
|
@ -5,8 +5,9 @@
|
|||||||
module System.Serverman.Actions.Repository (fetchRepo, findService) where
|
module System.Serverman.Actions.Repository (fetchRepo, findService) where
|
||||||
import System.Serverman.Utils
|
import System.Serverman.Utils
|
||||||
import System.Directory
|
import System.Directory
|
||||||
import System.Serverman.Services
|
import System.Serverman.Services hiding (info)
|
||||||
import System.Serverman.Actions.Env
|
import System.Serverman.Actions.Env
|
||||||
|
import System.Serverman.Log
|
||||||
import System.Serverman.Types
|
import System.Serverman.Types
|
||||||
|
|
||||||
import System.FilePath
|
import System.FilePath
|
||||||
@ -26,27 +27,37 @@ module System.Serverman.Actions.Repository (fetchRepo, findService) where
|
|||||||
findService :: String -> App (Maybe Service)
|
findService :: String -> App (Maybe Service)
|
||||||
findService n = do
|
findService n = do
|
||||||
(AppState { repository }) <- get
|
(AppState { repository }) <- get
|
||||||
|
verbose $ "searching for service " ++ n
|
||||||
return $ find (\a -> name a == n) repository
|
return $ find (\a -> name a == n) repository
|
||||||
|
|
||||||
fetchRepo :: App Repository
|
fetchRepo :: Bool -> App Repository
|
||||||
fetchRepo = do
|
fetchRepo update = do
|
||||||
|
verbose "fetching repository"
|
||||||
|
|
||||||
state@(AppState { repositoryURL }) <- get
|
state@(AppState { repositoryURL }) <- get
|
||||||
dir <- liftIO $ getAppUserDataDirectory "serverman"
|
dir <- liftIO $ getAppUserDataDirectory "serverman"
|
||||||
let path = dir </> "repository"
|
let path = dir </> "repository"
|
||||||
let source = dir </> "source"
|
let source = dir </> "source"
|
||||||
|
|
||||||
execIfMissing path $ do
|
execIfMissing path $ do
|
||||||
liftIO $ putStrLn $ "cloning " ++ repositoryURL ++ " in " ++ path
|
verbose "repository missing... cloning repository"
|
||||||
|
info $ "cloning " ++ repositoryURL ++ " in " ++ path
|
||||||
execute "git" ["clone", repositoryURL, path] "" True
|
execute "git" ["clone", repositoryURL, path] "" True
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
execIfMissing source $ do
|
execIfMissing source $ do
|
||||||
liftIO $ putStrLn $ "cloning " ++ sourceURL ++ " in " ++ source
|
verbose "serverman source missing... cloning repository"
|
||||||
|
|
||||||
|
info $ "cloning " ++ sourceURL ++ " in " ++ source
|
||||||
execute "git" ["clone", sourceURL, source] "" True
|
execute "git" ["clone", sourceURL, source] "" True
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
{-exec "git" ["pull", "origin", "master"] "" (Just path) True-}
|
when update $ do
|
||||||
{-exec "git" ["pull", "origin", "master"] "" (Just source) True-}
|
verbose "updating repository"
|
||||||
|
|
||||||
|
exec "git" ["pull", "origin", "master"] "" (Just path) True
|
||||||
|
exec "git" ["pull", "origin", "master"] "" (Just source) True
|
||||||
|
return ()
|
||||||
|
|
||||||
content <- liftIO $ readFile (path </> "repository.json")
|
content <- liftIO $ readFile (path </> "repository.json")
|
||||||
|
|
||||||
@ -65,10 +76,10 @@ module System.Serverman.Actions.Repository (fetchRepo, findService) where
|
|||||||
return $ rights list
|
return $ rights list
|
||||||
|
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
liftIO $ putStrLn $ "error parsing repository data, please try re-fetching the repository."
|
err $ "parsing repository data failed, please try re-fetching the repository."
|
||||||
return []
|
return []
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
liftIO $ putStrLn $ "error parsing repository data, please try re-fetching the repository."
|
err $ "parsing repository data failed, please try re-fetching the repository."
|
||||||
return []
|
return []
|
||||||
|
|
||||||
where
|
where
|
||||||
|
83
src/System/Serverman/Log.hs
Normal file
83
src/System/Serverman/Log.hs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
|
||||||
|
module System.Serverman.Log ( verbose
|
||||||
|
, info
|
||||||
|
, write
|
||||||
|
, progress
|
||||||
|
, warning
|
||||||
|
, err
|
||||||
|
, die
|
||||||
|
, progressListener) where
|
||||||
|
|
||||||
|
import System.Serverman.Types
|
||||||
|
|
||||||
|
import Text.Termcolor
|
||||||
|
import Text.Termcolor.Style
|
||||||
|
import qualified Text.Termcolor.Foreground as F
|
||||||
|
import qualified Text.Termcolor.Background as B
|
||||||
|
import qualified System.Exit as E
|
||||||
|
import Control.Concurrent.Async
|
||||||
|
import Control.Monad.State
|
||||||
|
import Control.Concurrent
|
||||||
|
import System.IO
|
||||||
|
import Control.Monad.Trans.Control
|
||||||
|
|
||||||
|
verbose :: String -> App ()
|
||||||
|
verbose str = do
|
||||||
|
(AppState { verboseMode }) <- get
|
||||||
|
liftIO $
|
||||||
|
when verboseMode $ do
|
||||||
|
putStrLn . format . F.gray $ read ("[verbose] " ++ str)
|
||||||
|
|
||||||
|
write :: String -> App ()
|
||||||
|
write str = liftIO . putStrLn . format . reset $ read str
|
||||||
|
|
||||||
|
info :: String -> App ()
|
||||||
|
info str = liftIO . putStrLn . format . reset $ read ("[info] " ++ str)
|
||||||
|
|
||||||
|
warning :: String -> App ()
|
||||||
|
warning str = liftIO . putStrLn . format . F.yellow $ read ("[warning] " ++ str)
|
||||||
|
|
||||||
|
err :: String -> App ()
|
||||||
|
err str = liftIO . putStrLn . format . bold . F.red $ read ("[error] " ++ str)
|
||||||
|
|
||||||
|
die :: String -> App ()
|
||||||
|
die str = liftIO . E.die . format . bold . F.red $ read ("[fatal error] " ++ str)
|
||||||
|
|
||||||
|
progress :: App (App ())
|
||||||
|
progress = do
|
||||||
|
state <- get
|
||||||
|
p <- progressListener
|
||||||
|
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
progressPrefix = "working "
|
||||||
|
progressCharacters = [". ", ".. ", "...", " ..", " .", " "]
|
||||||
|
progressDelay = 200000
|
||||||
|
progressListener :: App (App ())
|
||||||
|
progressListener = do
|
||||||
|
p <- liftedAsync $
|
||||||
|
mapM start (cycle [0..length progressCharacters])
|
||||||
|
|
||||||
|
return $ stop p
|
||||||
|
|
||||||
|
where
|
||||||
|
start n = do
|
||||||
|
liftIO . threadDelay $ progressDelay
|
||||||
|
|
||||||
|
liftedAsync $ do
|
||||||
|
let str = progressPrefix ++ (progressCharacters !! n)
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
putStr . format . (light . F.blue) $ read str
|
||||||
|
putStr $ "\ESC[" ++ (show $ length str) ++ "D\ESC[0;"
|
||||||
|
hFlush stdout
|
||||||
|
|
||||||
|
return ()
|
||||||
|
|
||||||
|
stop process = do
|
||||||
|
liftIO $ do
|
||||||
|
cancel process
|
||||||
|
putStr "\ESC[0;"
|
@ -1,5 +1,6 @@
|
|||||||
{-# LANGUAGE DeriveGeneric #-}
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
|
||||||
module System.Serverman.Types ( Service (..)
|
module System.Serverman.Types ( Service (..)
|
||||||
, Repository
|
, Repository
|
||||||
@ -8,10 +9,14 @@ module System.Serverman.Types ( Service (..)
|
|||||||
, App
|
, App
|
||||||
, Address (..)
|
, Address (..)
|
||||||
, Params
|
, Params
|
||||||
, runApp) where
|
, runApp
|
||||||
|
, liftedAsync) where
|
||||||
import Data.Default.Class
|
import Data.Default.Class
|
||||||
import GHC.Generics
|
import GHC.Generics
|
||||||
import Control.Monad.State
|
import Control.Monad.State
|
||||||
|
import Control.Concurrent.Async
|
||||||
|
import Control.Monad.Trans.Control
|
||||||
|
import System.Process
|
||||||
|
|
||||||
type Host = String
|
type Host = String
|
||||||
type Port = String
|
type Port = String
|
||||||
@ -71,12 +76,29 @@ module System.Serverman.Types ( Service (..)
|
|||||||
|
|
||||||
type Repository = [Service]
|
type Repository = [Service]
|
||||||
|
|
||||||
|
type SourcePort = String
|
||||||
|
type DestinationPort = String
|
||||||
data AppState = AppState { remoteMode :: Maybe (Address, String)
|
data AppState = AppState { remoteMode :: Maybe (Address, String)
|
||||||
, repository :: Repository
|
, repository :: Repository
|
||||||
, repositoryURL :: String
|
, repositoryURL :: String
|
||||||
, os :: OS
|
, os :: OS
|
||||||
, arguments :: [(String, Maybe String)]
|
, arguments :: [(String, Maybe String)]
|
||||||
} deriving (Show)
|
, helpArg :: Bool
|
||||||
|
, verboseMode :: Bool
|
||||||
|
, ports :: [(SourcePort, DestinationPort)]
|
||||||
|
, processes :: [ProcessHandle]
|
||||||
|
}
|
||||||
|
|
||||||
|
instance Show AppState where
|
||||||
|
show (AppState { remoteMode, repository, repositoryURL, os, arguments, ports, processes }) =
|
||||||
|
"remote: " ++ show remoteMode ++ "\n" ++
|
||||||
|
"repository:\n" ++
|
||||||
|
" - url: " ++ show repositoryURL ++ "\n" ++
|
||||||
|
" - packages: " ++ show repository ++ "\n" ++
|
||||||
|
"operating system: " ++ show os ++ "\n" ++
|
||||||
|
"arguments: " ++ show arguments ++ "\n" ++
|
||||||
|
"port forwarding: " ++ show ports ++ "\n" ++
|
||||||
|
"processes: " ++ show (length processes)
|
||||||
|
|
||||||
instance Default AppState where
|
instance Default AppState where
|
||||||
def = AppState { remoteMode = Nothing
|
def = AppState { remoteMode = Nothing
|
||||||
@ -84,9 +106,16 @@ module System.Serverman.Types ( Service (..)
|
|||||||
, repositoryURL = "https://github.com/mdibaiee/serverman-repository"
|
, repositoryURL = "https://github.com/mdibaiee/serverman-repository"
|
||||||
, os = Unknown
|
, os = Unknown
|
||||||
, arguments = []
|
, arguments = []
|
||||||
|
, helpArg = False
|
||||||
|
, verboseMode = False
|
||||||
|
, ports = []
|
||||||
|
, processes = []
|
||||||
}
|
}
|
||||||
type App = StateT AppState IO
|
type App = StateT AppState IO
|
||||||
|
|
||||||
runApp :: App a -> IO (a, AppState)
|
runApp :: App a -> IO (a, AppState)
|
||||||
runApp k = runStateT k def
|
runApp k = runStateT k def
|
||||||
|
|
||||||
|
liftedAsync :: MonadBaseControl IO m => m a -> m (Async (StM m a))
|
||||||
|
liftedAsync m = liftBaseWith $ \runInIO -> async (runInIO m)
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ module System.Serverman.Utils ( App (..)
|
|||||||
, splitAtElem
|
, splitAtElem
|
||||||
, semicolon
|
, semicolon
|
||||||
, block
|
, block
|
||||||
|
, mkHelp
|
||||||
, indent
|
, indent
|
||||||
, commas
|
, commas
|
||||||
, quote
|
, quote
|
||||||
@ -23,8 +24,9 @@ module System.Serverman.Utils ( App (..)
|
|||||||
, execute
|
, execute
|
||||||
, execRemote
|
, execRemote
|
||||||
, Address (..)
|
, Address (..)
|
||||||
, liftedAsync
|
|
||||||
, liftIO
|
, liftIO
|
||||||
|
, usingPort
|
||||||
|
, clearPort
|
||||||
, restartService
|
, restartService
|
||||||
, getPassword
|
, getPassword
|
||||||
, executeRoot) where
|
, executeRoot) where
|
||||||
@ -34,7 +36,7 @@ module System.Serverman.Utils ( App (..)
|
|||||||
import System.Directory
|
import System.Directory
|
||||||
import System.FilePath
|
import System.FilePath
|
||||||
import System.Process
|
import System.Process
|
||||||
import System.IO.Error
|
import System.IO.Error (tryIOError)
|
||||||
import Control.Concurrent.Async
|
import Control.Concurrent.Async
|
||||||
import Data.List
|
import Data.List
|
||||||
import Control.Exception
|
import Control.Exception
|
||||||
@ -47,31 +49,90 @@ module System.Serverman.Utils ( App (..)
|
|||||||
import System.Posix.Env
|
import System.Posix.Env
|
||||||
import qualified Control.Monad.State as ST
|
import qualified Control.Monad.State as ST
|
||||||
import Control.Monad.State hiding (liftIO)
|
import Control.Monad.State hiding (liftIO)
|
||||||
import Control.Monad.Trans.Control
|
|
||||||
import Data.Default.Class
|
import Data.Default.Class
|
||||||
import System.Unix.Chroot
|
import System.Unix.Chroot
|
||||||
import Control.Monad.Catch
|
import Control.Concurrent
|
||||||
|
import Control.Monad.Loops
|
||||||
|
|
||||||
import System.Serverman.Types
|
import System.Serverman.Types
|
||||||
|
import System.Serverman.Log
|
||||||
|
import Debug.Trace
|
||||||
|
|
||||||
liftIO :: (MonadIO m, MonadState AppState m, MonadMask m) => IO a -> m a
|
-- lift IO to App, also applying remote mode and port forwarding:
|
||||||
{-liftIO :: IO a -> App a-}
|
-- if in remote mode, chroot actions to the SSHFS directory
|
||||||
|
-- forward ports declared by `usingPort`
|
||||||
|
liftIO :: IO a -> App a
|
||||||
liftIO action = do
|
liftIO action = do
|
||||||
state@(AppState { remoteMode }) <- get
|
state@(AppState { remoteMode, ports }) <- get
|
||||||
|
verbose $ "liftIO " ++ show remoteMode ++ ", " ++ show ports
|
||||||
|
|
||||||
case remoteMode of
|
case remoteMode of
|
||||||
Nothing -> ST.liftIO action
|
Nothing -> ST.liftIO action
|
||||||
|
|
||||||
Just (Address host port user, _) -> do
|
Just rm@(Address host port user, key) -> do
|
||||||
tmp <- ST.liftIO getTemporaryDirectory
|
tmp <- ST.liftIO getTemporaryDirectory
|
||||||
let path = tmp </> (user ++ "@" ++ host)
|
let path = tmp </> (user ++ "@" ++ host)
|
||||||
|
|
||||||
fchroot path $ ST.liftIO action
|
verbose $ "forwarding ports"
|
||||||
|
mapM_ (portForward rm) ports
|
||||||
|
|
||||||
|
verbose $ "chroot directory " ++ path
|
||||||
|
|
||||||
|
fchroot path $ ST.liftIO action
|
||||||
|
where
|
||||||
|
portForward (Address host port user, key) (source, destination) = do
|
||||||
|
let forward = source ++ ":" ++ host ++ ":" ++ destination
|
||||||
|
connection = user ++ "@" ++ host ++ (if null port then "" else " -p " ++ port)
|
||||||
|
identity = " -o IdentityFile=" ++ key
|
||||||
|
|
||||||
|
(_, _, _, handle) <- ST.liftIO $ runInteractiveCommand $ "ssh -L " ++ forward ++ " " ++ connection ++ identity
|
||||||
|
|
||||||
|
state@(AppState { processes }) <- get
|
||||||
|
put $ state { processes = handle:processes }
|
||||||
|
return ()
|
||||||
|
|
||||||
|
-- take and return a port from open port pool, forwarding the specified port to that port
|
||||||
|
-- this allows connections to ports on a remote server
|
||||||
|
usingPort :: String -> App String
|
||||||
|
usingPort port = do
|
||||||
|
state@(AppState { ports, remoteMode }) <- get
|
||||||
|
|
||||||
|
case remoteMode of
|
||||||
|
Nothing -> return port
|
||||||
|
Just _ -> do
|
||||||
|
available <- head <$> dropWhileM checkPort range
|
||||||
|
put $ state { ports = (available, port):ports }
|
||||||
|
return available
|
||||||
|
where
|
||||||
|
range = map show [8000..9999]
|
||||||
|
|
||||||
|
-- clear a port
|
||||||
|
clearPort :: String -> App ()
|
||||||
|
clearPort port = do
|
||||||
|
state@(AppState { ports, remoteMode }) <- get
|
||||||
|
let newPorts = filter ((/= port) . fst) ports
|
||||||
|
put $ state { ports = newPorts }
|
||||||
|
return ()
|
||||||
|
|
||||||
|
-- check whether a port is open or not
|
||||||
|
checkPort :: String -> App Bool
|
||||||
|
checkPort port = do
|
||||||
|
result <- execute "netstat" ["-an", "|", "grep", port] "" False
|
||||||
|
case result of
|
||||||
|
Left _ -> return False
|
||||||
|
Right output ->
|
||||||
|
if (not . null) output then
|
||||||
|
return True
|
||||||
|
else
|
||||||
|
return False
|
||||||
|
|
||||||
|
-- generates a string in format `<key><delimiter><value>\n`
|
||||||
|
-- e.g. |keyvalue [("first", "line"), ("second", "one")] "="| outputs "first=line\nsecond=one"
|
||||||
keyvalue :: [(String, String)] -> String -> String
|
keyvalue :: [(String, String)] -> String -> String
|
||||||
keyvalue ((a, b):xs) delimit = a ++ delimit ++ b ++ "\n" ++ keyvalue xs delimit
|
keyvalue ((a, b):xs) delimit = a ++ delimit ++ b ++ "\n" ++ keyvalue xs delimit
|
||||||
keyvalue [] _ = ""
|
keyvalue [] _ = ""
|
||||||
|
|
||||||
|
-- parse a `<key><delimiter><value>` string into a list of (key, value) pairs
|
||||||
parseKeyValue :: String -> Char -> [(String, String)]
|
parseKeyValue :: String -> Char -> [(String, String)]
|
||||||
parseKeyValue text delimit = map parsePair (lines text)
|
parseKeyValue text delimit = map parsePair (lines text)
|
||||||
where
|
where
|
||||||
@ -80,6 +141,7 @@ module System.Serverman.Utils ( App (..)
|
|||||||
(key, value) = splitAt delimitIndex line
|
(key, value) = splitAt delimitIndex line
|
||||||
in (key, tail value)
|
in (key, tail value)
|
||||||
|
|
||||||
|
-- split string at character
|
||||||
splitAtElem :: String -> Char -> [String]
|
splitAtElem :: String -> Char -> [String]
|
||||||
splitAtElem "" _ = []
|
splitAtElem "" _ = []
|
||||||
splitAtElem str char =
|
splitAtElem str char =
|
||||||
@ -91,21 +153,27 @@ module System.Serverman.Utils ( App (..)
|
|||||||
where
|
where
|
||||||
charIndex = char `elemIndex` str
|
charIndex = char `elemIndex` str
|
||||||
|
|
||||||
|
-- add a semicolon to end of each line in string
|
||||||
semicolon :: String -> String
|
semicolon :: String -> String
|
||||||
semicolon text = unlines $ map (++ ";") (lines text)
|
semicolon text = unlines $ map (++ ";") (lines text)
|
||||||
|
|
||||||
|
-- create a block with the following format: `<name> {\n<content>\n}`
|
||||||
|
-- content is |indent|ed
|
||||||
block :: String -> String -> String
|
block :: String -> String -> String
|
||||||
block blockName content = blockName ++ " {\n" ++ indent content ++ "}"
|
block blockName content = blockName ++ " {\n" ++ indent content ++ "}"
|
||||||
|
|
||||||
|
-- alias for |intercalate ", "|
|
||||||
commas :: [String] -> String
|
commas :: [String] -> String
|
||||||
commas text = intercalate ", " text
|
commas = intercalate ", "
|
||||||
|
|
||||||
|
-- execute an action if a path is missing
|
||||||
execIfMissing :: (Applicative f, Monad f, MonadIO f) => FilePath -> f () -> f ()
|
execIfMissing :: (Applicative f, Monad f, MonadIO f) => FilePath -> f () -> f ()
|
||||||
execIfMissing path action = do
|
execIfMissing path action = do
|
||||||
exists <- ST.liftIO $ doesPathExist path
|
exists <- ST.liftIO $ doesPathExist path
|
||||||
|
|
||||||
when (not exists) action
|
when (not exists) action
|
||||||
|
|
||||||
|
-- execute an action if a path exists
|
||||||
execIfExists :: (Applicative f, Monad f, MonadIO f) => FilePath -> f () -> f ()
|
execIfExists :: (Applicative f, Monad f, MonadIO f) => FilePath -> f () -> f ()
|
||||||
execIfExists path action = do
|
execIfExists path action = do
|
||||||
exists <- ST.liftIO $ doesPathExist path
|
exists <- ST.liftIO $ doesPathExist path
|
||||||
@ -118,6 +186,7 @@ module System.Serverman.Utils ( App (..)
|
|||||||
renameFileIfMissing :: FilePath -> String -> IO ()
|
renameFileIfMissing :: FilePath -> String -> IO ()
|
||||||
renameFileIfMissing path content = execIfMissing content (renameFile path content)
|
renameFileIfMissing path content = execIfMissing content (renameFile path content)
|
||||||
|
|
||||||
|
-- append a line after a specific string
|
||||||
appendAfter :: String -> String -> String -> String
|
appendAfter :: String -> String -> String -> String
|
||||||
appendAfter content after line =
|
appendAfter content after line =
|
||||||
let ls = lines content
|
let ls = lines content
|
||||||
@ -125,9 +194,11 @@ module System.Serverman.Utils ( App (..)
|
|||||||
|
|
||||||
in unlines appended
|
in unlines appended
|
||||||
|
|
||||||
|
-- indent all lines forward using \t
|
||||||
indent :: String -> String
|
indent :: String -> String
|
||||||
indent s = unlines $ map ("\t" ++) (lines s)
|
indent s = unlines $ map ("\t" ++) (lines s)
|
||||||
|
|
||||||
|
-- put single quotes around a text
|
||||||
quote :: String -> String
|
quote :: String -> String
|
||||||
quote input = "'" ++ input ++ "'"
|
quote input = "'" ++ input ++ "'"
|
||||||
|
|
||||||
@ -142,38 +213,49 @@ module System.Serverman.Utils ( App (..)
|
|||||||
execute :: String -> [String] -> String -> Bool -> App (Either String String)
|
execute :: String -> [String] -> String -> Bool -> App (Either String String)
|
||||||
execute cmd args stdin logErrors = exec cmd args stdin Nothing logErrors
|
execute cmd args stdin logErrors = exec cmd args stdin Nothing logErrors
|
||||||
|
|
||||||
|
-- execute a command in operating system
|
||||||
|
-- if in remote mode, runs `execRemote`
|
||||||
exec :: String -> [String] -> String -> Maybe FilePath -> Bool -> App (Either String String)
|
exec :: String -> [String] -> String -> Maybe FilePath -> Bool -> App (Either String String)
|
||||||
exec cmd args stdin cwd logErrors = do
|
exec cmd args stdin cwd logErrors = do
|
||||||
|
verbose $ "exec: " ++ cmd ++ " " ++ show args
|
||||||
(AppState { remoteMode }) <- get
|
(AppState { remoteMode }) <- get
|
||||||
|
|
||||||
if isJust remoteMode then do
|
if isJust remoteMode then do
|
||||||
let (addr, key) = fromJust remoteMode
|
let (addr, key) = fromJust remoteMode
|
||||||
|
|
||||||
execRemote addr (Just key) (Just "serverman") "" cmd args stdin cwd logErrors
|
execRemote addr (Just key) (Just "serverman") "" cmd args stdin cwd logErrors
|
||||||
else liftIO $ do
|
else do
|
||||||
let command = escape $ cmd ++ " " ++ intercalate " " args
|
let command = escape $ cmd ++ " " ++ intercalate " " args
|
||||||
cp = (proc (escape cmd) (map escape args)) { cwd = cwd }
|
cp = (proc (escape cmd) (map escape args)) { cwd = cwd }
|
||||||
|
|
||||||
process <- async $ do
|
verbose $ "executing command " ++ command
|
||||||
result <- tryIOError $ readCreateProcessWithExitCode cp stdin
|
|
||||||
|
process <- liftedAsync $ do
|
||||||
|
result <- liftIO . tryIOError $ readCreateProcessWithExitCode cp stdin
|
||||||
|
verbose "command executed"
|
||||||
|
|
||||||
case result of
|
case result of
|
||||||
Right (ExitSuccess, stdout, _) -> return $ Right stdout
|
Right (ExitSuccess, stdout, _) -> do
|
||||||
|
verbose $ "command successful: " ++ stdout
|
||||||
|
return $ Right stdout
|
||||||
|
|
||||||
Right (ExitFailure code, stdout, stderr) -> do
|
Right (ExitFailure code, stdout, stderr) -> do
|
||||||
|
when (not logErrors) $ verbose $ "command failed: " ++ show code ++ ", stderr: " ++ stderr
|
||||||
when logErrors $ do
|
when logErrors $ do
|
||||||
putStrLn $ "exit code: " ++ show code
|
err command
|
||||||
putStrLn stdout
|
err $ "exit code: " ++ show code
|
||||||
putStrLn stderr
|
err stdout
|
||||||
putStrLn $ commandError command
|
err stderr
|
||||||
return $ Left stdout
|
return $ Left stdout
|
||||||
Left err -> do
|
Left e -> do
|
||||||
|
when (not logErrors) $ verbose $ "couldn't execute command: " ++ show e
|
||||||
when logErrors $ do
|
when logErrors $ do
|
||||||
putStrLn $ show err
|
err command
|
||||||
putStrLn $ commandError command
|
err $ show e
|
||||||
return $ Left (show err)
|
return $ Left (show e)
|
||||||
|
|
||||||
wait process
|
(result, _) <- liftIO $ wait process
|
||||||
|
return result
|
||||||
|
|
||||||
where
|
where
|
||||||
escape :: String -> String
|
escape :: String -> String
|
||||||
@ -181,37 +263,56 @@ module System.Serverman.Utils ( App (..)
|
|||||||
where
|
where
|
||||||
specialCharacters = ["$"]
|
specialCharacters = ["$"]
|
||||||
|
|
||||||
|
-- run a command on a server using SSH
|
||||||
execRemote :: Address -> Maybe String -> Maybe String -> String -> String -> [String] -> String -> Maybe String -> Bool -> App (Either String String)
|
execRemote :: Address -> Maybe String -> Maybe String -> String -> String -> [String] -> String -> Maybe String -> Bool -> App (Either String String)
|
||||||
execRemote addr@(Address host port user) maybeKey maybeUser password cmd args stdin cwd logErrors = do
|
execRemote addr@(Address host port user) maybeKey maybeUser password cmd args stdin cwd logErrors = do
|
||||||
tmp <- liftIO getTemporaryDirectory
|
tmp <- ST.liftIO getTemporaryDirectory
|
||||||
let passwordFile = tmp </> "pw"
|
let passwordFile = tmp </> "pw"
|
||||||
|
|
||||||
let userArgument = if isJust maybeUser then ["echo", password, "|", "sudo -S", "-u", fromJust maybeUser] else []
|
let userArgument = case maybeUser of
|
||||||
keyArgument = if isJust maybeKey then ["-o", "IdentityFile=" ++ fromJust maybeKey] ++ noPassword else noKey
|
Just user -> if (not . null) password then
|
||||||
|
["echo", password, "|", "sudo -S", "-u", user]
|
||||||
|
else
|
||||||
|
["sudo -u", user]
|
||||||
|
Nothing -> []
|
||||||
|
keyArgument = case maybeKey of
|
||||||
|
Just key ->
|
||||||
|
["-o", "IdentityFile=" ++ key] ++ noPassword
|
||||||
|
Nothing -> noKey
|
||||||
|
|
||||||
p = if null port then [] else ["-p", port]
|
p = if null port then [] else ["-p", port]
|
||||||
connection = takeWhile (/= ':') (show addr)
|
connection = takeWhile (/= ':') (show addr)
|
||||||
|
|
||||||
cumulated = p ++ keyArgument ++ options
|
cumulated = p ++ keyArgument ++ options
|
||||||
command = userArgument ++ ["sh -c \"", cmd] ++ args ++ ["\""]
|
command = userArgument ++ ["sh -c \"", cmd] ++ args ++ ["\""]
|
||||||
|
complete = "ssh" : (cumulated ++ [connection] ++ command)
|
||||||
|
|
||||||
(backupEnv, passwordFile) <- liftIO $ do
|
verbose $ "backing up environment variables"
|
||||||
backupEnv <- getEnvironment
|
backupEnv <- ST.liftIO getEnvironment
|
||||||
|
|
||||||
writeFile passwordFile $ "echo " ++ password
|
verbose $ "writing passwordFile for SSH " ++ passwordFile
|
||||||
setFileMode passwordFile accessModes
|
when (not . null $ password) $
|
||||||
setEnv "SSH_ASKPASS" passwordFile True
|
ST.liftIO $ do
|
||||||
|
writeFile passwordFile $ "echo " ++ password
|
||||||
return (backupEnv, passwordFile)
|
setFileMode passwordFile accessModes
|
||||||
|
setEnv "SSH_ASKPASS" passwordFile True
|
||||||
|
|
||||||
state <- get
|
state <- get
|
||||||
let (AppState { remoteMode = backup }) = state
|
let (AppState { remoteMode = backup }) = state
|
||||||
put $ state { remoteMode = Nothing }
|
put $ state { remoteMode = Nothing }
|
||||||
result <- exec "setsid" ("ssh" : cumulated ++ [connection] ++ command) stdin cwd logErrors
|
|
||||||
|
verbose $ "executing command in remote " ++ show complete
|
||||||
|
|
||||||
|
newEnv <- liftIO getEnvironment
|
||||||
|
verbose $ "env " ++ keyvalue newEnv "="
|
||||||
|
|
||||||
|
result <- exec "setsid" complete stdin cwd logErrors
|
||||||
put $ state { remoteMode = backup }
|
put $ state { remoteMode = backup }
|
||||||
|
|
||||||
liftIO $ do
|
verbose $ "reseting environment and deleting password file"
|
||||||
|
ST.liftIO $ do
|
||||||
setEnvironment backupEnv
|
setEnvironment backupEnv
|
||||||
removeFile passwordFile
|
execIfExists passwordFile $ removeFile passwordFile
|
||||||
|
|
||||||
return result
|
return result
|
||||||
where
|
where
|
||||||
@ -219,6 +320,7 @@ module System.Serverman.Utils ( App (..)
|
|||||||
noKey = ["-o", "PubkeyAuthentication=no", "-o", "PasswordAuthentication=yes"]
|
noKey = ["-o", "PubkeyAuthentication=no", "-o", "PasswordAuthentication=yes"]
|
||||||
options = ["-o", "StrictHostKeyChecking=no"]
|
options = ["-o", "StrictHostKeyChecking=no"]
|
||||||
|
|
||||||
|
-- replace in string
|
||||||
replace :: String -> String -> String -> String
|
replace :: String -> String -> String -> String
|
||||||
replace str replacable alt =
|
replace str replacable alt =
|
||||||
foldl' rep "" str
|
foldl' rep "" str
|
||||||
@ -232,11 +334,15 @@ module System.Serverman.Utils ( App (..)
|
|||||||
dropEnd n = reverse . drop n . reverse
|
dropEnd n = reverse . drop n . reverse
|
||||||
|
|
||||||
restartService :: String -> App (Either String String)
|
restartService :: String -> App (Either String String)
|
||||||
restartService service = executeRoot "systemctl" ["restart", service] "" True
|
restartService service = do
|
||||||
|
verbose $ "restarting service " ++ service
|
||||||
|
executeRoot "systemctl" ["restart", service] "" True
|
||||||
|
|
||||||
|
-- execute using sudo
|
||||||
executeRoot :: String -> [String] -> String -> Bool -> App (Either String String)
|
executeRoot :: String -> [String] -> String -> Bool -> App (Either String String)
|
||||||
executeRoot cmd args stdin logErrors = execute "sudo" (cmd:args) stdin logErrors
|
executeRoot cmd args stdin logErrors = execute "sudo" (cmd:args) stdin logErrors
|
||||||
|
|
||||||
|
-- read password from user input (don't show the input)
|
||||||
getPassword :: IO String
|
getPassword :: IO String
|
||||||
getPassword = do
|
getPassword = do
|
||||||
tc <- getTerminalAttributes stdInput
|
tc <- getTerminalAttributes stdInput
|
||||||
@ -245,5 +351,10 @@ module System.Serverman.Utils ( App (..)
|
|||||||
setTerminalAttributes stdInput tc Immediately
|
setTerminalAttributes stdInput tc Immediately
|
||||||
return password
|
return password
|
||||||
|
|
||||||
liftedAsync :: MonadBaseControl IO m => m a -> m (Async (StM m a))
|
-- make tabularized help string
|
||||||
liftedAsync m = liftBaseWith $ \runInIO -> async (runInIO m)
|
mkHelp :: String -> [(String, String)] -> String
|
||||||
|
mkHelp name entries = name ++ "\n" ++
|
||||||
|
indent (keyvalue tabularized " ")
|
||||||
|
where
|
||||||
|
maxKey = maximum $ map (length . fst) entries
|
||||||
|
tabularized = map (\(key, value) -> (key ++ (replicate (maxKey - length key + 1) ' '), value)) entries
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
module System.Term ( initialize ) where
|
module System.Term ( initialize ) where
|
||||||
import qualified System.Serverman as S
|
import qualified System.Serverman as S
|
||||||
|
import System.Serverman.Log
|
||||||
|
|
||||||
import System.Environment
|
import System.Environment
|
||||||
import System.Directory
|
import System.Directory
|
||||||
import System.Exit
|
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
@ -15,57 +15,116 @@ module System.Term ( initialize ) where
|
|||||||
import Data.Default.Class
|
import Data.Default.Class
|
||||||
import System.FilePath
|
import System.FilePath
|
||||||
import Data.List
|
import Data.List
|
||||||
|
import System.Process
|
||||||
|
|
||||||
import System.Serverman.Utils hiding (liftIO)
|
import System.Serverman.Utils hiding (liftIO)
|
||||||
import System.Serverman.Actions.Repository
|
import System.Serverman.Actions.Repository
|
||||||
|
|
||||||
initialize = do
|
initialize = do
|
||||||
|
-- read arguments
|
||||||
args <- getArgs
|
args <- getArgs
|
||||||
|
|
||||||
dir <- liftIO $ getAppUserDataDirectory "serverman"
|
dir <- getAppUserDataDirectory "serverman"
|
||||||
let path = dir </> "repository"
|
|
||||||
|
|
||||||
|
-- parse parameters
|
||||||
let params = parseParams args
|
let params = parseParams args
|
||||||
liftIO $ print params
|
isHelp = or $ map (`elem` args) ["help", "--help", "-h", "-?"]
|
||||||
|
|
||||||
-- Fetch repository first
|
-- Fetch repository first
|
||||||
S.runApp $ do
|
S.runApp $ do
|
||||||
|
when (verboseM params) $ do
|
||||||
|
state <- get
|
||||||
|
put $ state { verboseMode = True }
|
||||||
|
verbose "verbose mode on"
|
||||||
|
|
||||||
|
verbose $ show params
|
||||||
|
|
||||||
|
-- fetch repository if running for the first time, set state
|
||||||
S.run (S.fetchRepository)
|
S.run (S.fetchRepository)
|
||||||
|
|
||||||
|
-- detect local operating system
|
||||||
S.run (S.detectOS)
|
S.run (S.detectOS)
|
||||||
|
|
||||||
state@(S.AppState { S.repository }) <- get
|
state@(S.AppState { S.repository }) <- get
|
||||||
put $ state { arguments = rest params }
|
put $ state { arguments = rest params, helpArg = isHelp }
|
||||||
|
|
||||||
case params of
|
case params of
|
||||||
(Params { listServices = True }) -> liftIO $ do
|
-- list services in repository
|
||||||
mapM_ print repository
|
(Params { listServices = True }) -> do
|
||||||
|
mapM_ (write . show) repository
|
||||||
|
|
||||||
|
-- install a service
|
||||||
p@(Params { install = Just service }) -> do
|
p@(Params { install = Just service }) -> do
|
||||||
|
verbose $ "preparing to install " ++ service
|
||||||
ms <- findService service
|
ms <- findService service
|
||||||
case ms of
|
case ms of
|
||||||
Just s -> handleRemote p $ S.install s
|
Just s -> handleRemote p $ S.install s
|
||||||
Nothing -> liftIO $ putStrLn $ "service not found: " ++ service
|
Nothing -> die $ "service not found: " ++ service
|
||||||
|
|
||||||
|
p@(Params { update = True }) -> S.run (S.updateRepository)
|
||||||
|
|
||||||
|
p@(Params { manage = Just (act, service) }) -> do
|
||||||
|
verbose $ "preparing to " ++ show act ++ " " ++ service
|
||||||
|
ms <- findService service
|
||||||
|
case ms of
|
||||||
|
Just s -> do
|
||||||
|
case act of
|
||||||
|
Start ->
|
||||||
|
handleRemote p $ S.start s
|
||||||
|
Stop ->
|
||||||
|
handleRemote p $ S.stop s
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
die $ "could not find any service matching " ++ service
|
||||||
|
|
||||||
|
-- install and call a service
|
||||||
p@(Params { rest = (x:xs), remote }) -> do
|
p@(Params { rest = (x:xs), remote }) -> do
|
||||||
case x of
|
case x of
|
||||||
(service, Nothing) -> do
|
(service, Nothing) -> do
|
||||||
|
verbose $ "preparing to call " ++ service
|
||||||
|
|
||||||
ms <- findService service
|
ms <- findService service
|
||||||
case ms of
|
case ms of
|
||||||
Just s -> do
|
Just s -> do
|
||||||
handleRemote p $ S.install s
|
when (not isHelp) $ do
|
||||||
|
handleRemote p $ S.install s
|
||||||
|
|
||||||
S.run $ S.call s remote
|
S.run $ S.call s remote
|
||||||
|
|
||||||
Nothing -> liftIO $ putStrLn $ "could not find any service matching " ++ service
|
Nothing -> do
|
||||||
_ -> liftIO $ putStrLn $ "could not understand your input"
|
if isHelp then
|
||||||
|
servermanHelp
|
||||||
|
else
|
||||||
|
die $ "could not find any service matching " ++ service
|
||||||
|
_ -> servermanHelp
|
||||||
|
|
||||||
{-S.run (S.call (head repository) [])-}
|
-- after the program is done, terminate remaining processes
|
||||||
|
(S.AppState { S.processes }) <- get
|
||||||
|
mapM_ (liftIO . terminateProcess) processes
|
||||||
|
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
where
|
where
|
||||||
|
-- if remote mode is set, read the file and run the action
|
||||||
|
-- on servers, otherwise run action locally
|
||||||
handleRemote (Params { remote = Just file }) action = do
|
handleRemote (Params { remote = Just file }) action = do
|
||||||
list <- liftIO $ map read . lines <$> readFile file
|
list <- liftIO $ map read . lines <$> readFile file
|
||||||
S.run (S.remote list action)
|
S.run (S.remote list action)
|
||||||
handleRemote (Params { remote = Nothing }) action = S.run action
|
handleRemote (Params { remote = Nothing }) action = S.run action
|
||||||
|
|
||||||
|
servermanHelp = do
|
||||||
|
write "serverman [--options] [command/service] [--service-options]"
|
||||||
|
|
||||||
|
write $ mkHelp "commands"
|
||||||
|
[ ("install <service>", "install a service")
|
||||||
|
, ("repository list", "list services")
|
||||||
|
, ("repository update", "update repository")
|
||||||
|
, ("service start <service>", "start the service")
|
||||||
|
, ("service stop <service>", "stop the service")
|
||||||
|
, ("--remote <file>", "run in remote mode: takes a path to a file containing username@ip:port lines")]
|
||||||
|
|
||||||
|
write "to learn about a service's options, run |serverman <service> --help|"
|
||||||
|
|
||||||
|
|
||||||
data Manage = Start | Stop deriving (Eq, Show)
|
data Manage = Start | Stop deriving (Eq, Show)
|
||||||
data Params = Params { listServices :: Bool
|
data Params = Params { listServices :: Bool
|
||||||
@ -73,9 +132,19 @@ module System.Term ( initialize ) where
|
|||||||
, manage :: Maybe (Manage, String)
|
, manage :: Maybe (Manage, String)
|
||||||
, update :: Bool
|
, update :: Bool
|
||||||
, remote :: Maybe FilePath
|
, remote :: Maybe FilePath
|
||||||
, help :: Bool
|
|
||||||
, rest :: [(String, Maybe String)]
|
, rest :: [(String, Maybe String)]
|
||||||
} deriving (Show)
|
, verboseM :: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
instance Show Params where
|
||||||
|
show (Params { listServices, install, manage, update, remote, rest, verboseM }) =
|
||||||
|
keyvalue [ ("list-services", show listServices)
|
||||||
|
, ("install", show install)
|
||||||
|
, ("manage", show manage)
|
||||||
|
, ("update", show update)
|
||||||
|
, ("remote", show remote)
|
||||||
|
, ("rest", show rest)
|
||||||
|
, ("verbose", show verboseM)] ": "
|
||||||
|
|
||||||
instance Default Params where
|
instance Default Params where
|
||||||
def = Params { listServices = False
|
def = Params { listServices = False
|
||||||
@ -83,8 +152,8 @@ module System.Term ( initialize ) where
|
|||||||
, manage = Nothing
|
, manage = Nothing
|
||||||
, remote = Nothing
|
, remote = Nothing
|
||||||
, update = False
|
, update = False
|
||||||
, help = False
|
|
||||||
, rest = []
|
, rest = []
|
||||||
|
, verboseM = False
|
||||||
}
|
}
|
||||||
|
|
||||||
parseParams :: [String] -> Params
|
parseParams :: [String] -> Params
|
||||||
@ -94,9 +163,7 @@ module System.Term ( initialize ) where
|
|||||||
parseParams ("service":"stop":s:xs) = (parseParams xs) { manage = Just (Stop, s) }
|
parseParams ("service":"stop":s:xs) = (parseParams xs) { manage = Just (Stop, s) }
|
||||||
parseParams ("install":s:xs) = (parseParams xs) { install = Just s }
|
parseParams ("install":s:xs) = (parseParams xs) { install = Just s }
|
||||||
parseParams ("--remote":s:xs) = (parseParams xs) { remote = Just s }
|
parseParams ("--remote":s:xs) = (parseParams xs) { remote = Just s }
|
||||||
parseParams ("--help":xs) = (parseParams xs) { help = True }
|
parseParams ("--verbose":xs) = (parseParams xs) { verboseM = True }
|
||||||
parseParams ("-h":xs) = (parseParams xs) { help = True }
|
|
||||||
parseParams [] = def { help = True }
|
|
||||||
parseParams x = def { rest = toPairs x }
|
parseParams x = def { rest = toPairs x }
|
||||||
where
|
where
|
||||||
toPairs [] = []
|
toPairs [] = []
|
||||||
|
@ -2,6 +2,7 @@ flags: {}
|
|||||||
extra-package-dbs: []
|
extra-package-dbs: []
|
||||||
packages:
|
packages:
|
||||||
- '.'
|
- '.'
|
||||||
|
- '../termcolors'
|
||||||
extra-deps:
|
extra-deps:
|
||||||
- concurrent-extra-0.7.0.10
|
- concurrent-extra-0.7.0.10
|
||||||
- stack-1.3.2
|
- stack-1.3.2
|
||||||
|
4
test/Utils.hs
Normal file
4
test/Utils.hs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import System.Serverman.Utils
|
||||||
|
import Test.QuickCheck
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user