kibro-0.3: executable/Main.hs
{-# LANGUAGE FlexibleInstances #-}
------------------------------------------------------------------------------
-- Main Kibro executable
module Main where
import System.Posix.Signals (installHandler, sigPIPE, Handler(Ignore))
import Prelude hiding (catch)
import Control.Arrow hiding ((<+>))
import Control.Concurrent
import Control.Exception
import Control.Monad.Error
import Control.Monad.State
import Control.Monad.Reader
import Data.List
import Data.Maybe
import System
import System.IO
import qualified System.IO.Strict as SIO
import System.FilePath
import System.Directory
import System.Process
import Text.Printf
import Text.Regex
------------------------------------------------------------------------------
-- Main start-up functions
-- | Wrapper for start-up in main executable
main :: IO ()
main = do
installHandler sigPIPE Ignore Nothing
getArgs >>= tryCommand
-- | Tries the given command
tryCommand :: [String] -> IO ()
tryCommand [] = do name <- getProgName
error $ "no command given (try `" ++ name ++ " help')"
tryCommand (command:args) =
case find ((==command) . cmdName) commands of
Just cmd -> startCommand cmd args
Nothing -> error $ printf "No such command `%s'" command
-- | Starts the Kibro program's first command
startCommand :: Cmd -> [String] -> IO ()
startCommand cmd args = do
pwd <- getCurrentDirectory
name <- getProgName
let kibroSt = KibroSt { kibProject = Nothing
, kibProjDir = error "project directory not defined"
, kibCmd = cmd
, kibArgs = args
, kibAppName = name }
hSetBuffering stdout NoBuffering
status <- evalStateT (runErrorT (cmdAct cmd args)) kibroSt
either error (const $ return ()) status
------------------------------------------------------------------------------
-- Kibro commands
-- | Command list
commands :: [Cmd]
commands = [startCmd,stopCmd,buildCmd,refreshCmd
,restartCmd,configureCmd,newCmd,helpCmd]
----------------------------------------
-- `configure' command
configureCmd :: Cmd
configureCmd = Cmd { cmdName = "configure"
, cmdDesc = "Use this to rewrite config files (lighttpd.conf, ..)"
, cmdFlags = []
, cmdAct = action } where
action :: [String] -> Command ()
action _ = do initProject
configureProject
configureProject :: Command ()
configureProject = do
writeLightyConf
----------------------------------------
-- `refresh' command
refreshCmd :: Cmd
refreshCmd = Cmd { cmdName = "refresh"
, cmdDesc = "Rebuild source, if success, restart FastCGI/Lighttpd"
, cmdFlags = []
, cmdAct = action } where
action :: [String] -> Command ()
action _ = do initProject
cmdAct buildCmd []
stopFastCGI
spawnFCGI
----------------------------------------
-- `restart' command
restartCmd :: Cmd
restartCmd = Cmd { cmdName = "restart"
, cmdDesc = "Stop and start Lighttpd and/or FastCGI server"
, cmdFlags = []
, cmdAct = action } where
action :: [String] -> Command ()
action _ = do initProject
cmdAct stopCmd []
cmdAct startCmd []
----------------------------------------
-- `stop' command
stopCmd :: Cmd
stopCmd = Cmd { cmdName = "stop"
, cmdDesc = "Stop Lighttpd and/or FastCGI server"
, cmdFlags = []
, cmdAct = action } where
action :: [String] -> Command ()
action _ = do
initProject
stopProject
stopProject :: Command ()
stopProject = do stopFastCGI
stopLighttpd
stopLighttpd = onlyIfLighttpd $ stopDaemon "lighttpd" "Stopping Lighttpd ... "
stopFastCGI = stopDaemon "fastcgi" "Stopping FastCGI ... "
stopDaemon :: String -> String -> Command ()
stopDaemon name caption = do
dirs <- projectDirs
let lighty = fromJust (lookup name dirs)
pid = lighty </> (name ++ ".pid")
exists <- liftIO $ doesFileExist pid
if exists
then do liftIO $ putStrLn caption
endDaemon pid
else liftIO $ putStrLn $ "no such file " ++ pid
----------------------------------------
-- `start' command
startCmd :: Cmd
startCmd = Cmd { cmdName = "start"
, cmdDesc = "Start Lighttpd and FastCGI server"
, cmdFlags = []
, cmdAct = action } where
action :: [String] -> Command ()
action _ = do
initProject
ensureBuilt
startProject
startProject :: Command ()
startProject = do
spawnFCGI
startLighty
startLighty :: Command ()
startLighty = onlyIfLighttpd $ do
dirs <- projectDirs
let cmd = "lighttpd -f " ++ conf
lighty = fromJust (lookup "lighttpd" dirs)
conf = lighty </> "lighttpd.conf"
pid = lighty </> "lighttpd.pid"
performIfNotExists pid (Just "Lighttpd daemon already running.") $ do
liftIO $ putStrLn "Spawning Lighttpd daemon ... "
runShellCmd cmd
liftIO $ putStrLn "Lighttpd daemon started."
spawnFCGI :: Command ()
spawnFCGI = do
name <- projectName
dirs <- projectDirs
dir <- projectDir
out <- gets $ projOutDir . fromJust . kibProject
let cmd = printf "spawn-fcgi -f %s -s %s -P %s" fcgi sock pid
fpath = fromJust $ lookup "fastcgi" dirs
ppath = fromJust $ lookup "public" dirs
fcgi = dir </> out </> name ++ ".fcgi"
sock = fpath </> name ++ ".sock"
pid = fpath </> "fastcgi.pid"
performIfNotExists pid (Just "FastCGI daemon already running.") $ do
liftIO $ putStrLn "Spawning FastCGI daemon ... "
runShellCmd cmd
liftIO $ putStrLn "Done."
ensureBuilt :: Command ()
ensureBuilt = do
name <- projectName
dir <- projectDir
out <- gets $ projOutDir . fromJust . kibProject
let fcgi = dir </> out </> name ++ ".fcgi"
performIfNotExists fcgi Nothing $ do
liftIO $ putStrLn "Not yet built, building ..."
cmdAct buildCmd []
liftIO $ putStrLn "Finished building."
----------------------------------------
-- `help' command
-- | Display help about commands
helpCmd :: Cmd
helpCmd = Cmd { cmdName = "help"
, cmdDesc = "Help about commands (try `help <command>')"
, cmdFlags = []
, cmdAct = action } where
action :: [String] -> Command ()
action [] = cmdList
action (cmd:_) = cmdHelp cmd
-- | List all the commands
cmdList :: Command ()
cmdList = do
let longest = foldr1 max $ map (length . cmdName) commands
appName <- gets kibAppName
liftIO $ do
printf "Usage: %s COMMAND [FLAGS]\n\n" appName
putStrLn "Commands:"
mapM_ (showHelp longest) commands
putStrLn "\nTypical step for creating Kibro project:"
putStrLn " kibro new [PROJECT_NAME]"
where showHelp len (Cmd name desc _ _) = do
printf "%s %s\n" (fill ' ' len name) desc
-- | Show help for a command
cmdHelp :: String -> Command ()
cmdHelp command =
case find ((==command) . cmdName) commands of
Nothing -> cmdError $ printf "No such command `%s'" command
Just cmd -> do
appName <- gets kibAppName
liftIO $ do
let longest = foldr1 max $ map (length . fst) (cmdFlags cmd)
printf "Usage: %s %s [FLAGS]\n\n" appName command
printf "Flags for %s:\n" command
mapM_ (showFlag longest) (cmdFlags cmd)
where showFlag len (name,desc) = do
printf " --%s %s\n" (fill ' ' len name) desc
----------------------------------------
-- `build' command
-- | Build the current project
buildCmd :: Cmd
buildCmd = Cmd { cmdName = "build"
, cmdDesc = "Build the current project"
, cmdFlags = []
, cmdAct = action } where
action :: [String] -> Command ()
action _ = do
initProject
buildProject
buildProject :: Command ()
buildProject = do
dir <- projectDir
main <- projectMain
name <- projectName
let cmd = "ghc --make " ++ dir </> main ++ " -o public" </> fcgi ++ " -threaded"
fcgi = name ++ ".fcgi"
runShellCmd cmd
----------------------------------------
-- `new' command
-- | Create a new project
newCmd :: Cmd
newCmd = Cmd { cmdName = "new"
, cmdDesc = "Create a new project"
-- TODO
, cmdFlags = [("start","Start after creating")]
, cmdAct = action } where
action :: [String] -> Command ()
action [] = cmdError "`new' command needs a project name"
action (name:_) = do
case match regex name of
Nothing -> cmdError $ "invalid project name, should be " ++ regex
Just _ -> do newProject name; tryMakeProject
where regex = "^[a-z][a-z0-9_-]+$"
-- | Try to make a project
tryMakeProject :: Command ()
tryMakeProject = do
makeProjDir
writeDefMain
writeLightyConf
writeProjConfig
-- | Make the project directory structure
makeProjDir :: Command ()
makeProjDir = do
dir <- projectDir
dirs <- map snd `fmap` projectDirs
exists <- liftIO $ doesDirectoryExist dir
if exists
then do dir' <- liftIO $ makeRelativeToCurrentDirectory dir
cmdError $ "directory `" ++ dir' ++ "' already exists"
else liftIO $ do putStrLn "Creating directory structure ..."
mapM_ createDir dirs
putStrLn "Finished creating directory structure."
where createDir dir = do putStrLn $ " " ++ dir
createDirectoryIfMissing True dir
-- | Set the current project in the state
newProject :: String -> Command ()
newProject name = do
pwd <- liftIO $ getCurrentDirectory
let project = KibroProject { projName = name
, projDirs = defaultDirs
, projOutDir = "public"
, projMainIs = "src" </> "Main.hs"
, projLighttpd = True }
modify $ \s -> s { kibProjDir = Just $ pwd </> name
, kibProject = Just project }
-- | Write default Main.hs file
writeDefMain :: Command ()
writeDefMain = do
dir <- projectDir
main <- projectMain
liftIO $ do
putStr $ "Writing Main.hs ... "
writeFile (dir </> main) mainSrc
putStrLn "done."
-- | Write the lighttpd.conf file
writeLightyConf :: Command ()
writeLightyConf = onlyIfLighttpd $ do
dir <- projectDir
dirs <- projectDirs
conf <- lighttpdDotConf
case lookup "lighttpd" dirs of
Nothing -> cmdError "lighttpd config path not found in project config"
Just path -> do liftIO $ do
putStr $ "Writing lighttpd.conf ... "
writeFile (path </> "lighttpd.conf") conf
putStrLn "done."
writeCustom path
writeCustom :: FilePath -> Command ()
writeCustom path = onlyIfLighttpd $ liftIO $ do
let custom = path </> "custom.conf"
exists <- doesFileExist custom
putStr "Writing custom.conf ... "
if exists
then putStrLn "already exists, skipping."
else do writeFile custom customDotConf
putStrLn "done."
-- | Write the Kibro configuration to .kibro file
writeProjConfig :: Command ()
writeProjConfig = do
dir <- projectDir
proj <- fromJust `fmap` gets kibProject
let name = projName proj
liftIO $ do
putStr $ "Writing " ++ name ++ ".kibro ... "
writeFile (dir </> name ++ ".kibro") $ show proj
putStrLn "done."
-- | Initialise the Kibro project by reading the configuration file
initProject :: Command ()
initProject = do
proj <- gets kibProject
when (not $ isJust proj) $ do
pwd <- liftIO $ getCurrentDirectory
configs <- liftIO $ getDirectoryContents pwd
let config = filter (isJust . match "^[a-z][a-z0-9_-]+\\.kibro$") configs
case config of
[config] -> do liftIO $ putStr $ "Reading config file " ++ config ++ " ... "
config_ <- liftIO $ SIO.readFile config
let config' = read config_
modify $ \s -> s { kibProject = Just config'
, kibProjDir = Just pwd }
liftIO $ putStrLn "done."
-- TODO: --config option
[] -> cmdError "no kibro file found. try the `new' command"
_ -> cmdError "more than one kibro file found"
----------------
-- Command types
-- Throw an error, prefixing the command that threw it to the message
cmdError :: [Char] -> Command a
cmdError err = do
name <- gets $ cmdName . kibCmd
throwError $ name ++ ": " ++ err
data Cmd = Cmd
{ cmdName :: String
, cmdDesc :: String
, cmdFlags :: [(String,String)]
, cmdAct :: [String] -> Command ()
}
instance Show Cmd where
show (Cmd name desc _ _) = "Cmd { cmdName = " ++ show name ++
", cmdDesc = " ++ show desc ++ " }"
type Command = ErrorT String Kibro
----------------
-- Kibro types
-- | Kibro monad
type Kibro = StateT KibroSt IO
-- | Kibro running state
data KibroSt = KibroSt
{ kibProject :: Maybe KibroProject
, kibProjDir :: Maybe FilePath
, kibCmd :: Cmd
, kibArgs :: [String]
, kibAppName :: String
} deriving Show
-- Helper functions
projectMain = gets $ projMainIs . fromJust . kibProject
projectName = gets $ projName . fromJust . kibProject
projectDir = gets $ fromJust . kibProjDir
projectLighttpd = gets $ projLighttpd . fromJust . kibProject
onlyIfLighttpd :: Command () -> Command ()
onlyIfLighttpd m = do doIt <- projectLighttpd
when doIt m
projectDirs = do
dir <- projectDir
gets (map (second (dir </>)) . projDirs . fromJust . kibProject)
-- | Project type
data KibroProject = KibroProject
{ projName :: String
, projDirs :: [(String,FilePath)]
, projOutDir :: FilePath
, projMainIs :: FilePath
, projLighttpd :: Bool
} deriving (Eq,Show,Read)
-----------------------------------------------------------------------------
-- Default values
mainSrc :: String
mainSrc = "module Main where\n\
\\n\
\import Kibro\n\
\\n\
\main = startKibro pages\n\
\\n\
\pages = [(\".\", example)]\n\
\\n\
\example = output \"Change me! :-)\"\n"
lighttpdDotConf :: Command String
lighttpdDotConf = do
root <- projectDir
name <- projectName
dirs <- gets $ projDirs . fromJust . kibProject
let appDir = fromJust $ lookup "app" dirs
fastCGIDir = fromJust $ lookup "fastcgi" dirs
lightDir = fromJust $ lookup "lighttpd" dirs
pubDir = fromJust $ lookup "public" dirs
return $ showSettings
[("Do not modify this file. You should modify custom.conf" #)
,"var.k_base_dir" .=. dir root
,"var.k_app_dir" .=. var "k_base_dir" <+> dir appDir
,"var.k_fastcgi_dir" .=. var "k_base_dir" <+> dir fastCGIDir
,"var.k_lighttpd_dir" .=. var "k_base_dir" <+> dir lightDir
,"var.k_public_dir" .=. var "k_base_dir" <+> dir pubDir
,"var.k_fcgi_filename" .=. name ++ ".fcgi"
,"var.k_fcgi_path" .=. var "k_public_dir" <+> var "k_fcgi_filename"
,"var.k_socket_path" .=. var "k_fastcgi_dir" <+> name ++ ".sock"
,"var.k_error_log" .=. var "k_lighttpd_dir" <+> "error.log"
,"var.k_port" .=. (3000 :: Int)
,("This value must not be changed, Kibro depends on it for stopping" #)
,"server.pid-file" .=. var "k_lighttpd_dir" <+> "lighttpd.pid"
,include "custom.conf"]
where dir = init . (</> " ")
customDotConf :: String
customDotConf = showSettings
[("Only change these if you know what you are doing" #)
,"fastcgi.server" .=. [var "k_fcgi_path" .=>. [["socket" .=>. var "k_socket_path"]]]
,"server.error-handler-404" .=. var "k_fcgi_filename"
,"index-file.names" .=. [var "k_fcgi_filename"]
,("Shouldn't need to change these" #)
,"server.document-root" .=. var "k_public_dir"
,"server.errorlog" .=. var "k_error_log"
,("Feel free" #)
,"server.modules" .=. ["mod_rewrite","mod_redirect","mod_fastcgi"]
,"server.port" .=. var "k_port"
,"server.dir-listing" .=."enable"
,"dir-listing.encoding" .=."utf-8"
,"mimetype.assign" .=. ["" .=>. "text/plain"]]
-- | Default directory structure
defaultDirs :: [(String,FilePath)]
defaultDirs =
[("app","app")
,("lighttpd","app" </> "lighttpd")
,("fastcgi","app" </> "fastcgi")
,("public","public")
,("src","src")]
------------------------------------------------------------------------------
-- Lighttpd config
-- | Class for generalising the properties.
class LightyProperty a where toProp :: a -> Prop
-- | Necessary instances.
instance LightyProperty [[Char]] where toProp = List
instance LightyProperty [Char] where toProp = String
instance LightyProperty Int where toProp = Number
instance LightyProperty [Prop] where toProp = Props
instance LightyProperty [[Prop]] where toProp = Props . map Props
instance LightyProperty Prop where toProp = id
-- | A property.
data Prop = String String
| Number Int
| List [String]
| Assign Prop Prop
| Variable String
| Props [Prop]
| Concat Prop Prop
-- | A property "setting".
data LightyLine = Set String Prop | Comment String | Include String
instance Show Prop where
show (String s) = show s
show (Number n) = show n
show (List xs) = "(" ++ commasShow xs ++ ")" where
show (Assign p v) = show p ++ " => " ++ show v
show (Props ps) = "(" ++ commasShow ps ++ ")"
show (Variable v) = v
show (Concat p1 p2) = show p1 ++ " + " ++ show p2
instance Show LightyLine where
show (Set s p) = s ++ " = " ++ show p
show (Comment s) = "# " ++ s
show (Include s) = "include " ++ show s
-- | Print a list of settings in the lighttpd config format.
showSettings :: [LightyLine] -> String
showSettings = unlines . map show
commasShow :: Show a => [a] -> String
commasShow = commas . map show
---------------------------------------
-- Combinators
-- | "Set" a property.
(.=.) :: LightyProperty a => String -> a -> LightyLine
p .=. v = Set p (toProp v)
infixr 0 .=.
-- | "Assign" a value to something.
(.=>.) :: (LightyProperty a,LightyProperty b) => a -> b -> Prop
(.=>.) a b = Assign (toProp a) (toProp b)
infixr 0 .=>.
var = Variable
(<+>) :: (LightyProperty a,LightyProperty b) => a -> b -> Prop
a <+> b = Concat (toProp a) (toProp b)
infixr 1 <+>
(#) = Comment
include = Include
------------------------------------------------------------------------------
-- Generic utilities
-- Perform some action if a given file doesn't exist, otherwise print message
performIfNotExists :: FilePath -> Maybe String -> Command () -> Command ()
performIfNotExists path msg m = do
exists <- liftIO $ doesFileExist path
if not exists then m else maybe (return ()) (liftIO . putStrLn) msg
-- Run shell command
runShellCmd :: String -> Command ()
runShellCmd cmd = do
id <- liftIO $ runCommand cmd
status <- liftIO $ waitForProcess id
case status of
ExitSuccess -> return ()
ExitFailure _ -> cmdError $ "the following command failed:\n " ++ cmd
-- Stricter command runner
runShellCmd' :: String -> Command ()
runShellCmd' cmd = do
out <- liftIO $ run cmd ""
case out of
Right (ExitSuccess,err,_) | null err -> return ()
| otherwise -> fail err
Right (_,err,_) -> fail err
Left err -> fail err
where fail err = cmdError $ "there were errors with the \
\following command:\n " ++ cmd ++ "\n\n" ++
err
-- Silent runner
runShellCmdSilent :: String -> Command ()
runShellCmdSilent cmd = do
liftIO $ run cmd ""
return ()
-- Big-ass-but-stable process launcher
run :: String -> String -> IO (Either String (ExitCode,String,String))
run cmd input = do
pipe <- catch (Right `fmap` runInteractiveCommand cmd)
(const $ return $ Left "")
case pipe of
Right (inp,out,err,pid) -> do
catch (do hSetBuffering inp NoBuffering
hPutStr inp input
hClose inp
errv <- newEmptyMVar
outv <- newEmptyMVar
output <- hGetContents out
errput <- hGetContents err
forkIO $ evaluate (length output) >> putMVar outv ()
forkIO $ evaluate (length errput) >> putMVar errv ()
takeMVar errv
takeMVar outv
e <- catch (waitForProcess pid)
(const $ return $ ExitFailure 1)
return $ Right (e,errput,output))
(const $ return $ Left "Broken pipe")
_ -> return $ Left "Unable to launch process"
fill c len text = take len $ text ++ repeat c
match = matchRegex . mkRegex
commas = concat . intersperse ","
endDaemon pid = do runShellCmdSilent $ "kill `cat \"" ++ pid ++ "\"`"
runShellCmdSilent $ "rm \"" ++ pid ++ "\""
return ()