1{-# LANGUAGE ConstraintKinds #-}
2{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE FlexibleInstances #-}
5{-# LANGUAGE LambdaCase #-}
6{-# LANGUAGE MultiParamTypeClasses #-}
7{-# LANGUAGE OverloadedStrings #-}
8{-# LANGUAGE Rank2Types #-}
9{-# LANGUAGE ScopedTypeVariables #-}
10{-# LANGUAGE TypeFamilies #-}
11{-# LANGUAGE UndecidableInstances #-}
12{-# OPTIONS_GHC -fno-warn-orphans #-} -- Due to instance HasJS x (EventWriterT t w m)
13module Obelisk.Run where
15import Prelude hiding ((.), id)
17import Control.Category
18import Control.Concurrent
19import Control.Exception
20import Control.Lens ((%~), (^?), _Just, _Right)
21import qualified Data.Attoparsec.ByteString.Char8 as A
22import Data.ByteString (ByteString)
23import qualified Data.ByteString.Char8 as BSC
24import qualified Data.ByteString.Lazy.Char8 as BSLC
25import Data.Dependent.Sum (DSum (..))
26import Data.Functor.Identity
27import Data.Functor.Sum
28import Data.List (uncons)
30import qualified Data.Map as Map
32import Data.Semigroup ((<>))
33import Data.Streaming.Network (bindPortTCP)
34import Data.String (fromString)
35import Data.Text (Text)
36import qualified Data.Text as T
37import qualified Data.Text.Encoding as T
38import Language.Javascript.JSaddle.Run (syncPoint)
39import Language.Javascript.JSaddle.WebSockets
40import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
41import qualified Network.HTTP.ReverseProxy as RP
42import qualified Network.HTTP.Types as H
44import Network.Wai (Application)
45import qualified Network.Wai as W
46import Network.Wai.Handler.Warp
47import Network.Wai.Handler.Warp.Internal (settingsHost, settingsPort)
48import Network.WebSockets (ConnectionOptions)
49import Network.WebSockets.Connection (defaultConnectionOptions)
50import qualified Obelisk.Asset.Serve.Snap as Snap
52import Obelisk.Frontend
53import Obelisk.Route.Frontend
55import Snap.Core (Snap)
56import System.Environment
59import System.Exit (ExitCode(..))
61import qualified Text.URI as URI
66 :: Int -- ^ Port to run the backend
67 -> ([Text] -> Snap ()) -- ^ Static asset handler
68 -> Backend fullRoute frontendRoute -- ^ Backend
69 -> Frontend (R frontendRoute) -- ^ Frontend
71run port serveStaticAsset backend frontend = do
73 let handleBackendErr (e :: IOException) = hPutStrLn stderr $ "backend stopped; make a change to your code to reload - error " <> show e
74 --TODO: Use Obelisk.Backend.runBackend; this will require separating the checking and running phases
75 case checkEncoder $ _backend_routeEncoder backend of
76 Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
77 Right validFullEncoder -> do
78 publicConfigs <- getPublicConfigs
79 backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $ do
80 _backend_run backend $ \serveRoute -> do
81 runSnapWithCommandLineArgs $ do
82 getRouteWith validFullEncoder >>= \case
83 Identity r -> case r of
84 InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
85 InR obeliskRoute :=> Identity a ->
86 serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend publicConfigs $ obeliskRoute :/ a
87 let conf = defRunConfig { _runConfig_redirectPort = port }
88 runWidget conf publicConfigs frontend validFullEncoder `finally` killThread backendTid
90-- Convenience wrapper to handle path segments for 'Snap.serveAsset'
91runServeAsset :: FilePath -> [Text] -> Snap ()
92runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate "/"
94getConfigRoute :: Map Text ByteString -> Either Text URI
95getConfigRoute configs = case Map.lookup "common/route" configs of
97 let stripped = T.strip (T.decodeUtf8 r)
98 in case URI.mkURI stripped of
99 Just route -> Right route
100 Nothing -> Left $ "Couldn't parse route as URI; value read was: " <> T.pack (show stripped)
101 Nothing -> Left $ "Couldn't find config file common/route; it should contain the site's canonical root URI" <> T.pack (show $ Map.keys configs)
105 -> Map Text ByteString
106 -> Frontend (R route)
107 -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
109runWidget conf configs frontend validFullEncoder = do
110 uri <- either (fail . T.unpack) pure $ getConfigRoute configs
111 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
112 redirectHost = _runConfig_redirectHost conf
113 redirectPort = _runConfig_redirectPort conf
115 putStrLn $ "Frontend running on " <> T.unpack (URI.render uri)
116 settings = setBeforeMainLoop beforeMainLoop (setPort port (setTimeout 3600 defaultSettings))
118 (bindPortTCPRetry settings (logPortBindErr port) (_runConfig_retryTimeout conf))
121 man <- newManager defaultManagerSettings
122 app <- obeliskApp configs defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
123 runSettingsSocket settings skt app)
126 :: forall route backendRoute
127 . Map Text ByteString
129 -> Frontend (R route)
130 -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
134obeliskApp configs opts frontend validFullEncoder uri backend = do
135 let mode = FrontendMode
136 { _frontendMode_hydrate = True
137 , _frontendMode_adjustRoute = False
140 runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
142 jsaddlePath <- URI.mkPathPiece "jsaddle"
143 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
144 Right (jsaddleWarpRouteValidEncoder :: Encoder Identity (Either Text) (R JSaddleWarpRoute) PageName) <- return $ checkEncoder jsaddleWarpRouteEncoder
145 jsaddle <- jsaddleWithAppOr opts entryPoint $ \_ sendResponse -> sendResponse $ W.responseLBS H.status500 [("Content-Type", "text/plain")] "obeliskApp: jsaddle got a bad URL"
146 return $ \req sendResponse -> case tryDecode validFullEncoder (W.pathInfo req, mempty) of --TODO: Query strings
147 Identity r -> case r of
148 InR (ObeliskRoute_Resource ResourceRoute_JSaddleWarp) :=> Identity jsaddleRoute -> case jsaddleRoute of
149 JSaddleWarpRoute_JavaScript :/ () -> sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] $ jsaddleJs' (Just jsaddleUri) False
150 _ -> flip jsaddle sendResponse $ req
151 { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
153 InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
154 let cookies = maybe [] parseCookies $ lookup (fromString "Cookie") (W.requestHeaders req)
155 html <- renderJsaddleFrontend configs cookies (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
156 sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
157 _ -> backend req sendResponse
160 :: Map Text ByteString
166renderJsaddleFrontend configs cookies urlEnc r f =
167 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
168 jsaddlePreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "/jsaddle/jsaddle.js") blank
169 in renderFrontendHtml configs cookies urlEnc r f jsaddlePreload jsaddleScript
171-- | like 'bindPortTCP' but reconnects on exception
172bindPortTCPRetry :: Settings
173 -> (IOError -> IO ()) -- ^ Action to run the first time an exception is caught
176bindPortTCPRetry settings m n = catch (bindPortTCP (settingsPort settings) (settingsHost settings)) $ \(e :: IOError) -> do
178 threadDelay $ 1000000 * n
179 bindPortTCPRetry settings (\_ -> pure ()) n
181logPortBindErr :: Int -> IOError -> IO ()
182logPortBindErr p e = getProcessIdForPort p >>= \case
183 Nothing -> putStrLn $ "runWidget: " <> show e
184 Just pid -> putStrLn $ unwords [ "Port", show p, "is being used by process ID", show pid <> ".", "Please kill that process or change the port in config/common/route."]
186getProcessIdForPort :: Int -> IO (Maybe Int)
187getProcessIdForPort port = do
188 -- First check if 'ss' is available
189 (c, _, _) <- readProcessWithExitCode "which" ["ss"] mempty
192 xs <- lines <$> readProcess "ss" ["-lptn", "sport = " <> show port] mempty
194 Just (_, x:_) -> return $ A.maybeResult $ A.parse parseSsPid $ BSC.pack x
198parseSsPid :: A.Parser Int
200 _ <- A.count 5 $ A.takeWhile (not . A.isSpace) *> A.skipSpace
201 _ <- A.skipWhile (/= ':') >> A.string ":((" >> A.skipWhile (/= ',')
202 A.string ",pid=" *> A.decimal
204fallbackProxy :: ByteString -> Int -> Manager -> Application
205fallbackProxy host port = RP.waiProxyTo handleRequest RP.defaultOnExc
206 where handleRequest _req = return $ RP.WPRProxyDest $ RP.ProxyDest host port
208data RunConfig = RunConfig
209 { _runConfig_port :: Int
210 , _runConfig_redirectHost :: ByteString
211 , _runConfig_redirectPort :: Int
212 , _runConfig_retryTimeout :: Int -- seconds
215defRunConfig :: RunConfig
216defRunConfig = RunConfig
217 { _runConfig_port = 8000
218 , _runConfig_redirectHost = "127.0.0.1"
219 , _runConfig_redirectPort = 3001
220 , _runConfig_retryTimeout = 1
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list