module Language.Haskell.Reload (runApp, app) where
import Language.Haskell.Reload.Build
import Language.Haskell.Reload.Config
import Language.Haskell.Reload.FileBrowser
import Language.Haskell.Reload.Project
import Paths_reload
import Data.Aeson (Value(..),encode,object,(.=))
import Network.Wai
import Network.Wai.Handler.Warp
import Network.Wai.Middleware.Static
import Network.HTTP.Types.Status
import Web.Scotty
import System.Directory
import Control.Monad
import Control.Monad.IO.Class
import System.FilePath
import System.IO
import qualified Data.ByteString.Lazy as B
import Data.Text.Lazy (fromStrict,unpack)
import qualified Data.Text.Lazy.Encoding as T
import Data.List (isInfixOf)
import Network.Wai.Handler.WebSockets
import Network.WebSockets
import Control.Concurrent
import Control.Concurrent.Async (race)
import Control.Exception
import Data.IORef
#if WINDOWS
import Foreign
import Foreign.C.String
#else
import System.Process (rawSystem)
#endif
scottyDef
:: FilePath
-> IORef Bool
-> BuildState
-> ScottyM ()
scottyDef dataDir active buildState = do
middleware $ staticPolicy (addBase dataDir)
get "/" $ file "web/index.html"
get (regex "^/ping$") $ do
liftIO $ writeIORef active True
status ok200
json Null
get (regex "^/files(.*)$") $ do
path <- param "1"
let norm = case path of
('/':r)->r
_ -> path
checkPath norm $ do
fss <- liftIO $ do
mc <- config (bsRoot buildState)
let hf=if showHiddenFiles mc then ShowHidden else HideHidden
listFiles (bsRoot buildState) norm hf
json fss
put (regex "^/files/(.*)$") $ do
path <- param "1"
checkPath path $ do
ex <- liftIO $ do
let full = (bsRoot buildState) </> path
ex <- doesDirectoryExist full
createDirectoryIfMissing True full
return ex
if ex
then status noContent204
else status created201
json Null
delete (regex "^/files/(.*)$") $ do
path <- param "1"
checkPath path $ do
ex <- liftIO $ do
let full = (bsRoot buildState) </> path
ex <- doesDirectoryExist full
when ex $ removeDirectoryRecursive full
return ex
if ex
then status ok200
else status noContent204
json Null
get (regex "^/file/(.*)$") $ do
path <- param "1"
checkPath path $ do
let fp = (bsRoot buildState) </> path
ex <- liftIO $ doesFileExist fp
if ex
then do
cnt <- liftIO $ B.readFile fp
setHeader "Content-Type" $ fromStrict $ getMIMEText path
raw cnt
else status notFound404
put (regex "^/file/(.*)$") $ do
path <- param "1"
checkPath path $ do
b <- body
ex <- liftIO $ do
let p = (bsRoot buildState) </> path
exd <- doesDirectoryExist p
if exd
then
return Nothing
else do
ex <- doesFileExist p
when (not ex) $ do
createDirectoryIfMissing True $ takeDirectory p
B.writeFile p b
rebuild buildState path
return $ Just ex
case ex of
Just True -> status ok200
Just False -> status created201
Nothing -> status forbidden403
json Null
delete (regex "^/file/(.*)$") $ do
path <- param "1"
checkPath path $ do
ex <- liftIO $ do
let p = (bsRoot buildState) </> path
ex <- doesFileExist p
when ex $ do
removeFile p
rebuild buildState path
return ex
if ex
then status ok200
else status noContent204
json Null
post (regex "^/launch/(.*)$") $ do
name <- param "1"
bs <- body
let s = unpack $ T.decodeUtf8 bs
liftIO $ launch name s buildState
json Null
get (regex "^/root$") $ do
json $ bsRoot buildState
get (regex "^/targets$") $ do
tgts <- liftIO $ do
mcabal <- cabalFileInFolder $ bsRoot buildState
case mcabal of
Nothing -> return []
Just cabal -> readTargets cabal
json tgts
get (regex "^/targetGroups$") $ do
tgts <- liftIO $ do
mcabal <- cabalFileInFolder $ bsRoot buildState
case mcabal of
Nothing -> return []
Just cabal -> readTargetGroups cabal
json tgts
get (regex "^/info/(.*)$") $ do
path <- param "1"
s <- param "word"
checkPath path $ do
ss <- liftIO $ info buildState path s
json ss
get (regex "^/complete/(.*)$") $ do
path <- param "1"
s <- param "word"
checkPath path $ do
ss <- liftIO $ complete buildState path s
json ss
post (regex "^/format/(.*)$") $ do
path <- param "1"
checkPath path $ do
liftIO $ do
mc <- config (bsRoot buildState)
let f=formatCommand mc
let buildResult = bsBuildResult buildState
ior <- newIORef []
runExec (f ++ " " ++ path) (bsRoot buildState)
(\str line -> if ("out" == str)
then modifyIORef ior (\i->line:i)
else putStrLn line)
(\str -> when ("out" == str) $ do
ls <- reverse <$> readIORef ior
withFile path WriteMode (\h-> mapM_ (hPutStrLn h) ls)
putMVar buildResult (object ["reload" .= path]))
checkPath :: FilePath -> ActionM () -> ActionM ()
checkPath path f = do
if (".." `isInfixOf` path || isAbsolute path)
then do
status notFound404
json Null
else f
fullApp
:: IORef Bool
-> Bool
-> IO Application
fullApp active withRepl = do
staticDir <- getDataDir
root <- getCurrentDirectory
buildResult <- newEmptyMVar
buildState <- startBuild root buildResult withRepl
when withRepl $ putStrLn $ "Ready!"
sco <- scottyApp $ scottyDef (staticDir </> "web") active buildState
return $ websocketsOr defaultConnectionOptions (wsApp buildResult) sco
app
:: Bool
-> IO Application
app withRepl = do
active <- newIORef True
fullApp active withRepl
wsApp
:: (MVar Value)
-> ServerApp
wsApp buildResult pending_conn = do
conn <- acceptRequest pending_conn
forkPingThread conn 30
sendBuild conn
where
sendBuild conn = do
v <- takeMVar buildResult
sendTextData conn (encode v)
catch (do
b::B.ByteString <- receiveData conn
when ("\"OK\""== b) $ sendBuild conn
) (\(e::ConnectionException) -> do
void $ tryPutMVar buildResult v
throw e
)
runApp
:: Int
-> IO ()
runApp port = do
ready <- newEmptyMVar
let setts =
setBeforeMainLoop (putMVar ready ()) $
setPort port defaultSettings
putStrLn $ "Preparing to serve on http://localhost:" ++(show port)++"..."
active <- newIORef True
fmap (either id id) $ race
(runSettings setts =<< fullApp active True)
(takeMVar ready >> launchBrowser port "" >> loop active)
loop
:: IORef Bool
-> IO ()
loop active = do
let seconds = 120
threadDelay $ 1000000 * seconds
b <- readIORef active
if b
then writeIORef active False >> loop active
else return ()
#if WINDOWS
foreign import ccall "launch"
launch' :: Int -> CString -> IO ()
#endif
launchBrowser
:: Int
-> String
-> IO ()
#if WINDOWS
launchBrowser port s = withCString s $ launch' port
#else
launchBrowser port s = forkIO (rawSystem
#if MAC
"open"
#else
"xdg-open"
#endif
["http://127.0.0.1:" ++ show port ++ "/" ++ s] >> return ()) >> return ()
#endif