{-# LANGUAGE CPP #-}
module Darcs.Util.Download
( setDebugHTTP
, disableHTTPPipelining
, maxPipelineLength
, Cachable(Cachable, Uncachable, MaxAge)
, environmentHelpProxy
, environmentHelpProxyPassword
, ConnectionError
#ifdef HAVE_CURL
, copyUrl
, copyUrlFirst
, waitUrl
#endif
) where
import Data.IORef ( newIORef, readIORef, writeIORef, IORef )
import System.IO.Unsafe ( unsafePerformIO )
import Darcs.Prelude
import Darcs.Util.Download.Request
( Cachable(Cachable,MaxAge,Uncachable)
, ConnectionError
)
#ifdef HAVE_CURL
import Control.Arrow ( (&&&) )
import Control.Concurrent ( forkIO )
import Control.Concurrent.STM.TChan
( isEmptyTChan, newTChanIO, readTChan, writeTChan, TChan )
import Control.Concurrent.MVar ( isEmptyMVar, modifyMVar_, modifyMVar, newEmptyMVar,
newMVar, putMVar, readMVar, withMVar, MVar )
import Control.Monad ( unless, when )
import Control.Monad.State ( evalStateT, get, modify, put, StateT )
import Control.Monad.STM ( atomically )
import Control.Monad.Trans ( liftIO )
import Data.Map ( Map )
import qualified Data.Map as Map
import Data.Tuple ( swap )
import System.Directory ( copyFile, renameFile )
import Crypto.Random ( seedNew, seedToInteger )
import Darcs.Util.AtExit ( atexit )
import Darcs.Util.File ( removeFileMayNotExist )
import Numeric ( showHex )
import Darcs.Util.Progress ( debugMessage )
import Darcs.Util.Download.Request
import qualified Darcs.Util.Download.Curl as Curl
#endif
{-# NOINLINE maxPipelineLengthRef #-}
maxPipelineLengthRef :: IORef Int
maxPipelineLengthRef :: IORef Int
maxPipelineLengthRef = IO (IORef Int) -> IORef Int
forall a. IO a -> a
unsafePerformIO (IO (IORef Int) -> IORef Int) -> IO (IORef Int) -> IORef Int
forall a b. (a -> b) -> a -> b
$ do
Bool
enabled <- IO Bool
pipeliningEnabled
#ifdef HAVE_CURL
unless enabled $ debugMessage $
"Warning: pipelining is disabled, because libcurl version darcs was "
++ "compiled with is too old (< 7.19.1)"
#endif
Int -> IO (IORef Int)
forall a. a -> IO (IORef a)
newIORef (Int -> IO (IORef Int)) -> Int -> IO (IORef Int)
forall a b. (a -> b) -> a -> b
$ if Bool
enabled then Int
100 else Int
1
maxPipelineLength :: IO Int
maxPipelineLength :: IO Int
maxPipelineLength = IORef Int -> IO Int
forall a. IORef a -> IO a
readIORef IORef Int
maxPipelineLengthRef
#ifdef HAVE_CURL
{-# NOINLINE urlNotifications #-}
urlNotifications :: MVar (Map String (MVar (Maybe String)))
urlNotifications = unsafePerformIO $ newMVar Map.empty
{-# NOINLINE urlChan #-}
urlChan :: TChan UrlRequest
urlChan = unsafePerformIO $ do
ch <- newTChanIO
_ <- forkIO (urlThread ch)
return ch
type UrlM a = StateT UrlState IO a
urlThread :: TChan UrlRequest -> IO ()
urlThread ch = do
junk <- flip showHex "" <$> seedToInteger <$> seedNew
evalStateT urlThread' (UrlState Map.empty emptyQ 0 junk)
where
urlThread' :: UrlM ()
urlThread' = do
empty <- liftIO $ atomically $ isEmptyTChan ch
(l, w) <- (pipeLength &&& waitToStart) `fmap` get
reqs <- if not empty || (nullQ w && l == 0)
then liftIO readAllRequests
else return []
mapM_ addReq reqs
checkWaitToStart
waitNextUrl
urlThread'
readAllRequests :: IO [UrlRequest]
readAllRequests = do
r <- atomically $ readTChan ch
debugMessage $ "URL.urlThread (" ++ url r ++ "\n"++
"-> " ++ file r ++ ")"
empty <- atomically $ isEmptyTChan ch
reqs <- if not empty
then readAllRequests
else return []
return (r : reqs)
addReq :: UrlRequest -> UrlM ()
addReq (UrlRequest u f c p) = do
d <- liftIO (alreadyDownloaded u)
if d
then dbg "Ignoring UrlRequest of URL that is already downloaded."
else do
(ip, wts) <- (inProgress &&& waitToStart) `fmap` get
case Map.lookup u ip of
Nothing -> modify $ \st ->
st { inProgress = Map.insert u (f, [], c) ip
, waitToStart = addUsingPriority p u wts }
Just (f', fs', c') -> do
let new_c = minCachable c c'
when (c /= c') $ do
let new_p = Map.insert u (f', fs', new_c) ip
modify (\s -> s { inProgress = new_p })
dbg $ "Changing " ++ u ++ " request cachability from "
++ show c ++ " to " ++ show new_c
when (u `elemQ` wts && p == High) $ do
modify $ \s ->
s { waitToStart = pushQ u (deleteQ u wts) }
dbg $ "Moving " ++ u ++ " to head of download queue."
if f `notElem` (f' : fs')
then do
let new_ip = Map.insert u (f', f : fs', new_c) ip
modify (\s -> s { inProgress = new_ip })
dbg "Adding new file to existing UrlRequest."
else dbg $ "Ignoring UrlRequest of file that's "
++ "already queued."
alreadyDownloaded :: String -> IO Bool
alreadyDownloaded u = do
n <- withMVar urlNotifications $ return . Map.lookup u
maybe (return True) (\v -> not `fmap` isEmptyMVar v) n
checkWaitToStart :: UrlM ()
checkWaitToStart = do
st <- get
let l = pipeLength st
mpl <- liftIO maxPipelineLength
when (l < mpl) $
case readQ (waitToStart st) of
Nothing -> return ()
Just (u, rest) -> do
case Map.lookup u (inProgress st) of
Nothing -> error $ "bug in URL.checkWaitToStart " ++ u
Just (f, _, c) -> do
dbg $ "URL.requestUrl (" ++ u ++ "\n"
++ "-> " ++ f ++ ")"
let f_new = createDownloadFileName f st
err <- liftIO $ requestUrl u f_new c
if null err
then do
liftIO $ atexit (removeFileMayNotExist f_new)
put $ st { waitToStart = rest
, pipeLength = l + 1 }
else do
dbg $ "Failed to start download URL " ++ u
++ ": " ++ err
liftIO $ do
removeFileMayNotExist f_new
downloadComplete u err
put $ st { waitToStart = rest }
checkWaitToStart
copyUrlFirst :: String -> FilePath -> Cachable -> IO ()
copyUrlFirst = copyUrlWithPriority High
copyUrl :: String -> FilePath -> Cachable -> IO ()
copyUrl = copyUrlWithPriority Low
copyUrlWithPriority :: Priority -> String -> String -> Cachable -> IO ()
copyUrlWithPriority p u f c = do
debugMessage $ "URL.copyUrlWithPriority (" ++ u ++ "\n"
++ "-> " ++ f ++ ")"
v <- newEmptyMVar
old_mv <- modifyMVar urlNotifications (return . swap . Map.insertLookupWithKey (\_k _n old -> old) u v)
case old_mv of
Nothing -> atomically $ writeTChan urlChan $ UrlRequest u f c p
Just _ -> debugMessage $ "URL.copyUrlWithPriority already in progress, skip (" ++ u ++ "\n" ++ "-> " ++ f ++ ")"
createDownloadFileName :: FilePath -> UrlState -> FilePath
createDownloadFileName f st = f ++ "-new_" ++ randomJunk st
waitNextUrl :: UrlM ()
waitNextUrl = do
st <- get
let l = pipeLength st
when (l > 0) $ do
dbg "URL.waitNextUrl start"
(u, e, ce) <- liftIO waitNextUrl'
let p = inProgress st
liftIO $ case Map.lookup u p of
Nothing ->
error $ "bug in URL.waitNextUrl: " ++ u
Just (f, fs, _) -> if null e
then do
renameFile (createDownloadFileName f st) f
mapM_ (safeCopyFile st f) fs
downloadComplete u e
debugMessage $
"URL.waitNextUrl succeeded: " ++ u ++ " " ++ f
else do
removeFileMayNotExist (createDownloadFileName f st)
downloadComplete u (maybe e show ce)
debugMessage $
"URL.waitNextUrl failed: " ++ u ++ " " ++ f ++ " " ++ e
unless (null u) . put $ st { inProgress = Map.delete u p
, pipeLength = l - 1 }
where
safeCopyFile st f t = do
let new_t = createDownloadFileName t st
copyFile f new_t
renameFile new_t t
downloadComplete :: String -> String -> IO ()
downloadComplete u e = do
r <- withMVar urlNotifications (return . Map.lookup u)
case r of
Just notifyVar ->
putMVar notifyVar $ if null e then Nothing else Just e
Nothing -> debugMessage $ "downloadComplete URL '" ++ u
++ "' downloaded several times"
waitUrl :: String -> IO ()
waitUrl u = do
debugMessage $ "URL.waitUrl " ++ u
r <- withMVar urlNotifications (return . Map.lookup u)
case r of
Nothing -> return ()
Just var -> do
mbErr <- readMVar var
modifyMVar_ urlNotifications (return . Map.delete u)
flip (maybe (return ())) mbErr $ \e -> do
debugMessage $ "Failed to download URL " ++ u ++ ": " ++ e
fail e
dbg :: String -> StateT a IO ()
dbg = liftIO . debugMessage
requestUrl :: String -> FilePath -> Cachable -> IO String
requestUrl = Curl.requestUrl
waitNextUrl' :: IO (String, String, Maybe ConnectionError)
waitNextUrl' = Curl.waitNextUrl
minCachable :: Cachable -> Cachable -> Cachable
minCachable Uncachable _ = Uncachable
minCachable _ Uncachable = Uncachable
minCachable (MaxAge a) (MaxAge b) = MaxAge $ min a b
minCachable (MaxAge a) _ = MaxAge a
minCachable _ (MaxAge b) = MaxAge b
minCachable _ _ = Cachable
#endif
disableHTTPPipelining :: IO ()
disableHTTPPipelining :: IO ()
disableHTTPPipelining = IORef Int -> Int -> IO ()
forall a. IORef a -> a -> IO ()
writeIORef IORef Int
maxPipelineLengthRef Int
1
setDebugHTTP :: IO ()
pipeliningEnabled :: IO Bool
#ifdef HAVE_CURL
setDebugHTTP = Curl.setDebugHTTP
pipeliningEnabled = Curl.pipeliningEnabled
#else
setDebugHTTP :: IO ()
setDebugHTTP = () -> IO ()
forall (m :: * -> *) a. Monad m => a -> m a
return ()
pipeliningEnabled :: IO Bool
pipeliningEnabled = Bool -> IO Bool
forall (m :: * -> *) a. Monad m => a -> m a
return Bool
True
#endif
environmentHelpProxy :: ([String], [String])
environmentHelpProxy :: ([String], [String])
environmentHelpProxy =
( [ String
"HTTP_PROXY", String
"HTTPS_PROXY", String
"FTP_PROXY", String
"ALL_PROXY", String
"NO_PROXY"]
, [ String
"If Darcs was built with libcurl, the environment variables"
, String
"HTTP_PROXY, HTTPS_PROXY and FTP_PROXY can be set to the URL of a"
, String
"proxy in the form"
, String
""
, String
" [protocol://]<host>[:port]"
, String
""
, String
"In which case libcurl will use the proxy for the associated protocol"
, String
"(HTTP, HTTPS and FTP). The environment variable ALL_PROXY can be used"
, String
"to set a single proxy for all libcurl requests."
, String
""
, String
"If the environment variable NO_PROXY is a comma-separated list of"
, String
"host names, access to those hosts will bypass proxies defined by the"
, String
"above variables. For example, it is quite common to avoid proxying"
, String
"requests to machines on the local network with"
, String
""
, String
" NO_PROXY=localhost,*.localdomain"
, String
""
, String
"For compatibility with lynx et al, lowercase equivalents of these"
, String
"environment variables (e.g. $http_proxy) are also understood and are"
, String
"used in preference to the uppercase versions."
, String
""
, String
"If Darcs was not built with libcurl, all these environment variables"
, String
"are silently ignored, and there is no way to use a web proxy."
]
)
environmentHelpProxyPassword :: ([String], [String])
environmentHelpProxyPassword :: ([String], [String])
environmentHelpProxyPassword =
( [ String
"DARCS_PROXYUSERPWD" ]
, [ String
"If Darcs was built with libcurl, and you are using a web proxy that"
, String
"requires authentication, you can set the $DARCS_PROXYUSERPWD"
, String
"environment variable to the username and password expected by the"
, String
"proxy, separated by a colon. This environment variable is silently"
, String
"ignored if Darcs was not built with libcurl."
]
)