2015-04-04 18:34:03 +00:00
|
|
|
{- Metered IO and actions
|
2013-03-28 21:03:04 +00:00
|
|
|
-
|
2018-03-13 01:46:58 +00:00
|
|
|
- Copyright 2012-2018 Joey Hess <id@joeyh.name>
|
2013-03-28 21:03:04 +00:00
|
|
|
-
|
2014-05-10 14:01:27 +00:00
|
|
|
- License: BSD-2-clause
|
2013-03-28 21:03:04 +00:00
|
|
|
-}
|
|
|
|
|
2016-12-08 20:28:07 +00:00
|
|
|
{-# LANGUAGE TypeSynonymInstances, BangPatterns #-}
|
2013-03-28 21:03:04 +00:00
|
|
|
|
2019-11-21 19:38:06 +00:00
|
|
|
module Utility.Metered (
|
|
|
|
MeterUpdate,
|
|
|
|
nullMeterUpdate,
|
|
|
|
combineMeterUpdate,
|
|
|
|
BytesProcessed(..),
|
|
|
|
toBytesProcessed,
|
|
|
|
fromBytesProcessed,
|
|
|
|
addBytesProcessed,
|
|
|
|
zeroBytesProcessed,
|
|
|
|
withMeteredFile,
|
|
|
|
meteredWrite,
|
|
|
|
meteredWrite',
|
|
|
|
meteredWriteFile,
|
|
|
|
offsetMeterUpdate,
|
|
|
|
hGetContentsMetered,
|
|
|
|
hGetMetered,
|
|
|
|
defaultChunkSize,
|
|
|
|
watchFileSize,
|
|
|
|
OutputHandler(..),
|
|
|
|
ProgressParser,
|
|
|
|
commandMeter,
|
|
|
|
commandMeter',
|
|
|
|
demeterCommand,
|
|
|
|
demeterCommandEnv,
|
|
|
|
avoidProgress,
|
|
|
|
rateLimitMeterUpdate,
|
|
|
|
Meter,
|
|
|
|
mkMeter,
|
|
|
|
setMeterTotalSize,
|
|
|
|
updateMeter,
|
|
|
|
displayMeterHandle,
|
|
|
|
clearMeterHandle,
|
|
|
|
bandwidthMeter,
|
|
|
|
) where
|
2013-03-28 21:03:04 +00:00
|
|
|
|
|
|
|
import Common
|
2017-05-16 03:32:17 +00:00
|
|
|
import Utility.Percentage
|
|
|
|
import Utility.DataUnits
|
|
|
|
import Utility.HumanTime
|
2013-03-28 21:03:04 +00:00
|
|
|
|
|
|
|
import qualified Data.ByteString.Lazy as L
|
|
|
|
import qualified Data.ByteString as S
|
|
|
|
import System.IO.Unsafe
|
|
|
|
import Foreign.Storable (Storable(sizeOf))
|
|
|
|
import System.Posix.Types
|
2014-07-25 20:20:32 +00:00
|
|
|
import Data.Int
|
2015-11-17 00:27:01 +00:00
|
|
|
import Control.Concurrent
|
2015-04-03 20:48:30 +00:00
|
|
|
import Control.Concurrent.Async
|
2015-11-17 00:27:01 +00:00
|
|
|
import Control.Monad.IO.Class (MonadIO)
|
2016-09-08 17:17:43 +00:00
|
|
|
import Data.Time.Clock
|
|
|
|
import Data.Time.Clock.POSIX
|
2013-03-28 21:03:04 +00:00
|
|
|
|
|
|
|
{- An action that can be run repeatedly, updating it on the bytes processed.
|
|
|
|
-
|
|
|
|
- Note that each call receives the total number of bytes processed, so
|
|
|
|
- far, *not* an incremental amount since the last call. -}
|
|
|
|
type MeterUpdate = (BytesProcessed -> IO ())
|
|
|
|
|
2014-08-01 19:09:49 +00:00
|
|
|
nullMeterUpdate :: MeterUpdate
|
|
|
|
nullMeterUpdate _ = return ()
|
|
|
|
|
2015-11-16 23:32:30 +00:00
|
|
|
combineMeterUpdate :: MeterUpdate -> MeterUpdate -> MeterUpdate
|
|
|
|
combineMeterUpdate a b = \n -> a n >> b n
|
|
|
|
|
2013-03-28 21:03:04 +00:00
|
|
|
{- Total number of bytes processed so far. -}
|
|
|
|
newtype BytesProcessed = BytesProcessed Integer
|
external special remotes mostly implemented (untested)
This has not been tested at all. It compiles!
The only known missing things are support for encryption, and for get/set
of special remote configuration, and of key state. (The latter needs
separate work to add a new per-key log file to store that state.)
Only thing I don't much like is that initremote needs to be passed both
type=external and externaltype=foo. It would be better to have just
type=foo
Most of this is quite straightforward code, that largely wrote itself given
the types. The only tricky parts were:
* Need to lock the remote when using it to eg make a request, because
in theory git-annex could have multiple threads that each try to use
a remote at the same time. I don't think that git-annex ever does
that currently, but better safe than sorry.
* Rather than starting up every external special remote program when
git-annex starts, they are started only on demand, when first used.
This will avoid slowdown, especially when running fast git-annex query
commands. Once started, they keep running until git-annex stops, currently,
which may not be ideal, but it's hard to know a better time to stop them.
* Bit of a chicken and egg problem with caching the cost of the remote,
because setting annex-cost in the git config needs the remote to already
be set up. Managed to finesse that.
This commit was sponsored by Lukas Anzinger.
2013-12-26 22:23:13 +00:00
|
|
|
deriving (Eq, Ord, Show)
|
2013-03-28 21:03:04 +00:00
|
|
|
|
|
|
|
class AsBytesProcessed a where
|
|
|
|
toBytesProcessed :: a -> BytesProcessed
|
|
|
|
fromBytesProcessed :: BytesProcessed -> a
|
|
|
|
|
2014-07-25 20:20:32 +00:00
|
|
|
instance AsBytesProcessed BytesProcessed where
|
|
|
|
toBytesProcessed = id
|
|
|
|
fromBytesProcessed = id
|
|
|
|
|
2013-03-28 21:03:04 +00:00
|
|
|
instance AsBytesProcessed Integer where
|
|
|
|
toBytesProcessed i = BytesProcessed i
|
|
|
|
fromBytesProcessed (BytesProcessed i) = i
|
|
|
|
|
|
|
|
instance AsBytesProcessed Int where
|
|
|
|
toBytesProcessed i = BytesProcessed $ toInteger i
|
|
|
|
fromBytesProcessed (BytesProcessed i) = fromInteger i
|
|
|
|
|
2014-07-25 20:20:32 +00:00
|
|
|
instance AsBytesProcessed Int64 where
|
|
|
|
toBytesProcessed i = BytesProcessed $ toInteger i
|
|
|
|
fromBytesProcessed (BytesProcessed i) = fromInteger i
|
|
|
|
|
2013-03-28 21:03:04 +00:00
|
|
|
instance AsBytesProcessed FileOffset where
|
|
|
|
toBytesProcessed sz = BytesProcessed $ toInteger sz
|
|
|
|
fromBytesProcessed (BytesProcessed sz) = fromInteger sz
|
|
|
|
|
|
|
|
addBytesProcessed :: AsBytesProcessed v => BytesProcessed -> v -> BytesProcessed
|
|
|
|
addBytesProcessed (BytesProcessed i) v =
|
|
|
|
let (BytesProcessed n) = toBytesProcessed v
|
|
|
|
in BytesProcessed $! i + n
|
|
|
|
|
|
|
|
zeroBytesProcessed :: BytesProcessed
|
|
|
|
zeroBytesProcessed = BytesProcessed 0
|
|
|
|
|
|
|
|
{- Sends the content of a file to an action, updating the meter as it's
|
|
|
|
- consumed. -}
|
|
|
|
withMeteredFile :: FilePath -> MeterUpdate -> (L.ByteString -> IO a) -> IO a
|
|
|
|
withMeteredFile f meterupdate a = withBinaryFile f ReadMode $ \h ->
|
|
|
|
hGetContentsMetered h meterupdate >>= a
|
|
|
|
|
|
|
|
{- Writes a ByteString to a Handle, updating a meter as it's written. -}
|
|
|
|
meteredWrite :: MeterUpdate -> Handle -> L.ByteString -> IO ()
|
2016-12-07 17:37:35 +00:00
|
|
|
meteredWrite meterupdate h = void . meteredWrite' meterupdate h
|
|
|
|
|
|
|
|
meteredWrite' :: MeterUpdate -> Handle -> L.ByteString -> IO BytesProcessed
|
|
|
|
meteredWrite' meterupdate h = go zeroBytesProcessed . L.toChunks
|
2013-03-28 21:03:04 +00:00
|
|
|
where
|
2016-12-07 17:37:35 +00:00
|
|
|
go sofar [] = return sofar
|
2013-03-28 21:03:04 +00:00
|
|
|
go sofar (c:cs) = do
|
|
|
|
S.hPut h c
|
2016-12-08 20:28:07 +00:00
|
|
|
let !sofar' = addBytesProcessed sofar $ S.length c
|
2013-03-28 21:03:04 +00:00
|
|
|
meterupdate sofar'
|
|
|
|
go sofar' cs
|
|
|
|
|
|
|
|
meteredWriteFile :: MeterUpdate -> FilePath -> L.ByteString -> IO ()
|
|
|
|
meteredWriteFile meterupdate f b = withBinaryFile f WriteMode $ \h ->
|
|
|
|
meteredWrite meterupdate h b
|
|
|
|
|
2014-07-25 20:20:32 +00:00
|
|
|
{- Applies an offset to a MeterUpdate. This can be useful when
|
|
|
|
- performing a sequence of actions, such as multiple meteredWriteFiles,
|
resume interrupted chunked downloads
Leverage the new chunked remotes to automatically resume downloads.
Sort of like rsync, although of course not as efficient since this
needs to start at a chunk boundry.
But, unlike rsync, this method will work for S3, WebDAV, external
special remotes, etc, etc. Only directory special remotes so far,
but many more soon!
This implementation will also properly handle starting a download
from one remote, interrupting, and resuming from another one, and so on.
(Resuming interrupted chunked uploads is similarly doable, although
slightly more expensive.)
This commit was sponsored by Thomas Djärv.
2014-07-27 22:52:42 +00:00
|
|
|
- that all update a common meter progressively. Or when resuming.
|
2014-07-25 20:20:32 +00:00
|
|
|
-}
|
|
|
|
offsetMeterUpdate :: MeterUpdate -> BytesProcessed -> MeterUpdate
|
|
|
|
offsetMeterUpdate base offset = \n -> base (offset `addBytesProcessed` n)
|
|
|
|
|
2013-03-28 21:03:04 +00:00
|
|
|
{- This is like L.hGetContents, but after each chunk is read, a meter
|
|
|
|
- is updated based on the size of the chunk.
|
2014-11-03 22:37:05 +00:00
|
|
|
-
|
|
|
|
- All the usual caveats about using unsafeInterleaveIO apply to the
|
|
|
|
- meter updates, so use caution.
|
|
|
|
-}
|
|
|
|
hGetContentsMetered :: Handle -> MeterUpdate -> IO L.ByteString
|
2016-12-07 18:25:01 +00:00
|
|
|
hGetContentsMetered h = hGetMetered h Nothing
|
2014-11-03 22:37:05 +00:00
|
|
|
|
2016-12-07 18:25:01 +00:00
|
|
|
{- Reads from the Handle, updating the meter after each chunk is read.
|
|
|
|
-
|
|
|
|
- Stops at EOF, or when the requested number of bytes have been read.
|
|
|
|
- Closes the Handle at EOF, but otherwise leaves it open.
|
2013-03-28 21:03:04 +00:00
|
|
|
-
|
|
|
|
- Note that the meter update is run in unsafeInterleaveIO, which means that
|
|
|
|
- it can be run at any time. It's even possible for updates to run out
|
|
|
|
- of order, as different parts of the ByteString are consumed.
|
|
|
|
-}
|
2016-12-07 18:25:01 +00:00
|
|
|
hGetMetered :: Handle -> Maybe Integer -> MeterUpdate -> IO L.ByteString
|
|
|
|
hGetMetered h wantsize meterupdate = lazyRead zeroBytesProcessed
|
2013-03-28 21:03:04 +00:00
|
|
|
where
|
|
|
|
lazyRead sofar = unsafeInterleaveIO $ loop sofar
|
|
|
|
|
|
|
|
loop sofar = do
|
2016-12-07 18:25:01 +00:00
|
|
|
c <- S.hGet h (nextchunksize (fromBytesProcessed sofar))
|
2013-03-28 21:03:04 +00:00
|
|
|
if S.null c
|
|
|
|
then do
|
|
|
|
hClose h
|
|
|
|
return $ L.empty
|
|
|
|
else do
|
2016-12-08 20:28:07 +00:00
|
|
|
let !sofar' = addBytesProcessed sofar (S.length c)
|
2013-03-28 21:03:04 +00:00
|
|
|
meterupdate sofar'
|
2014-11-03 22:37:05 +00:00
|
|
|
if keepgoing (fromBytesProcessed sofar')
|
|
|
|
then do
|
|
|
|
{- unsafeInterleaveIO causes this to be
|
|
|
|
- deferred until the data is read from the
|
|
|
|
- ByteString. -}
|
|
|
|
cs <- lazyRead sofar'
|
|
|
|
return $ L.append (L.fromChunks [c]) cs
|
|
|
|
else return $ L.fromChunks [c]
|
2016-12-07 18:25:01 +00:00
|
|
|
|
|
|
|
keepgoing n = case wantsize of
|
|
|
|
Nothing -> True
|
|
|
|
Just sz -> n < sz
|
|
|
|
|
|
|
|
nextchunksize n = case wantsize of
|
|
|
|
Nothing -> defaultChunkSize
|
|
|
|
Just sz ->
|
|
|
|
let togo = sz - n
|
|
|
|
in if togo < toInteger defaultChunkSize
|
|
|
|
then fromIntegral togo
|
|
|
|
else defaultChunkSize
|
2013-03-28 21:03:04 +00:00
|
|
|
|
|
|
|
{- Same default chunk size Lazy ByteStrings use. -}
|
|
|
|
defaultChunkSize :: Int
|
|
|
|
defaultChunkSize = 32 * k - chunkOverhead
|
|
|
|
where
|
|
|
|
k = 1024
|
2015-04-19 04:38:29 +00:00
|
|
|
chunkOverhead = 2 * sizeOf (1 :: Int) -- GHC specific
|
2014-12-17 17:21:55 +00:00
|
|
|
|
2017-05-25 18:30:18 +00:00
|
|
|
{- Runs an action, watching a file as it grows and updating the meter.
|
|
|
|
-
|
|
|
|
- The file may already exist, and the action could throw the original file
|
|
|
|
- away and start over. To avoid reporting the original file size followed
|
|
|
|
- by a smaller size in that case, wait until the file starts growing
|
|
|
|
- before updating the meter for the first time.
|
|
|
|
-}
|
2015-11-17 00:27:01 +00:00
|
|
|
watchFileSize :: (MonadIO m, MonadMask m) => FilePath -> MeterUpdate -> m a -> m a
|
|
|
|
watchFileSize f p a = bracket
|
2017-05-25 18:30:18 +00:00
|
|
|
(liftIO $ forkIO $ watcher =<< getsz)
|
2015-11-17 00:27:01 +00:00
|
|
|
(liftIO . void . tryIO . killThread)
|
|
|
|
(const a)
|
|
|
|
where
|
|
|
|
watcher oldsz = do
|
|
|
|
threadDelay 500000 -- 0.5 seconds
|
2017-05-25 18:30:18 +00:00
|
|
|
sz <- getsz
|
|
|
|
when (sz > oldsz) $
|
|
|
|
p sz
|
|
|
|
watcher sz
|
|
|
|
getsz = catchDefaultIO zeroBytesProcessed $
|
|
|
|
toBytesProcessed <$> getFileSize f
|
2015-11-17 00:27:01 +00:00
|
|
|
|
2015-04-04 18:34:03 +00:00
|
|
|
data OutputHandler = OutputHandler
|
|
|
|
{ quietMode :: Bool
|
|
|
|
, stderrHandler :: String -> IO ()
|
|
|
|
}
|
|
|
|
|
2014-12-17 17:21:55 +00:00
|
|
|
{- Parses the String looking for a command's progress output, and returns
|
2015-04-03 20:48:30 +00:00
|
|
|
- Maybe the number of bytes done so far, and any any remainder of the
|
2014-12-17 17:21:55 +00:00
|
|
|
- string that could be an incomplete progress output. That remainder
|
|
|
|
- should be prepended to future output, and fed back in. This interface
|
|
|
|
- allows the command's output to be read in any desired size chunk, or
|
|
|
|
- even one character at a time.
|
|
|
|
-}
|
|
|
|
type ProgressParser = String -> (Maybe BytesProcessed, String)
|
|
|
|
|
|
|
|
{- Runs a command and runs a ProgressParser on its output, in order
|
2015-04-03 20:48:30 +00:00
|
|
|
- to update a meter.
|
|
|
|
-}
|
2015-04-04 18:34:03 +00:00
|
|
|
commandMeter :: ProgressParser -> OutputHandler -> MeterUpdate -> FilePath -> [CommandParam] -> IO Bool
|
2019-08-15 18:47:22 +00:00
|
|
|
commandMeter progressparser oh meterupdate cmd params = do
|
|
|
|
ret <- commandMeter' progressparser oh meterupdate cmd params
|
|
|
|
return $ case ret of
|
|
|
|
Just ExitSuccess -> True
|
|
|
|
_ -> False
|
|
|
|
|
|
|
|
commandMeter' :: ProgressParser -> OutputHandler -> MeterUpdate -> FilePath -> [CommandParam] -> IO (Maybe ExitCode)
|
|
|
|
commandMeter' progressparser oh meterupdate cmd params =
|
2015-04-07 00:18:57 +00:00
|
|
|
outputFilter cmd params Nothing
|
|
|
|
(feedprogress zeroBytesProcessed [])
|
|
|
|
handlestderr
|
2014-12-17 17:21:55 +00:00
|
|
|
where
|
|
|
|
feedprogress prev buf h = do
|
2015-02-10 16:34:34 +00:00
|
|
|
b <- S.hGetSome h 80
|
|
|
|
if S.null b
|
2015-04-07 00:18:57 +00:00
|
|
|
then return ()
|
2014-12-17 17:21:55 +00:00
|
|
|
else do
|
2015-04-04 18:34:03 +00:00
|
|
|
unless (quietMode oh) $ do
|
2015-04-03 20:48:30 +00:00
|
|
|
S.hPut stdout b
|
|
|
|
hFlush stdout
|
2019-12-18 17:26:06 +00:00
|
|
|
let s = decodeBS b
|
2014-12-17 17:21:55 +00:00
|
|
|
let (mbytes, buf') = progressparser (buf++s)
|
|
|
|
case mbytes of
|
|
|
|
Nothing -> feedprogress prev buf' h
|
|
|
|
(Just bytes) -> do
|
|
|
|
when (bytes /= prev) $
|
2015-04-04 18:34:03 +00:00
|
|
|
meterupdate bytes
|
2014-12-17 17:21:55 +00:00
|
|
|
feedprogress bytes buf' h
|
2015-04-04 18:34:03 +00:00
|
|
|
|
|
|
|
handlestderr h = unlessM (hIsEOF h) $ do
|
|
|
|
stderrHandler oh =<< hGetLine h
|
|
|
|
handlestderr h
|
|
|
|
|
|
|
|
{- Runs a command, that may display one or more progress meters on
|
|
|
|
- either stdout or stderr, and prevents the meters from being displayed.
|
|
|
|
-
|
|
|
|
- The other command output is handled as configured by the OutputHandler.
|
|
|
|
-}
|
|
|
|
demeterCommand :: OutputHandler -> FilePath -> [CommandParam] -> IO Bool
|
|
|
|
demeterCommand oh cmd params = demeterCommandEnv oh cmd params Nothing
|
|
|
|
|
|
|
|
demeterCommandEnv :: OutputHandler -> FilePath -> [CommandParam] -> Maybe [(String, String)] -> IO Bool
|
2019-08-15 18:47:22 +00:00
|
|
|
demeterCommandEnv oh cmd params environ = do
|
|
|
|
ret <- outputFilter cmd params environ
|
|
|
|
(\outh -> avoidProgress True outh stdouthandler)
|
|
|
|
(\errh -> avoidProgress True errh $ stderrHandler oh)
|
|
|
|
return $ case ret of
|
|
|
|
Just ExitSuccess -> True
|
|
|
|
_ -> False
|
2015-04-04 18:34:03 +00:00
|
|
|
where
|
2015-04-07 00:18:57 +00:00
|
|
|
stdouthandler l =
|
|
|
|
unless (quietMode oh) $
|
|
|
|
putStrLn l
|
2015-04-04 18:34:03 +00:00
|
|
|
|
2015-04-04 18:53:17 +00:00
|
|
|
{- To suppress progress output, while displaying other messages,
|
|
|
|
- filter out lines that contain \r (typically used to reset to the
|
|
|
|
- beginning of the line when updating a progress display).
|
|
|
|
-}
|
|
|
|
avoidProgress :: Bool -> Handle -> (String -> IO ()) -> IO ()
|
|
|
|
avoidProgress doavoid h emitter = unlessM (hIsEOF h) $ do
|
|
|
|
s <- hGetLine h
|
|
|
|
unless (doavoid && '\r' `elem` s) $
|
|
|
|
emitter s
|
|
|
|
avoidProgress doavoid h emitter
|
2015-04-07 00:18:57 +00:00
|
|
|
|
|
|
|
outputFilter
|
|
|
|
:: FilePath
|
|
|
|
-> [CommandParam]
|
|
|
|
-> Maybe [(String, String)]
|
|
|
|
-> (Handle -> IO ())
|
|
|
|
-> (Handle -> IO ())
|
2019-08-15 18:47:22 +00:00
|
|
|
-> IO (Maybe ExitCode)
|
|
|
|
outputFilter cmd params environ outfilter errfilter = catchMaybeIO $ do
|
2015-04-07 00:18:57 +00:00
|
|
|
(_, Just outh, Just errh, pid) <- createProcess p
|
|
|
|
{ std_out = CreatePipe
|
|
|
|
, std_err = CreatePipe
|
|
|
|
}
|
|
|
|
void $ async $ tryIO (outfilter outh) >> hClose outh
|
|
|
|
void $ async $ tryIO (errfilter errh) >> hClose errh
|
2019-08-15 18:47:22 +00:00
|
|
|
waitForProcess pid
|
2015-04-07 00:18:57 +00:00
|
|
|
where
|
|
|
|
p = (proc cmd (toCommand params))
|
|
|
|
{ env = environ }
|
2016-09-08 17:17:43 +00:00
|
|
|
|
|
|
|
-- | Limit a meter to only update once per unit of time.
|
|
|
|
--
|
|
|
|
-- It's nice to display the final update to 100%, even if it comes soon
|
2018-03-13 01:46:58 +00:00
|
|
|
-- after a previous update. To make that happen, the Meter has to know
|
|
|
|
-- its total size.
|
|
|
|
rateLimitMeterUpdate :: NominalDiffTime -> Meter -> MeterUpdate -> IO MeterUpdate
|
|
|
|
rateLimitMeterUpdate delta (Meter totalsizev _ _ _) meterupdate = do
|
2016-09-08 17:17:43 +00:00
|
|
|
lastupdate <- newMVar (toEnum 0 :: POSIXTime)
|
|
|
|
return $ mu lastupdate
|
|
|
|
where
|
2018-03-16 16:06:45 +00:00
|
|
|
mu lastupdate n@(BytesProcessed i) = readMVar totalsizev >>= \case
|
2016-09-08 17:17:43 +00:00
|
|
|
Just t | i >= t -> meterupdate n
|
|
|
|
_ -> do
|
|
|
|
now <- getPOSIXTime
|
|
|
|
prev <- takeMVar lastupdate
|
|
|
|
if now - prev >= delta
|
|
|
|
then do
|
|
|
|
putMVar lastupdate now
|
|
|
|
meterupdate n
|
|
|
|
else putMVar lastupdate prev
|
2017-05-16 03:32:17 +00:00
|
|
|
|
2018-03-16 16:06:45 +00:00
|
|
|
data Meter = Meter (MVar (Maybe Integer)) (MVar MeterState) (MVar String) DisplayMeter
|
2017-05-16 03:32:17 +00:00
|
|
|
|
|
|
|
type MeterState = (BytesProcessed, POSIXTime)
|
|
|
|
|
2018-03-13 01:46:58 +00:00
|
|
|
type DisplayMeter = MVar String -> Maybe Integer -> (BytesProcessed, POSIXTime) -> (BytesProcessed, POSIXTime) -> IO ()
|
2017-05-16 03:32:17 +00:00
|
|
|
|
|
|
|
type RenderMeter = Maybe Integer -> (BytesProcessed, POSIXTime) -> (BytesProcessed, POSIXTime) -> String
|
|
|
|
|
|
|
|
-- | Make a meter. Pass the total size, if it's known.
|
2018-03-13 01:46:58 +00:00
|
|
|
mkMeter :: Maybe Integer -> DisplayMeter -> IO Meter
|
|
|
|
mkMeter totalsize displaymeter = Meter
|
2018-03-16 16:06:45 +00:00
|
|
|
<$> newMVar totalsize
|
2017-05-16 03:32:17 +00:00
|
|
|
<*> ((\t -> newMVar (zeroBytesProcessed, t)) =<< getPOSIXTime)
|
|
|
|
<*> newMVar ""
|
|
|
|
<*> pure displaymeter
|
|
|
|
|
2018-03-13 01:46:58 +00:00
|
|
|
setMeterTotalSize :: Meter -> Integer -> IO ()
|
2018-03-16 16:06:45 +00:00
|
|
|
setMeterTotalSize (Meter totalsizev _ _ _) = void . swapMVar totalsizev . Just
|
2018-03-13 01:46:58 +00:00
|
|
|
|
2017-05-16 03:32:17 +00:00
|
|
|
-- | Updates the meter, displaying it if necessary.
|
2018-04-06 19:58:16 +00:00
|
|
|
updateMeter :: Meter -> MeterUpdate
|
2018-03-13 01:46:58 +00:00
|
|
|
updateMeter (Meter totalsizev sv bv displaymeter) new = do
|
2017-05-16 03:32:17 +00:00
|
|
|
now <- getPOSIXTime
|
|
|
|
(old, before) <- swapMVar sv (new, now)
|
2018-03-13 01:46:58 +00:00
|
|
|
when (old /= new) $ do
|
2018-03-16 16:06:45 +00:00
|
|
|
totalsize <- readMVar totalsizev
|
2018-03-13 01:46:58 +00:00
|
|
|
displaymeter bv totalsize (old, before) (new, now)
|
2017-05-16 03:32:17 +00:00
|
|
|
|
|
|
|
-- | Display meter to a Handle.
|
2018-03-13 01:46:58 +00:00
|
|
|
displayMeterHandle :: Handle -> RenderMeter -> DisplayMeter
|
|
|
|
displayMeterHandle h rendermeter v msize old new = do
|
|
|
|
let s = rendermeter msize old new
|
2017-05-16 03:32:17 +00:00
|
|
|
olds <- swapMVar v s
|
|
|
|
-- Avoid writing when the rendered meter has not changed.
|
|
|
|
when (olds /= s) $ do
|
|
|
|
let padding = replicate (length olds - length s) ' '
|
|
|
|
hPutStr h ('\r':s ++ padding)
|
|
|
|
hFlush h
|
|
|
|
|
|
|
|
-- | Clear meter displayed by displayMeterHandle.
|
|
|
|
clearMeterHandle :: Meter -> Handle -> IO ()
|
2018-03-13 01:46:58 +00:00
|
|
|
clearMeterHandle (Meter _ _ v _) h = do
|
2017-05-16 03:32:17 +00:00
|
|
|
olds <- readMVar v
|
|
|
|
hPutStr h $ '\r' : replicate (length olds) ' ' ++ "\r"
|
|
|
|
hFlush h
|
|
|
|
|
|
|
|
-- | Display meter in the form:
|
2018-03-14 17:39:14 +00:00
|
|
|
-- 10% 1.3MiB 300 KiB/s 16m40s
|
2017-05-16 03:32:17 +00:00
|
|
|
-- or when total size is not known:
|
2018-03-14 17:39:14 +00:00
|
|
|
-- 1.3 MiB 300 KiB/s
|
2017-05-16 03:32:17 +00:00
|
|
|
bandwidthMeter :: RenderMeter
|
|
|
|
bandwidthMeter mtotalsize (BytesProcessed old, before) (BytesProcessed new, now) =
|
|
|
|
unwords $ catMaybes
|
2018-03-14 17:39:14 +00:00
|
|
|
[ Just percentamount
|
|
|
|
-- Pad enough for max width: "100% xxxx.xx KiB xxxx KiB/s"
|
|
|
|
, Just $ replicate (29 - length percentamount - length rate) ' '
|
2017-05-16 03:32:17 +00:00
|
|
|
, Just rate
|
|
|
|
, estimatedcompletion
|
|
|
|
]
|
|
|
|
where
|
2018-03-14 17:39:14 +00:00
|
|
|
amount = roughSize' memoryUnits True 2 new
|
|
|
|
percentamount = case mtotalsize of
|
|
|
|
Just totalsize ->
|
|
|
|
let p = showPercentage 0 $
|
|
|
|
percentage totalsize (min new totalsize)
|
|
|
|
in p ++ replicate (6 - length p) ' ' ++ amount
|
|
|
|
Nothing -> amount
|
2017-05-16 03:32:17 +00:00
|
|
|
rate = roughSize' memoryUnits True 0 bytespersecond ++ "/s"
|
|
|
|
bytespersecond
|
|
|
|
| duration == 0 = fromIntegral transferred
|
|
|
|
| otherwise = floor $ fromIntegral transferred / duration
|
|
|
|
transferred = max 0 (new - old)
|
|
|
|
duration = max 0 (now - before)
|
|
|
|
estimatedcompletion = case mtotalsize of
|
|
|
|
Just totalsize
|
|
|
|
| bytespersecond > 0 ->
|
|
|
|
Just $ fromDuration $ Duration $
|
2018-03-20 03:26:41 +00:00
|
|
|
(totalsize - new) `div` bytespersecond
|
2017-05-16 03:32:17 +00:00
|
|
|
_ -> Nothing
|