Statistics
| Branch: | Tag: | Revision:

root / src / Ganeti / Utils.hs @ 5e46225d

History | View | Annotate | Download (25.2 kB)

1
{-# LANGUAGE FlexibleContexts #-}
2

    
3
{-| Utility functions. -}
4

    
5
{-
6

    
7
Copyright (C) 2009, 2010, 2011, 2012, 2013 Google Inc.
8

    
9
This program is free software; you can redistribute it and/or modify
10
it under the terms of the GNU General Public License as published by
11
the Free Software Foundation; either version 2 of the License, or
12
(at your option) any later version.
13

    
14
This program is distributed in the hope that it will be useful, but
15
WITHOUT ANY WARRANTY; without even the implied warranty of
16
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17
General Public License for more details.
18

    
19
You should have received a copy of the GNU General Public License
20
along with this program; if not, write to the Free Software
21
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22
02110-1301, USA.
23

    
24
-}
25

    
26
module Ganeti.Utils
27
  ( debug
28
  , debugFn
29
  , debugXy
30
  , sepSplit
31
  , stdDev
32
  , if'
33
  , select
34
  , applyIf
35
  , commaJoin
36
  , ensureQuoted
37
  , tryRead
38
  , formatTable
39
  , printTable
40
  , parseUnit
41
  , parseUnitAssumeBinary
42
  , plural
43
  , niceSort
44
  , niceSortKey
45
  , exitIfBad
46
  , exitErr
47
  , exitWhen
48
  , exitUnless
49
  , logWarningIfBad
50
  , rStripSpace
51
  , newUUID
52
  , getCurrentTime
53
  , getCurrentTimeUSec
54
  , clockTimeToString
55
  , clockTimeToCTime
56
  , cTimeToClockTime
57
  , chompPrefix
58
  , warn
59
  , wrap
60
  , trim
61
  , defaultHead
62
  , exitIfEmpty
63
  , splitEithers
64
  , recombineEithers
65
  , resolveAddr
66
  , monadicThe
67
  , setOwnerAndGroupFromNames
68
  , setOwnerWGroupR
69
  , formatOrdinal
70
  , tryAndLogIOError
71
  , lockFile
72
  , FStat
73
  , nullFStat
74
  , getFStat
75
  , getFStatSafe
76
  , needsReload
77
  , watchFile
78
  , safeRenameFile
79
  , FilePermissions(..)
80
  , ensurePermissions
81
  ) where
82

    
83
import Control.Concurrent
84
import Control.Exception (try)
85
import Control.Monad
86
import Control.Monad.Error
87
import Data.Char (toUpper, isAlphaNum, isDigit, isSpace)
88
import qualified Data.Either as E
89
import Data.Function (on)
90
import Data.IORef
91
import Data.List
92
import qualified Data.Map as M
93
import Foreign.C.Types (CTime(..))
94
import Numeric (showOct)
95
import System.Directory (renameFile, createDirectoryIfMissing)
96
import System.FilePath.Posix (takeDirectory)
97
import System.INotify
98
import System.Posix.Types
99

    
100
import Debug.Trace
101
import Network.Socket
102

    
103
import Ganeti.BasicTypes
104
import qualified Ganeti.ConstantUtils as ConstantUtils
105
import Ganeti.Logging
106
import Ganeti.Runtime
107
import System.IO
108
import System.Exit
109
import System.Posix.Files
110
import System.Posix.IO
111
import System.Posix.User
112
import System.Time
113

    
114
-- * Debug functions
115

    
116
-- | To be used only for debugging, breaks referential integrity.
117
debug :: Show a => a -> a
118
debug x = trace (show x) x
119

    
120
-- | Displays a modified form of the second parameter before returning
121
-- it.
122
debugFn :: Show b => (a -> b) -> a -> a
123
debugFn fn x = debug (fn x) `seq` x
124

    
125
-- | Show the first parameter before returning the second one.
126
debugXy :: Show a => a -> b -> b
127
debugXy = seq . debug
128

    
129
-- * Miscellaneous
130

    
131
-- | Apply the function if condition holds, otherwise use default value.
132
applyIf :: Bool -> (a -> a) -> a -> a
133
applyIf b f x = if b then f x else x
134

    
135
-- | Comma-join a string list.
136
commaJoin :: [String] -> String
137
commaJoin = intercalate ","
138

    
139
-- | Split a list on a separator and return an array.
140
sepSplit :: Eq a => a -> [a] -> [[a]]
141
sepSplit sep s
142
  | null s    = []
143
  | null xs   = [x]
144
  | null ys   = [x,[]]
145
  | otherwise = x:sepSplit sep ys
146
  where (x, xs) = break (== sep) s
147
        ys = drop 1 xs
148

    
149
-- | Simple pluralize helper
150
plural :: Int -> String -> String -> String
151
plural 1 s _ = s
152
plural _ _ p = p
153

    
154
-- | Ensure a value is quoted if needed.
155
ensureQuoted :: String -> String
156
ensureQuoted v = if not (all (\c -> isAlphaNum c || c == '.') v)
157
                 then '\'':v ++ "'"
158
                 else v
159

    
160
-- * Mathematical functions
161

    
162
-- Simple and slow statistical functions, please replace with better
163
-- versions
164

    
165
-- | Standard deviation function.
166
stdDev :: [Double] -> Double
167
stdDev lst =
168
  -- first, calculate the list length and sum lst in a single step,
169
  -- for performance reasons
170
  let (ll', sx) = foldl' (\(rl, rs) e ->
171
                           let rl' = rl + 1
172
                               rs' = rs + e
173
                           in rl' `seq` rs' `seq` (rl', rs')) (0::Int, 0) lst
174
      ll = fromIntegral ll'::Double
175
      mv = sx / ll
176
      av = foldl' (\accu em -> let d = em - mv in accu + d * d) 0.0 lst
177
  in sqrt (av / ll) -- stddev
178

    
179
-- *  Logical functions
180

    
181
-- Avoid syntactic sugar and enhance readability. These functions are proposed
182
-- by some for inclusion in the Prelude, and at the moment they are present
183
-- (with various definitions) in the utility-ht package. Some rationale and
184
-- discussion is available at <http://www.haskell.org/haskellwiki/If-then-else>
185

    
186
-- | \"if\" as a function, rather than as syntactic sugar.
187
if' :: Bool -- ^ condition
188
    -> a    -- ^ \"then\" result
189
    -> a    -- ^ \"else\" result
190
    -> a    -- ^ \"then\" or "else" result depending on the condition
191
if' True x _ = x
192
if' _    _ y = y
193

    
194
-- * Parsing utility functions
195

    
196
-- | Parse results from readsPrec.
197
parseChoices :: (Monad m, Read a) => String -> String -> [(a, String)] -> m a
198
parseChoices _ _ ((v, ""):[]) = return v
199
parseChoices name s ((_, e):[]) =
200
    fail $ name ++ ": leftover characters when parsing '"
201
           ++ s ++ "': '" ++ e ++ "'"
202
parseChoices name s _ = fail $ name ++ ": cannot parse string '" ++ s ++ "'"
203

    
204
-- | Safe 'read' function returning data encapsulated in a Result.
205
tryRead :: (Monad m, Read a) => String -> String -> m a
206
tryRead name s = parseChoices name s $ reads s
207

    
208
-- | Format a table of strings to maintain consistent length.
209
formatTable :: [[String]] -> [Bool] -> [[String]]
210
formatTable vals numpos =
211
    let vtrans = transpose vals  -- transpose, so that we work on rows
212
                                 -- rather than columns
213
        mlens = map (maximum . map length) vtrans
214
        expnd = map (\(flds, isnum, ml) ->
215
                         map (\val ->
216
                                  let delta = ml - length val
217
                                      filler = replicate delta ' '
218
                                  in if delta > 0
219
                                     then if isnum
220
                                          then filler ++ val
221
                                          else val ++ filler
222
                                     else val
223
                             ) flds
224
                    ) (zip3 vtrans numpos mlens)
225
   in transpose expnd
226

    
227
-- | Constructs a printable table from given header and rows
228
printTable :: String -> [String] -> [[String]] -> [Bool] -> String
229
printTable lp header rows isnum =
230
  unlines . map ((++) lp . (:) ' ' . unwords) $
231
  formatTable (header:rows) isnum
232

    
233
-- | Converts a unit (e.g. m or GB) into a scaling factor.
234
parseUnitValue :: (Monad m) => Bool -> String -> m Rational
235
parseUnitValue noDecimal unit
236
  -- binary conversions first
237
  | null unit                     = return 1
238
  | unit == "m" || upper == "MIB" = return 1
239
  | unit == "g" || upper == "GIB" = return kbBinary
240
  | unit == "t" || upper == "TIB" = return $ kbBinary * kbBinary
241
  -- SI conversions
242
  | unit == "M" || upper == "MB"  = return mbFactor
243
  | unit == "G" || upper == "GB"  = return $ mbFactor * kbDecimal
244
  | unit == "T" || upper == "TB"  = return $ mbFactor * kbDecimal * kbDecimal
245
  | otherwise = fail $ "Unknown unit '" ++ unit ++ "'"
246
  where upper = map toUpper unit
247
        kbBinary = 1024 :: Rational
248
        kbDecimal = if noDecimal then kbBinary else 1000
249
        decToBin = kbDecimal / kbBinary -- factor for 1K conversion
250
        mbFactor = decToBin * decToBin -- twice the factor for just 1K
251

    
252
-- | Tries to extract number and scale from the given string.
253
--
254
-- Input must be in the format NUMBER+ SPACE* [UNIT]. If no unit is
255
-- specified, it defaults to MiB. Return value is always an integral
256
-- value in MiB; if the first argument is True, all kilos are binary.
257
parseUnitEx :: (Monad m, Integral a, Read a) => Bool -> String -> m a
258
parseUnitEx noDecimal str =
259
  -- TODO: enhance this by splitting the unit parsing code out and
260
  -- accepting floating-point numbers
261
  case (reads str::[(Int, String)]) of
262
    [(v, suffix)] ->
263
      let unit = dropWhile (== ' ') suffix
264
      in do
265
        scaling <- parseUnitValue noDecimal unit
266
        return $ truncate (fromIntegral v * scaling)
267
    _ -> fail $ "Can't parse string '" ++ str ++ "'"
268

    
269
-- | Tries to extract number and scale from the given string.
270
--
271
-- Input must be in the format NUMBER+ SPACE* [UNIT]. If no unit is
272
-- specified, it defaults to MiB. Return value is always an integral
273
-- value in MiB.
274
parseUnit :: (Monad m, Integral a, Read a) => String -> m a
275
parseUnit = parseUnitEx False
276

    
277
-- | Tries to extract a number and scale from a given string, taking
278
-- all kilos to be binary.
279
parseUnitAssumeBinary :: (Monad m, Integral a, Read a) => String -> m a
280
parseUnitAssumeBinary = parseUnitEx True
281

    
282
-- | Unwraps a 'Result', exiting the program if it is a 'Bad' value,
283
-- otherwise returning the actual contained value.
284
exitIfBad :: String -> Result a -> IO a
285
exitIfBad msg (Bad s) = exitErr (msg ++ ": " ++ s)
286
exitIfBad _ (Ok v) = return v
287

    
288
-- | Exits immediately with an error message.
289
exitErr :: String -> IO a
290
exitErr errmsg = do
291
  hPutStrLn stderr $ "Error: " ++ errmsg
292
  exitWith (ExitFailure 1)
293

    
294
-- | Exits with an error message if the given boolean condition if true.
295
exitWhen :: Bool -> String -> IO ()
296
exitWhen True msg = exitErr msg
297
exitWhen False _  = return ()
298

    
299
-- | Exits with an error message /unless/ the given boolean condition
300
-- if true, the opposite of 'exitWhen'.
301
exitUnless :: Bool -> String -> IO ()
302
exitUnless cond = exitWhen (not cond)
303

    
304
-- | Unwraps a 'Result', logging a warning message and then returning a default
305
-- value if it is a 'Bad' value, otherwise returning the actual contained value.
306
logWarningIfBad :: String -> a -> Result a -> IO a
307
logWarningIfBad msg defVal (Bad s) = do
308
  logWarning $ msg ++ ": " ++ s
309
  return defVal
310
logWarningIfBad _ _ (Ok v) = return v
311

    
312
-- | Try an IO interaction, log errors and unfold as a 'Result'.
313
tryAndLogIOError :: IO a -> String -> (a -> Result b) -> IO (Result b)
314
tryAndLogIOError io msg okfn =
315
 try io >>= either
316
   (\ e -> do
317
       let combinedmsg = msg ++ ": " ++ show (e :: IOError)
318
       logError combinedmsg
319
       return . Bad $ combinedmsg)
320
   (return . okfn)
321

    
322
-- | Print a warning, but do not exit.
323
warn :: String -> IO ()
324
warn = hPutStrLn stderr . (++) "Warning: "
325

    
326
-- | Helper for 'niceSort'. Computes the key element for a given string.
327
extractKey :: [Either Integer String]  -- ^ Current (partial) key, reversed
328
           -> String                   -- ^ Remaining string
329
           -> ([Either Integer String], String)
330
extractKey ek [] = (reverse ek, [])
331
extractKey ek xs@(x:_) =
332
  let (span_fn, conv_fn) = if isDigit x
333
                             then (isDigit, Left . read)
334
                             else (not . isDigit, Right)
335
      (k, rest) = span span_fn xs
336
  in extractKey (conv_fn k:ek) rest
337

    
338
{-| Sort a list of strings based on digit and non-digit groupings.
339

    
340
Given a list of names @['a1', 'a10', 'a11', 'a2']@ this function
341
will sort the list in the logical order @['a1', 'a2', 'a10', 'a11']@.
342

    
343
The sort algorithm breaks each name in groups of either only-digits or
344
no-digits, and sorts based on each group.
345

    
346
Internally, this is not implemented via regexes (like the Python
347
version), but via actual splitting of the string in sequences of
348
either digits or everything else, and converting the digit sequences
349
in /Left Integer/ and the non-digit ones in /Right String/, at which
350
point sorting becomes trivial due to the built-in 'Either' ordering;
351
we only need one extra step of dropping the key at the end.
352

    
353
-}
354
niceSort :: [String] -> [String]
355
niceSort = niceSortKey id
356

    
357
-- | Key-version of 'niceSort'. We use 'sortBy' and @compare `on` fst@
358
-- since we don't want to add an ordering constraint on the /a/ type,
359
-- hence the need to only compare the first element of the /(key, a)/
360
-- tuple.
361
niceSortKey :: (a -> String) -> [a] -> [a]
362
niceSortKey keyfn =
363
  map snd . sortBy (compare `on` fst) .
364
  map (\s -> (fst . extractKey [] $ keyfn s, s))
365

    
366
-- | Strip space characthers (including newline). As this is
367
-- expensive, should only be run on small strings.
368
rStripSpace :: String -> String
369
rStripSpace = reverse . dropWhile isSpace . reverse
370

    
371
-- | Returns a random UUID.
372
-- This is a Linux-specific method as it uses the /proc filesystem.
373
newUUID :: IO String
374
newUUID = do
375
  contents <- readFile ConstantUtils.randomUuidFile
376
  return $! rStripSpace $ take 128 contents
377

    
378
-- | Returns the current time as an 'Integer' representing the number
379
-- of seconds from the Unix epoch.
380
getCurrentTime :: IO Integer
381
getCurrentTime = do
382
  TOD ctime _ <- getClockTime
383
  return ctime
384

    
385
-- | Returns the current time as an 'Integer' representing the number
386
-- of microseconds from the Unix epoch (hence the need for 'Integer').
387
getCurrentTimeUSec :: IO Integer
388
getCurrentTimeUSec = do
389
  TOD ctime pico <- getClockTime
390
  -- pico: 10^-12, micro: 10^-6, so we have to shift seconds left and
391
  -- picoseconds right
392
  return $ ctime * 1000000 + pico `div` 1000000
393

    
394
-- | Convert a ClockTime into a (seconds-only) timestamp.
395
clockTimeToString :: ClockTime -> String
396
clockTimeToString (TOD t _) = show t
397

    
398
-- | Convert a ClockTime into a (seconds-only) 'EpochTime' (AKA @time_t@).
399
clockTimeToCTime :: ClockTime -> EpochTime
400
clockTimeToCTime (TOD secs _) = fromInteger secs
401

    
402
-- | Convert a ClockTime into a (seconds-only) 'EpochTime' (AKA @time_t@).
403
cTimeToClockTime :: EpochTime -> ClockTime
404
cTimeToClockTime (CTime timet) = TOD (toInteger timet) 0
405

    
406
{-| Strip a prefix from a string, allowing the last character of the prefix
407
(which is assumed to be a separator) to be absent from the string if the string
408
terminates there.
409

    
410
\>>> chompPrefix \"foo:bar:\" \"a:b:c\"
411
Nothing
412

    
413
\>>> chompPrefix \"foo:bar:\" \"foo:bar:baz\"
414
Just \"baz\"
415

    
416
\>>> chompPrefix \"foo:bar:\" \"foo:bar:\"
417
Just \"\"
418

    
419
\>>> chompPrefix \"foo:bar:\" \"foo:bar\"
420
Just \"\"
421

    
422
\>>> chompPrefix \"foo:bar:\" \"foo:barbaz\"
423
Nothing
424
-}
425
chompPrefix :: String -> String -> Maybe String
426
chompPrefix pfx str =
427
  if pfx `isPrefixOf` str || str == init pfx
428
    then Just $ drop (length pfx) str
429
    else Nothing
430

    
431
-- | Breaks a string in lines with length \<= maxWidth.
432
--
433
-- NOTE: The split is OK if:
434
--
435
-- * It doesn't break a word, i.e. the next line begins with space
436
--   (@isSpace . head $ rest@) or the current line ends with space
437
--   (@null revExtra@);
438
--
439
-- * It breaks a very big word that doesn't fit anyway (@null revLine@).
440
wrap :: Int      -- ^ maxWidth
441
     -> String   -- ^ string that needs wrapping
442
     -> [String] -- ^ string \"broken\" in lines
443
wrap maxWidth = filter (not . null) . map trim . wrap0
444
  where wrap0 :: String -> [String]
445
        wrap0 text
446
          | length text <= maxWidth = [text]
447
          | isSplitOK               = line : wrap0 rest
448
          | otherwise               = line' : wrap0 rest'
449
          where (line, rest) = splitAt maxWidth text
450
                (revExtra, revLine) = break isSpace . reverse $ line
451
                (line', rest') = (reverse revLine, reverse revExtra ++ rest)
452
                isSplitOK =
453
                  null revLine || null revExtra || startsWithSpace rest
454
                startsWithSpace (x:_) = isSpace x
455
                startsWithSpace _     = False
456

    
457
-- | Removes surrounding whitespace. Should only be used in small
458
-- strings.
459
trim :: String -> String
460
trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace
461

    
462
-- | A safer head version, with a default value.
463
defaultHead :: a -> [a] -> a
464
defaultHead def []    = def
465
defaultHead _   (x:_) = x
466

    
467
-- | A 'head' version in the I/O monad, for validating parameters
468
-- without which we cannot continue.
469
exitIfEmpty :: String -> [a] -> IO a
470
exitIfEmpty _ (x:_) = return x
471
exitIfEmpty s []    = exitErr s
472

    
473
-- | Obtain the unique element of a list in an arbitrary monad.
474
monadicThe :: (Eq a, Monad m) => String -> [a] -> m a
475
monadicThe s [] = fail s
476
monadicThe s (x:xs)
477
  | all (x ==) xs = return x
478
  | otherwise = fail s
479

    
480
-- | Split an 'Either' list into two separate lists (containing the
481
-- 'Left' and 'Right' elements, plus a \"trail\" list that allows
482
-- recombination later.
483
--
484
-- This is splitter; for recombination, look at 'recombineEithers'.
485
-- The sum of \"left\" and \"right\" lists should be equal to the
486
-- original list length, and the trail list should be the same length
487
-- as well. The entries in the resulting lists are reversed in
488
-- comparison with the original list.
489
splitEithers :: [Either a b] -> ([a], [b], [Bool])
490
splitEithers = foldl' splitter ([], [], [])
491
  where splitter (l, r, t) e =
492
          case e of
493
            Left  v -> (v:l, r, False:t)
494
            Right v -> (l, v:r, True:t)
495

    
496
-- | Recombines two \"left\" and \"right\" lists using a \"trail\"
497
-- list into a single 'Either' list.
498
--
499
-- This is the counterpart to 'splitEithers'. It does the opposite
500
-- transformation, and the output list will be the reverse of the
501
-- input lists. Since 'splitEithers' also reverses the lists, calling
502
-- these together will result in the original list.
503
--
504
-- Mismatches in the structure of the lists (e.g. inconsistent
505
-- lengths) are represented via 'Bad'; normally this function should
506
-- not fail, if lists are passed as generated by 'splitEithers'.
507
recombineEithers :: (Show a, Show b) =>
508
                    [a] -> [b] -> [Bool] -> Result [Either a b]
509
recombineEithers lefts rights trail =
510
  foldM recombiner ([], lefts, rights) trail >>= checker
511
    where checker (eithers, [], []) = Ok eithers
512
          checker (_, lefts', rights') =
513
            Bad $ "Inconsistent results after recombination, l'=" ++
514
                show lefts' ++ ", r'=" ++ show rights'
515
          recombiner (es, l:ls, rs) False = Ok (Left l:es,  ls, rs)
516
          recombiner (es, ls, r:rs) True  = Ok (Right r:es, ls, rs)
517
          recombiner (_,  ls, rs) t = Bad $ "Inconsistent trail log: l=" ++
518
                                      show ls ++ ", r=" ++ show rs ++ ",t=" ++
519
                                      show t
520

    
521
-- | Default hints for the resolver
522
resolveAddrHints :: Maybe AddrInfo
523
resolveAddrHints =
524
  Just defaultHints { addrFlags = [AI_NUMERICHOST, AI_NUMERICSERV] }
525

    
526
-- | Resolves a numeric address.
527
resolveAddr :: Int -> String -> IO (Result (Family, SockAddr))
528
resolveAddr port str = do
529
  resolved <- getAddrInfo resolveAddrHints (Just str) (Just (show port))
530
  return $ case resolved of
531
             [] -> Bad "Invalid results from lookup?"
532
             best:_ -> Ok (addrFamily best, addrAddress best)
533

    
534
-- | Set the owner and the group of a file (given as names, not numeric id).
535
setOwnerAndGroupFromNames :: FilePath -> GanetiDaemon -> GanetiGroup -> IO ()
536
setOwnerAndGroupFromNames filename daemon dGroup = do
537
  -- TODO: it would be nice to rework this (or getEnts) so that runtimeEnts
538
  -- is read only once per daemon startup, and then cached for further usage.
539
  runtimeEnts <- runResultT getEnts
540
  ents <- exitIfBad "Can't find required user/groups" runtimeEnts
541
  -- note: we use directly ! as lookup failures shouldn't happen, due
542
  -- to the map construction
543
  let uid = reUserToUid ents M.! daemon
544
  let gid = reGroupToGid ents M.! dGroup
545
  setOwnerAndGroup filename uid gid
546

    
547
-- | Resets permissions so that the owner can read/write and the group only
548
-- read. All other permissions are cleared.
549
setOwnerWGroupR :: FilePath -> IO ()
550
setOwnerWGroupR path = setFileMode path mode
551
  where mode = foldl unionFileModes nullFileMode
552
                     [ownerReadMode, ownerWriteMode, groupReadMode]
553

    
554
-- | Formats an integral number, appending a suffix.
555
formatOrdinal :: (Integral a, Show a) => a -> String
556
formatOrdinal num
557
  | num > 10 && num < 20 = suffix "th"
558
  | tens == 1            = suffix "st"
559
  | tens == 2            = suffix "nd"
560
  | tens == 3            = suffix "rd"
561
  | otherwise            = suffix "th"
562
  where tens     = num `mod` 10
563
        suffix s = show num ++ s
564

    
565
-- | Attempt, in a non-blocking way, to obtain a lock on a given file; report
566
-- back success.
567
lockFile :: FilePath -> IO (Result ())
568
lockFile path = runResultT . liftIO $ do
569
  handle <- openFile path WriteMode
570
  fd <- handleToFd handle
571
  setLock fd (WriteLock, AbsoluteSeek, 0, 0)
572

    
573
-- | File stat identifier.
574
type FStat = (EpochTime, FileID, FileOffset)
575

    
576
-- | Null 'FStat' value.
577
nullFStat :: FStat
578
nullFStat = (-1, -1, -1)
579

    
580
-- | Computes the file cache data from a FileStatus structure.
581
buildFileStatus :: FileStatus -> FStat
582
buildFileStatus ofs =
583
    let modt = modificationTime ofs
584
        inum = fileID ofs
585
        fsize = fileSize ofs
586
    in (modt, inum, fsize)
587

    
588
-- | Wrapper over 'buildFileStatus'. This reads the data from the
589
-- filesystem and then builds our cache structure.
590
getFStat :: FilePath -> IO FStat
591
getFStat p = liftM buildFileStatus (getFileStatus p)
592

    
593
-- | Safe version of 'getFStat', that ignores IOErrors.
594
getFStatSafe :: FilePath -> IO FStat
595
getFStatSafe fpath = liftM (either (const nullFStat) id)
596
                       ((try $ getFStat fpath) :: IO (Either IOError FStat))
597

    
598
-- | Check if the file needs reloading
599
needsReload :: FStat -> FilePath -> IO (Maybe FStat)
600
needsReload oldstat path = do
601
  newstat <- getFStat path
602
  return $ if newstat /= oldstat
603
             then Just newstat
604
             else Nothing
605

    
606
-- | Until the given point in time (useconds since the epoch), wait
607
-- for the output of a given method to change and return the new value;
608
-- make use of the promise that the output only changes if the reference
609
-- has a value different than the given one.
610
watchFileEx :: (Eq a, Eq b) => Integer -> b -> IORef b -> a -> IO a -> IO a
611
watchFileEx endtime base ref old read_fn = do
612
  current <- getCurrentTimeUSec
613
  if current > endtime then read_fn else do
614
    val <- readIORef ref
615
    if val /= base
616
      then do
617
        new <- read_fn
618
        if new /= old then return new else do
619
          logDebug "Observed change not relevant"
620
          threadDelay 100000
621
          watchFileEx endtime val ref old read_fn
622
      else do 
623
       threadDelay 100000
624
       watchFileEx endtime base ref old read_fn
625

    
626
-- | Within the given timeout (in seconds), wait for for the output
627
-- of the given method to change and return the new value; make use of
628
-- the promise that the method will only change its value, if
629
-- the given file changes on disk. If the file does not exist on disk, return
630
-- immediately.
631
watchFile :: Eq a => FilePath -> Int -> a -> IO a -> IO a
632
watchFile fpath timeout old read_fn = do
633
  current <- getCurrentTimeUSec
634
  let endtime = current + fromIntegral timeout * 1000000
635
  fstat <- getFStatSafe fpath
636
  ref <- newIORef fstat
637
  inotify <- initINotify
638
  let do_watch e = do
639
                     logDebug $ "Notified of change in " ++ fpath 
640
                                  ++ "; event: " ++ show e
641
                     when (e == Ignored)
642
                       (addWatch inotify [Modify, Delete] fpath do_watch
643
                          >> return ())
644
                     fstat' <- getFStatSafe fpath
645
                     writeIORef ref fstat'
646
  _ <- addWatch inotify [Modify, Delete] fpath do_watch
647
  newval <- read_fn
648
  if newval /= old
649
    then do
650
      logDebug $ "File " ++ fpath ++ " changed during setup of inotify"
651
      killINotify inotify
652
      return newval
653
    else do
654
      result <- watchFileEx endtime fstat ref old read_fn
655
      killINotify inotify
656
      return result
657

    
658
-- | Type describing ownership and permissions of newly generated
659
-- directories and files. All parameters are optional, with nothing
660
-- meaning that the default value should be left untouched.
661

    
662
data FilePermissions = FilePermissions { fpOwner :: Maybe String
663
                                       , fpGroup :: Maybe String
664
                                       , fpPermissions :: FileMode
665
                                       }
666

    
667
-- | Ensure that a given file or directory has the permissions, and
668
-- possibly ownerships, as required.
669
ensurePermissions :: FilePath -> FilePermissions -> IO (Result ())
670
ensurePermissions fpath perms = do
671
  eitherFileStatus <- try $ getFileStatus fpath
672
                      :: IO (Either IOError FileStatus)
673
  (flip $ either (return . Bad . show)) eitherFileStatus $ \fstat -> do
674
    ownertry <- case fpOwner perms of
675
      Nothing -> return $ Right ()
676
      Just owner -> try $ do
677
        ownerid <- userID `liftM` getUserEntryForName owner
678
        unless (ownerid == fileOwner fstat) $ do
679
          logDebug $ "Changing owner of " ++ fpath ++ " to " ++ owner
680
          setOwnerAndGroup fpath ownerid (-1)
681
    grouptry <- case fpGroup perms of
682
      Nothing -> return $ Right ()
683
      Just grp -> try $ do
684
        groupid <- groupID `liftM` getGroupEntryForName grp
685
        unless (groupid == fileGroup fstat) $ do
686
          logDebug $ "Changing group of " ++ fpath ++ " to " ++ grp
687
          setOwnerAndGroup fpath (-1) groupid
688
    let fp = fpPermissions perms
689
    permtry <- if fileMode fstat == fp
690
      then return $ Right ()
691
      else try $ do
692
        logInfo $ "Changing permissions of " ++ fpath ++ " to "
693
                    ++ showOct fp ""
694
        setFileMode fpath fp
695
    let errors = E.lefts ([ownertry, grouptry, permtry] :: [Either IOError ()])
696
    if null errors
697
      then return $ Ok ()
698
      else return . Bad $ show errors
699

    
700
-- | Safely rename a file, creating the target directory, if needed.
701
safeRenameFile :: FilePermissions -> FilePath -> FilePath -> IO (Result ())
702
safeRenameFile perms from to = do
703
  directtry <- try $ renameFile from to
704
  case (directtry :: Either IOError ()) of
705
    Right () -> return $ Ok ()
706
    Left _ -> do
707
      result <- try $ do
708
        let dir = takeDirectory to
709
        createDirectoryIfMissing True dir
710
        _ <- ensurePermissions dir perms
711
        renameFile from to
712
      return $ either (Bad . show) Ok (result :: Either IOError ())