Improve the safety of the playground and limit the resources it may use. (#144)
* Improve the safety of the playground and limit the resources it may use. * Don't send new code to server until 0.5s after the last keypress.
4
.github/workflows/gh-pages.yml
vendored
|
|
@ -85,9 +85,9 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
CWD=`pwd`
|
CWD=`pwd`
|
||||||
stack build
|
stack build
|
||||||
cd reanimate-playground
|
cd playground
|
||||||
stack build
|
stack build
|
||||||
stack exec --cwd ../ playground snippets reanimate-playground/snippets > viewer-elm/dist/snippets.js
|
stack exec --cwd ../ playground snippets playground/snippets > viewer-elm/dist/snippets.js
|
||||||
cd viewer-elm
|
cd viewer-elm
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ ADD reanimate.cabal stack.yaml ./
|
||||||
RUN stack build --only-dependencies --no-install-ghc --system-ghc --haddock
|
RUN stack build --only-dependencies --no-install-ghc --system-ghc --haddock
|
||||||
|
|
||||||
# Install discord-bot dependencies and cache the layer
|
# Install discord-bot dependencies and cache the layer
|
||||||
ADD reanimate-playground/playground.cabal reanimate-playground/stack.yaml ./reanimate-playground/
|
ADD playground/playground.cabal playground/stack.yaml ./playground/
|
||||||
RUN cd reanimate-playground && \
|
RUN cd playground && \
|
||||||
stack build --only-dependencies --no-install-ghc --system-ghc
|
stack build --only-dependencies --no-install-ghc --system-ghc
|
||||||
|
|
||||||
# Add source after dependencies have been installed as to not invalidate the caches
|
# Add source after dependencies have been installed as to not invalidate the caches
|
||||||
|
|
@ -38,8 +38,8 @@ ADD Setup.hs ./
|
||||||
RUN stack build --no-install-ghc --system-ghc
|
RUN stack build --no-install-ghc --system-ghc
|
||||||
|
|
||||||
# Add bot sources and build it
|
# Add bot sources and build it
|
||||||
ADD reanimate-playground reanimate-playground
|
ADD playground playground
|
||||||
RUN (cd reanimate-playground && \
|
RUN (cd playground && \
|
||||||
stack install --no-install-ghc --system-ghc) && \
|
stack install --no-install-ghc --system-ghc) && \
|
||||||
playground test
|
playground test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,16 @@
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
{-
|
||||||
|
Mitigated attacks:
|
||||||
|
* unsafeIO: Blocked by fixed import list.
|
||||||
|
* TemplateHaskell: Blocked by parsing code.
|
||||||
|
* Code injection: Blocked by parsing code.
|
||||||
|
* Exhausting memory: Blocked by RTS flags on ghci.
|
||||||
|
* Rendering too long: Blocked by both soft and hard timeouts.
|
||||||
|
* Generating huge error messages: Error messages are truncated.
|
||||||
|
* Take up disk space: Space limits are checked before each frame is rendered.
|
||||||
|
-}
|
||||||
module Main (main) where
|
module Main (main) where
|
||||||
|
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
|
|
@ -56,8 +66,23 @@ playgroundVersion = T.pack $
|
||||||
formatTime defaultTimeLocale "%F" playgroundCommitDate ++
|
formatTime defaultTimeLocale "%F" playgroundCommitDate ++
|
||||||
" (" ++ take 5 (giHash gi) ++ ")"
|
" (" ++ take 5 (giHash gi) ++ ")"
|
||||||
|
|
||||||
computeLimit :: Int
|
-- Seconds of wall time if the render queue is empty.
|
||||||
computeLimit = 15 * 10^(6::Int) -- 15 seconds
|
totalTimeLimitLong :: NominalDiffTime
|
||||||
|
totalTimeLimitLong = 30
|
||||||
|
|
||||||
|
-- Seconds of wall time if the render queue is full.
|
||||||
|
totalTimeLimitShort :: NominalDiffTime
|
||||||
|
totalTimeLimitShort = 5
|
||||||
|
|
||||||
|
frameTimeLimit = 5
|
||||||
|
|
||||||
|
-- Disk space limit in MiB
|
||||||
|
diskSpaceLimit :: Double
|
||||||
|
diskSpaceLimit = 50
|
||||||
|
|
||||||
|
-- Limit animation runtimes to 1 minute.
|
||||||
|
maxAnimationDuration :: Double
|
||||||
|
maxAnimationDuration = 60
|
||||||
|
|
||||||
-- Maximum size of error messages
|
-- Maximum size of error messages
|
||||||
charLimit :: Int
|
charLimit :: Int
|
||||||
|
|
@ -94,6 +119,7 @@ main = do
|
||||||
putStrLn $ "const playgroundVersion = " ++ show playgroundVersion ++ ";"
|
putStrLn $ "const playgroundVersion = " ++ show playgroundVersion ++ ";"
|
||||||
[] -> do
|
[] -> do
|
||||||
root <- cacheDir
|
root <- cacheDir
|
||||||
|
-- The http server is only used for local development.
|
||||||
tid <- forkIO $ run 10162 (staticApp $ defaultWebAppSettings root)
|
tid <- forkIO $ run 10162 (staticApp $ defaultWebAppSettings root)
|
||||||
serverMain backend `finally` killThread tid
|
serverMain backend `finally` killThread tid
|
||||||
_ -> do
|
_ -> do
|
||||||
|
|
@ -151,6 +177,7 @@ requestHandler backend conn = loop =<< newMVar True
|
||||||
, renderFrameCount = \i -> sendWebMessage conn (WebFrameCount i)
|
, renderFrameCount = \i -> sendWebMessage conn (WebFrameCount i)
|
||||||
, renderFrameReady = \i path -> sendWebMessage conn (WebFrame i path)
|
, renderFrameReady = \i path -> sendWebMessage conn (WebFrame i path)
|
||||||
, renderError = \msg -> sendWebMessage conn (WebError msg)
|
, renderError = \msg -> sendWebMessage conn (WebError msg)
|
||||||
|
, renderWarning = \msg -> sendWebMessage conn (WebWarning msg)
|
||||||
, renderWanted = wantThisRequest
|
, renderWanted = wantThisRequest
|
||||||
}
|
}
|
||||||
loop wantThisRequest
|
loop wantThisRequest
|
||||||
|
|
@ -166,6 +193,12 @@ data CacheResult
|
||||||
| CacheHitPartial Int IntSet
|
| CacheHitPartial Int IntSet
|
||||||
deriving (Show)
|
deriving (Show)
|
||||||
|
|
||||||
|
ppCacheResult :: CacheResult -> String
|
||||||
|
ppCacheResult CacheMiss = "CacheMiss"
|
||||||
|
ppCacheResult (CacheHit frames) = "CacheHit " ++ show frames
|
||||||
|
ppCacheResult (CacheHitPartial frames partial) =
|
||||||
|
"CacheHitPartial " ++ show (IntSet.size partial) ++ "/" ++ show frames
|
||||||
|
|
||||||
checkCache :: String -> IO CacheResult
|
checkCache :: String -> IO CacheResult
|
||||||
checkCache key = handle (\SomeException{} -> pure CacheMiss) $ do
|
checkCache key = handle (\SomeException{} -> pure CacheMiss) $ do
|
||||||
root <- cacheDir
|
root <- cacheDir
|
||||||
|
|
@ -191,13 +224,14 @@ data Render = Render
|
||||||
, renderFrameCount :: Int -> IO ()
|
, renderFrameCount :: Int -> IO ()
|
||||||
, renderFrameReady :: Int -> FilePath -> IO ()
|
, renderFrameReady :: Int -> FilePath -> IO ()
|
||||||
, renderError :: String -> IO ()
|
, renderError :: String -> IO ()
|
||||||
|
, renderWarning :: String -> IO ()
|
||||||
, renderWanted :: MVar Bool
|
, renderWanted :: MVar Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
requestRender :: Backend -> Render -> IO ()
|
requestRender :: Backend -> Render -> IO ()
|
||||||
requestRender backend render = do
|
requestRender backend render = do
|
||||||
cache <- checkCache (renderHash render)
|
cache <- checkCache (renderHash render)
|
||||||
logMsg $ "Cache: " ++ show cache
|
logMsg $ "Cache: " ++ ppCacheResult cache
|
||||||
case cache of
|
case cache of
|
||||||
CacheMiss -> void $ forkIO $ putMVar (backendQueue backend) render
|
CacheMiss -> void $ forkIO $ putMVar (backendQueue backend) render
|
||||||
CacheHit frames -> do
|
CacheHit frames -> do
|
||||||
|
|
@ -210,7 +244,7 @@ requestRender backend render = do
|
||||||
forM_ (IntSet.toList frameSet) $ \i -> do
|
forM_ (IntSet.toList frameSet) $ \i -> do
|
||||||
let path = renderHash render </> show i <.> "svg"
|
let path = renderHash render </> show i <.> "svg"
|
||||||
renderFrameReady render i path
|
renderFrameReady render i path
|
||||||
void $ forkIO $ putMVar (backendQueue backend) render
|
void $ forkIO $ putMVar (backendQueue backend) render{ renderFrameCount = \_ -> return () }
|
||||||
|
|
||||||
newGhci :: IO Ghci
|
newGhci :: IO Ghci
|
||||||
newGhci = do
|
newGhci = do
|
||||||
|
|
@ -225,51 +259,99 @@ newBackend :: IO Backend
|
||||||
newBackend = do
|
newBackend = do
|
||||||
ghciRef <- newMVar =<< newGhci
|
ghciRef <- newMVar =<< newGhci
|
||||||
queue <- newEmptyMVar
|
queue <- newEmptyMVar
|
||||||
|
root <- cacheDir
|
||||||
tid <- forkIO $ forever $ do
|
tid <- forkIO $ forever $ do
|
||||||
req <- takeMVar queue
|
req <- takeMVar queue
|
||||||
guardWanted req $ withHaskellFile (renderCode req) $ \hs -> do
|
|
||||||
ghci <- readMVar ghciRef
|
|
||||||
catch @GhciError (loadAndRender req ghci hs) (\_ -> restartGhci ghciRef req)
|
|
||||||
return $ Backend ghciRef queue
|
|
||||||
where
|
|
||||||
restartGhci ghciRef req = do
|
|
||||||
renderError req "Ghci crashed. Restarting."
|
|
||||||
modifyMVar_ ghciRef (const newGhci)
|
|
||||||
loadAndRender req ghci hs =
|
|
||||||
guardGhci req ghci (":load " ++ hs) $ \_ -> guardWanted req $
|
|
||||||
guardGhci req ghci "Reanimate.duration animation" $ \out -> do
|
|
||||||
root <- cacheDir
|
|
||||||
let dur = read (unlines out) :: Double
|
|
||||||
frameCount = round (dur * fromIntegral frameRate) :: Int
|
|
||||||
durFile = root </> renderHash req </> "frames"
|
|
||||||
createDirectoryIfMissing True (root </> renderHash req)
|
|
||||||
writeFile durFile (show frameCount)
|
|
||||||
renderFrameCount req frameCount
|
|
||||||
renderFrames req ghci
|
|
||||||
renderFrames req ghci = guardWanted req $ do
|
|
||||||
root <- cacheDir
|
|
||||||
let svgFolder = root </> renderHash req
|
let svgFolder = root </> renderHash req
|
||||||
createDirectoryIfMissing True svgFolder
|
createDirectoryIfMissing True svgFolder
|
||||||
let cmd = printf "Reanimate.renderOneFrame \"%s\" 0 False %d animation" svgFolder frameRate
|
startTime <- getCurrentTime
|
||||||
guardGhci req ghci cmd $ \out ->
|
ghci <- readMVar ghciRef
|
||||||
case unlines out of
|
let guardTimeout action = do
|
||||||
"Done\n" -> return ()
|
now <- getCurrentTime
|
||||||
|
emptyQueue <- isEmptyMVar queue
|
||||||
|
let timeLimit = if emptyQueue then totalTimeLimitLong else totalTimeLimitShort
|
||||||
|
if (diffUTCTime now startTime < timeLimit)
|
||||||
|
then action
|
||||||
|
else do
|
||||||
|
renderWarning req "Render timed out"
|
||||||
|
logMsg "Request timed out"
|
||||||
|
guardFileSize action = do
|
||||||
|
size <- getDirectorySize svgFolder
|
||||||
|
if size < round (diskSpaceLimit*1024*1024)
|
||||||
|
then action
|
||||||
|
else do
|
||||||
|
renderWarning req "Disk space limit hit"
|
||||||
|
logMsg "Disk space limit hit"
|
||||||
|
guardWanted action = do
|
||||||
|
wanted <- readMVar (renderWanted req)
|
||||||
|
if wanted
|
||||||
|
then action
|
||||||
|
else logMsg "Results no longer wanted"
|
||||||
|
guardGhci cmd action = do
|
||||||
|
mbValue <- timeout (round (frameTimeLimit * 1e6)) $
|
||||||
|
splitGhciOutput ghci cmd
|
||||||
|
case mbValue of
|
||||||
|
Nothing -> do
|
||||||
|
renderWarning req "Frame render timed out."
|
||||||
|
forkIO $ stopGhci ghci
|
||||||
|
modifyMVar_ ghciRef (const newGhci)
|
||||||
|
Just (err, out)
|
||||||
|
| null err -> action out
|
||||||
|
| otherwise -> do
|
||||||
|
logMsg $ "Error:\n" ++ take charLimit (unlines err)
|
||||||
|
renderError req (take charLimit (unlines err))
|
||||||
|
restartGhci = do
|
||||||
|
renderWarning req "Ghci crashed. Restarting."
|
||||||
|
logMsg "Ghci crashed. Restarting."
|
||||||
|
forkIO $ stopGhci ghci
|
||||||
|
modifyMVar_ ghciRef (const newGhci)
|
||||||
|
|
||||||
|
loadAndRender hs =
|
||||||
|
guardGhci (":load " ++ hs) $ \_ -> guardWanted $
|
||||||
|
guardGhci "Reanimate.duration animation" $ \out -> do
|
||||||
|
let dur = max 1 (min maxAnimationDuration (read (unlines out))) :: Double
|
||||||
|
frameCount = round (dur * fromIntegral frameRate) :: Int
|
||||||
|
durFile = root </> renderHash req </> "frames"
|
||||||
|
writeFile durFile (show frameCount)
|
||||||
|
renderFrameCount req frameCount
|
||||||
|
renderFrames dur
|
||||||
|
renderFrames dur = guardWanted $ guardTimeout $ guardFileSize $ do
|
||||||
|
let cmd = printf
|
||||||
|
"Reanimate.renderLimitedFrames \"%s\" 0 False %d \
|
||||||
|
\(Reanimate.setDuration %f animation)"
|
||||||
|
svgFolder frameRate dur
|
||||||
|
done <- newIORef False
|
||||||
|
mbErrors <- streamGhci ghci cmd $ \msg ->
|
||||||
|
case msg of
|
||||||
|
"Done" -> writeIORef done True
|
||||||
_ -> do
|
_ -> do
|
||||||
let frameIdx = read (unlines out)
|
let frameIdx = read msg
|
||||||
path = renderHash req </> show frameIdx <.> "svg"
|
path = renderHash req </> show frameIdx <.> "svg"
|
||||||
renderFrameReady req frameIdx path
|
renderFrameReady req frameIdx path
|
||||||
renderFrames req ghci
|
isDone <- readIORef done
|
||||||
guardGhci req ghci cmd action = do
|
case mbErrors of
|
||||||
(err, out) <- splitGhciOutput ghci cmd
|
Nothing -> do
|
||||||
if not (null err)
|
renderWarning req "Frame render timed out."
|
||||||
then do
|
forkIO $ stopGhci ghci
|
||||||
logMsg $ "Error:\n" ++ unlines err
|
modifyMVar_ ghciRef (const newGhci)
|
||||||
renderError req (unlines err)
|
Just [] | isDone ->
|
||||||
else action out
|
logMsg "Render finished."
|
||||||
guardWanted req action = do
|
Just [] ->
|
||||||
wanted <- readMVar (renderWanted req)
|
renderFrames dur
|
||||||
unless wanted $ logMsg "Results no longer wanted"
|
Just errMsgs -> do
|
||||||
when wanted action
|
logMsg $ "Error:\n" ++ take charLimit (unlines errMsgs)
|
||||||
|
renderError req (take charLimit (unlines errMsgs))
|
||||||
|
|
||||||
|
guardWanted $ withHaskellFile (renderCode req) $ \hs -> do
|
||||||
|
catch @GhciError
|
||||||
|
(loadAndRender hs)
|
||||||
|
(\_ -> restartGhci)
|
||||||
|
return $ Backend ghciRef queue
|
||||||
|
|
||||||
|
getDirectorySize :: FilePath -> IO Integer
|
||||||
|
getDirectorySize root = do
|
||||||
|
files <- getDirectoryContents root
|
||||||
|
sum <$> mapM getFileSize [ root </> file | file <- files, takeExtension file == ".svg" ]
|
||||||
|
|
||||||
cacheDir :: IO FilePath
|
cacheDir :: IO FilePath
|
||||||
cacheDir = do
|
cacheDir = do
|
||||||
|
|
@ -302,6 +384,11 @@ withHaskellFile m action = withSystemTempFile "playground.hs" $ \target h -> do
|
||||||
\import Linear.Vector\n\
|
\import Linear.Vector\n\
|
||||||
\import Text.Printf\n\
|
\import Text.Printf\n\
|
||||||
\import Codec.Picture.Types\n\
|
\import Codec.Picture.Types\n\
|
||||||
|
\-- Used for testing:\n\
|
||||||
|
\-- import System.IO.Unsafe\n\
|
||||||
|
\-- import Control.Concurrent\n\
|
||||||
|
\-- import Control.Exception\n\
|
||||||
|
\-- svgDelay d = unsafePerformIO (threadDelay d >> evaluate SVG.None)\n\
|
||||||
\{-# LINE 1 \"playground\" #-}\n"
|
\{-# LINE 1 \"playground\" #-}\n"
|
||||||
T.appendFile target $ T.pack $ prettyPrint m
|
T.appendFile target $ T.pack $ prettyPrint m
|
||||||
action target
|
action target
|
||||||
|
|
@ -323,6 +410,18 @@ reqGhcOutput ghci cmd = do
|
||||||
error (unlines err)
|
error (unlines err)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
streamGhci :: Ghci -> String -> (String -> IO ()) -> IO (Maybe [String])
|
||||||
|
streamGhci ghci cmd cb = do
|
||||||
|
err <- newIORef []
|
||||||
|
ret <- timeout tLimit $ execStream ghci cmd $ \strm msg ->
|
||||||
|
case strm of
|
||||||
|
Stdout -> cb msg
|
||||||
|
Stderr -> modifyIORef err (++[msg])
|
||||||
|
errMsgs <- readIORef err
|
||||||
|
pure (ret >> pure errMsgs)
|
||||||
|
where
|
||||||
|
tLimit = round (frameTimeLimit * 1e6)
|
||||||
|
|
||||||
logMsg :: String -> IO ()
|
logMsg :: String -> IO ()
|
||||||
logMsg msg = do
|
logMsg msg = do
|
||||||
now <- getCurrentTime
|
now <- getCurrentTime
|
||||||
|
|
@ -357,6 +456,7 @@ parseHaskell txt =
|
||||||
data WebMessage
|
data WebMessage
|
||||||
= WebStatus String
|
= WebStatus String
|
||||||
| WebError String
|
| WebError String
|
||||||
|
| WebWarning String
|
||||||
| WebFrameCount Int
|
| WebFrameCount Int
|
||||||
| WebFrame Int FilePath
|
| WebFrame Int FilePath
|
||||||
|
|
||||||
|
|
@ -365,6 +465,7 @@ sendWebMessage conn msg = sendTextData conn $
|
||||||
case msg of
|
case msg of
|
||||||
WebStatus txt -> T.pack "status\n" <> T.pack txt
|
WebStatus txt -> T.pack "status\n" <> T.pack txt
|
||||||
WebError txt -> T.pack "error\n" <> T.pack txt
|
WebError txt -> T.pack "error\n" <> T.pack txt
|
||||||
|
WebWarning txt -> T.pack "warning\n" <> T.pack txt
|
||||||
WebFrameCount n -> T.pack $ "frame_count\n" ++ show n
|
WebFrameCount n -> T.pack $ "frame_count\n" ++ show n
|
||||||
WebFrame n path -> T.pack $ "frame\n" ++ show n ++ "\n" ++ path
|
WebFrame n path -> T.pack $ "frame\n" ++ show n ++ "\n" ++ path
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 299 B After Width: | Height: | Size: 299 B |
|
Before Width: | Height: | Size: 714 B After Width: | Height: | Size: 714 B |
|
Before Width: | Height: | Size: 398 B After Width: | Height: | Size: 398 B |
|
Before Width: | Height: | Size: 190 B After Width: | Height: | Size: 190 B |
|
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 307 B |
|
Before Width: | Height: | Size: 302 B After Width: | Height: | Size: 302 B |
|
|
@ -58,6 +58,7 @@ function playgroundInit(elt) {
|
||||||
node: elt
|
node: elt
|
||||||
});
|
});
|
||||||
app.ports.sendSocketCommand.subscribe(sendSocketCommand);
|
app.ports.sendSocketCommand.subscribe(sendSocketCommand);
|
||||||
|
var tHandler = setTimeout(function(){},0);
|
||||||
return {
|
return {
|
||||||
play: function () {
|
play: function () {
|
||||||
app.ports.receiveControlMsg.send('play');
|
app.ports.receiveControlMsg.send('play');
|
||||||
|
|
@ -78,8 +79,11 @@ function playgroundInit(elt) {
|
||||||
app.ports.receiveControlMsg.send('seek-10');
|
app.ports.receiveControlMsg.send('seek-10');
|
||||||
},
|
},
|
||||||
newCode: function(code) {
|
newCode: function(code) {
|
||||||
|
clearTimeout(tHandler);
|
||||||
|
tHandler = setTimeout(function() {
|
||||||
lastScript = code;
|
lastScript = code;
|
||||||
app.ports.receiveEditorMsg.send(code);
|
app.ports.receiveEditorMsg.send(code);
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 190 B After Width: | Height: | Size: 190 B |
|
Before Width: | Height: | Size: 182 B After Width: | Height: | Size: 182 B |
|
|
@ -131,6 +131,7 @@ type alias Animation =
|
||||||
, player : Player
|
, player : Player
|
||||||
, bestFrame : Maybe String
|
, bestFrame : Maybe String
|
||||||
, frameDeltas : List Float
|
, frameDeltas : List Float
|
||||||
|
, warning : Maybe String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -142,6 +143,7 @@ initAnimation frameCount =
|
||||||
, player = Playing 0
|
, player = Playing 0
|
||||||
, bestFrame = Nothing
|
, bestFrame = Nothing
|
||||||
, frameDeltas = Fps.init
|
, frameDeltas = Fps.init
|
||||||
|
, warning = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -352,6 +354,12 @@ processMessage data model =
|
||||||
"error" :: errorLines ->
|
"error" :: errorLines ->
|
||||||
Problem (CompilationError (String.join "\n" errorLines))
|
Problem (CompilationError (String.join "\n" errorLines))
|
||||||
|
|
||||||
|
[ "warning", warning ] ->
|
||||||
|
case model of
|
||||||
|
Animating animation ->
|
||||||
|
Animating { animation | warning = Just warning }
|
||||||
|
_ -> Problem (CompilationError warning)
|
||||||
|
|
||||||
[ "frame_count", n ] ->
|
[ "frame_count", n ] ->
|
||||||
case String.toInt n of
|
case String.toInt n of
|
||||||
Just frameCount ->
|
Just frameCount ->
|
||||||
|
|
@ -396,8 +404,8 @@ view model =
|
||||||
Problem problem ->
|
Problem problem ->
|
||||||
problemView problem
|
problemView problem
|
||||||
|
|
||||||
Animating { bestFrame } ->
|
Animating { bestFrame, warning } ->
|
||||||
frameView bestFrame
|
frameView bestFrame warning
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -415,8 +423,8 @@ framesPerMillisecond =
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
frameView : Maybe String -> Html Msg
|
frameView : Maybe String -> Maybe String -> Html Msg
|
||||||
frameView bestFrame =
|
frameView bestFrame mbWarning =
|
||||||
let
|
let
|
||||||
image =
|
image =
|
||||||
case bestFrame of
|
case bestFrame of
|
||||||
|
|
@ -425,9 +433,14 @@ frameView bestFrame =
|
||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
Html.text ""
|
Html.text ""
|
||||||
|
warn =
|
||||||
|
case mbWarning of
|
||||||
|
Just txt -> Html.span [ class "warning" ] [Html.text txt]
|
||||||
|
Nothing -> Html.span [] []
|
||||||
in
|
in
|
||||||
Html.div [ class "viewer" ]
|
Html.div [ class "viewer" ]
|
||||||
[ image
|
[ image
|
||||||
|
, warn
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ module Reanimate.Render
|
||||||
( render
|
( render
|
||||||
, renderSvgs
|
, renderSvgs
|
||||||
, renderSnippets -- :: Animation -> IO ()
|
, renderSnippets -- :: Animation -> IO ()
|
||||||
, renderOneFrame
|
, renderLimitedFrames
|
||||||
, Format(..)
|
, Format(..)
|
||||||
, Raster(..)
|
, Raster(..)
|
||||||
, Width, Height, FPS
|
, Width, Height, FPS
|
||||||
|
|
@ -82,14 +82,21 @@ renderSvgs folder offset _prettyPrint ani = do
|
||||||
hPutStrLn stderr msg
|
hPutStrLn stderr msg
|
||||||
exitWith (ExitFailure 1)
|
exitWith (ExitFailure 1)
|
||||||
|
|
||||||
-- | Select a single frame that doesn't already exist in the output
|
-- | Render as many frames as possible in 2 seconds. Limited to 20 frames.
|
||||||
-- folder and render it. If all frames have been rendered, print "Done".
|
renderLimitedFrames :: FilePath -> Int -> Bool -> Int -> Animation -> IO ()
|
||||||
renderOneFrame :: FilePath -> Int -> Bool -> Int -> Animation -> IO ()
|
renderLimitedFrames folder offset _prettyPrint rate ani = do
|
||||||
renderOneFrame folder offset _prettyPrint rate ani =
|
now <- getCurrentTime
|
||||||
worker (frameOrder rate frameCount)
|
worker (addUTCTime timeLimit now) frameLimit (frameOrder rate frameCount)
|
||||||
where
|
where
|
||||||
worker [] = putStrLn "Done"
|
timeLimit = 2
|
||||||
worker (x:xs) = do
|
frameLimit = 20 :: Int
|
||||||
|
worker _ 0 _ = return ()
|
||||||
|
worker _ _ [] = putStrLn "Done"
|
||||||
|
worker localTimeLimit l (x:xs) = do
|
||||||
|
curTime <- getCurrentTime
|
||||||
|
if curTime > localTimeLimit
|
||||||
|
then return ()
|
||||||
|
else do
|
||||||
let nth = (x+offset) `mod` frameCount
|
let nth = (x+offset) `mod` frameCount
|
||||||
now = (duration ani / (fromIntegral frameCount - 1)) * fromIntegral nth
|
now = (duration ani / (fromIntegral frameCount - 1)) * fromIntegral nth
|
||||||
frame = frameAt (if frameCount <= 1 then 0 else now) ani
|
frame = frameAt (if frameCount <= 1 then 0 else now) ani
|
||||||
|
|
@ -98,11 +105,12 @@ renderOneFrame folder offset _prettyPrint rate ani =
|
||||||
tmpPath = path <.> "tmp"
|
tmpPath = path <.> "tmp"
|
||||||
haveFile <- doesFileExist path
|
haveFile <- doesFileExist path
|
||||||
if haveFile
|
if haveFile
|
||||||
then worker xs
|
then worker localTimeLimit l xs
|
||||||
else do
|
else do
|
||||||
writeFile tmpPath svg
|
writeFile tmpPath svg
|
||||||
renameOrCopyFile tmpPath path
|
renameOrCopyFile tmpPath path
|
||||||
print nth
|
print nth
|
||||||
|
worker localTimeLimit (l-1) xs
|
||||||
frameCount = round (duration ani * fromIntegral rate) :: Int
|
frameCount = round (duration ani * fromIntegral rate) :: Int
|
||||||
|
|
||||||
-- XXX: Merge with 'renderSvgs'
|
-- XXX: Merge with 'renderSvgs'
|
||||||
|
|
|
||||||