carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1-- | @todo-haskell --socket <path> --db <path>@ on Carloku, or
2-- @todo-haskell --listen 127.0.0.1:<port> --db <path>@ locally.
3module Main (main) where
4
5import Control.Exception (bracket, catch, throwIO)
6import Control.Monad (forM_, unless)
7import Data.ByteString.Char8 qualified as B8
8import Data.Text qualified as T
9import Data.Word (Word8)
10import Network.Socket
11import Network.Wai.Handler.Warp
12import System.Environment (getArgs, lookupEnv)
13import System.Exit (exitWith, ExitCode (..))
14import System.IO (hPutStrLn, stderr)
15import System.IO.Error (isDoesNotExistError)
16import System.Posix.Files (removeLink)
17import System.Posix.Signals (Handler (CatchOnce), installHandler, sigINT, sigTERM)
18import Text.Read (readMaybe)
19
20import Todo.App
21import Todo.Db qualified as Db
22
23data Bind
24 = UnixSocket FilePath
25 | Loopback (Word8, Word8, Word8, Word8) PortNumber
26
27data Options = Options
28 { optBind :: Maybe Bind
29 , optDb :: Maybe FilePath
30 }
31
32main :: IO ()
33main = do
34 args <- getArgs
35 (target, dbPath) <- case parseArgs args (Options Nothing Nothing) of
36 Right (Options (Just b) (Just d)) -> pure (b, d)
37 Right _ -> usage "both --db and one of --socket or --listen are required"
38 Left err -> usage err
39 host <- lookupEnv "CARLOS_HOST"
40 let config = case target of
41 -- On Carloku the edge serves https://$CARLOS_HOST.
42 UnixSocket _ -> Config (B8.pack . ("https://" <>) <$> host) True
43 Loopback addr port -> Config (Just (B8.pack ("http://" <> showAddr addr port))) False
44 bracket (Db.open dbPath) Db.close $ \db ->
45 bracket (listenOn target) (cleanUp target) $ \sock -> do
46 hPutStrLn stderr ("todo-haskell " <> T.unpack version <> " listening on " <> describe target)
47 runSettingsSocket settings sock (app config db)
48 hPutStrLn stderr "todo-haskell stopped"
49
50-- | Warp closes the listening socket on SIGTERM or SIGINT, lets requests
51-- in flight finish (for up to five seconds), and then 'runSettingsSocket'
52-- returns, so 'main' closes the database and exits 0.
53settings :: Settings
54settings =
55 setInstallShutdownHandler onSignal
56 . setGracefulShutdownTimeout (Just 5)
57 $ defaultSettings
58 where
59 onSignal closeSocket =
60 forM_ [sigTERM, sigINT] $ \sig -> installHandler sig (CatchOnce closeSocket) Nothing
61
62listenOn :: Bind -> IO Socket
63listenOn = \case
64 UnixSocket path -> do
65 removeIfExists path
66 sock <- socket AF_UNIX Stream defaultProtocol
67 bind sock (SockAddrUnix path)
68 listen sock 1024
69 pure sock
70 Loopback addr port -> do
71 sock <- socket AF_INET Stream defaultProtocol
72 setSocketOption sock ReuseAddr 1
73 bind sock (SockAddrInet port (tupleToHostAddress addr))
74 listen sock 1024
75 pure sock
76
77cleanUp :: Bind -> Socket -> IO ()
78cleanUp target sock = do
79 close sock
80 case target of
81 UnixSocket path -> removeIfExists path
82 Loopback _ _ -> pure ()
83
84removeIfExists :: FilePath -> IO ()
85removeIfExists path = removeLink path `catch` \e -> unless (isDoesNotExistError e) (throwIO e)
86
87parseArgs :: [String] -> Options -> Either String Options
88parseArgs args o = case args of
89 [] -> Right o
90 "--socket" : path : rest -> parseArgs rest o {optBind = Just (UnixSocket path)}
91 "--listen" : addr : rest -> do
92 b <- parseListen addr
93 parseArgs rest o {optBind = Just b}
94 "--db" : path : rest -> parseArgs rest o {optDb = Just path}
95 flag : _ -> Left ("unexpected argument " <> show flag)
96
97-- | Only 127.0.0.1: this app never listens anywhere else.
98parseListen :: String -> Either String Bind
99parseListen s = case break (== ':') s of
100 ("127.0.0.1", ':' : portText)
101 | Just port <- readMaybe portText
102 , port > 0 && port < (65536 :: Int) ->
103 Right (Loopback (127, 0, 0, 1) (fromIntegral port))
104 _ -> Left ("--listen wants 127.0.0.1:<port>, not " <> show s)
105
106showAddr :: (Word8, Word8, Word8, Word8) -> PortNumber -> String
107showAddr (a, b, c, d) port = concat [show a, ".", show b, ".", show c, ".", show d, ":", show port]
108
109describe :: Bind -> String
110describe = \case
111 UnixSocket path -> path
112 Loopback addr port -> showAddr addr port
113
114usage :: String -> IO a
115usage err = do
116 hPutStrLn stderr ("todo-haskell: " <> err)
117 hPutStrLn stderr "usage: todo-haskell (--socket <path> | --listen 127.0.0.1:<port>) --db <path>"
118 exitWith (ExitFailure 2)
119