packages feed

kibro 0.1 → 0.3

raw patch · 5 files changed

+872/−225 lines, 5 filesdep +regexprdep +strictdep +unixdep −HDBCdep −HDBC-sqlite3dep −validatePVP ok

version bump matches the API change (PVP)

Dependencies added: regexpr, strict, unix

Dependencies removed: HDBC, HDBC-sqlite3, validate

API changes (from Hackage documentation)

- Kibro: con :: KibroSt c -> c
- Kibro: data KibroT m c a
- Kibro: getParams :: (IConnection c) => Kibro c [String]
- Kibro: gets :: (MonadState s m) => (s -> a) -> m a
- Kibro: instance (IConnection c) => MonadCGI (KibroT IO c)
- Kibro: instance (Monad m) => Functor (KibroT m c)
- Kibro: instance (Monad m) => Monad (KibroT m c)
- Kibro: instance (Monad m) => MonadState (KibroSt c) (KibroT m c)
- Kibro: instance (Monad m, IConnection c) => Applicative (KibroT m c)
- Kibro: instance (MonadIO m) => MonadIO (KibroT m c)
- Kibro: io :: (MonadIO m) => IO a -> m a
- Kibro: kibro :: (IConnection c) => IO c -> PageList c -> IO ()
- Kibro: outputHtml :: (HTML a, IConnection c) => a -> Kibro c CGIResult
- Kibro: readSessDef :: (IConnection c, Read a) => String -> a -> KibroT IO c a
- Kibro: type Kibro c a = KibroT IO c a
- Kibro: type Page c = Kibro c CGIResult
- Kibro: type PageList c = [(String, Page c)]
- Kibro.DB: commit :: (IConnection c) => Kibro c ()
- Kibro.DB: exec :: (IConnection c) => String -> [SqlValue] -> Kibro c Integer
- Kibro.DB: query :: (IConnection c) => String -> [SqlValue] -> Kibro c [[SqlValue]]
- Kibro.DB.Sqlite3: data Connection :: *
- Kibro.DB.Sqlite3: db :: String -> IO Connection
+ Kibro: getURIMatch :: Kibro v MatchResult
+ Kibro: getValue :: Kibro v v
+ Kibro: instance (Monad m) => Applicative (KibroT m v)
+ Kibro: instance (Monad m) => Functor (KibroT m v)
+ Kibro: instance (Monad m) => Monad (KibroT m v)
+ Kibro: instance (Monad m) => MonadState (KibroSt v) (KibroT m v)
+ Kibro: instance (MonadIO m) => MonadIO (KibroT m v)
+ Kibro: instance Eq Session
+ Kibro: instance MonadCGI (KibroT IO v)
+ Kibro: instance Show Session
+ Kibro: modifyRSess :: (Read a, Show a) => String -> (a -> a) -> Kibro v (Maybe a)
+ Kibro: modifyRSessDef :: (Read a, Show a) => String -> (a -> a) -> a -> Kibro v a
+ Kibro: modifySess :: String -> (String -> String) -> Kibro v (Maybe String)
+ Kibro: modifySessDef :: String -> (String -> String) -> String -> Kibro v String
+ Kibro: startKibro :: [PageAssign ()] -> IO ()
+ Kibro: startKibro' :: v -> (IO () -> IO ThreadId) -> [PageAssign v] -> IO ()
+ Kibro: type PageAssign v = (String, Page v)
- Kibro: deleteSess :: (IConnection c) => String -> Kibro c ()
+ Kibro: deleteSess :: String -> Kibro v ()
- Kibro: getInputDef :: (MonadCGI m) => String -> String -> m String
+ Kibro: getInputDef :: String -> String -> Kibro v String
- Kibro: getSess :: (IConnection c) => String -> Kibro c (Maybe String)
+ Kibro: getSess :: String -> Kibro v (Maybe String)
- Kibro: getSessDef :: (IConnection c) => String -> String -> KibroT IO c String
+ Kibro: getSessDef :: String -> String -> Kibro v String
- Kibro: putSess :: (IConnection c) => String -> String -> Kibro c ()
+ Kibro: putSess :: String -> String -> Kibro v ()
- Kibro: readInputDef :: (MonadCGI m, Read a) => String -> a -> m a
+ Kibro: readInputDef :: String -> String -> Kibro v String
- Kibro: readSess :: (IConnection c, Read a) => String -> Kibro c (Maybe a)
+ Kibro: readSess :: (Read a) => String -> Kibro v (Maybe a)
- Kibro: writeSess :: (IConnection c, Show a) => String -> a -> Kibro c ()
+ Kibro: writeSess :: (Show a) => String -> a -> Kibro v ()

Files

executable/Main.hs view
@@ -1,44 +1,635 @@+{-# 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 Data.Maybe-import Data.List-import System.FilePath-import Data.Validate-import Control.Applicative--import Data.Lighttpd--import New-import Help-import Build-import Start-import Restart-import Stop-import Configure-import Clean+import Text.Regex -import Utils+------------------------------------------------------------------------------+-- Main start-up functions +-- | Wrapper for start-up in main executable main :: IO () main = do-  args <- getArgs-  case args of-    (cmd:opts) -> runCmd cmd opts-    _          -> showHelp+  installHandler sigPIPE Ignore Nothing+  getArgs >>= tryCommand -runCmd :: String -> [String] -> IO ()-runCmd cmd xs = fromMaybe showHelp $ lookup cmd commands >>= return . ($ xs)+-- | 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 -commands :: [(String, [String] -> IO ())]-commands = [("new",new)-           ,("build",build)-           ,("start",start)-           ,("restart",restart)-           ,("stop",stop)-           ,("refresh",refresh)-           ,("configure",configure)-           ,("clean",clean)]+-- | 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 ()
kibro.cabal view
@@ -1,5 +1,5 @@ name:          kibro-version:       0.1+version:       0.3 synopsis:      Web development framework. description:   Web development framework. category:      Web@@ -12,12 +12,12 @@ cabal-Version: >= 1.2  library-   build-depends: base,HDBC,mtl,regex-compat,random,safe,xhtml,containers,fastcgi,cgi,HDBC-sqlite3,data-default-   exposed-modules: Kibro,Kibro.DB,Kibro.DB.Sqlite3+   build-depends: base,mtl,regex-compat,regexpr,random,safe,xhtml,containers,fastcgi,cgi,data-default+   exposed-modules: Kibro    hs-source-dirs: library/    GHC-Prof-options: -auto-all  executable kibro   main-is: Main.hs-  build-depends: base,regex-compat,directory,filepath,haskell98,validate,process+  build-depends: base,filepath,directory,haskell98,process,strict,unix   hs-source-dirs: executable/
library/Kibro.hs view
@@ -1,198 +1,297 @@ {-# LANGUAGE GeneralizedNewtypeDeriving, FlexibleInstances #-}-module Kibro -    (-      module Network.CGI-    , kibro-    , Kibro, KibroT-    , Page-    , PageList-    , gets-    , con-    , getParams-    , getSess-    , readSess-    , putSess-    , writeSess-    , getSessDef++module Kibro+    ( -- * Start Kibro+      startKibro+    , startKibro'+      -- * Value which the Kibro monad holds+    , getValue+      -- * Input utilities+    , getURIMatch     , getInputDef-    , readSessDef     , readInputDef+      -- * Session utilities+    , getSess+    , putSess     , deleteSess-    , outputHtml-    , (<<$)-    , io+    , modifySess+    , getSessDef+    , modifySessDef+    , readSess+    , writeSess+    , modifyRSess+    , modifyRSessDef+      -- * HTML utilities     , stylesheet-    )-    where+    , (<<$)+    , PageAssign+      -- * Module re-exports+    , module Network.CGI)+ where -import Network.CGI hiding (Html)-import Network.FastCGI-import Network.CGI.Monad-import qualified Data.Map as M+import Control.Exception+import Control.Applicative+import Control.Arrow+import Control.Monad.Reader import Control.Monad.State-import Database.HDBC import Control.Concurrent-import Text.Regex-import Data.Maybe import Data.List-import System.Random+import Data.Map (Map)+import Data.Maybe+import qualified Data.Map as M+import Foreign.Marshal.Alloc+import Foreign.Storable+import Network.FastCGI+import Network.CGI+import Network.CGI.Monad import Safe+import System.IO+import System.Random+import Text.RegexPR import Text.XHtml.Strict-import Control.Applicative-import Control.Arrow-import Data.Function ---------------------------------------------------------------------------------- Main Kibro entry point---+---------------------------------------------------------------------------+-- Server start up -kibro :: IConnection c => IO c -> PageList c -> IO ()-kibro getDb ps = do-  ss  <- newMVar M.empty-  ids <- newMVar [0..]-  runFastCGIConcurrent' forkOS 1000 (handleErrors (kibroMain getDb ss ids ps))+-- | Same as startKibro', but with value as () and uses forkIO to fork+startKibro :: [PageAssign ()] -> IO ()+startKibro = startKibro' () forkOS --- TODO: use applicative on mvar-kibroMain :: IConnection c => (IO c) -> SessionsVar -> IdsVar -> PageList c -> CGI CGIResult-kibroMain getDb ssvar ids ps = do-  -- Connect to the database for this session-  db <- io $ getDb-  -- Get or create a new session-  (id,session) <- getSession ids ssvar-  -- Get the page to run-  (params,page) <- pageMatch ps <$> fromMaybe "" <$> getVar "REQUEST_URI"-  -- Run a page, returning a new session and result-  (session',result) <- runKibro page (KibroSt db session params)-  -- Update the session-  io $ modifyMVar_ ssvar (return . M.insert id session')-  --  ...-  return result+-- | Start Kibro FastCGI server+startKibro' :: v                       -- ^ The value to be passed to pages+            -> (IO () -> IO ThreadId)  -- ^ How to fork threads+            -> [PageAssign v]          -- ^ Page list of (regex,page action)+            -> IO ()+startKibro' value fork pages = do+  ids <- genIds+  state <- newMVar (ids,M.empty)+  let cgiMain = handleErrors $ runReaderT (kibroCGIMain value pages) state+  runFastCGIConcurrent' fork 1000 cgiMain -pageMatch :: IConnection c => PageList c -> String -> ([String],Page c)-pageMatch ps path = extract $ look $ map (first match) ps where-    match regex = matchRegex (mkRegex regex) path-    look = find (isJust . fst)-    extract = maybe ([],notFound) (first fromJust)+----------------------------------------------------------------------------+-- Page request handler -runKibro :: IConnection c => Kibro c CGIResult -> KibroSt c -> CGI (Session,CGIResult)+-- | Main CGI action for Kibro+kibroCGIMain :: v -> [PageAssign v] -> Manager CGIResult+kibroCGIMain value ps = do+  var <- ask+  (_,sessions) <- liftIO $ readMVar var+  (params,page) <- lift $ pageMatch ps <$> fromMaybe "" <$> getVar "REQUEST_URI"+  session <- getSession+  (session',result) <- lift $ runKibro page (KibroSt session params value)+  updateSession session'+  return result++-- | Run a Kibro action, returning the new session and result+runKibro :: Kibro v CGIResult -> KibroSt v -> CGI (Session,CGIResult) runKibro p st = evalStateT (unKibro (getSess p)) st where     getSess a = do r <- a                    ss <- gets session                    return (ss,r) -notFound :: IConnection c => Page c+-- | Match a uri against a regex, returning the parameters from the regex+--   and the page action+pageMatch :: [PageAssign v] -> String -> (MatchResult,Page v)+pageMatch ps path = extract $ look $ map (first match) ps where+    match regex = matchRegexPR regex path+    look = find (isJust . fst)+    extract = maybe (undefined,notFound) (first fromJust)++-- | Simple 404 page+notFound :: Kibro v CGIResult notFound = getVar "REQUEST_URI" >>= outputNotFound . fromMaybe "" -type PageList c = [(String, Page c)]-type Page c = Kibro c CGIResult+---------------------------------------------------------------------------+-- Session manager monad -------------------------------------------------------------------------------+-- | Manager monad+type Manager = ReaderT SessionState (CGIT IO)+-- | List of (regular expression,page action) pairs+type PageAssign v = (String,Page v)+-- | Page action+type Page v = Kibro v CGIResult+-- | Session state; session ids and associated data+type SessionState = MVar ([Integer],Map Integer Session)+-- | Browser instance session+data Session = Session+    { sessId     :: Integer+    , sessValues :: Map String String+    } deriving (Eq,Show)++---------------------------------------------------------------------------+-- Kibro monad++type Kibro = KibroT IO++-- | A state containing the current session and a database connection.+data KibroSt v = KibroSt { session    :: Session+                         , match      :: MatchResult +                         , kibroValue :: v }++type MatchResult = ((String, (String, String)), [(Int, String)])++newtype KibroT m v a = Kibro { unKibro :: (StateT (KibroSt v) (CGIT m) a) }+    deriving (Monad, MonadIO, MonadState (KibroSt v), Functor)++instance Monad m => Applicative (KibroT m v) where+    pure = return+    (<*>) = ap++instance MonadCGI (KibroT IO v) where+    cgiAddHeader n v = Kibro $ lift $ cgiAddHeader n v+    cgiGet x = Kibro $ lift $ cgiGet x++--------------------------------------------------------------------------- -- Sessions----getSession :: IdsVar -> SessionsVar -> CGI (SessID,Session)-getSession ids ssvar = readCookie "sid" >>= maybe new fromId where-    fromId id = do ss <- io $ readMVar ssvar-                   maybe new (\s -> return (id,s)) (M.lookup id ss)-    new = newSession ids ssvar -newSession :: IdsVar -> SessionsVar -> CGI (SessID,Session)-newSession ids ssvar = do-  id <- newId ids-  io $ modifyMVar ssvar $ \m ->-      let e = M.insert "key" (show id) M.empty -      in return (M.insert id e m,(id,e))+-- | Update a session in the MVar+updateSession :: Session -> Manager ()+updateSession session@(Session id _) = do+  var <- ask+  liftIO $ modifyMVar_ var $ \(ids,sessions) -> do+    return (ids,M.insert id session sessions) -newId :: IdsVar -> CGI SessID-newId ids = do-  id <- io $ modifyMVar ids $ \(x:xs) -> do-              r <- getStdRandom random-              return (xs,(x,r))-  setCookie (newCookie "sid" $ show id) { cookiePath = Just "/" }-  return id+-- | Get the current session or make a new one and return it+getSession :: Manager Session+getSession = do+  var <- ask+  sId <- lift $ readCookie "sid"+  sess <- liftIO $ modifyMVar var $ \state@(ids,sessions) -> do+    case sId >>= flip M.lookup sessions of+      Just session -> return (state,(False,session))+      Nothing      -> let sId' = head ids+                          session = Session sId' M.empty+                          newState = (tail ids,M.insert sId' session sessions)+                      in return (newState,(True,session))+  when (fst sess) $ lift $ setCookie (newCookie "sid" $ show (sessId $ snd sess)) { cookiePath = Just "/" }+  return $ snd sess+                          -getSess :: IConnection c => String -> Kibro c (Maybe String)-getSess k = do s <- gets session-               return $ M.lookup k s+-- | Generate an infinite list of session ids+genIds :: IO [SessionId]+genIds = nub . randomRs (1,1000^(20::Int)) <$> betterStdGen -readSess :: (IConnection c, Read a) => String -> Kibro c (Maybe a)-readSess k = getSess k >>= return . (>>= readMay)+-- | A better random number generator which uses /dev/random when entropy+--   is available+betterStdGen :: IO StdGen+betterStdGen = alloca $ \p -> do+    h <- openBinaryFile "/dev/urandom" ReadMode+    hGetBuf h p $ sizeOf (undefined :: Int)+    hClose h+    mkStdGen <$> peek p -putSess :: (IConnection c) => String -> String -> Kibro c ()-putSess k v = do st <- get-                 let news = M.insert k v (session st)-                 put st { session = news }+-- | Session identity per browser instance+type SessionId = Integer -deleteSess :: (IConnection c) => String -> Kibro c ()-deleteSess k = do st <- get-                  let news = M.delete k (session st)-                  put st { session = news }+---------------------------------------------------------------------------+-- Kibro utilities -writeSess :: (IConnection c, Show a) => String -> a -> Kibro c ()-writeSess k v = putSess k (show v)-  -type SessionsVar = MVar Sessions-type IdsVar = MVar [Integer]+----------------------------------------+-- Value utilities ---------------------------------------------------------------------------------- The Kibro monad---+getValue :: Kibro v v+getValue = gets kibroValue --- | A state containing the current session and a database connection.-data KibroSt c = KibroSt-    { con     :: c-    , session :: Session-    , params  :: [String] }+----------------------------------------+-- URL utilities -type Session = M.Map String String-type Sessions = M.Map SessID Session-type SessID = (Integer,Int)+getURIMatch :: Kibro v MatchResult+getURIMatch = gets match -newtype KibroT m c a = Kibro { unKibro :: (StateT (KibroSt c) (CGIT m) a) }-    deriving (Monad, MonadIO, MonadState (KibroSt c), Functor)+----------------------------------------+-- Input utilities -instance (Monad m, IConnection c) => Applicative (KibroT m c) where-    pure = return-    (<*>) = ap+getInputDef :: String -> String -> Kibro v String+getInputDef k v = fromMaybe v <$> getInput k -instance IConnection c => MonadCGI (KibroT IO c) where-    cgiAddHeader n v = Kibro $ lift $ cgiAddHeader n v-    cgiGet x = Kibro $ lift $ cgiGet x+readInputDef :: String -> String -> Kibro v String+readInputDef k v = fromMaybe v <$> readInput k -type Kibro c a = KibroT IO c a+----------------------------------------+-- Session utilities ---------------------------------------------------------------------------------- Kibro utilities---+-- | Get session value or return default value+getSessDef :: String -> String -> Kibro v String+getSessDef k v = fromMaybe v <$> getSess k -getParams :: IConnection c => Kibro c [String] -getParams = gets params+-- | Read session value or return default value+readSessDef :: (Read a) => String -> a -> Kibro v a+readSessDef k v = fromMaybe v <$> readSess k -outputHtml :: (HTML a, IConnection c) => a -> Kibro c CGIResult-outputHtml = output . html . showHtmlFragment where-    html c = "<html>" ++ c ++ "</html>"+-- | Same as modifySessDef, but with Read/Show instance values+modifyRSessDef :: (Read a,Show a) => String -> (a -> a) -> a -> Kibro v a+modifyRSessDef k f v = do +  v <- readSessDef k v+  let v' = f v+  writeSess k v'+  return v' -defGet n = liftM (fromMaybe n)+-- | Same as modifySess, but with Read/Show instance values+modifyRSess :: (Read a,Show a) => String -> (a -> a) -> Kibro v (Maybe a)+modifyRSess k f = do+  v <- readSess k+  case v of+    Just v  -> do writeSess k $ f v+                  return $ Just v+    Nothing -> return Nothing --- Simply utilities+-- | Read a session value+readSess :: (Read a) => String -> Kibro v (Maybe a)+readSess k = getSess k >>= return . (>>= readMay) -readSessDef  s v = defGet v $ readSess s-readInputDef s v = defGet v $ readInput s-getSessDef   s v = defGet v $ getSess s-getInputDef  s v = defGet v $ getInput s+-- | Show a session value and put it+writeSess :: (Show a) => String -> a -> Kibro v ()+writeSess k v = putSess k (show v) --- HTML utilities+-- | Modify a session value, if the value does not exist, no change occurs+modifySess :: String -> (String -> String) -> Kibro v (Maybe String)+modifySess k f = do +  v <- getSess k+  case v of+    Nothing -> return Nothing+    Just v  -> do let v' = f v+                  putSess k v'+                  return $ Just v'++-- | Modify a session value, if the value does not exist, the default value+--   is modified and inserted+modifySessDef :: String -> (String -> String) -> String -> Kibro v String+modifySessDef k f v = do+  v <- getSessDef k v+  let v' = f v+  putSess k v'+  return $ v'++-- | Get a session value+getSess :: String -> Kibro v (Maybe String)+getSess k = do (Session _ s) <- gets session+               return $ M.lookup k s++-- | Put a session value+putSess :: String -> String -> Kibro v ()+putSess k v = sessMod (M.insert k v)++-- | Delete a session value+deleteSess :: String -> Kibro v ()+deleteSess = sessMod . M.delete++-- | Modify a session value+sessMod :: (Map String String -> Map String String) -> Kibro v ()+sessMod mod = do (Session id s) <- gets session+                 modify $ \state -> state { session = Session id (mod s) }++----------------------------------------+-- Some HTML utilities++-- | Simple stylesheet element+stylesheet :: String -> Html+stylesheet url = thelink ! [rel "stylesheet",thetype "text/css",href url] << ""++-- | <a href='x'>y</a>+ahref :: HTML a => String -> a -> HotLink ahref url = hotlink url . toHtml  -- | Nice operator for removing parentheses. (<<$) :: (HTML a) => (Html -> b) -> a -> b a <<$ b = a << b infixr 0 <<$--stylesheet url = thelink ! [rel "stylesheet",thetype "text/css",href url] << ""----------------------------------------------------------------------------------- Utilities for this file-----io :: MonadIO m => IO a -> m a-io = liftIO
− library/Kibro/DB.hs
@@ -1,35 +0,0 @@-module Kibro.DB-    (query,exec,Kibro.DB.commit-    ,module Database.HDBC)-    where--import Kibro hiding (handleErrors)-import Database.HDBC hiding (commit)-import qualified Database.HDBC as HDBC-import Control.Exception--runSql :: IConnection c -       => (c -> String -> [SqlValue] -> IO a) -- ^ SQL function.-       -> c                                   -- ^ SQL connection.-       -> String                              -- ^ Query.-       -> [SqlValue]                          -- ^ Parameters.-       -> Kibro c a                           -- ^ Result.-runSql f dbh s vs = io $ handleSql throwSqlError $ f dbh s vs where-    throwSqlError (SqlError _ _ e) =-        throwIO $ AssertionFailed $-                    "SQL error: " ++ show s ++ " " ++ show vs ++ e--query :: IConnection c => String -> [SqlValue] -> Kibro c [[SqlValue]]-query sql values = do-  dbh <- gets con-  runSql quickQuery' dbh sql values--exec :: IConnection c => String -> [SqlValue] -> Kibro c Integer-exec sql values = do-  dbh <- gets con-  runSql run dbh sql values--commit :: IConnection c => Kibro c ()-commit = do-  dbh <- gets con-  io $ HDBC.commit dbh
− library/Kibro/DB/Sqlite3.hs
@@ -1,8 +0,0 @@-module Kibro.DB.Sqlite3 -    (db,Connection)-    where--import Database.HDBC.Sqlite3--db :: String -> IO Connection-db = connectSqlite3