packages feed

shellout (empty) → 0.1.0.0

raw patch · 7 files changed

+391/−0 lines, 7 filesdep +asyncdep +basedep +shelloutsetup-changed

Dependencies added: async, base, shellout, stm, text, typed-process

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for shellout++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Logan McPhail (c) 2018++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Author name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,70 @@+# shellout++A simple library for running standard processes in a threaded manager+, and responding to/doing things with stdout, stderr, and exits as they happen.++This was mainly built out of frustration that most libraries wait for a task+to complete before giving output.++Just initialize with a driver and off you go!++## Example++Here's a short annotated example on how to use this:++```haskell+{-# LANGUAGE OverloadedStrings #-}+module Main where++import qualified Shellout++import           Control.Concurrent (threadDelay)+import           Data.Monoid        ((<>))+import           Data.Text          (Text)+import qualified Data.Text.IO       as T++-- create a type to hold data that will be passed between all of your handlers,+-- where you could store things like the task name, a spinner's position+-- and last spin time, etc.+newtype Task = Task {name :: Text}++main :: IO ()+main = do+  -- create a 'driver', with functions that handle each type of output+  let driver = Shellout.Driver+        -- (optionally) do something with the task name, and initialize+        -- data that will be passed between your handlers. you could store+        -- things like the task name, a spinner's position and last spin time, etc.+        { Shellout.initialState =+          \taskName -> Task {name = taskName}++        -- what to do when polling returns `Nothing`, so it's waiting on+        -- more output from the task, but the task still hasn't exited.+        -- In this example, we just sleep.+        , Shellout.handleNothing =+          \task -> threadDelay 1000 >> pure task++        -- what to do on stdout from the shell command. You could colorize it,+        -- append it to list of output (you could keep a list of them in the `Task`), etc.+        , Shellout.handleOut =+          \task txt -> T.putStrLn (name task <> ": " <> txt) >> pure task++        -- what to do on stderr. Same things go as stdout.+        , Shellout.handleErr =+          \task txt -> T.putStrLn (name task <> ": " <> txt) >> pure task++        -- what to do when a task completes successfully+        , Shellout.handleSuccess =+          \task -> T.putStrLn $ (name task) <> " complete."++        -- what to do when a task doesn't complete successfully+        , Shellout.handleFailure =+          \task -> T.putStrLn $ (name task) <> " failed."+        }++  shell <- Shellout.new driver++  shell "listing directory" "ls"++  shell "listing hidden files" "ls -lah"+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ examples/Main.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import qualified Shellout++import           Control.Concurrent (threadDelay)+import           Data.Monoid        ((<>))+import           Data.Text          (Text)+import qualified Data.Text.IO       as T++-- create a type to hold data that will be passed between all of your handlers,+-- where you could store things like the task name, a spinner's position+-- and last spin time, etc.+newtype Task = Task {name :: Text}++main :: IO ()+main = do+  -- create a 'driver', with functions that handle each type of output+  let driver = Shellout.Driver+        -- (optionally) do something with the task name, and initialize+        -- data that will be passed between your handlers.+        { Shellout.initialState =+          \taskName -> Task {name = taskName}++        -- what to do when polling returns `Nothing`, so it's waiting on+        -- more output from the task, but the task still hasn't exited.+        -- In this example, we just sleep.+        , Shellout.handleNothing =+          \task -> threadDelay 1000 >> pure task++        -- what to do on stdout from the shell command. You could colorize it,+        -- append it to list of output (you could keep a list of them in the `Task`), etc.+        , Shellout.handleOut =+          \task txt -> T.putStrLn (name task <> ": " <> txt) >> pure task++        -- what to do on stderr. Same things go as stdout.+        , Shellout.handleErr =+          \task txt -> T.putStrLn (name task <> ": " <> txt) >> pure task++        -- what to do when a task completes successfully+        , Shellout.handleSuccess =+          \task -> T.putStrLn $ (name task) <> " complete."++        -- what to do when a task doesn't complete successfully+        , Shellout.handleFailure =+          \task -> T.putStrLn $ (name task) <> " failed."+        }++  shell <- Shellout.new driver++  shell "listing directory" "ls"++  shell "listing hidden files too" "ls -lah"
+ shellout.cabal view
@@ -0,0 +1,59 @@+-- This file has been generated from package.yaml by hpack version 0.20.0.+--+-- see: https://github.com/sol/hpack+--+-- hash: d6b4502f7da1a40315422c36f8a951aea21ff169c7154604924dd8c6d673639d++name:           shellout+version:        0.1.0.0+synopsis:       A threaded manager for Haskell that can run and stream external process output/err/exits+description:    Please see the README on Github at <https://github.com/loganmac/shellout#readme>+category:       System+homepage:       https://github.com/loganmac/shellout#readme+bug-reports:    https://github.com/loganmac/shellout/issues+author:         Logan McPhail+maintainer:     logan.airnomad@gmail.com+copyright:      2018 Logan McPhail+license:        BSD3+license-file:   LICENSE+build-type:     Simple+cabal-version:  >= 1.10++extra-source-files:+    ChangeLog.md+    README.md++source-repository head+  type: git+  location: https://github.com/loganmac/shellout++library+  hs-source-dirs:+      src+  build-depends:+      async+    , base >=4.7 && <5+    , stm+    , text+    , typed-process+  exposed-modules:+      Shellout+  other-modules:+      Paths_shellout+  default-language: Haskell2010++executable example+  main-is: Main.hs+  hs-source-dirs:+      examples+  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      async+    , base >=4.7 && <5+    , shellout+    , stm+    , text+    , typed-process+  other-modules:+      Paths_shellout+  default-language: Haskell2010
+ src/Shellout.hs view
@@ -0,0 +1,174 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-| Shell is a threaded manager that can run external processes+    and call functions of the `Driver` on process output/err/exit.+-}+module Shellout+(Driver(..), Shell, TaskName, Cmd, new)+where++import           Control.Concurrent       (MVar, newEmptyMVar, putMVar,+                                           takeMVar)+import           Control.Concurrent.Async (Async, async, link)+import           Control.Concurrent.STM   (TQueue, atomically, newTQueue,+                                           readTQueue, tryReadTQueue,+                                           writeTQueue)+import           Control.Monad            (unless)+import           Data.Text                (Text, unpack)+import           Data.Text.IO             (hGetLine)+import           GHC.IO.Handle            (Handle)+import           System.Exit              (ExitCode (..), exitWith)+import           System.IO                (hIsEOF)+import           System.Process.Typed     (closed, createPipe, getStderr,+                                           getStdout, setStderr, setStdin,+                                           setStdout, shell, waitExitCode,+                                           withProcess)++--------------------------------------------------------------------------------+-- TYPES++-- | Driver is a collection of functions that+-- describe what to do on process output+data Driver a = Driver++  { -- | (optionally) do something with the task name, and initialize+  -- data that will be passed between your handlers. you could store+  -- things like the task name, a spinner's position and last spin time, etc.+  initialState    :: Text -> a+  -- | what to do when polling returns `Nothing`, so it's waiting on+  -- more output from the task, but the task still hasn't exited.+  -- usually it's sleep.+  , handleNothing :: a -> IO a+  -- | what to do on stdout from the shell command. You could colorize it,+  -- append it to list of output (you could keep a list in the `a`), etc.+  , handleOut     :: a -> Text -> IO a+  -- | what to do on stderr. Same things go as stdout.+  , handleErr     :: a -> Text -> IO a+  -- | what to do when a task completes successfully+  , handleSuccess :: a -> IO ()+  -- | what to do when a task doesn't complete successfully+  , handleFailure :: a -> IO ()+  }++-- | The output of running an external process+data Output = Msg Text | Err Text | Success | Failure Int++-- | Processor has an input channel (for sending commands)+-- and an output channel (for reading the output)+data Processor = Processor (TQueue Text) (TQueue Output)++-- | Shell takes a task name and an external command and+-- executes the given callbacks in the provided driver+type Shell = (TaskName -> Cmd -> IO ())++-- | Task is the description of an external process+type TaskName = Text++-- | Cmd is the external command like 'cat file.txt' to run+type Cmd = Text++--------------------------------------------------------------------------------+-- API++-- | creates a new processor to run external processes in,+-- spawns a thread to run the processor loop, then returns+-- a `Shell` that can send commands to the processor loop,+-- and react to them with functions from the `Driver`+new :: Driver a -> IO Shell+new driver = do+  processor <- Processor <$> newChan <*> newChan+  _         <- spawn $ processorLoop processor+  pure $ execute processor driver+  where newChan = atomically newTQueue++--------------------------------------------------------------------------------+-- EXECUTE / OUTPUT LOOP++-- | executes the given command in the processor+execute :: forall a. Processor -> Driver a -> TaskName -> Cmd -> IO ()+execute (Processor input output) driver task cmd = do+  send input cmd                               -- send the command to the Processor thread+  loop (initialState driver task)              -- start the output loop+  where+    -- | try to read from the channel, returning Nothing if no value available+    maybeReceive = atomically . tryReadTQueue++    loop :: a -> IO ()+    loop acc = do+      out <- maybeReceive output+      case out of+        Nothing -> do+          newAcc <- handleNothing driver acc+          loop newAcc+        Just (Msg msg) -> do+          newAcc <- handleOut driver acc msg+          loop newAcc+        Just (Err msg) -> do+          newAcc <- handleErr driver acc msg+          loop newAcc+        Just Success ->+          handleSuccess driver acc+        Just (Failure c) -> do+          handleFailure driver acc+          exitWith $ ExitFailure c++--------------------------------------------------------------------------------+-- | PROCESSOR LOOP++-- | run the command, putting any stdout, stderr, and exits into the output channel.+-- will wait until stdout and stderr are empty to write the exit code.+processorLoop :: Processor -> IO ()+processorLoop processor@(Processor input output) = do+  cmd <- atomically $ readTQueue input -- receive input++  let config = setStdin closed+             $ setStdout createPipe+             $ setStderr createPipe+             $ shell (unpack cmd)++  withProcess config $ \p -> do+    stdoutLock <- newEmptyMVar -- create locks for stdout/stderr+    stderrLock <- newEmptyMVar++    _ <- spawn $ sendOutput Msg (getStdout p) stdoutLock+    _ <- spawn $ sendOutput Err (getStderr p) stderrLock++    code <- waitExitCode p -- wait for the exit and output locks+    takeMVar stdoutLock+    takeMVar stderrLock++    let result = case code of+          ExitSuccess   -> Success+          ExitFailure i -> Failure i++    send output result++  processorLoop processor++  where+    -- | read from the handle until it's empty, writing the result+    -- (wrapped in the given wrapper type) to the output channel+    -- and then releasing the given lock+    sendOutput :: (Text -> Output) -> Handle -> MVar () -> IO ()+    sendOutput wrap handle lock = do+      let loop = do+            isDone <- hIsEOF handle+            unless isDone $ do+              out <- hGetLine handle+              send output $ wrap out+              loop+      loop+      putMVar lock () -- release the lock++--------------------------------------------------------------------------------+-- THREAD AND CHANNEL HELPERS++-- | new async thread, linked to calling thread (will rethrow exceptions on linked thread)+spawn :: IO a -> IO (Async a)+spawn x = do+  thread <- async x+  link thread+  pure thread++-- | write to the channel+send :: TQueue a -> a -> IO ()+send x = atomically . writeTQueue x