packages feed

hack2-handler-mongrel2-http-2011.6.25: src/Hack2/Handler/Mongrel2HTTP.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Hack2.Handler.Mongrel2HTTP 
(

  runWithConfig
, ServerConfig(..)
, Mongrel2.Handler(..)
) where

import Prelude ()
import Air.Env hiding (def, Default, log)

import Hack2
import Data.Default (def, Default)
import Data.ByteString.Char8 (ByteString, pack)
import qualified Data.ByteString.Char8 as B
import Data.Enumerator (Enumerator, Iteratee (..), ($$), joinI, run_, Enumeratee, Step, (=$), ($=), enumList, concatEnums)
import Blaze.ByteString.Builder (Builder, fromByteString, fromLazyByteString, toByteString)

import Data.Maybe (listToMaybe, fromMaybe, isJust, fromJust)
import Data.Map (toAscList, fromAscList)

import Control.Monad (when, forever)

import Hack2.Handler.Mongrel2.IO
import qualified Hack2.Handler.Mongrel2.Types as Mongrel2

import qualified Control.Exception as Exception
import System.IO (hPutStr, stderr)

import System.Posix.Signals
import Control.Concurrent

import qualified Data.Map as Map
import Blaze.Text.Int (integral)
import Data.Monoid
import qualified Data.Aeson as Aeson
import Safe (readMay, readDef)

import Hack2.Handler.Mongrel2.Utils

import Control.Concurrent.STM
import Data.Int
import qualified Data.Set as Set
import qualified Data.List as List
import qualified Prelude as P

buildSpace :: Builder
buildSpace = fromByteString " "

buildNewLine :: Builder
buildNewLine = fromByteString "\r\n"

make_response_enumerator :: Response -> Enumerator ByteString IO a
make_response_enumerator response = 
  concatEnums 
    [
      header_enum
    , response.body.unHackEnumerator
    ]
  where
    header_enum = enumList 1 - return - make_response_header - response

make_response_header :: Response -> ByteString
make_response_header response = 
  [
    fromByteString "HTTP/1.1"
  , buildSpace
  , integral - response.status
  , buildSpace
  , fromByteString - (statusReasonMap.Map.lookup (response.status) .fromMaybe "Unknown Status Code")
  
  , buildNewLine
  , mconcat - response.headers.map build_header
  , buildNewLine
  ]
  
  .mconcat
  .toByteString
  
  where
    build_header :: (ByteString, ByteString) -> Builder
    build_header (header, value) = 
      [
        fromByteString header
      , fromByteString ": "
      , fromByteString value
      , buildNewLine
      ]
      .mconcat

-- example json request string
-- {"PATH":"/","x-forwarded-for":"127.0.0.1","accept":"*/*","user-agent":"curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8l zlib/1.2.3","host":"127.0.0.1:6767","METHOD":"GET","VERSION":"HTTP/1.1","URI":"/","PATTERN":"/"}

-- {  requestMethod  :: RequestMethod
-- ,  scriptName     :: ByteString
-- ,  pathInfo       :: ByteString
-- ,  queryString    :: ByteString
-- ,  serverName     :: ByteString
-- ,  serverPort     :: Int
-- ,  httpHeaders    :: [(ByteString, ByteString)]
-- ,  hackVersion    :: (Int, Int, Int)
-- ,  hackUrlScheme  :: HackUrlScheme
-- ,  hackInput      :: HackEnumerator
-- ,  hackErrors     :: HackErrors
-- ,  hackHeaders    :: [(ByteString, ByteString)]



requestToEnv :: Mongrel2.Request -> Env
requestToEnv request = 
  let
    maybe_string (Aeson.String x) = Just - x
    maybe_string _ = Nothing
    
    _headers = request.Mongrel2.requestHeaders.jsonToList
    
    _host = _headers.lookup "host" .fromMaybe ""
    _query = _headers.lookup "QUERY" .fromMaybe ""
    
    _mongrel2_header_names = 
      
        [
          "PATH"
        , "host"
        , "METHOD"
        , "VERSION"
        ]
        
    _http_headers = _headers
    
    _input_enum :: (forall a . Enumerator ByteString IO a)
    _input_enum = enumList 1 [request.Mongrel2.requestBody]
    
  in
  def
    {
      requestMethod = ( _headers.lookup "METHOD" >>= B.unpack > readMay ).fromMaybe GET
    , pathInfo = request.Mongrel2.requestPath
    , queryString = _query
    , serverName = _host.B.takeWhile (is_not ':')
    , serverPort = _host.B.dropWhile (is_not ':') .B.dropWhile (is ':') .B.unpack.readDef 80
    , httpHeaders = _http_headers
    , hackInput = HackEnumerator _input_enum
    }
  



-- copied from snap-core
statusReasonMap :: Map.Map Int ByteString
statusReasonMap = Map.fromAscList [
        (100, "Continue"),
        (101, "Switching Protocols"),
        (200, "OK"),
        (201, "Created"),
        (202, "Accepted"),
        (203, "Non-Authoritative Information"),
        (204, "No Content"),
        (205, "Reset Content"),
        (206, "Partial Content"),
        (300, "Multiple Choices"),
        (301, "Moved Permanently"),
        (302, "Found"),
        (303, "See Other"),
        (304, "Not Modified"),
        (305, "Use Proxy"),
        (307, "Temporary Redirect"),
        (400, "Bad Request"),
        (401, "Unauthorized"),
        (402, "Payment Required"),
        (403, "Forbidden"),
        (404, "Not Found"),
        (405, "Method Not Allowed"),
        (406, "Not Acceptable"),
        (407, "Proxy Authentication Required"),
        (408, "Request Time-out"),
        (409, "Conflict"),
        (410, "Gone"),
        (411, "Length Required"),
        (412, "Precondition Failed"),
        (413, "Request Entity Too Large"),
        (414, "Request-URI Too Large"),
        (415, "Unsupported Media Type"),
        (416, "Requested range not satisfiable"),
        (417, "Expectation Failed"),
        (500, "Internal Server Error"),
        (501, "Not Implemented"),
        (502, "Bad Gateway"),
        (503, "Service Unavailable"),
        (504, "Gateway Time-out"),
        (505, "HTTP Version not supported")
    ]
    
    

--  

data ServerConfig = ServerConfig
  {
    handler :: Mongrel2.Handler
  , number_of_application_instances :: Int
  }

instance Default ServerConfig where
  def = ServerConfig
    {
      handler = def
    , number_of_application_instances = 10
    }


exit_handler :: MVar () -> IO ()
exit_handler exit_indicator = do
  log "Exiting..."
  putMVar exit_indicator ()



-- log :: String -> IO ()
-- log = const (return ())
  

runWithConfig :: ServerConfig -> Application -> IO ()
runWithConfig config app = do
    exit_indicator <- newEmptyMVar

    installHandler sigINT (Catch (exit_handler exit_indicator)) Nothing
    
    disconnected <- newTVarIO def
        
    threads <- [1..config.number_of_application_instances] .mapM (\instance_id -> 
        forkIO - server_safe_loop instance_id disconnected
      )
    
    fork - forever - clean_up_disconnected_client_ids disconnected
    
    log "taking exit_indicator"
    takeMVar exit_indicator
    log "done taking exit_indicator"

    
    -- log "killing threads ..."
    -- threads.mapM_ killThread
    -- 
    -- log "waiting threads termination ..."
    -- sleep 2
    -- 
    -- log "all threads killed"
    -- log "Mongrel2 main will exist"
    

    
    where
      server_safe_loop instance_id disconnected = do
        forever - do
          catch_all "ZeroMQ error:" - loop instance_id (config.handler) disconnected app

catch_all :: String -> IO () -> IO ()
catch_all message io_action = 
  Exception.catch io_action - \e ->
    log_error - message + " " + (e :: Exception.SomeException).show + "\n"

clean_up_disconnected_client_ids :: TVar (Set.Set Int64) -> IO ()
clean_up_disconnected_client_ids disconnected = do
  sleep 10
  
  atomically - do
    ids <- readTVar disconnected
    let set_length = ids.Set.size
    
        max_length = 10000
        
    when (set_length >= max_length) - do
      let new_ids = ids.to_list.List.sort.drop (set_length P.- max_length).Set.fromList
      writeTVar disconnected new_ids
    
  
loop :: Int -> Mongrel2.Handler -> TVar (Set.Set Int64) -> Application -> IO ()
loop instance_id _handler disconnected app = do

  withConnectedHandler _handler - \connected_handler -> do

    -- catch_all "Hack2 Application error:" - do
      
      log - "instance " + show instance_id + " waiting ..."
      request <- receiveRequest connected_handler
      
      log - "instance " + show instance_id + " handling ..."
      -- putStrLn - "instance " + show instance_id + " handling ..."


      let uuid = request.Mongrel2.requestUuid
          client_id = request.Mongrel2.requestClientId
          
          json_request = request.requestJsonBody
      
      case json_request of
        Nothing -> do
          app_response <- app - requestToEnv request

          let { resposne = 
            Mongrel2.Response
              {
                Mongrel2.responseUuid = uuid
              , Mongrel2.responseClientId = client_id
              , Mongrel2.responseBody = make_response_enumerator app_response
              }
            }

          sendResponse connected_handler disconnected resposne
        Just xs -> do
          case xs.lookup "type" of
            Just "disconnect" -> do
              log - "client " + show client_id + " disconnected"
              atomically - do
                _ids <- readTVar disconnected
                writeTVar disconnected - _ids.Set.insert client_id
            
            _ ->
              log_error - "Unknown json reponse: " + request.Mongrel2.requestBody.B.unpack