File: MercurialCommandServer.hs

package info (click to toggle)
haskell-filestore 0.6.5-4
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 224 kB
  • sloc: haskell: 1,604; makefile: 4
file content (248 lines) | stat: -rw-r--r-- 10,600 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
{-# LANGUAGE DeriveDataTypeable #-}
{- |
   Module      : Data.FileStore.MercurialCommandServer
   Copyright   : Copyright (C) 2011 John Lenz (lenz@math.uic.edu)
   License     : BSD 3

   Maintainer  : John MacFarlane <jgm@berkeley.edu>
   Stability   : alpha
   Portability : GHC 6.10 required

   In version 1.9, mercurial introduced a command server which allows
   a single instance of mercurial to be launched and multiple commands
   can be executed without requiring mercurial to start and stop.  See
   http://mercurial.selenic.com/wiki/CommandServer
-}

module Data.FileStore.MercurialCommandServer
    ( runMercurialCommand
    , rawRunMercurialCommand
    )
where

import Control.Applicative ((<$>))
import Control.Exception (Exception, onException, throwIO)
import Control.Monad (when)
import Data.Bits (shiftL, shiftR, (.|.))
import Data.Char (isLower, isUpper)
import Data.FileStore.Utils (runShellCommand)
import Data.IORef (IORef, newIORef, readIORef, atomicModifyIORef)
import Data.List (intercalate, isPrefixOf)
import Data.List.Split (splitOn)
import Data.Typeable (Typeable)
import Data.Word (Word32)
import System.Exit (ExitCode(..))
import System.IO (Handle, hClose, hPutStr, hFlush)
import System.IO.Unsafe (unsafePerformIO)
import System.Process (runInteractiveProcess)

import qualified Data.ByteString as B
import qualified Data.ByteString.UTF8 as UTF8
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.UTF8 as LUTF8
import qualified Data.Map as M
import qualified System.Info as SI

-- | Maximum number of servers to keep around
maxPoolSize :: Int
maxPoolSize = 2

-- | Run a mercurial command and return error status, error output, standard output.  The repository
-- is used as working directory.
runMercurialCommand :: FilePath -> String -> [String] -> IO (ExitCode, String, BL.ByteString)
runMercurialCommand repo command args = do
  server <- getServer repo
  case server of
     Nothing -> rawRunMercurialCommand repo command args
     Just h  -> do ret <- runMercurialServer command args h `onException` cleanupServer h
                   putServer repo h
                   return ret

-- | Run a mercurial command directly without using the server.
rawRunMercurialCommand :: FilePath -> String -> [String] -> IO (ExitCode, String, BL.ByteString)
rawRunMercurialCommand repo command args = do
   let env = [("HGENCODING","utf8")]
   (status, err, out) <- runShellCommand repo (Just env) "hg" (command : args)
   return (status, LUTF8.toString err, out)

-- | Create a new command server for the given repository
createServer :: FilePath -> IO (Handle,Handle,Handle)
createServer repo = do
    (hin,hout,herr,_) <- runInteractiveProcess "hg" ["serve", "--cmdserver", "pipe"] (Just repo) Nothing
    hello <- readMessage hout
    case hello of
       MessageO _ -> return (hin,hout,herr)
       MessageE x -> throwIO $ MercurialServerException (UTF8.toString x)
       _          -> throwIO $ MercurialServerException "unknown hello message"

-- | Cleanup a command sever.  Mercurial will automatically exit itself
--   when the handles are closed.
cleanupServer :: (Handle,Handle,Handle) -> IO ()
cleanupServer (hin,hout,herr) = hClose hin >> hClose hout >> hClose herr

-- | format a command for sending to the server
formatCommand :: String -> [String] -> B.ByteString
formatCommand cmd args = UTF8.fromString $ intercalate "\0" $ cmd : args

-- | run a command using the mercurial server
runMercurialServer :: String -> [String] -> (Handle,Handle,Handle) -> IO (ExitCode, String, BL.ByteString)
runMercurialServer cmd args (hin,hout,herr) = do
    hPutStr hin "runcommand\n"
    let fcmd = formatCommand cmd args
    hWriteWord32be hin $ fromIntegral $ B.length fcmd
    B.hPut hin fcmd
    hFlush hin
    processUntilR hout herr

-- | Read messages from the server until the command finishes or an error message appears
processUntilR :: Handle -> Handle -> IO (ExitCode, String, BL.ByteString)
processUntilR hout _ = loop BL.empty BL.empty
  where loop out err =
          do m <- readMessage hout
             case m of
                MessageO x -> loop (BL.append out $ BL.fromChunks [x]) err
                MessageE x -> loop out (BL.append err $ BL.fromChunks [x])
                MessageR c -> if c == 0
                                then return (ExitSuccess, "", out)
                                else return (ExitFailure c, LUTF8.toString err, out)

data MercurialMessage = MessageO B.ByteString
                      | MessageE B.ByteString
                      | MessageR Int

data MercurialServerException = MercurialServerException String
  deriving (Show,Typeable)
instance Exception MercurialServerException

-- | Read a single message
readMessage :: Handle -> IO MercurialMessage
readMessage hout = do
    buf <- B.hGet hout 1
    when (buf == B.empty) $
       throwIO $ MercurialServerException "Unknown channel"
    let c = B8.head buf
    -- Mercurial says unknown lower case channels can be ignored, but upper case channels
    -- must be handled.  Currently there are two upper case channels, 'I' and 'L' which
    -- are both used for user input/output.  So error on any upper case channel.
    when (isUpper c) $
       throwIO $ MercurialServerException $ "Unknown channel " ++ show c
    len <- hReadWord32be hout
    bdata <- B.hGet hout len
    when (B.length bdata /= len) $
       throwIO $ MercurialServerException "Mercurial did not produce enough output"
    case c of
      'r' | len >= 4 -> return $ MessageR $ bsReadWord32be bdata
      'r'            -> throwIO $ MercurialServerException $ "return value is fewer than 4 bytes"
      'o'            -> return $ MessageO bdata
      'e'            -> return $ MessageE bdata
      _ | isLower c  -> readMessage hout -- skip this message
      _              -> throwIO $ MercurialServerException $ "Unknown channel " ++ show c

-- | Read a 32-bit big-endian into an Int
hReadWord32be :: Handle -> IO Int
hReadWord32be h = do
    s <- B.hGet h 4
    when (B.length s /= 4) $
      throwIO $ MercurialServerException "unable to read int"
    return $ bsReadWord32be s

-- | Read a 32-bit big-endian from a bytestring into an Int
bsReadWord32be :: B.ByteString -> Int
bsReadWord32be s = (fromIntegral (s `B.index` 0) `shiftL` 24) .|.
                   (fromIntegral (s `B.index` 1) `shiftL` 16) .|.
                   (fromIntegral (s `B.index` 2) `shiftL`  8) .|.
                   (fromIntegral (s `B.index` 3) )

-- | Write a Word32 in big-endian to the handle
hWriteWord32be :: Handle -> Word32 -> IO ()
hWriteWord32be h w = B.hPut h buf
  where buf = B.pack [  -- fromIntegeral to convert to Word8
                fromIntegral (w `shiftR` 24),
                fromIntegral (w `shiftR` 16),
                fromIntegral (w `shiftR`  8),
                fromIntegral w
              ]

-------------------------------------------------------------------
-- Maintain a pool of mercurial servers.  Currently stored in a
-- global IORef.  The code must provide two functions, to get
-- and put a server from the pool.  The code above takes care of
-- cleaning up if an exception occurs.
-------------------------------------------------------------------

data MercurialGlobalState = MercurialGlobalState {
    useCommandServer :: Maybe Bool
  , serverHandles    :: M.Map FilePath [(Handle,Handle,Handle)]
} deriving (Show)

-- | See http://www.haskell.org/haskellwiki/Top_level_mutable_state
mercurialGlobalVar :: IORef MercurialGlobalState
{-# NOINLINE mercurialGlobalVar #-}
mercurialGlobalVar = unsafePerformIO (newIORef (MercurialGlobalState Nothing M.empty))

-- | Pull a server out of the pool.  Returns nothing if the mercurial version
--   does not support servers.
getServer :: FilePath -> IO (Maybe (Handle, Handle, Handle))
getServer repo = do
    use <- useCommandServer <$> readIORef mercurialGlobalVar
    case use of
      Just False -> return Nothing
      Nothing    -> do isok <- checkVersion
                       atomicModifyIORef mercurialGlobalVar $ \state ->
                          (state { useCommandServer = Just isok }, ())
                       getServer repo
      Just True  -> allocateServer repo

-- | Helper function called once we know that mercurial supports servers
allocateServer :: FilePath -> IO (Maybe (Handle, Handle, Handle))
allocateServer repo = do
    ret <- atomicModifyIORef mercurialGlobalVar $ \state ->
             case M.lookup repo (serverHandles state) of
                Just (x:xs) -> (state { serverHandles = M.insert repo xs (serverHandles state)}, Right x)
                _           -> (state, Left ())
    case ret of
      Right x -> return $ Just x
      Left () -> Just <$> createServer repo

-- | Puts a server back in the pool if the pool is not full,
--   otherwise closes the server.
putServer :: FilePath -> (Handle,Handle,Handle) -> IO ()
putServer repo h = do
    ret <- atomicModifyIORef mercurialGlobalVar $ \state -> do
              case M.lookup repo (serverHandles state) of
                  Just xs | length xs >= maxPoolSize -> (state, Right ())
                  Just xs -> (state { serverHandles = M.insert repo (h:xs) (serverHandles state)}, Left ())
                  Nothing -> (state { serverHandles = M.insert repo [h] (serverHandles state)}, Left ())
    case ret of
      Right () -> cleanupServer h
      Left  () -> return ()

-- | Check if the mercurial version supports servers
--   On windows, don't even try because talking to hg over a pipe does not
--   currently work correctly.
checkVersion :: IO Bool
checkVersion
    | isOperatingSystem "mingw32" = return False
    | otherwise                   = do
        (status,_,out) <- runShellCommand "." Nothing "hg" ["version", "-q"]
        case status of
          ExitFailure _ -> return False
          ExitSuccess   -> return $ parseVersion (LUTF8.toString out) >= [2,0]

-- | Helps to find out what operating system we are on
--   Example usage:
--      isOperatingSystem "mingw32" (on windows)
--      isOperatingSystem "darwin"
--      isOperatingSystem "linux"
isOperatingSystem :: String -> Bool
isOperatingSystem sys = SI.os == sys

-- | hg version -q returns something like "Mercurial Distributed SCM (version 1.9.1)"
--   This function returns the list [1,9,1]
parseVersion :: String -> [Int]
parseVersion b = if starts then verLst else [0]
  where msg = "Mercurial Distributed SCM (version "
        starts = isPrefixOf msg b
        ver    = takeWhile (/= ')') $ drop (length msg) b
        verLst = map read $ splitOn "." ver