+{-# LANGUAGE
+ DoAndIfThenElse
+ , RecordWildCards
+ , ScopedTypeVariables
+ , UnicodeSyntax
+ #-}
module Network.HTTP.Lucu.ResponseWriter
- ( responseWriter -- Handle -> InteractionQueue -> IO ()
+ ( responseWriter
)
where
-
-import qualified Data.ByteString.Lazy.Char8 as B
-import Data.ByteString.Lazy.Char8 (ByteString)
-import Control.Concurrent.STM
-import Control.Exception
-import Control.Monad
-import Data.Maybe
+import qualified Blaze.ByteString.Builder.HTTP as BB
+import Control.Applicative
+import Control.Concurrent
+import Control.Concurrent.STM
+import Control.Exception
+import Control.Monad
+import qualified Data.Ascii as A
+import Data.Monoid.Unicode
import qualified Data.Sequence as S
-import Data.Sequence (Seq, ViewR(..))
-import Network.HTTP.Lucu.Interaction
-import Network.HTTP.Lucu.Response
-import Prelude hiding (catch)
-import System.IO
-
-import Debug.Trace
+import Data.Sequence (ViewR(..))
+import Network.HTTP.Lucu.Config
+import Network.HTTP.Lucu.HandleLike
+import Network.HTTP.Lucu.HttpVersion
+import Network.HTTP.Lucu.Interaction
+import Network.HTTP.Lucu.Response
+import Prelude.Unicode
+import System.IO (hPutStrLn, stderr)
+import System.IO.Error
+data Context h
+ = Context {
+ cConfig ∷ !Config
+ , cHandle ∷ !h
+ , cQueue ∷ !InteractionQueue
+ , cReader ∷ !ThreadId
+ }
-responseWriter :: Handle -> InteractionQueue -> IO ()
-responseWriter h tQueue
- = catch awaitSomethingToWrite $ \ exc
- -> case exc of
- IOException _ -> return ()
- _ -> print exc
+responseWriter ∷ HandleLike h ⇒ Config → h → InteractionQueue → ThreadId → IO ()
+responseWriter cnf h tQueue readerTID
+ = awaitSomethingToWrite (Context cnf h tQueue readerTID)
+ `catches`
+ [ Handler handleIOE
+ , Handler handleAsyncE
+ , Handler handleBIOS
+ , Handler handleOthers
+ ]
where
- awaitSomethingToWrite :: IO ()
- awaitSomethingToWrite
- = do action
- <- atomically $
- do -- キューが空でなくなるまで待つ
- queue <- readTVar tQueue
- when (S.null queue)
- retry
- let _ :> itr = S.viewr queue
-
- -- GettingBody 状態にあり、Continue が期待され
- -- てゐて、それがまだ送信前なのであれば、
- -- Continue を送信する。
- state <- readTVar (itrState itr)
-
- if state == GettingBody then
- writeContinueIfNecessary itr
- else
- if state >= DecidingBody then
- writeHeaderOrBodyIfNecessary itr
- else
- retry
- action
-
- writeContinueIfNecessary :: Interaction -> STM (IO ())
- writeContinueIfNecessary itr
- = do expectedContinue <- readTVar (itrExpectedContinue itr)
- if expectedContinue then
-
- do wroteContinue <- readTVar $ itrWroteContinue itr
- if wroteContinue then
- -- 既に Continue を書込み濟
- retry
+ handleIOE ∷ IOException → IO ()
+ handleIOE e
+ | isIllegalOperation e
+ = return () -- EPIPE: should be ignored at all.
+ | otherwise
+ = terminate e
+
+ handleAsyncE ∷ AsyncException → IO ()
+ handleAsyncE ThreadKilled = terminate'
+ handleAsyncE e = terminate e
+
+ handleBIOS ∷ BlockedIndefinitelyOnSTM → IO ()
+ handleBIOS = terminate
+
+ handleOthers ∷ SomeException → IO ()
+ handleOthers = terminate
+
+ terminate ∷ Exception e ⇒ e → IO ()
+ terminate e
+ = do hPutStrLn stderr "requestWriter caught an exception:"
+ hPutStrLn stderr (show $ toException e)
+ terminate'
+
+ terminate' ∷ IO ()
+ terminate' = hClose h
+
+awaitSomethingToWrite ∷ HandleLike h ⇒ Context h → IO ()
+awaitSomethingToWrite ctx@(Context {..})
+ = join $
+ atomically $
+ do queue ← readTVar cQueue
+ case S.viewr queue of
+ EmptyR → retry
+ queue' :> itr → do writeTVar cQueue queue'
+ return $ writeSomething ctx itr
+
+writeSomething ∷ HandleLike h ⇒ Context h → SomeInteraction → IO ()
+writeSomething ctx itr
+ = let writer = writeResponseForNI ctx <$> fromInteraction itr <|>
+ writeResponseForSEI ctx <$> fromInteraction itr <|>
+ writeResponseForSYI ctx <$> fromInteraction itr
+ in
+ case writer of
+ Just f → f
+ Nothing → fail "Internal error: unknown interaction type"
+
+writeResponseForNI ∷ HandleLike h
+ ⇒ Context h
+ → NormalInteraction
+ → IO ()
+writeResponseForNI = writeContinueIfNeeded
+
+writeContinueIfNeeded ∷ HandleLike h
+ ⇒ Context h
+ → NormalInteraction
+ → IO ()
+writeContinueIfNeeded ctx@(Context {..}) ni@(NI {..})
+ = do isNeeded ← atomically $ readTMVar niSendContinue
+ when isNeeded
+ $ do let cont = Response {
+ resVersion = HttpVersion 1 1
+ , resStatus = Continue
+ , resHeaders = (∅)
+ }
+ hPutBuilder cHandle $ A.toBuilder $ printResponse cont
+ hFlush cHandle
+ writeHeader ctx ni
+
+writeHeader ∷ HandleLike h
+ ⇒ Context h
+ → NormalInteraction
+ → IO ()
+writeHeader ctx@(Context {..}) ni@(NI {..})
+ = do res ← atomically $
+ do state ← readTVar niState
+ if state ≥ SendingBody then
+ readTVar niResponse
+ else
+ retry -- Too early to write header fields.
+ hPutBuilder cHandle $ A.toBuilder $ printResponse res
+ hFlush cHandle
+ writeBodyIfNeeded ctx ni
+
+writeBodyIfNeeded ∷ HandleLike h
+ ⇒ Context h
+ → NormalInteraction
+ → IO ()
+writeBodyIfNeeded ctx ni@(NI {..})
+ = join $
+ atomically $
+ do willDiscardBody ← readTVar niWillDiscardBody
+ if willDiscardBody then
+ return $ discardBody ctx ni
+ else
+ if niWillChunkBody then
+ return $ writeChunkedBody ctx ni
+ else
+ return $ writeNonChunkedBody ctx ni
+
+discardBody ∷ HandleLike h
+ ⇒ Context h
+ → NormalInteraction
+ → IO ()
+discardBody ctx ni@(NI {..})
+ = join $
+ atomically $
+ do chunk ← tryTakeTMVar niBodyToSend
+ case chunk of
+ Just _ → return $ discardBody ctx ni
+ Nothing → do state ← readTVar niState
+ if state ≡ Done then
+ return $ finalize ctx ni
+ else
+ retry
+
+writeChunkedBody ∷ HandleLike h
+ ⇒ Context h
+ → NormalInteraction
+ → IO ()
+writeChunkedBody ctx@(Context {..}) ni@(NI {..})
+ = join $
+ atomically $
+ do chunk ← tryTakeTMVar niBodyToSend
+ case chunk of
+ Just b → return $
+ do hPutBuilder cHandle $ BB.chunkedTransferEncoding b
+ hFlush cHandle
+ writeChunkedBody ctx ni
+ Nothing → do state ← readTVar niState
+ if state ≡ Done then
+ return $
+ do hPutBuilder cHandle BB.chunkedTransferTerminator
+ hFlush cHandle
+ finalize ctx ni
else
- return $ writeContinue itr
- else
- retry
-
- writeHeaderOrBodyIfNecessary :: Interaction -> STM (IO ())
- writeHeaderOrBodyIfNecessary itr
- -- DecidingBody 以降の状態にあり、まだヘッダを出力する前であ
- -- れば、ヘッダを出力する。ヘッダ出力後であり、bodyToSend が
- -- 空でなければ、それを出力する。空である時は、もし状態が
- -- Done であれば後処理をする。
- = do wroteHeader <- readTVar (itrWroteHeader itr)
-
- if not wroteHeader then
- return $ writeHeader itr
- else
- do bodyToSend <- readTVar (itrBodyToSend itr)
-
- if B.null bodyToSend then
- do state <- readTVar (itrState itr)
-
- if state == Done then
- return $ finalize itr
- else
- retry
+ retry
+
+writeNonChunkedBody ∷ HandleLike h
+ ⇒ Context h
+ → NormalInteraction
+ → IO ()
+writeNonChunkedBody ctx@(Context {..}) ni@(NI {..})
+ = join $
+ atomically $
+ do chunk ← tryTakeTMVar niBodyToSend
+ case chunk of
+ Just b → return $
+ do hPutBuilder cHandle b
+ hFlush cHandle
+ writeNonChunkedBody ctx ni
+ Nothing → do state ← readTVar niState
+ if state ≡ Done then
+ return $ finalize ctx ni
else
- return $ writeBodyChunk itr
-
- writeContinue :: Interaction -> IO ()
- writeContinue itr = fail "FIXME: not implemented"
-
- writeHeader :: Interaction -> IO ()
- writeHeader itr
- = do res <- atomically $ do writeTVar (itrWroteHeader itr) True
- readTVar (itrResponse itr)
- hPutResponse h (fromJust res)
- hFlush h
- awaitSomethingToWrite
-
- writeBodyChunk :: Interaction -> IO ()
- writeBodyChunk itr = fail "FIXME: not implemented"
-
- finishBodyChunk :: Interaction -> IO ()
- finishBodyChunk itr = return () -- FIXME: not implemented
-
- finalize :: Interaction -> IO ()
- finalize itr
- = do finishBodyChunk itr
- willClose <- atomically $ do queue <- readTVar tQueue
-
- let (remaining :> _) = S.viewr queue
- writeTVar tQueue remaining
-
- readTVar $ itrWillClose itr
- if willClose then
- hClose h
- else
- awaitSomethingToWrite
+ retry
+
+finalize ∷ HandleLike h ⇒ Context h → NormalInteraction → IO ()
+finalize ctx@(Context {..}) (NI {..})
+ = join $
+ atomically $
+ do willClose ← readTVar niWillClose
+ sentContinue ← takeTMVar niSendContinue
+ return $
+ if needToClose willClose sentContinue then
+ -- The RequestReader is probably blocking on
+ -- hWaitForInput so we have to kill it before closing
+ -- the socket. THINKME: Couldn't that somehow be
+ -- avoided?
+ do killThread cReader
+ hClose cHandle
+ else
+ awaitSomethingToWrite ctx
+ where
+ needToClose ∷ Bool → Bool → Bool
+ needToClose willClose sentContinue
+ -- Explicitly instructed to close the connection.
+ | willClose = True
+ -- We've sent both "HTTP/1.1 100 Continue" and a final
+ -- response, so nothing prevents our connection from keeping
+ -- alive.
+ | sentContinue = False
+ -- We've got "Expect: 100-continue" but have sent a final
+ -- response without sending "HTTP/1.1 100
+ -- Continue". According to the RFC 2616 (HTTP/1.1), it is
+ -- undecidable whether the client will send us its
+ -- (rejected) request body OR start a completely new request
+ -- in this situation. So the only possible thing to do is to
+ -- brutally shutdown the connection.
+ | niExpectedContinue = True
+ -- The client didn't expect 100-continue so we haven't sent
+ -- one. No need to do anything special.
+ | otherwise = False
+
+writeResponseForSEI ∷ HandleLike h
+ ⇒ Context h
+ → SemanticallyInvalidInteraction
+ → IO ()
+writeResponseForSEI ctx@(Context {..}) (SEI {..})
+ = do hPutBuilder cHandle $ A.toBuilder $ printResponse seiResponse
+ unless seiWillDiscardBody $
+ if seiWillChunkBody then
+ do hPutBuilder cHandle $ BB.chunkedTransferEncoding seiBodyToSend
+ hPutBuilder cHandle BB.chunkedTransferTerminator
+ else
+ hPutBuilder cHandle seiBodyToSend
+ hFlush cHandle
+ if seiWillClose ∨ seiExpectedContinue then
+ do killThread cReader
+ hClose cHandle
+ else
+ awaitSomethingToWrite ctx
+
+writeResponseForSYI ∷ HandleLike h
+ ⇒ Context h
+ → SyntacticallyInvalidInteraction
+ → IO ()
+writeResponseForSYI (Context {..}) (SYI {..})
+ = do hPutBuilder cHandle $ A.toBuilder $ printResponse syiResponse
+ hPutBuilder cHandle syiBodyToSend
+ hFlush cHandle
+ killThread cReader
+ hClose cHandle