@@ -1,9 +1,13 @@
11{-# LANGUAGE DataKinds #-}
22{-# LANGUAGE DoAndIfThenElse #-}
3+{-# LANGUAGE GADTs #-}
34{-# LANGUAGE GeneralizedNewtypeDeriving #-}
45{-# LANGUAGE MultiParamTypeClasses #-}
6+{-# LANGUAGE OverloadedStrings #-}
7+{-# LANGUAGE PatternGuards #-}
58{-# LANGUAGE RecordWildCards #-}
69{-# LANGUAGE ScopedTypeVariables #-}
10+{-# LANGUAGE TemplateHaskell #-}
711{-# LANGUAGE TupleSections #-}
812
913module Main (main) where
@@ -11,10 +15,22 @@ module Main (main) where
1115import Prelude ()
1216import Prelude.Compat
1317
18+import Data.FileEmbed (embedStringFile)
1419import Data.Monoid ((<>))
20+import Data.String (IsString(..))
21+import Data.Text (Text, unpack)
22+import Data.Traversable (for)
1523import Data.Version (showVersion)
1624
17−import Control.Applicative (many)
25+import Control.Applicative (many, (<|>))
26+import Control.Concurrent (forkIO)
27+import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar,
28+ tryPutMVar)
29+import Control.Concurrent.STM (TVar, atomically, newTVarIO, writeTVar,
30+ readTVarIO,
31+ TChan, newBroadcastTChanIO, dupTChan,
32+ readTChan, writeTChan)
33+import Control.Exception (fromException)
1834import Control.Monad
1935import Control.Monad.IO.Class (liftIO)
2036import Control.Monad.Trans.Class
@@ -23,21 +39,33 @@ import Control.Monad.Trans.State.Strict (StateT, evalStateT)
2339import Control.Monad.Trans.Reader (ReaderT, runReaderT)
2440
2541import qualified Language.PureScript as P
42+import qualified Language.PureScript.Bundle as Bundle
2643import Language.PureScript.Interactive
2744
45+import Network.HTTP.Types.Header (hContentType, hCacheControl,
46+ hPragma, hExpires)
47+import Network.HTTP.Types.Status (status200, status404, status503)
48+import qualified Network.Wai as Wai
49+import qualified Network.Wai.Handler.Warp as Warp
50+import qualified Network.Wai.Handler.WebSockets as WS
51+import qualified Network.WebSockets as WS
52+
2853import qualified Options.Applicative as Opts
2954
3055import qualified Paths_purescript as Paths
3156
3257import System.Console.Haskeline
58+import System.IO.UTF8 (readUTF8File)
3359import System.Exit
60+import System.FilePath ((</>))
3461import System.FilePath.Glob (glob)
62+import System.Process (readProcessWithExitCode)
3563
3664-- | Command line options
3765data PSCiOptions = PSCiOptions
3866 { psciMultiLineMode :: Bool
3967 , psciInputFile :: [FilePath]
40− , psciInputNodeFlags :: [String]
68+ , psciBackend :: Backend
4169 }
4270
4371multiLineMode :: Opts.Parser Bool
@@ -60,10 +88,21 @@ nodeFlagsFlag = Opts.option parser $
6088 where
6189 parser = words <$> Opts.str
6290
91+port :: Opts.Parser Int
92+port = Opts.option Opts.auto $
93+ Opts.long "port"
94+ <> Opts.short 'p'
95+ <> Opts.help "The web server port"
96+
97+backend :: Opts.Parser Backend
98+backend =
99+ (browserBackend <$> port)
100+ <|> (nodeBackend <$> nodeFlagsFlag)
101+
63102psciOptions :: Opts.Parser PSCiOptions
64103psciOptions = PSCiOptions <$> multiLineMode
65104 <*> many inputFile
66− <*> nodeFlagsFlag
105+ <*> backend
67106
68107version :: Opts.Parser (a -> a)
69108version = Opts.abortOption (Opts.InfoMsg (showVersion Paths.version)) $
@@ -92,6 +131,195 @@ getCommand singleLineMode = handleInterrupt (return (Right Nothing)) $ do
92131 go :: [String] -> InputT m String
93132 go ls = maybe (return . unlines $ reverse ls) (go . (:ls)) =<< getInputLine " "
94133
134+-- | Make a JavaScript bundle for the browser.
135+bundle :: IO (Either Bundle.ErrorMessage String)
136+bundle = runExceptT $ do
137+ inputFiles <- liftIO (glob (".psci_modules" </> "node_modules" </> "*" </> "*.js"))
138+ input <- for inputFiles $ \filename -> do
139+ js <- liftIO (readUTF8File filename)
140+ mid <- Bundle.guessModuleIdentifier filename
141+ length js `seq` return (mid, js)
142+ Bundle.bundle input [] Nothing "PSCI"
143+
144+indexJS :: IsString string => string
145+indexJS = $(embedStringFile "psci/static/index.js")
146+
147+indexPage :: IsString string => string
148+indexPage = $(embedStringFile "psci/static/index.html")
149+
150+-- | All of the functions required to implement a PSCi backend
151+data Backend = forall state. Backend
152+ { _backendSetup :: IO state
153+ -- ^ Initialize, and call the continuation when the backend is ready
154+ , _backendEval :: state -> String -> IO ()
155+ -- ^ Evaluate JavaScript code
156+ , _backendReload :: state -> IO ()
157+ -- ^ Reload the compiled code
158+ , _backendShutdown :: state -> IO ()
159+ -- ^ Shut down the backend
160+ }
161+
162+-- | Commands which can be sent to the browser
163+data BrowserCommand
164+ = Eval (MVar String)
165+ -- ^ Evaluate the latest JS
166+ | Reload
167+ -- ^ Reload the page
168+
169+-- | State for the browser backend
170+data BrowserState = BrowserState
171+ { browserCommands :: TChan BrowserCommand
172+ -- ^ A channel which receives data when the compiled JS has
173+ -- been updated
174+ , browserShutdownNotice :: MVar ()
175+ -- ^ An MVar which becomes full when the server should shut down
176+ , browserIndexJS :: TVar (Maybe String)
177+ -- ^ A TVar holding the latest compiled JS
178+ , browserBundleJS :: TVar (Maybe String)
179+ -- ^ A TVar holding the latest bundled JS
180+ }
181+
182+browserBackend :: Int -> Backend
183+browserBackend serverPort = Backend setup evaluate reload shutdown
184+ where
185+ setup :: IO BrowserState
186+ setup = do
187+ shutdownVar <- newEmptyMVar
188+ cmdChan <- newBroadcastTChanIO
189+ indexJs <- newTVarIO Nothing
190+ bundleJs <- newTVarIO Nothing
191+
192+ let
193+ handleWebsocket :: WS.PendingConnection -> IO ()
194+ handleWebsocket pending = do
195+ conn <- WS.acceptRequest pending
196+ -- Fork a thread to keep the connection alive
197+ WS.forkPingThread conn 10
198+ -- Clone the command channel
199+ cmdChanCopy <- atomically $ dupTChan cmdChan
200+ -- Listen for commands
201+ forever $ do
202+ cmd <- atomically $ readTChan cmdChanCopy
203+ case cmd of
204+ Eval resultVar -> void $ do
205+ WS.sendTextData conn ("eval" :: Text)
206+ result <- WS.receiveData conn
207+ -- With many connected clients, all but one of
208+ -- these attempts will fail.
209+ tryPutMVar resultVar (unpack result)
210+ Reload -> do
211+ WS.sendTextData conn ("reload" :: Text)
212+
213+ shutdownHandler :: IO () -> IO ()
214+ shutdownHandler stopServer = void . forkIO $ do
215+ () <- takeMVar shutdownVar
216+ stopServer
217+
218+ onException :: Maybe Wai.Request -> SomeException -> IO ()
219+ onException req ex
220+ | Just (_ :: WS.ConnectionException) <- fromException ex
221+ = return () -- ignore websocket disconnects
222+ | otherwise = Warp.defaultOnException req ex
223+
224+ staticServer :: Wai.Application
225+ staticServer req respond =
226+ case Wai.pathInfo req of
227+ [] ->
228+ respond $ Wai.responseLBS status200
229+ [(hContentType, "text/html")]
230+ indexPage
231+ ["js", "index.js"] ->
232+ respond $ Wai.responseLBS status200
233+ [(hContentType, "application/javascript")]
234+ indexJS
235+ ["js", "latest.js"] -> do
236+ may <- readTVarIO indexJs
237+ case may of
238+ Nothing ->
239+ respond $ Wai.responseLBS status503 [] "Service not available"
240+ Just js ->
241+ respond $ Wai.responseLBS status200
242+ [ (hContentType, "application/javascript")
243+ , (hCacheControl, "no-cache, no-store, must-revalidate")
244+ , (hPragma, "no-cache")
245+ , (hExpires, "0")
246+ ]
247+ (fromString js)
248+ ["js", "bundle.js"] -> do
249+ may <- readTVarIO bundleJs
250+ case may of
251+ Nothing ->
252+ respond $ Wai.responseLBS status503 [] "Service not available"
253+ Just js ->
254+ respond $ Wai.responseLBS status200
255+ [ (hContentType, "application/javascript")]
256+ (fromString js)
257+ _ -> respond $ Wai.responseLBS status404 [] "Not found"
258+
259+ let browserState = BrowserState cmdChan shutdownVar indexJs bundleJs
260+ createBundle browserState
261+
262+ putStrLn $ "Serving http://localhost:" <> show serverPort <> "/. Waiting for connections..."
263+ _ <- forkIO $ Warp.runSettings ( Warp.setInstallShutdownHandler shutdownHandler
264+ . Warp.setPort serverPort
265+ . Warp.setOnException onException
266+ $ Warp.defaultSettings
267+ ) $
268+ WS.websocketsOr WS.defaultConnectionOptions
269+ handleWebsocket
270+ staticServer
271+ return browserState
272+
273+ createBundle :: BrowserState -> IO ()
274+ createBundle state = do
275+ putStrLn "Bundling Javascript..."
276+ ejs <- bundle
277+ case ejs of
278+ Left err -> do
279+ putStrLn (unlines (Bundle.printErrorMessage err))
280+ exitFailure
281+ Right js -> do
282+ atomically $ writeTVar (browserBundleJS state) (Just js)
283+
284+ reload :: BrowserState -> IO ()
285+ reload state = do
286+ createBundle state
287+ atomically $ writeTChan (browserCommands state) Reload
288+
289+ shutdown :: BrowserState -> IO ()
290+ shutdown state = putMVar (browserShutdownNotice state) ()
291+
292+ evaluate :: BrowserState -> String -> IO ()
293+ evaluate state js = liftIO $ do
294+ resultVar <- newEmptyMVar
295+ atomically $ do
296+ writeTVar (browserIndexJS state) (Just js)
297+ writeTChan (browserCommands state) (Eval resultVar)
298+ result <- takeMVar resultVar
299+ putStrLn result
300+
301+nodeBackend :: [String] -> Backend
302+nodeBackend nodeArgs = Backend setup eval reload shutdown
303+ where
304+ setup :: IO ()
305+ setup = return ()
306+
307+ eval :: () -> String -> IO ()
308+ eval _ _ = do
309+ writeFile indexFile "require('$PSCI')['$main']();"
310+ process <- findNodeProcess
311+ result <- traverse (\node -> readProcessWithExitCode node (nodeArgs ++ [indexFile]) "") process
312+ case result of
313+ Just (ExitSuccess, out, _) -> putStrLn out
314+ Just (ExitFailure _, _, err) -> putStrLn err
315+ Nothing -> putStrLn "Couldn't find node.js"
316+
317+ reload :: () -> IO ()
318+ reload _ = return ()
319+
320+ shutdown :: () -> IO ()
321+ shutdown _ = return ()
322+
95323-- | Get command line options and drop into the REPL
96324main :: IO ()
97325main = getOpt >>= loop
@@ -106,27 +334,31 @@ main = getOpt >>= loop
106334 exitFailure
107335 (externs, env) <- ExceptT . runMake . make $ modules
108336 return (modules, externs, env)
109− case e of
110− Left errs -> putStrLn (P.prettyPrintMultipleErrors P.defaultPPEOptions errs) >> exitFailure
111− Right (modules, externs, env) -> do
112− historyFilename <- getHistoryFilename
113− let settings = defaultSettings { historyFile = Just historyFilename }
114− initialState = PSCiState [] [] (zip (map snd modules) externs)
115− config = PSCiConfig inputFiles psciInputNodeFlags env
116− runner = flip runReaderT config
117− . flip evalStateT initialState
118− . runInputT (setComplete completion settings)
119− putStrLn prologueMessage
120− runner go
121− where
122− go :: InputT (StateT PSCiState (ReaderT PSCiConfig IO)) ()
123− go = do
124− c <- getCommand (not psciMultiLineMode)
125− case c of
126− Left err -> outputStrLn err >> go
127− Right Nothing -> go
128− Right (Just QuitPSCi) -> outputStrLn quitMessage
129− Right (Just c') -> do
130− handleInterrupt (outputStrLn "Interrupted.")
131− (withInterrupt (lift (handleCommand c')))
132− go
337+ case psciBackend of
338+ Backend setup eval reload (shutdown :: state -> IO ()) -> do
339+ case e of
340+ Left errs -> putStrLn (P.prettyPrintMultipleErrors P.defaultPPEOptions errs) >> exitFailure
341+ Right (modules, externs, env) -> do
342+ historyFilename <- getHistoryFilename
343+ let settings = defaultSettings { historyFile = Just historyFilename }
344+ initialState = PSCiState [] [] (zip (map snd modules) externs)
345+ config = PSCiConfig inputFiles env
346+ runner = flip runReaderT config
347+ . flip evalStateT initialState
348+ . runInputT (setComplete completion settings)
349+
350+ go :: state -> InputT (StateT PSCiState (ReaderT PSCiConfig IO)) ()
351+ go state = do
352+ c <- getCommand (not psciMultiLineMode)
353+ case c of
354+ Left err -> outputStrLn err >> go state
355+ Right Nothing -> go state
356+ Right (Just QuitPSCi) -> do
357+ outputStrLn quitMessage
358+ liftIO $ shutdown state
359+ Right (Just c') -> do
360+ handleInterrupt (outputStrLn "Interrupted.")
361+ (withInterrupt (lift (handleCommand (liftIO . eval state) (liftIO (reload state)) c')))
362+ go state
363+ putStrLn prologueMessage
364+ setup >>= runner . go