packages feed

yesod-fast-devel-0.1.2.0: Main.hs

module Main where

import           Control.Concurrent           (forkIO, threadDelay)
import           Control.Concurrent.STM.TChan (TChan, dupTChan, newTChan,
                                               readTChan, writeTChan)
import           Control.Exception            (bracket)
import           Control.Monad                (forever, unless, when)
import           Control.Monad.STM            (atomically)
import qualified Data.ByteString.Lazy         as ByteString
import           Data.Digest.Pure.MD5         (md5)
import           Data.Maybe
import           Data.Monoid
import           Data.Text                    (isInfixOf, pack)
import           Options.Applicative
import           Paths_yesod_fast_devel
import           System.Console.ANSI
import           System.Directory             (copyFile, doesDirectoryExist,
                                               findExecutable)
import           System.Exit
import           System.FilePath              (takeDirectory)
import           System.FilePath.Glob
import           System.FilePath.Posix        (takeBaseName)
import           System.FSNotify              (Event (..), watchTree,
                                               withManager)
import           System.IO                    (BufferMode (..), Handle,
                                               hPutStrLn, hSetBuffering, stderr,
                                               stdout)
import           System.Process

data Options
  = PatchDevelMain { pdmFilePath :: FilePath}
  | PrintPatchedMain
  | StartServer { ssFilePath :: FilePath}

options :: ParserInfo Options
options =
  info
    (allCommands <**> helper)
    (header "Faster yesod-devel with GHCi and Browser Sync")
  where
    allCommands =
      subparser
        (patchDevelMain <> printPatchedMain <> startServer <>
         metavar "patch | server| print-patched-main")
    printPatchedMain =
      command
        "print-patched-main"
        (info (pure PrintPatchedMain) (progDesc "Print the patched DevelMain"))
    startServer =
      command
        "server"
        (info
           (StartServer . fromMaybe "app/DevelMain.hs" <$>
            optional (argument str (metavar "devel-main-path")) <**>
            helper)
           (progDesc "Start the development servers"))
    patchDevelMain =
      command
        "patch"
        (info
           (PatchDevelMain . fromMaybe "app/DevelMain.hs" <$>
            optional (argument str (metavar "devel-main-path")) <**>
            helper)
           (progDesc "Patch your devel main with browser-sync"))

main :: IO ()
main = do
  cmd <- execParser options
  case cmd of
    PatchDevelMain fp -> initYesodFastDevel fp
    StartServer fp -> go fp
    PrintPatchedMain ->
      putStrLn =<< readFile =<< getDataFileName "PatchedDevelMain.hs"
  where
    go develMainPth = do
      hSetBuffering stdout LineBuffering
      hSetBuffering stderr LineBuffering
      chan <- atomically newTChan
      _ <- forkIO $ do
          hPutStrLn stderr "Watching files for changes..."
          watchThread chan
      _ <- forkIO $ do
          hPutStrLn stderr "Spawning browser-sync..."
          browserSyncThread
      hPutStrLn stderr "Spawning GHCi..."
      _ <- replThread develMainPth chan
      return ()

initYesodFastDevel :: FilePath -> IO ()
initYesodFastDevel develMainPth = do
  verifyDirectory
  verifyDevelMain
  patchedDevelMain <- getDataFileName "PatchedDevelMain.hs"
  copyFile patchedDevelMain develMainPth
  putStrLn "Patched `DevelMain.hs`"
  browserSyncPth <- findExecutable "browser-sync"
  putStrLn "Make sure you have `foreign-store` on your cabal file"
  when (isNothing browserSyncPth) $
    putStrLn "Install `browser-sync` to have livereload at port 4000"
  exitSuccess
  where
    verifyDirectory = do
      let dir = takeDirectory develMainPth
      putStrLn ("Verifying `" ++ dir ++ "` exists")
      dexists <- doesDirectoryExist dir
      unless dexists $ do
        hPutStrLn stderr ("Directory `" ++ dir ++ "` not found")
        exitFailure
    verifyDevelMain = do
      putStrLn "Verifying `DevelMain.hs` isn't modified"
      userDevelMd5 <- md5 <$> ByteString.readFile develMainPth
      originalDevelMd5 <-
        md5 <$> (ByteString.readFile =<< getDataFileName "OriginalDevelMain.hs")
      patchedDevelMd5 <-
        md5 <$> (ByteString.readFile =<< getDataFileName "PatchedDevelMain.hs")
      when (userDevelMd5 == patchedDevelMd5) $ do
        putStrLn "DevelMain.hs is already patched"
        exitSuccess
      when (userDevelMd5 /= originalDevelMd5) $ do
        hPutStrLn stderr "Found a weird DevelMain.hs on your project"
        hPutStrLn stderr "Use `yesod-fast-devel print-patched-main`"
        exitFailure

browserSyncThread :: IO ()
browserSyncThread = do
  browserSyncPth <- findExecutable "browser-sync"
  when (isJust browserSyncPth) $ callCommand cmd
  where
    cmd =
      "browser-sync start --no-open --files=\"devel-main-since\" --proxy \"localhost:3000\" --port 4000"

watchThread :: TChan Event -> IO ()
watchThread writeChan =
  withManager $ \mgr
    -- start a watching job (in the background)
   -> do
    _ <- watchTree mgr "." shouldReload (reloadApplication writeChan)
    -- sleep forever (until interrupted)
    forever $ threadDelay 1000000000

replThread :: FilePath -> TChan Event -> IO ()
replThread develMainPth chan = do
  readChan <- atomically (dupTChan chan)
  bracket newRepl onError (onSuccess readChan)
  where
    onError (_, _, _, process) = do
      interruptProcessGroupOf process
      threadDelay 100000
      terminateProcess process
      threadDelay 100000
      waitForProcess process
    onSuccess readChan (Just replIn, _, _, _) = do
      hSetBuffering replIn LineBuffering
      threadDelay 1000000
      hPutStrLn replIn loadString
      hPutStrLn replIn startString
      forever $ do
        event <- atomically (readTChan readChan)
        putStrLn "-----------------------------"
        setSGR [SetColor Foreground Vivid Yellow]
        print event
        setSGR [Reset]
        putStrLn "-----------------------------"
        hPutStrLn replIn loadString
        hPutStrLn replIn startString
    onSuccess _ (_, _, _, _) = do
      hPutStrLn stderr "Can't open GHCi's stdin"
      exitFailure
    startString = "update"
    loadString = ":load " ++ develMainPth

shouldReload :: Event -> Bool
shouldReload event = not (or conditions)
  where
    fp =
      case event of
        Added filePath _    -> filePath
        Modified filePath _ -> filePath
        Removed filePath _  -> filePath
    conditions =
      [ notInPath ".git"
      , notInPath "yesod-devel"
      , notInPath "dist"
      , notInFile "#"
      , notInPath ".cabal-sandbox"
      , notInFile "flycheck_"
      , notInPath ".stack-work"
      , notInGlob (compile "**/*.sqlite3-*")
      , notInGlob (compile "*.sqlite3-*")
      , notInFile "stack.yaml"
      , notInGlob (compile "*.hi")
      , notInGlob (compile "**/*.hi")
      , notInGlob (compile "*.o")
      , notInGlob (compile "**/*.o")
      , notInFile "devel-main-since"
      ]
    notInPath t = t `isInfixOf` pack fp
    notInFile t = t `isInfixOf` pack (takeBaseName fp)
    notInGlob pt = match pt fp

reloadApplication :: TChan Event -> Event -> IO ()
reloadApplication chan event = atomically (writeTChan chan event)

newRepl :: IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
newRepl =
  createProcess $ newProc "stack" ["ghci", "--ghc-options", "-O0 -fobject-code"]

newProc :: FilePath -> [String] -> CreateProcess
newProc cmd args =
  CreateProcess
  { cmdspec = RawCommand cmd args
  , cwd = Nothing
  , env = Nothing
  , std_in = CreatePipe
  , std_out = Inherit
  , std_err = Inherit
  , close_fds = False
  , create_group = True
  , delegate_ctlc = False
  }