{-# LANGUAGE DeriveGeneric #-} -- | Make a checkpointable session -- provided that session state implements `Initial`. module Control.Restartable.Checkpoint(restartable, Ending(..)) where import Data.Aeson ( decodeFileStrict, encodeFile ) import GHC.Generics(Generic) import System.Posix.Process(executeFile) import System.Environment(getArgs, getProgName) import System.IO.Error ( catchIOError ) import Control.Restartable.Initial -- | Reload old value from file, or initialize it from scratch if file is not present. restore :: Initial a => FilePath -> IO a restore filepath = do result <- decodeFileStrict filepath `catchIOError` (\_ -> return $ Just initial) case result of Nothing -> error "Cannot load initial value!" Just x -> return x -- | Way in which game terminated. data Ending = Quit -- exit application | Restart -- save state, and resume with a new executable on the same path | Continue -- save state, and resume execution deriving (Eq, Ord, Show, Generic) -- | Restart current executable with the same arguments. restartExecutable :: IO () restartExecutable = do args <- getArgs exeName <- getProgName executeFile exeName True args Nothing -- Never returns -- | Should be used to wrap `main`. restartable :: Initial a => FilePath -> (a -> IO (a, Ending)) -> IO () restartable savefile app = do initialState <- restore savefile go initialState where go initialState = do (finalState, ending) <- app initialState encodeFile savefile finalState case ending of -- Reload code (after recompile). Restart -> restartExecutable -- Resume execution after saving the state Continue -> go finalState -- Exit application Quit -> return ()