{-# LANGUAGE CPP, OverloadedStrings, UnicodeSyntax #-} module Network.Wai.Cli (module Network.Wai.Cli) where import Network.Wai (responseLBS, Application) import qualified Network.Wai.Handler.CGI as CGI import Network.Wai.Handler.Warp hiding (run) #ifdef WaiCliTLS import Network.Wai.Handler.WarpTLS #endif import Network.Wai.Middleware.RequestLogger (logStdoutDev) import Network.HTTP.Types (serviceUnavailable503) import Network.Socket.Activation import qualified Network.Socket as S import GHC.Conc (getNumCapabilities, forkIO) import System.Posix.Internals (setNonBlockingFD) import System.Posix.Signals (installHandler, sigTERM, Handler(CatchOnce)) import Data.Streaming.Network (bindPath) import System.Console.ANSI import Control.Concurrent.STM import Control.Monad import Control.Monad.Trans (liftIO) import Control.Exception (bracket) import Options data GracefulMode = ServeNormally | Serve503 data WaiOptions = WaiOptions { port ∷ Int , socket ∷ String , protocol ∷ String #ifdef WaiCliTLS , tlsKeyFile ∷ String , tlsCertFile ∷ String #endif , gracefulMode ∷ String , devlogging ∷ Maybe Bool } instance Options WaiOptions where defineOptions = pure WaiOptions <*> simpleOption "port" 3000 "The port the app should listen for connections on (for http)" <*> simpleOption "socket" "wai.sock" "The UNIX domain socket path the app should listen for connections on (for unix)" #ifdef WaiCliTLS <*> simpleOption "protocol" "http" "The protocol for the server. One of: http, http+tls, unix, unix+tls, activate, activate+tls, cgi" <*> simpleOption "tlskey" "" "Path to the TLS private key file for +tls protocols" <*> simpleOption "tlscert" "" "Path to the TLS certificate bundle file for +tls protocols" #else <*> simpleOption "protocol" "http" "The protocol for the server. One of: http, unix, activate, cgi" #endif <*> simpleOption "graceful" "serve-normally" "Graceful shutdown mode. One of: none, serve-normally, serve-503" <*> simpleOption "devlogging" Nothing "Whether development logging should be enabled" runActivated ∷ (Settings → S.Socket → Application → IO ()) → Settings → Application → IO () runActivated run warps app = do sockets ← getActivatedSockets case sockets of Just socks → void $ forM socks $ \sock → do setNonBlockingFD (S.fdSocket sock) True forkIO $ run warps sock app Nothing → putStrLn "No sockets to activate" runGraceful ∷ GracefulMode → (Settings → Application → IO ()) → Settings → Application → IO () runGraceful mode run warps app = do -- based on https://gist.github.com/NathanHowell/5435345 -- XXX: an option to stop accepting (need change stuff inside Warp?) shutdown ← newEmptyTMVarIO activeConnections ← newTVarIO (0 ∷ Int) _ ← installHandler sigTERM (CatchOnce $ atomically $ putTMVar shutdown ()) Nothing let warps' = setOnOpen (\_ → atomically (modifyTVar' activeConnections (+1)) >> return True) $ setOnClose (\_ → atomically (modifyTVar' activeConnections (subtract 1)) >> return ()) warps let app' = case mode of ServeNormally → app Serve503 → \req respond → do shouldRun ← liftIO . atomically $ isEmptyTMVar shutdown if shouldRun then app req respond else respond $ responseLBS serviceUnavailable503 [] "" void $ forkIO $ run warps' app' atomically $ do takeTMVar shutdown conns ← readTVar activeConnections when (conns /= 0) retry waiMain ∷ (WaiOptions → IO ()) → (WaiOptions → IO ()) → Application → IO () waiMain putListening putWelcome app = runCommand $ \opts _ → do #ifdef WaiCliTLS let tlss = tlsSettings (tlsCertFile opts) (tlsKeyFile opts) #endif let warps = setBeforeMainLoop (putListening opts) $ setPort (port opts) defaultSettings let app' = if devlogging opts == Just True then logStdoutDev app else app if protocol opts == "cgi" then CGI.run app' else do let run = case protocol opts of "http" → runSettings "unix" → \warps' app'' → bracket (bindPath $ socket opts) S.close (\sock → runSettingsSocket warps' sock app'') "activate" → runActivated runSettingsSocket #ifdef WaiCliTLS "http+tls" → runTLS tlss "unix+tls" → \warps' app'' → bracket (bindPath $ socket opts) S.close (\sock → runTLSSocket tlss warps' sock app'') "activate+tls" → runActivated (runTLSSocket tlss) #endif x → \_ _ → putStrLn $ "Unsupported protocol: " ++ x putWelcome opts case gracefulMode opts of "none" → run warps app' "serve-normally" → runGraceful ServeNormally run warps app' "serve-503" → runGraceful Serve503 run warps app' x → putStrLn $ "Unsupported graceful mode: " ++ x defPutListening ∷ WaiOptions → IO () defPutListening opts = getNumCapabilities >>= putMain where putMain cpus = reset "Running on " >> blue (protocol opts) >> putProto >> reset " with " >> green (show cpus ++ " CPUs") >> putStrLn "" putProto = case protocol opts of "http" → reset " port " >> boldMagenta (show $ port opts) "unix" → reset " socket " >> boldMagenta (show $ socket opts) "activate" → reset " activated socket" #ifdef SweetrollTLS "http+tls" → reset " (TLS) port " >> boldMagenta (show $ port opts) "unix+tls" → reset " (TLS) socket " >> boldMagenta (show $ socket opts) "activate+tls" → reset " (TLS) activated socket" #endif _ → setReset setReset = setSGR [ Reset ] boldMagenta x = setReset >> setSGR [ SetConsoleIntensity BoldIntensity, SetColor Foreground Vivid Magenta ] >> putStr x green x = setReset >> setSGR [ SetColor Foreground Dull Green ] >> putStr x blue x = setReset >> setSGR [ SetColor Foreground Dull Blue ] >> putStr x reset x = setReset >> putStr x defWaiMain ∷ Application → IO () defWaiMain = waiMain defPutListening (\_ → return ())