From 56c3394144abbb9862dc67379d3253c76ae4df97 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 27 Oct 2015 14:34:10 -0400 Subject: Explicit Info/NoInfo for RevertableProperty (API change) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RevertableProperty used to be assumed to contain info, but this is now made explicit, with RevertableProperty HasInfo or RevertableProperty NoInfo. Transition guide: - If you define a RevertableProperty, expect some type check failures like: "Expecting one more argument to ‘RevertableProperty’". - Change it to "RevertableProperty NoInfo" - The compiler will then tell you if it needs "HasInfo" instead. - If you have code that uses the RevertableProperty constructor that fails to type check, use the more powerful operator --- debian/changelog | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) (limited to 'debian') diff --git a/debian/changelog b/debian/changelog index ba94f6bf..6b3f6940 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,14 +1,24 @@ propellor (2.13.0) UNRELEASED; urgency=medium - * Added Propellor.Property.Concurrent for concurrent properties. - (Note that no output multiplexing is currently done.) + * RevertableProperty used to be assumed to contain info, but this is + now made explicit, with RevertableProperty HasInfo or + RevertableProperty NoInfo. (API change) + Transition guide: + - If you define a RevertableProperty, expect some type check + failures like: "Expecting one more argument to ‘RevertableProperty’". + - Change it to "RevertableProperty NoInfo" + - The compiler will then tell you if it needs "HasInfo" instead. + - If you have code that uses the RevertableProperty constructor + that fails to type check, use the more powerful operator + instead to create the RevertableProperty. * Various property combinators that combined a RevertableProperty with a non-revertable property used to yield a RevertableProperty. This was a bug, because the combined property could not be fully - reverted in many cases. Fixed by making the combined property - instead be a Property HasInfo. + reverted in many cases, and the result is now a non-revertable property. * combineWith now takes an additional parameter to control how revert actions are combined (API change). + * Added Propellor.Property.Concurrent for concurrent properties. + (Note that no output multiplexing is currently done.) * Add File.isCopyOf. Thanks, Per Olofsson. -- Joey Hess Sat, 24 Oct 2015 15:16:45 -0400 -- cgit v1.3-2-g0d8e From 20b04d366b2cff90c39d06fd424ae3e8b67e49f6 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 27 Oct 2015 17:02:23 -0400 Subject: make Propellor.Message use lock to handle concurrent threads outputting messages Not yet handled: Output from concurrent programs. --- debian/changelog | 2 +- src/Propellor/Engine.hs | 33 +------------ src/Propellor/Message.hs | 92 ++++++++++++++++++++++++++++++++---- src/Propellor/PrivData.hs | 22 +++++---- src/Propellor/Property/Concurrent.hs | 4 +- 5 files changed, 100 insertions(+), 53 deletions(-) (limited to 'debian') diff --git a/debian/changelog b/debian/changelog index 6b3f6940..1699b27b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -18,7 +18,7 @@ propellor (2.13.0) UNRELEASED; urgency=medium * combineWith now takes an additional parameter to control how revert actions are combined (API change). * Added Propellor.Property.Concurrent for concurrent properties. - (Note that no output multiplexing is currently done.) + (Note that no command output multiplexing is currently done.) * Add File.isCopyOf. Thanks, Per Olofsson. -- Joey Hess Sat, 24 Oct 2015 15:16:45 -0400 diff --git a/src/Propellor/Engine.hs b/src/Propellor/Engine.hs index f0bcdac8..36a05b28 100644 --- a/src/Propellor/Engine.hs +++ b/src/Propellor/Engine.hs @@ -9,14 +9,12 @@ module Propellor.Engine ( fromHost, fromHost', onlyProcess, - processChainOutput, ) where import System.Exit import System.IO import Data.Monoid import Control.Applicative -import System.Console.ANSI import "mtl" Control.Monad.RWS.Strict import System.PosixCompat import System.Posix.IO @@ -29,8 +27,6 @@ import Propellor.Exception import Propellor.Info import Propellor.Property import Utility.Exception -import Utility.PartialPrelude -import Utility.Monad -- | Gets the Properties of a Host, and ensures them all, -- with nice display of what's being done. @@ -38,9 +34,7 @@ mainProperties :: Host -> IO () mainProperties host = do ret <- runPropellor host $ ensureProperties [ignoreInfo $ infoProperty "overall" (ensureProperties ps) mempty mempty] - whenConsole $ - setTitle "propellor: done" - hFlush stdout + messagesDone case ret of FailedChange -> exitWith (ExitFailure 1) _ -> exitWith ExitSuccess @@ -98,28 +92,3 @@ onlyProcess lockfile a = bracket lock unlock (const a) return l unlock = closeFd alreadyrunning = error "Propellor is already running on this host!" - --- | Reads and displays each line from the Handle, except for the last line --- which is a Result. -processChainOutput :: Handle -> IO Result -processChainOutput h = go Nothing - where - go lastline = do - v <- catchMaybeIO (hGetLine h) - debug ["read from chained propellor: ", show v] - case v of - Nothing -> case lastline of - Nothing -> do - debug ["chained propellor output nothing; assuming it failed"] - return FailedChange - Just l -> case readish l of - Just r -> pure r - Nothing -> do - debug ["chained propellor output did not end with a Result; assuming it failed"] - putStrLn l - hFlush stdout - return FailedChange - Just s -> do - maybe noop (\l -> unless (null l) (putStrLn l)) lastline - hFlush stdout - go (Just s) diff --git a/src/Propellor/Message.hs b/src/Propellor/Message.hs index 9c6cb57c..0961a356 100644 --- a/src/Propellor/Message.hs +++ b/src/Propellor/Message.hs @@ -1,6 +1,26 @@ {-# LANGUAGE PackageImports #-} -module Propellor.Message where +-- | This module handles all display of output to the console when +-- propellor is ensuring Properties. +-- +-- When two threads both try to display a message concurrently, +-- the messages will be displayed sequentially. + +module Propellor.Message ( + getMessageHandle, + isConsole, + forceConsole, + actionMessage, + actionMessageOn, + warningMessage, + infoMessage, + errorMessage, + debug, + checkDebugMode, + enableDebugMode, + processChainOutput, + messagesDone, +) where import System.Console.ANSI import System.IO @@ -16,6 +36,7 @@ import System.IO.Unsafe (unsafePerformIO) import Control.Concurrent import Propellor.Types +import Utility.PartialPrelude import Utility.Monad import Utility.Env import Utility.Process @@ -23,6 +44,7 @@ import Utility.Exception data MessageHandle = MessageHandle { isConsole :: Bool + , outputLock :: MVar () } -- | A shared global variable for the MessageHandle. @@ -30,30 +52,44 @@ data MessageHandle = MessageHandle globalMessageHandle :: MVar MessageHandle globalMessageHandle = unsafePerformIO $ do c <- hIsTerminalDevice stdout - newMVar $ MessageHandle c + o <- newMVar () + newMVar $ MessageHandle c o +-- | Gets the global MessageHandle. getMessageHandle :: IO MessageHandle getMessageHandle = readMVar globalMessageHandle +-- | Takes a lock while performing an action. Any other threads +-- that try to lockOutput at the same time will block. +lockOutput :: (MonadIO m, MonadMask m) => m a -> m a +lockOutput a = do + lck <- liftIO $ outputLock <$> getMessageHandle + bracket_ (liftIO $ takeMVar lck) (liftIO $ putMVar lck ()) a + +-- | Force console output. This can be used when stdout is not directly +-- connected to a console, but is eventually going to be displayed at a +-- console. forceConsole :: IO () forceConsole = modifyMVar_ globalMessageHandle $ \mh -> pure (mh { isConsole = True }) +-- | Only performs the action when at the console, or when console +-- output has been forced. whenConsole :: IO () -> IO () whenConsole a = whenM (isConsole <$> getMessageHandle) a -- | Shows a message while performing an action, with a colored status -- display. -actionMessage :: (MonadIO m, ActionResult r) => Desc -> m r -> m r +actionMessage :: (MonadIO m, MonadMask m, ActionResult r) => Desc -> m r -> m r actionMessage = actionMessage' Nothing -- | Shows a message while performing an action on a specified host, -- with a colored status display. -actionMessageOn :: (MonadIO m, ActionResult r) => HostName -> Desc -> m r -> m r +actionMessageOn :: (MonadIO m, MonadMask m, ActionResult r) => HostName -> Desc -> m r -> m r actionMessageOn = actionMessage' . Just -actionMessage' :: (MonadIO m, ActionResult r) => Maybe HostName -> Desc -> m r -> m r -actionMessage' mhn desc a = do +actionMessage' :: (MonadIO m, MonadMask m, ActionResult r) => Maybe HostName -> Desc -> m r -> m r +actionMessage' mhn desc a = lockOutput $ do liftIO $ whenConsole $ do setTitle $ "propellor: " ++ desc hFlush stdout @@ -80,14 +116,18 @@ actionMessage' mhn desc a = do setSGR [] warningMessage :: MonadIO m => String -> m () -warningMessage s = liftIO $ +warningMessage s = liftIO $ lockOutput $ colorLine Vivid Magenta $ "** warning: " ++ s +infoMessage :: MonadIO m => [String] -> m () +infoMessage ls = liftIO $ lockOutput $ + mapM_ putStrLn ls + errorMessage :: MonadIO m => String -> m a -errorMessage s = liftIO $ do +errorMessage s = liftIO $ lockOutput $ do colorLine Vivid Red $ "** error: " ++ s error "Cannot continue!" - + colorLine :: ColorIntensity -> Color -> String -> IO () colorLine intensity color msg = do whenConsole $ @@ -120,3 +160,37 @@ enableDebugMode = do <*> pure (simpleLogFormatter "[$time] $msg") updateGlobalLogger rootLoggerName $ setLevel DEBUG . setHandlers [f] + +-- | Reads and displays each line from the Handle, except for the last line +-- which is a Result. +processChainOutput :: Handle -> IO Result +processChainOutput h = go Nothing + where + go lastline = do + v <- catchMaybeIO (hGetLine h) + debug ["read from chained propellor: ", show v] + case v of + Nothing -> case lastline of + Nothing -> do + debug ["chained propellor output nothing; assuming it failed"] + return FailedChange + Just l -> case readish l of + Just r -> pure r + Nothing -> do + debug ["chained propellor output did not end with a Result; assuming it failed"] + lockOutput $ do + putStrLn l + hFlush stdout + return FailedChange + Just s -> do + lockOutput $ do + maybe noop (\l -> unless (null l) (putStrLn l)) lastline + hFlush stdout + go (Just s) + +-- | Called when all messages about properties have been printed. +messagesDone :: IO () +messagesDone = lockOutput $ do + whenConsole $ + setTitle "propellor: done" + hFlush stdout diff --git a/src/Propellor/PrivData.hs b/src/Propellor/PrivData.hs index aac37d14..e59f42c3 100644 --- a/src/Propellor/PrivData.hs +++ b/src/Propellor/PrivData.hs @@ -106,9 +106,9 @@ withPrivData' feed srclist c mkprop = addinfo $ mkprop $ \a -> missing = do Context cname <- mkHostContext hc <$> asks hostName warningMessage $ "Missing privdata " ++ intercalate " or " fieldnames ++ " (for " ++ cname ++ ")" - liftIO $ putStrLn $ "Fix this by running:" - liftIO $ showSet $ - map (\s -> (privDataField s, Context cname, describePrivDataSource s)) srclist + infoMessage $ + "Fix this by running:" : + showSet (map (\s -> (privDataField s, Context cname, describePrivDataSource s)) srclist) return FailedChange addinfo p = infoProperty (propertyDesc p) @@ -121,11 +121,14 @@ withPrivData' feed srclist c mkprop = addinfo $ mkprop $ \a -> fieldlist = map privDataField srclist hc = asHostContext c -showSet :: [(PrivDataField, Context, Maybe PrivDataSourceDesc)] -> IO () -showSet l = forM_ l $ \(f, Context c, md) -> do - putStrLn $ " propellor --set '" ++ show f ++ "' '" ++ c ++ "' \\" - maybe noop (\d -> putStrLn $ " " ++ d) md - putStrLn "" +showSet :: [(PrivDataField, Context, Maybe PrivDataSourceDesc)] -> [String] +showSet = concatMap go + where + go (f, Context c, md) = catMaybes + [ Just $ " propellor --set '" ++ show f ++ "' '" ++ c ++ "' \\" + , maybe Nothing (\d -> Just $ " " ++ d) md + , Just "" + ] addPrivData :: (PrivDataField, Maybe PrivDataSourceDesc, HostContext) -> Property HasInfo addPrivData v = pureInfoProperty (show v) (PrivInfo (S.singleton v)) @@ -207,7 +210,8 @@ listPrivDataFields hosts = do showtable $ map mkrow missing section "How to set missing data:" - showSet $ map (\(f, c) -> (f, c, join $ M.lookup (f, c) descmap)) missing + mapM_ putStrLn $ showSet $ + map (\(f, c) -> (f, c, join $ M.lookup (f, c) descmap)) missing where header = ["Field", "Context", "Used by"] mkrow k@(field, Context context) = diff --git a/src/Propellor/Property/Concurrent.hs b/src/Propellor/Property/Concurrent.hs index c57f5228..645a5dfd 100644 --- a/src/Propellor/Property/Concurrent.hs +++ b/src/Propellor/Property/Concurrent.hs @@ -1,7 +1,7 @@ {-# LANGUAGE FlexibleContexts #-} --- | Note that this module does not yet arrange for any output multiplexing, --- so the output of concurrent properties will be scrambled together. +-- | Note that any output of commands run by +-- concurrent properties will be scrambled together. module Propellor.Property.Concurrent ( concurrently, -- cgit v1.3-2-g0d8e From 357ffb9fd34ebd36e07dece8e45450dbd2f0e8ec Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Wed, 28 Oct 2015 00:12:38 -0400 Subject: concurrency docs --- debian/changelog | 2 +- src/Propellor/Message.hs | 16 ++++++++-------- src/Propellor/Property/Concurrent.hs | 37 +++++++++++++++++++++++++++++++++--- 3 files changed, 43 insertions(+), 12 deletions(-) (limited to 'debian') diff --git a/debian/changelog b/debian/changelog index 1699b27b..6c154e1a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -18,7 +18,7 @@ propellor (2.13.0) UNRELEASED; urgency=medium * combineWith now takes an additional parameter to control how revert actions are combined (API change). * Added Propellor.Property.Concurrent for concurrent properties. - (Note that no command output multiplexing is currently done.) + * execProcess and everything built on it is now concurrent output safe. * Add File.isCopyOf. Thanks, Per Olofsson. -- Joey Hess Sat, 24 Oct 2015 15:16:45 -0400 diff --git a/src/Propellor/Message.hs b/src/Propellor/Message.hs index 4be8263e..3792129b 100644 --- a/src/Propellor/Message.hs +++ b/src/Propellor/Message.hs @@ -238,22 +238,22 @@ messagesDone = lockOutput $ do setTitle "propellor: done" hFlush stdout --- | Wrapper around `System.Process.createProcess` that prevents processes --- that are running concurrently from writing to the stdout/stderr at the --- same time. +-- | Wrapper around `System.Process.createProcess` that prevents +-- multiple processes that are running concurrently from writing +-- to stdout/stderr at the same time. -- --- The first process run by createProcess is allowed to write to +-- The first process is allowed to write to -- stdout and stderr in the usual way. -- --- However, if a second createProcess runs concurrently with the +-- However, if another process runs concurrently with the -- first, any stdout or stderr that would have been displayed by it is -- instead buffered. The buffered output will be displayed the next time it -- is safe to do so (ie, after the first process exits). -- --- `Propellor.Property.Cmd` has some other useful actions for running --- commands, which are based on this. --- -- Also does debug logging of all commands run. +-- +-- Unless you manually import System.Process, every part of propellor +-- that runs a process uses this. createProcessConcurrent :: P.CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, P.ProcessHandle) createProcessConcurrent p | hasoutput (P.std_out p) || hasoutput (P.std_err p) = diff --git a/src/Propellor/Property/Concurrent.hs b/src/Propellor/Property/Concurrent.hs index 645a5dfd..74afecc4 100644 --- a/src/Propellor/Property/Concurrent.hs +++ b/src/Propellor/Property/Concurrent.hs @@ -1,14 +1,38 @@ {-# LANGUAGE FlexibleContexts #-} --- | Note that any output of commands run by --- concurrent properties will be scrambled together. +-- | Propellor properties can be made to run concurrently, using this +-- module. This can speed up propellor, at the expense of using more CPUs +-- and other resources. +-- +-- It's up to you to make sure that properties that you make run concurrently +-- don't implicitly depend on one-another. The worst that can happen +-- though, is that propellor fails to ensure some of the properties, +-- and tells you what went wrong. +-- +-- Another potential problem is that output of concurrent properties could +-- interleave into a scrambled mess. This is mostly prevented; all messages +-- output by propellor are concurrency safe, including `errorMessage`, +-- `infoMessage`, etc. However, if you write a property that directly +-- uses `print` or `putStrLn`, you can still experience this problem. +-- +-- Similarly, when properties run external commands, the command's output +-- can be a problem for concurrency. No need to worry; +-- `Propellor.Property.Cmd.createProcess` is concurrent output safe +-- (it actually uses `Propellor.Message.createProcessConcurrent`), and +-- everything else in propellor that runs external commands is built on top +-- of that. Of course, if you import System.Process and use it in a +-- property, you can bypass that and shoot yourself in the foot. +-- +-- Finally, anything that directly accesses the tty can bypass +-- these protections. That's sometimes done for eg, password prompts. +-- A well-written property should avoid running interactive commands +-- anyway. module Propellor.Property.Concurrent ( concurrently, concurrentList, props, getNumProcessors, - withCapabilities, concurrentSatisfy, ) where @@ -20,6 +44,12 @@ import GHC.Conc (getNumProcessors) import Control.Monad.RWS.Strict -- | Ensures two properties concurrently. +-- +-- > & foo `concurrently` bar +-- +-- To ensure three properties concurrently, just use this combinator twice: +-- +-- > & foo `concurrently` bar `concurrently` baz concurrently :: (IsProp p1, IsProp p2, Combines p1 p2, IsProp (CombinedType p1 p2)) => p1 @@ -95,6 +125,7 @@ withCapabilities n a = bracket setup cleanup (const a) return c cleanup = liftIO . setNumCapabilities +-- | Running Propellor actions concurrently. concurrentSatisfy :: Propellor Result -> Propellor Result -> Propellor Result concurrentSatisfy a1 a2 = do h <- ask -- cgit v1.3-2-g0d8e From 111ea88d4d7c54e9ab7950962ad22528d54dd959 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Wed, 28 Oct 2015 14:46:17 -0400 Subject: fix bad MVar use, use STM I had 2 MVars both involved in the same lock, and it seemed intractable to avoid deadlocks with them. STM makes it easy. At this point, the concurrent process stuff seems to work pretty well, but I'm not 100% sure it's not got some bugs. --- debian/changelog | 1 + debian/control | 2 + propellor.cabal | 6 +- src/Propellor/Bootstrap.hs | 3 +- src/Utility/ConcurrentOutput.hs | 173 ++++++++++++++++++++++------------------ 5 files changed, 104 insertions(+), 81 deletions(-) (limited to 'debian') diff --git a/debian/changelog b/debian/changelog index 6c154e1a..f3522b7c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -19,6 +19,7 @@ propellor (2.13.0) UNRELEASED; urgency=medium actions are combined (API change). * Added Propellor.Property.Concurrent for concurrent properties. * execProcess and everything built on it is now concurrent output safe. + * Propellor now depends on stm. * Add File.isCopyOf. Thanks, Per Olofsson. -- Joey Hess Sat, 24 Oct 2015 15:16:45 -0400 diff --git a/debian/control b/debian/control index 7f42c916..2956fdaa 100644 --- a/debian/control +++ b/debian/control @@ -17,6 +17,7 @@ Build-Depends: libghc-mtl-dev, libghc-transformers-dev, libghc-exceptions-dev (>= 0.6), + libghc-stm-dev, Maintainer: Gergely Nagy Standards-Version: 3.9.6 Vcs-Git: git://git.joeyh.name/propellor @@ -39,6 +40,7 @@ Depends: ${misc:Depends}, ${shlibs:Depends}, libghc-mtl-dev, libghc-transformers-dev, libghc-exceptions-dev (>= 0.6), + libghc-stm-dev, git, make, Description: property-based host configuration management in haskell diff --git a/propellor.cabal b/propellor.cabal index 20e82407..da43775f 100644 --- a/propellor.cabal +++ b/propellor.cabal @@ -39,7 +39,7 @@ Executable propellor Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5, IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal, containers (>= 0.5), network, async, time, QuickCheck, mtl, transformers, - exceptions (>= 0.6) + exceptions (>= 0.6), stm if (! os(windows)) Build-Depends: unix @@ -51,7 +51,7 @@ Executable propellor-config Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5, IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal, containers (>= 0.5), network, async, time, QuickCheck, mtl, transformers, - exceptions + exceptions, stm if (! os(windows)) Build-Depends: unix @@ -62,7 +62,7 @@ Library Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5, IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal, containers (>= 0.5), network, async, time, QuickCheck, mtl, transformers, - exceptions + exceptions, stm if (! os(windows)) Build-Depends: unix diff --git a/src/Propellor/Bootstrap.hs b/src/Propellor/Bootstrap.hs index 6a5d5acb..2318b910 100644 --- a/src/Propellor/Bootstrap.hs +++ b/src/Propellor/Bootstrap.hs @@ -65,7 +65,7 @@ depsCommand = "( " ++ intercalate " ; " (concat [osinstall, cabalinstall]) ++ " aptinstall p = "apt-get --no-upgrade --no-install-recommends -y install " ++ p - -- This is the same build deps listed in debian/control. + -- This is the same deps listed in debian/control. debdeps = [ "gnupg" , "ghc" @@ -81,6 +81,7 @@ depsCommand = "( " ++ intercalate " ; " (concat [osinstall, cabalinstall]) ++ " , "libghc-mtl-dev" , "libghc-transformers-dev" , "libghc-exceptions-dev" + , "libghc-stm-dev" , "make" ] diff --git a/src/Utility/ConcurrentOutput.hs b/src/Utility/ConcurrentOutput.hs index c6550b84..5535066f 100644 --- a/src/Utility/ConcurrentOutput.hs +++ b/src/Utility/ConcurrentOutput.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE BangPatterns #-} + -- | Concurrent output handling. module Utility.ConcurrentOutput ( @@ -14,6 +16,7 @@ import Control.Monad.IO.Class (liftIO, MonadIO) import Control.Applicative import System.IO.Unsafe (unsafePerformIO) import Control.Concurrent +import Control.Concurrent.STM import Control.Concurrent.Async import Data.Maybe import Data.List @@ -25,21 +28,23 @@ import Utility.Monad import Utility.Exception data OutputHandle = OutputHandle - { outputLock :: MVar () -- ^ empty when locked - , outputLockedBy :: MVar Locker + { outputLock :: TMVar (Maybe Locker) } data Locker = GeneralLock - | ProcessLock P.ProcessHandle + | ProcessLock P.ProcessHandle String + +instance Show Locker where + show GeneralLock = "GeneralLock" + show (ProcessLock _ cmd) = "ProcessLock " ++ cmd -- | A shared global variable for the OutputHandle. {-# NOINLINE globalOutputHandle #-} globalOutputHandle :: MVar OutputHandle globalOutputHandle = unsafePerformIO $ newMVar =<< OutputHandle - <$> newMVar () - <*> newEmptyMVar + <$> newTMVarIO Nothing -- | Gets the global OutputHandle. getOutputHandle :: IO OutputHandle @@ -58,61 +63,69 @@ takeOutputLock = void $ takeOutputLock' True tryTakeOutputLock :: IO Bool tryTakeOutputLock = takeOutputLock' False -takeOutputLock' :: Bool -> IO Bool -takeOutputLock' block = do +withLock :: (TMVar (Maybe Locker) -> STM a) -> IO a +withLock a = do lck <- outputLock <$> getOutputHandle - go =<< tryTakeMVar lck + atomically (a lck) + +-- The lock TMVar is kept full normally, even if only with Nothing, +-- so if we take it here, that blocks anyone else from trying +-- to take the lock while we are checking it. +takeOutputLock' :: Bool -> IO Bool +takeOutputLock' block = go =<< withLock tryTakeTMVar where - -- lck was full, and we've emptied it, so we hold the lock now. - go (Just ()) = havelock - -- lck is empty, so someone else is holding the lock. - go Nothing = do - lcker <- outputLockedBy <$> getOutputHandle - v' <- tryTakeMVar lcker - case v' of - Just (ProcessLock h) -> - -- if process has exited, lock is stale - ifM (isJust <$> P.getProcessExitCode h) - ( havelock - , if block - then do - void $ P.waitForProcess h - havelock - else do - putMVar lcker (ProcessLock h) - return False - ) - Just GeneralLock -> do - putMVar lcker GeneralLock - whenblock waitlock - Nothing -> whenblock waitlock + go Nothing = whenblock waitlock + -- Something has the lock. It may be stale, so check it. + -- We must always be sure to fill the TMVar back with Just or Nothing. + go (Just orig) = case orig of + Nothing -> havelock + (Just (ProcessLock h _)) -> + -- when process has exited, lock is stale + ifM (isJust <$> P.getProcessExitCode h) + ( havelock + , if block + then do + hPutStr stderr "WAITFORPROCESS in lock" + hFlush stderr + void $ P.waitForProcess h + hPutStr stderr "WAITFORPROCESS in lock done" + hFlush stderr + havelock + else do + withLock (`putTMVar` orig) + return False + ) + (Just GeneralLock) -> do + withLock (`putTMVar` orig) + whenblock waitlock havelock = do - updateOutputLocker GeneralLock + withLock (`putTMVar` Just GeneralLock) return True - waitlock = do - -- Wait for current lock holder to relinquish - -- it and take the lock. - lck <- outputLock <$> getOutputHandle - takeMVar lck - havelock + + -- Wait for current lock holder (if any) to relinquish + -- it and take the lock for ourselves. + waitlock = withLock $ \l -> do + v <- tryTakeTMVar l + case v of + Just (Just _) -> retry + _ -> do + putTMVar l (Just GeneralLock) + return True + whenblock a = if block then a else return False -- | Only safe to call after taking the output lock. dropOutputLock :: IO () -dropOutputLock = do - lcker <- outputLockedBy <$> getOutputHandle - lck <- outputLock <$> getOutputHandle - void $ takeMVar lcker - putMVar lck () +dropOutputLock = withLock $ \l -> do + void $ takeTMVar l + putTMVar l Nothing -- | Only safe to call after takeOutputLock; updates the Locker. updateOutputLocker :: Locker -> IO () -updateOutputLocker l = do - lcker <- outputLockedBy <$> getOutputHandle - void $ tryTakeMVar lcker - putMVar lcker l - modifyMVar_ lcker (const $ return l) +updateOutputLocker locker = withLock $ \l -> do + void $ takeTMVar l + putTMVar l (Just locker) -- | Use this around any IO actions that use `outputConcurrent` -- or `createProcessConcurrent` @@ -124,7 +137,7 @@ withConcurrentOutput a = a `finally` drain where -- Just taking the output lock is enough to ensure that anything -- that was buffering output has had a chance to flush its buffer. - drain = lockOutput (return ()) + drain = lockOutput noop -- | Displays a string to stdout, and flush output so it's displayed. -- @@ -158,28 +171,25 @@ outputConcurrent s = do -- as the output lock becomes free. createProcessConcurrent :: P.CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, P.ProcessHandle) createProcessConcurrent p - | willoutput (P.std_out p) || willoutput (P.std_err p) = + | willOutput (P.std_out p) || willOutput (P.std_err p) = ifM tryTakeOutputLock - ( do - hPutStrLn stderr "IS NOT CONCURRENT" - firstprocess - , do - hPutStrLn stderr "IS CONCURRENT" - concurrentprocess + ( firstprocess + , concurrentprocess ) | otherwise = P.createProcess p where - willoutput P.Inherit = True - willoutput _ = False + rediroutput ss h + | willOutput ss = P.UseHandle h + | otherwise = ss - rediroutput str h - | willoutput str = P.UseHandle h - | otherwise = str + cmd = case P.cmdspec p of + P.ShellCommand s -> s + P.RawCommand c ps -> unwords (c:ps) firstprocess = do r@(_, _, _, h) <- P.createProcess p `onException` dropOutputLock - updateOutputLocker (ProcessLock h) + updateOutputLocker (ProcessLock h cmd) -- Output lock is still held as we return; the process -- is running now, and once it exits the output lock will -- be stale and can then be taken by something else. @@ -196,8 +206,8 @@ createProcessConcurrent p hClose toouth hClose toerrh buf <- newMVar [] - void $ async $ outputDrainer fromouth stdout buf - void $ async $ outputDrainer fromerrh stderr buf + void $ async $ outputDrainer (P.std_out p) fromouth stdout buf + void $ async $ outputDrainer (P.std_err p) fromerrh stderr buf void $ async $ bufferWriter buf return r @@ -205,6 +215,10 @@ createProcessConcurrent p (from, to) <- createPipe (,) <$> fdToHandle to <*> fdToHandle from +willOutput :: P.StdStream -> Bool +willOutput P.Inherit = True +willOutput _ = False + type Buffer = [(Handle, BufferedActivity)] data BufferedActivity @@ -213,17 +227,22 @@ data BufferedActivity | InTempFile FilePath deriving (Eq) --- Drain output from the handle, and buffer it in memory. -outputDrainer :: Handle -> Handle -> MVar Buffer -> IO () -outputDrainer fromh toh buf = do - v <- tryIO $ B.hGetSome fromh 1024 - case v of - Right b | not (B.null b) -> do - modifyMVar_ buf $ addBuffer (toh, Output b) - outputDrainer fromh toh buf - _ -> do - modifyMVar_ buf $ pure . (++ [(toh, ReachedEnd)]) - hClose fromh +-- Drain output from the handle, and buffer it. +outputDrainer :: P.StdStream -> Handle -> Handle -> MVar Buffer -> IO () +outputDrainer ss fromh toh buf + | willOutput ss = go + | otherwise = atend + where + go = do + v <- tryIO $ B.hGetSome fromh 1024 + case v of + Right b | not (B.null b) -> do + modifyMVar_ buf $ addBuffer (toh, Output b) + go + _ -> atend + atend = do + modifyMVar_ buf $ pure . (++ [(toh, ReachedEnd)]) + hClose fromh -- Wait to lock output, and once we can, display everything -- that's put into buffer, until the end is signaled by Nothing @@ -262,8 +281,8 @@ addBuffer (toh, Output b) buf hClose h return ((toh, InTempFile tmp) : other) where - b' = B.concat (mapMaybe getOutput this) <> b - (this, other) = partition same buf + !b' = B.concat (mapMaybe getOutput this) <> b + !(this, other) = partition same buf same v = fst v == toh && case snd v of Output _ -> True _ -> False -- cgit v1.3-2-g0d8e From badbebad467399e9acf9e058024e6c69997a41d4 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Wed, 28 Oct 2015 15:43:20 -0400 Subject: wording --- debian/changelog | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'debian') diff --git a/debian/changelog b/debian/changelog index f3522b7c..8c4715f5 100644 --- a/debian/changelog +++ b/debian/changelog @@ -18,8 +18,9 @@ propellor (2.13.0) UNRELEASED; urgency=medium * combineWith now takes an additional parameter to control how revert actions are combined (API change). * Added Propellor.Property.Concurrent for concurrent properties. - * execProcess and everything built on it is now concurrent output safe. - * Propellor now depends on stm. + * Made the execProcess exported by propellor, and everything built on it, + avoid scrambled output when run concurrently. + * Propellor now depends on STM. * Add File.isCopyOf. Thanks, Per Olofsson. -- Joey Hess Sat, 24 Oct 2015 15:16:45 -0400 -- cgit v1.3-2-g0d8e From 5cde1ed21cc912db0b53846196f920fe52835dbc Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Wed, 28 Oct 2015 17:03:17 -0400 Subject: fix memory leak, and optimise when command output is very large --- debian/changelog | 3 ++ propellor.cabal | 6 +-- src/Utility/ConcurrentOutput.hs | 113 +++++++++++++++++++++++----------------- 3 files changed, 72 insertions(+), 50 deletions(-) (limited to 'debian') diff --git a/debian/changelog b/debian/changelog index 8c4715f5..c5538c7f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -21,6 +21,9 @@ propellor (2.13.0) UNRELEASED; urgency=medium * Made the execProcess exported by propellor, and everything built on it, avoid scrambled output when run concurrently. * Propellor now depends on STM. + * The cabal file now builds propellor with -O. While -O0 makes ghc + take less memory while building propellor, it can lead to bad memory + usage at runtime due to eg, disabled stream fusion. * Add File.isCopyOf. Thanks, Per Olofsson. -- Joey Hess Sat, 24 Oct 2015 15:16:45 -0400 diff --git a/propellor.cabal b/propellor.cabal index da43775f..a07109a7 100644 --- a/propellor.cabal +++ b/propellor.cabal @@ -34,7 +34,7 @@ Description: Executable propellor Main-Is: wrapper.hs - GHC-Options: -threaded -O0 -Wall -fno-warn-tabs + GHC-Options: -threaded -O -Wall -fno-warn-tabs Hs-Source-Dirs: src Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5, IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal, @@ -46,7 +46,7 @@ Executable propellor Executable propellor-config Main-Is: config.hs - GHC-Options: -threaded -O0 -Wall -fno-warn-tabs + GHC-Options: -threaded -O -Wall -fno-warn-tabs Hs-Source-Dirs: src Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5, IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal, @@ -57,7 +57,7 @@ Executable propellor-config Build-Depends: unix Library - GHC-Options: -O0 -Wall -fno-warn-tabs + GHC-Options: -O -Wall -fno-warn-tabs Hs-Source-Dirs: src Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5, IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal, diff --git a/src/Utility/ConcurrentOutput.hs b/src/Utility/ConcurrentOutput.hs index 5bf973de..be1562ac 100644 --- a/src/Utility/ConcurrentOutput.hs +++ b/src/Utility/ConcurrentOutput.hs @@ -1,8 +1,11 @@ {-# LANGUAGE BangPatterns #-} +{-# OPTIONS_GHC -fno-warn-tabs #-} -- | Concurrent output handling. module Utility.ConcurrentOutput ( + takeOutputLock, + dropOutputLock, withConcurrentOutput, outputConcurrent, createProcessConcurrent, @@ -146,6 +149,20 @@ outputConcurrent s = do hFlush stdout -- TODO +-- | This must be used to wait for processes started with +-- `createProcessConcurrent`. +-- +-- This is necessary because `System.Process.waitForProcess` has a +-- race condition when two threads check the same process. If the race +-- is triggered, one thread will successfully wait, but the other +-- throws a DoesNotExist exception. +waitForProcessConcurrent :: P.ProcessHandle -> IO ExitCode +waitForProcessConcurrent h = do + v <- tryWhenExists (P.waitForProcess h) + case v of + Just r -> return r + Nothing -> maybe (waitForProcessConcurrent h) return =<< P.getProcessExitCode h + -- | Wrapper around `System.Process.createProcess` that prevents -- multiple processes that are running concurrently from writing -- to stdout/stderr at the same time. @@ -196,37 +213,20 @@ createProcessConcurrent p , P.std_err = rediroutput (P.std_err p) toerrh } r <- P.createProcess p' - hClose toouth - hClose toerrh - buf <- newMVar [] - void $ async $ outputDrainer (P.std_out p) fromouth stdout buf - void $ async $ outputDrainer (P.std_err p) fromerrh stderr buf - void $ async $ bufferWriter buf + outbuf <- setupBuffer stdout toouth (P.std_out p) fromouth + errbuf <- setupBuffer stderr toerrh (P.std_err p) fromerrh + void $ async $ bufferWriter [outbuf, errbuf] return r pipe = do (from, to) <- createPipe (,) <$> fdToHandle to <*> fdToHandle from --- | This must be used to wait for processes started with --- `createProcessConcurrent`. --- --- This is necessary because `System.Process.waitForProcess` has a --- race condition when two threads check the same process. If the race --- is triggered, one thread will successfully wait, but the other --- throws a DoesNotExist exception. -waitForProcessConcurrent :: P.ProcessHandle -> IO ExitCode -waitForProcessConcurrent h = do - v <- tryWhenExists (P.waitForProcess h) - case v of - Just r -> return r - Nothing -> maybe (waitForProcessConcurrent h) return =<< P.getProcessExitCode h - willOutput :: P.StdStream -> Bool willOutput P.Inherit = True willOutput _ = False -type Buffer = [(Handle, BufferedActivity)] +type Buffer = [BufferedActivity] data BufferedActivity = ReachedEnd @@ -234,43 +234,62 @@ data BufferedActivity | InTempFile FilePath deriving (Eq) +instance Show BufferedActivity where + show ReachedEnd = "ReachedEnd" + show (Output b) = "Output " ++ show (B.length b) + show (InTempFile t) = "InTempFile " ++ t + +setupBuffer :: Handle -> Handle -> P.StdStream -> Handle -> IO (Handle, MVar Buffer, TMVar ()) +setupBuffer h toh ss fromh = do + hClose toh + buf <- newMVar [] + bufsig <- atomically newEmptyTMVar + void $ async $ outputDrainer ss fromh buf bufsig + return (h, buf, bufsig) + -- Drain output from the handle, and buffer it. -outputDrainer :: P.StdStream -> Handle -> Handle -> MVar Buffer -> IO () -outputDrainer ss fromh toh buf +outputDrainer :: P.StdStream -> Handle -> MVar Buffer -> TMVar () -> IO () +outputDrainer ss fromh buf bufsig | willOutput ss = go | otherwise = atend where go = do - v <- tryIO $ B.hGetSome fromh 1024 + v <- tryIO $ B.hGetSome fromh 1048576 case v of Right b | not (B.null b) -> do - modifyMVar_ buf $ addBuffer (toh, Output b) + modifyMVar_ buf $ addBuffer (Output b) + changed go _ -> atend atend = do - modifyMVar_ buf $ pure . ((toh, ReachedEnd) :) + modifyMVar_ buf $ pure . (ReachedEnd :) + changed hClose fromh + changed = atomically $ do + void $ tryTakeTMVar bufsig + putTMVar bufsig () -- Wait to lock output, and once we can, display everything --- that's put into buffer, until the end is signaled by Nothing --- for both stdout and stderr. -bufferWriter :: MVar Buffer -> IO () -bufferWriter buf = lockOutput (go [stdout, stderr]) +-- that's put into the buffers. +bufferWriter :: [(Handle, MVar Buffer, TMVar ())] -> IO () +bufferWriter = void . lockOutput . mapConcurrently go where - go [] = return () - go hs = do + go v@(outh, buf, bufsig) = do + atomically $ takeTMVar bufsig l <- takeMVar buf - forM_ (reverse l) $ \(h, ba) -> case ba of + putMVar buf [] + forM_ (reverse l) $ \ba -> case ba of Output b -> do - B.hPut h b - hFlush h + B.hPut outh b + hFlush outh + return () InTempFile tmp -> do - B.hPut h =<< B.readFile tmp + B.hPut outh =<< B.readFile tmp void $ tryWhenExists $ removeFile tmp ReachedEnd -> return () - let hs' = filter (\h -> not (any (== (h, ReachedEnd)) l)) hs - putMVar buf [] - go hs' + if any (== ReachedEnd) l + then return () + else go v -- Adds a value to the Buffer. When adding Output to a Handle, it's cheaper -- to combine it with any already buffered Output to that same Handle. @@ -278,22 +297,22 @@ bufferWriter buf = lockOutput (go [stdout, stderr]) -- When the total buffered Output exceeds 1 mb in size, it's moved out of -- memory, to a temp file. This should only happen rarely, but is done to -- avoid some verbose process unexpectedly causing excessive memory use. -addBuffer :: (Handle, BufferedActivity) -> Buffer -> IO Buffer -addBuffer (toh, Output b) buf - | B.length b' <= 1000000 = return ((toh, Output b') : other) +addBuffer :: BufferedActivity -> Buffer -> IO Buffer +addBuffer (Output b) buf + | B.length b' <= 1048576 = return (Output b' : other) | otherwise = do tmpdir <- getTemporaryDirectory (tmp, h) <- openTempFile tmpdir "output.tmp" B.hPut h b' hClose h - return ((toh, InTempFile tmp) : other) + return (InTempFile tmp : other) where !b' = B.concat (mapMaybe getOutput this) <> b - !(this, other) = partition same buf - same v = fst v == toh && case snd v of + !(this, other) = partition isOutput buf + isOutput v = case v of Output _ -> True _ -> False - getOutput v = case snd v of + getOutput v = case v of Output b'' -> Just b'' _ -> Nothing -addBuffer v buf = return (buf ++ [v]) +addBuffer v buf = return (v:buf) -- cgit v1.3-2-g0d8e From 39fa051833de3178639974fa4fc7c803c5918f0e Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 29 Oct 2015 00:38:53 -0400 Subject: generalize what can be output This adds a dependency on Text, but I don't mind propellor depending on it and am somewhat surprised it doesn't already. Using Text also lets this use encodeUtf8 instead of the nasty hack it was using to go from String -> ByteString. --- debian/changelog | 2 +- debian/control | 2 ++ propellor.cabal | 6 +++--- src/Propellor/Bootstrap.hs | 1 + src/Utility/ConcurrentOutput.hs | 35 +++++++++++++++++++++++++---------- 5 files changed, 32 insertions(+), 14 deletions(-) (limited to 'debian') diff --git a/debian/changelog b/debian/changelog index c5538c7f..6f75bce9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -20,7 +20,7 @@ propellor (2.13.0) UNRELEASED; urgency=medium * Added Propellor.Property.Concurrent for concurrent properties. * Made the execProcess exported by propellor, and everything built on it, avoid scrambled output when run concurrently. - * Propellor now depends on STM. + * Propellor now depends on STM and text. * The cabal file now builds propellor with -O. While -O0 makes ghc take less memory while building propellor, it can lead to bad memory usage at runtime due to eg, disabled stream fusion. diff --git a/debian/control b/debian/control index 2956fdaa..97fb3e6d 100644 --- a/debian/control +++ b/debian/control @@ -18,6 +18,7 @@ Build-Depends: libghc-transformers-dev, libghc-exceptions-dev (>= 0.6), libghc-stm-dev, + libghc-text-dev, Maintainer: Gergely Nagy Standards-Version: 3.9.6 Vcs-Git: git://git.joeyh.name/propellor @@ -41,6 +42,7 @@ Depends: ${misc:Depends}, ${shlibs:Depends}, libghc-transformers-dev, libghc-exceptions-dev (>= 0.6), libghc-stm-dev, + libghc-text-dev, git, make, Description: property-based host configuration management in haskell diff --git a/propellor.cabal b/propellor.cabal index a07109a7..6e871d6b 100644 --- a/propellor.cabal +++ b/propellor.cabal @@ -39,7 +39,7 @@ Executable propellor Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5, IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal, containers (>= 0.5), network, async, time, QuickCheck, mtl, transformers, - exceptions (>= 0.6), stm + exceptions (>= 0.6), stm, text if (! os(windows)) Build-Depends: unix @@ -51,7 +51,7 @@ Executable propellor-config Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5, IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal, containers (>= 0.5), network, async, time, QuickCheck, mtl, transformers, - exceptions, stm + exceptions, stm, text if (! os(windows)) Build-Depends: unix @@ -62,7 +62,7 @@ Library Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5, IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal, containers (>= 0.5), network, async, time, QuickCheck, mtl, transformers, - exceptions, stm + exceptions, stm, text if (! os(windows)) Build-Depends: unix diff --git a/src/Propellor/Bootstrap.hs b/src/Propellor/Bootstrap.hs index 2318b910..21772b34 100644 --- a/src/Propellor/Bootstrap.hs +++ b/src/Propellor/Bootstrap.hs @@ -82,6 +82,7 @@ depsCommand = "( " ++ intercalate " ; " (concat [osinstall, cabalinstall]) ++ " , "libghc-transformers-dev" , "libghc-exceptions-dev" , "libghc-stm-dev" + , "libghc-text-dev" , "make" ] diff --git a/src/Utility/ConcurrentOutput.hs b/src/Utility/ConcurrentOutput.hs index 94cd4202..c24744a3 100644 --- a/src/Utility/ConcurrentOutput.hs +++ b/src/Utility/ConcurrentOutput.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE BangPatterns, TypeSynonymInstances, FlexibleInstances #-} {-# OPTIONS_GHC -fno-warn-tabs #-} -- | @@ -20,6 +20,7 @@ module Utility.ConcurrentOutput ( withConcurrentOutput, flushConcurrentOutput, + Outputable(..), outputConcurrent, createProcessConcurrent, waitForProcessConcurrent, @@ -40,13 +41,14 @@ import Control.Concurrent.Async import Data.Maybe import Data.List import Data.Monoid -import qualified Data.ByteString as B import qualified System.Process as P import qualified Data.Set as S +import qualified Data.ByteString as B +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Utility.Monad import Utility.Exception -import Utility.FileSystemEncoding data OutputHandle = OutputHandle { outputLock :: TMVar Lock @@ -137,27 +139,40 @@ flushConcurrentOutput = do -- generating output, and flush any buffered output. lockOutput $ return () --- | Displays a string to stdout, and flush output so it's displayed. +-- | Values that can be output. +class Outputable v where + toOutput :: v -> B.ByteString + +instance Outputable B.ByteString where + toOutput = id + +instance Outputable T.Text where + toOutput = encodeUtf8 + +instance Outputable String where + toOutput = toOutput . T.pack + +-- | Displays a value to stdout, and flush output so it's displayed. -- --- Uses locking to ensure that the whole string is output atomically +-- Uses locking to ensure that the whole output occurs atomically -- even when other threads are concurrently generating output. -- -- When something else is writing to the console at the same time, this does --- not block. It buffers the string, so it will be displayed once the other +-- not block. It buffers the value, so it will be displayed once the other -- writer is done. -outputConcurrent :: String -> IO () -outputConcurrent s = bracket setup cleanup go +outputConcurrent :: Outputable v => v -> IO () +outputConcurrent v = bracket setup cleanup go where setup = tryTakeOutputLock cleanup False = return () cleanup True = dropOutputLock go True = do - putStr s + B.hPut stdout (toOutput v) hFlush stdout go False = do bv <- outputBuffer <$> getOutputHandle oldbuf <- atomically $ takeTMVar bv - newbuf <- addBuffer (Output (B.pack (decodeW8NUL s))) oldbuf + newbuf <- addBuffer (Output (toOutput v)) oldbuf atomically $ putTMVar bv newbuf -- | This must be used to wait for processes started with -- cgit v1.3-2-g0d8e From 62e73677a745b89d88788d368c9439ccb4452b7b Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Thu, 29 Oct 2015 11:08:01 -0400 Subject: merge patch from Santiago Vila --- debian/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'debian') diff --git a/debian/control b/debian/control index 97fb3e6d..1a7909a3 100644 --- a/debian/control +++ b/debian/control @@ -19,7 +19,7 @@ Build-Depends: libghc-exceptions-dev (>= 0.6), libghc-stm-dev, libghc-text-dev, -Maintainer: Gergely Nagy +Maintainer: Joey Hess Standards-Version: 3.9.6 Vcs-Git: git://git.joeyh.name/propellor Homepage: http://propellor.branchable.com/ -- cgit v1.3-2-g0d8e