@@ -1,9 +1,13 @@
1{-# LANGUAGE DataKinds #-}
1{-# LANGUAGE DataKinds #-}
2{-# LANGUAGE DoAndIfThenElse #-}
2{-# LANGUAGE DoAndIfThenElse #-}
3{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4{-# LANGUAGE MultiParamTypeClasses #-}
5{-# LANGUAGE MultiParamTypeClasses #-}
6+{-# LANGUAGE OverloadedStrings #-}
7+{-# LANGUAGE PatternGuards #-}
5{-# LANGUAGE RecordWildCards #-}
8{-# LANGUAGE RecordWildCards #-}
6{-# LANGUAGE ScopedTypeVariables #-}
9{-# LANGUAGE ScopedTypeVariables #-}
10+{-# LANGUAGE TemplateHaskell #-}
7{-# LANGUAGE TupleSections #-}
11{-# LANGUAGE TupleSections #-}
9module Main (main) where
13module Main (main) where
@@ -11,10 +15,22 @@ module Main (main) where
11import Prelude ()
15import Prelude ()
12import Prelude.Compat
16import Prelude.Compat
18+import Data.FileEmbed (embedStringFile)
14import Data.Monoid ((<>))
19import Data.Monoid ((<>))
20+import Data.String (IsString(..))
21+import Data.Text (Text, unpack)
22+import Data.Traversable (for)
15import Data.Version (showVersion)
23import Data.Version (showVersion)
17−import Control.Applicative (many)
25+import Control.Applicative (many, (<|>))
26+import Control.Concurrent (forkIO)
27+import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar,
29+import Control.Concurrent.STM (TVar, atomically, newTVarIO, writeTVar,
31+ TChan, newBroadcastTChanIO, dupTChan,
32+ readTChan, writeTChan)
33+import Control.Exception (fromException)
18import Control.Monad
34import Control.Monad
19import Control.Monad.IO.Class (liftIO)
35import Control.Monad.IO.Class (liftIO)
20import Control.Monad.Trans.Class
36import Control.Monad.Trans.Class
@@ -23,21 +39,33 @@ import Control.Monad.Trans.State.Strict (StateT, evalStateT)
23import Control.Monad.Trans.Reader (ReaderT, runReaderT)
39import Control.Monad.Trans.Reader (ReaderT, runReaderT)
25import qualified Language.PureScript as P
41import qualified Language.PureScript as P
42+import qualified Language.PureScript.Bundle as Bundle
26import Language.PureScript.Interactive
43import Language.PureScript.Interactive
45+import Network.HTTP.Types.Header (hContentType, hCacheControl,
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
28import qualified Options.Applicative as Opts
53import qualified Options.Applicative as Opts
30import qualified Paths_purescript as Paths
55import qualified Paths_purescript as Paths
32import System.Console.Haskeline
57import System.Console.Haskeline
58+import System.IO.UTF8 (readUTF8File)
33import System.Exit
59import System.Exit
60+import System.FilePath ((</>))
34import System.FilePath.Glob (glob)
61import System.FilePath.Glob (glob)
62+import System.Process (readProcessWithExitCode)
36-- | Command line options
64-- | Command line options
37data PSCiOptions = PSCiOptions
65data PSCiOptions = PSCiOptions
38 { psciMultiLineMode :: Bool
66 { psciMultiLineMode :: Bool
39 , psciInputFile :: [FilePath]
67 , psciInputFile :: [FilePath]
40− , psciInputNodeFlags :: [String]
68+ , psciBackend :: Backend
43multiLineMode :: Opts.Parser Bool
71multiLineMode :: Opts.Parser Bool
@@ -60,10 +88,21 @@ nodeFlagsFlag = Opts.option parser $
61 parser = words <$> Opts.str
89 parser = words <$> Opts.str
91+port :: Opts.Parser Int
92+port = Opts.option Opts.auto $
95+ <> Opts.help "The web server port"
97+backend :: Opts.Parser Backend
99+ (browserBackend <$> port)
100+ <|> (nodeBackend <$> nodeFlagsFlag)
63psciOptions :: Opts.Parser PSCiOptions
102psciOptions :: Opts.Parser PSCiOptions
64psciOptions = PSCiOptions <$> multiLineMode
103psciOptions = PSCiOptions <$> multiLineMode
65 <*> many inputFile
104 <*> many inputFile
66− <*> nodeFlagsFlag
105+ <*> backend
68version :: Opts.Parser (a -> a)
107version :: Opts.Parser (a -> a)
69version = Opts.abortOption (Opts.InfoMsg (showVersion Paths.version)) $
108version = Opts.abortOption (Opts.InfoMsg (showVersion Paths.version)) $
@@ -92,6 +131,195 @@ getCommand singleLineMode = handleInterrupt (return (Right Nothing)) $ do
92 go :: [String] -> InputT m String
131 go :: [String] -> InputT m String
93 go ls = maybe (return . unlines $ reverse ls) (go . (:ls)) =<< getInputLine " "
132 go ls = maybe (return . unlines $ reverse ls) (go . (:ls)) =<< getInputLine " "
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"
144+indexJS :: IsString string => string
145+indexJS = $(embedStringFile "psci/static/index.js")
147+indexPage :: IsString string => string
148+indexPage = $(embedStringFile "psci/static/index.html")
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
162+-- | Commands which can be sent to the browser
164+ = Eval (MVar String)
165+ -- ^ Evaluate the latest JS
167+ -- ^ Reload the page
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
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
182+browserBackend :: Int -> Backend
183+browserBackend serverPort = Backend setup evaluate reload shutdown
185+ setup :: IO BrowserState
187+ shutdownVar <- newEmptyMVar
188+ cmdChan <- newBroadcastTChanIO
189+ indexJs <- newTVarIO Nothing
190+ bundleJs <- newTVarIO Nothing
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
202+ cmd <- atomically $ readTChan cmdChanCopy
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)
211+ WS.sendTextData conn ("reload" :: Text)
213+ shutdownHandler :: IO () -> IO ()
214+ shutdownHandler stopServer = void . forkIO $ do
215+ () <- takeMVar shutdownVar
218+ onException :: Maybe Wai.Request -> SomeException -> IO ()
220+ | Just (_ :: WS.ConnectionException) <- fromException ex
221+ = return () -- ignore websocket disconnects
222+ | otherwise = Warp.defaultOnException req ex
224+ staticServer :: Wai.Application
225+ staticServer req respond =
226+ case Wai.pathInfo req of
228+ respond $ Wai.responseLBS status200
229+ [(hContentType, "text/html")]
231+ ["js", "index.js"] ->
232+ respond $ Wai.responseLBS status200
233+ [(hContentType, "application/javascript")]
235+ ["js", "latest.js"] -> do
236+ may <- readTVarIO indexJs
239+ respond $ Wai.responseLBS status503 [] "Service not available"
241+ respond $ Wai.responseLBS status200
242+ [ (hContentType, "application/javascript")
243+ , (hCacheControl, "no-cache, no-store, must-revalidate")
244+ , (hPragma, "no-cache")
248+ ["js", "bundle.js"] -> do
249+ may <- readTVarIO bundleJs
252+ respond $ Wai.responseLBS status503 [] "Service not available"
254+ respond $ Wai.responseLBS status200
255+ [ (hContentType, "application/javascript")]
257+ _ -> respond $ Wai.responseLBS status404 [] "Not found"
259+ let browserState = BrowserState cmdChan shutdownVar indexJs bundleJs
260+ createBundle browserState
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
268+ WS.websocketsOr WS.defaultConnectionOptions
273+ createBundle :: BrowserState -> IO ()
274+ createBundle state = do
275+ putStrLn "Bundling Javascript..."
279+ putStrLn (unlines (Bundle.printErrorMessage err))
282+ atomically $ writeTVar (browserBundleJS state) (Just js)
284+ reload :: BrowserState -> IO ()
287+ atomically $ writeTChan (browserCommands state) Reload
289+ shutdown :: BrowserState -> IO ()
290+ shutdown state = putMVar (browserShutdownNotice state) ()
292+ evaluate :: BrowserState -> String -> IO ()
293+ evaluate state js = liftIO $ do
294+ resultVar <- newEmptyMVar
296+ writeTVar (browserIndexJS state) (Just js)
297+ writeTChan (browserCommands state) (Eval resultVar)
298+ result <- takeMVar resultVar
301+nodeBackend :: [String] -> Backend
302+nodeBackend nodeArgs = Backend setup eval reload shutdown
307+ eval :: () -> String -> IO ()
309+ writeFile indexFile "require('$PSCI')['$main']();"
310+ process <- findNodeProcess
311+ result <- traverse (\node -> readProcessWithExitCode node (nodeArgs ++ [indexFile]) "") process
313+ Just (ExitSuccess, out, _) -> putStrLn out
314+ Just (ExitFailure _, _, err) -> putStrLn err
315+ Nothing -> putStrLn "Couldn't find node.js"
317+ reload :: () -> IO ()
318+ reload _ = return ()
320+ shutdown :: () -> IO ()
321+ shutdown _ = return ()
95-- | Get command line options and drop into the REPL
323-- | Get command line options and drop into the REPL
96main :: IO ()
324main :: IO ()
97main = getOpt >>= loop
325main = getOpt >>= loop
@@ -106,27 +334,31 @@ main = getOpt >>= loop
106 exitFailure
334 exitFailure
107 (externs, env) <- ExceptT . runMake . make $ modules
335 (externs, env) <- ExceptT . runMake . make $ modules
108 return (modules, externs, env)
336 return (modules, externs, env)
338+ Backend setup eval reload (shutdown :: state -> IO ()) -> do
109− case e of
339+ case e of
110− Left errs -> putStrLn (P.prettyPrintMultipleErrors P.defaultPPEOptions errs) >> exitFailure
340+ Left errs -> putStrLn (P.prettyPrintMultipleErrors P.defaultPPEOptions errs) >> exitFailure
111− Right (modules, externs, env) -> do
341+ Right (modules, externs, env) -> do
112− historyFilename <- getHistoryFilename
342+ historyFilename <- getHistoryFilename
113− let settings = defaultSettings { historyFile = Just historyFilename }
343+ let settings = defaultSettings { historyFile = Just historyFilename }
114− initialState = PSCiState [] [] (zip (map snd modules) externs)
344+ initialState = PSCiState [] [] (zip (map snd modules) externs)
115− config = PSCiConfig inputFiles psciInputNodeFlags env
345+ config = PSCiConfig inputFiles env
116− runner = flip runReaderT config
346+ runner = flip runReaderT config
117− . flip evalStateT initialState
347+ . flip evalStateT initialState
118− . runInputT (setComplete completion settings)
348+ . runInputT (setComplete completion settings)
119− putStrLn prologueMessage
349+
122− go :: InputT (StateT PSCiState (ReaderT PSCiConfig IO)) ()
350+ go :: state -> InputT (StateT PSCiState (ReaderT PSCiConfig IO)) ()
123− go = do
351+ go state = do
124− c <- getCommand (not psciMultiLineMode)
352+ c <- getCommand (not psciMultiLineMode)
125− case c of
353+ case c of
126− Left err -> outputStrLn err >> go
354+ Left err -> outputStrLn err >> go state
127− Right Nothing -> go
355+ Right Nothing -> go state
356+ Right (Just QuitPSCi) -> do
128− Right (Just QuitPSCi) -> outputStrLn quitMessage
357+ outputStrLn quitMessage
358+ liftIO $ shutdown state
129− Right (Just c') -> do
359+ Right (Just c') -> do
130− handleInterrupt (outputStrLn "Interrupted.")
360+ handleInterrupt (outputStrLn "Interrupted.")
131− (withInterrupt (lift (handleCommand c')))
361+ (withInterrupt (lift (handleCommand (liftIO . eval state) (liftIO (reload state)) c')))
363+ putStrLn prologueMessage
364+ setup >>= runner . go