reanimate-1.1.3.3: src/Reanimate/Driver/Daemon.hs
module Reanimate.Driver.Daemon where
import Control.Concurrent
import Control.Exception as E
import Control.Monad
import qualified Data.ByteString.Char8 as BS
import Network.Socket
import Network.Socket.ByteString
import qualified Reanimate.Driver.Server as Server
import System.FSNotify
import System.FilePath
import System.Environment
import Detach
{-
Main run message:
Reanimate has gone into daemon mode and will block until you hit
ctrl-c. While Reanimate is in daemon mode, you can open a new
console or terminal and execute your animation code again. It'll
automatically send the new animation to the browser window.
Linux users can pass --daemon to reanimate to run the process in
the background. Windows users have to use PowerShell and explicitly
fork the process:
Start-Process -NoNewWindow ./reanimate_exe
Connection to the browser window will be lost if you hit ctrl-c.
Executing with daemon:
Send animation to daemon and exit.
Executing without daemon:
Run daemon locally.
Render animation once.
Wait without exiting.
Detach if given --daemon flag. Only for Linux.
Executing on Linux:
Running 'main' will start the daemon if it isn't already running.
Then it'll render the animation and send it to the daemon.
The daemon will open a browser window.
Daemon stops after 30 minutes of inactivity.
Executing on Windows:
'main' will act as daemon and not return.
Powershell command for running in the background:
Start-Process -NoNewWindow ./reanimate_exe
Subsequent runs will send animation to daemon and quit.
In GHCi:
Start local daemon thread if necessary.
:cmd reanimateLive
1. wait for changes
2. ":r"
3. ":main"
4. ":cmd Reanimate.reanimateLive"
Web port: 9161
Daemon port: 9162?
-}
{-
Improvements over previous infrastructure:
- Multiple browser windows can be open.
- Browser window won't get stuck trying to open a connection.
- GHCi is robust and works with both cabal and stack.
- Refreshing the code will re-open closed browser windows.
-}
-- | Load a reanimate program in GHCi and make sure 'main' is available.
-- Then run:
-- @
-- :cmd reanimateLive
-- @
--
-- This works by sending the commands ':r' and ':main' to your GHCi instance
-- when any source file is changed.
reanimateLive :: IO String
reanimateLive = do
void ensureDaemon
args <- getArgs
case args of
["primed"] -> waitForChanges
_ -> return ()
return $ unlines
[ ":r"
, ":main"
, ":cmd System.Environment.withArgs [\"primed\"] Reanimate.reanimateLive" ]
-- | Load an animation in GHCi. Anything of type 'Animation' can be live reloaded.
--
-- @
-- :cmd reanimateLiveEntry "drawCircle"
-- @
--
-- This works by sending the commands ':r' and 'Reanimate.reanimate {entry}' to your
-- GHCi instance when any source file is changed.
reanimateLiveEntry :: String -> IO String
reanimateLiveEntry animation = do
void ensureDaemon
args <- getArgs
case args of
["primed"] -> waitForChanges
_ -> return ()
return $ unlines
[ ":r"
, "Reanimate.reanimate (" ++ animation ++ ")"
, ":cmd System.Environment.withArgs [\"primed\"] (Reanimate.reanimateLiveEntry " ++ show animation ++ ")"]
waitForChanges :: IO ()
waitForChanges = withManager $ \mgr -> do
lock <- newEmptyMVar
stop <- watchTree mgr "." check (const $ putMVar lock ())
takeMVar lock
stop
where
check event =
takeExtension (eventPath event) `elem` sourceExtensions ||
takeExtension (eventPath event) `elem` dataExtensions
sourceExtensions = [".hs", ".lhs"]
dataExtensions = [".jpg", ".png", ".bmp", ".pov", ".tex", ".csv"]
data DaemonCommand
= DaemonCount Int
| DaemonFrame Int FilePath
| DaemonStop
deriving (Show)
sendCommand :: DaemonCommand -> IO ()
sendCommand cmd = withSocketsDo $ handle (\SomeException{} -> return ()) $ do
addr <- resolve
E.bracket (open addr) close $ \sock -> do
void $ send sock $ case cmd of
DaemonCount count -> BS.pack $ unwords ["frame_count", show count]
DaemonFrame nth path -> BS.pack $ unwords ["frame", show nth, path]
DaemonStop -> BS.pack $ unwords ["stop"]
return ()
where
resolve = do
let hints = defaultHints { addrSocketType = Stream }
head <$> getAddrInfo (Just hints) (Just "127.0.0.1") (Just "9162")
open addr = E.bracketOnError (oSocket addr) close $ \sock -> do
connect sock $ addrAddress addr
return sock
hasDaemon :: IO Bool
hasDaemon = withSocketsDo $ handle (\SomeException{} -> return False) $ do
addr <- resolve
E.bracket (open addr) close (const $ return True)
where
resolve = do
let hints = defaultHints { addrSocketType = Stream }
head <$> getAddrInfo (Just hints) (Just "127.0.0.1") (Just "9162")
open addr = E.bracketOnError (oSocket addr) close $ \sock -> do
connect sock $ addrAddress addr
return sock
oSocket :: AddrInfo -> IO Socket
oSocket addr = socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
ensureDaemon :: IO Bool
ensureDaemon = do
daemon <- hasDaemon
if daemon
then pure True
else localDaemon
killDaemon :: IO ()
killDaemon = sendCommand DaemonStop
localDaemon :: IO Bool
localDaemon = do
killDaemon
detach Server.daemon