restricted-workers (empty) → 0.1.0
raw patch · 16 files changed
+1509/−0 lines, 16 filesdep +asyncdep +basedep +bytestringsetup-changed
Dependencies added: async, base, bytestring, cereal, data-default, directory, either, filepath, monad-control, mtl, network, selinux, stm, text, transformers, transformers-base, unix
Files
- LICENSE +30/−0
- README.md +73/−0
- Setup.hs +2/−0
- examples/CommandEvalProtocol.lhs +104/−0
- examples/EchoPool.lhs +119/−0
- examples/EchoWorker.lhs +100/−0
- examples/NewWorkerType.lhs +183/−0
- restricted-workers.cabal +83/−0
- src/SignalHandlers.hs +21/−0
- src/System/Restricted/Limits.hs +123/−0
- src/System/Restricted/Types.hs +141/−0
- src/System/Restricted/Worker.hs +104/−0
- src/System/Restricted/Worker/Internal.hs +121/−0
- src/System/Restricted/Worker/Pool.hs +125/−0
- src/System/Restricted/Worker/Protocol.hs +78/−0
- src/System/Restricted/Worker/Types.hs +102/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2013, Dan Frumin++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 Dan Frumin 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,73 @@+# Restricted Workers Library++This library provides an abstract interface for running various kinds+of workers under resource restrictions. It was originally developed as+part of the+interactive-diagrams (<http://github.com/co-dan/interactive-diagrams>)+project. You can read more about security restrictions in the wiki: <https://github.com/co-dan/interactive-diagrams/wiki/Restricted-Workers>++The library provides a convenient way of running worker processes,+saving data obtained by the workers at start-up, a simple pool+abstraction and a configurable security and resource limitations.++Right now there are several kinds of security restrictions that could+be applied to the worker process:++- RLimits+- chroot jail+- custom process euid+- cgroups+- process niceness+- SELinux security context++# Documentation++The easiest way to get a grip of the restricted-workers library is to+look at the examples below showing off the basic concepts of the+library. Another good idea would be to read haddock documentations+which feature comments for each exported function and type in the+library. Do not hesitate to bug me if you think that the documentation+in some places can be improved.++## Examples++The following examples will walk you through creating basic kinds of+workers (IOWorker), handling a pool of workers, communicating with+workers using 'System.Restricted.Workers.Protocol' and creating your+own types of workers.++- [EchoWorker.lhs](examples/EchoWorker.lhs) - basic usage of+ `IOWorker`+- [EchoPool.lhs](examples/EchoPool.lhs) - basic usage of+ `Workers.Pool`+- [CommandEvalProtocol.lhs](examples/CommandEvalProtocol.lhs) -+ rewriting our Echo worker to use the provided Protocol module+- [NewWorkerType.lhs](examples/NewWorkerTypes.lhs) - rolling out your+ own worker types++## Wiki page++<https://github.com/co-dan/interactive-diagrams/wiki/Restricted-Workers>++# External configurations ++Some restrictions require external configuration, below we provide+some example files for them that we use in interactive-diagrams:++- SELinux configuration:+ https://github.com/co-dan/interactive-diagrams/tree/master/selinux+ + Run `build.sh` to build the policy module, then `load.sh` to load+ it. Read the+ [blog post](http://parenz.wordpress.com/2013/07/15/interactive-diagrams-gsoc-progress-report/)+ which explains the policy.+ +- CGroups:+ https://github.com/co-dan/interactive-diagrams/blob/master/cgconfig.conf+ + CGroups configuration is pretty straightforward. You can load the+ configuration with+ + ```+ cgconfigparser -l cgconfig.conf+ ```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ examples/CommandEvalProtocol.lhs view
@@ -0,0 +1,104 @@+> {-# LANGUAGE DeriveGeneric #-}+> module Main where++We already know how to define simple workers that communicate with the+client using text. But sometimes it's necessary to transfer more+complex data to the worker. We provide a simple way of tackling this+problem using the 'System.Restricted.Worker.Protocol' module.++> import Control.Monad (forM_)+> import Control.Concurrent (threadDelay)+> import Data.Default (def)+> import Data.Serialize (Serialize)+> import GHC.Generics (Generic)+> import System.IO (BufferMode (..), Handle, hClose,+> hSetBuffering, stdin)+> +> import System.Restricted.Worker+> import System.Restricted.Worker.Protocol++Imaging we want our worker to receive commands in the form of the+following data structure:++> data CommandW = AddNumbers Int Int+> | Dance+> | Echo String+> | Bye+> deriving (Generic)+> +> instance Serialize CommandW where+> +> evalCommand :: CommandW -> IO ()+> evalCommand (AddNumbers a b) = print $ a + b+> evalCommand Dance = putStrLn "♪ *dum* *dum* ♫"+> evalCommand (Echo s) = putStrLn s+> evalCommand Bye = return ()++If we want to be able to use 'Worker.Protocol' with our data-types we+must define 'Serialize' instances for them. 'Serialize' is a typeclass+from the 'cereal' package and usually you can use the default+implementation (as you have seen above) as long as you can derive+'Generic' for your data-types.++After we are done with our type we can go on defining our handler.+'System.Restricted.Worker.Protocol' provides us with useful functions+for sending and receiving data:++```haskell+-- | Send some serialiazable data over a handle.+-- Returns 'ByteString' representing the encoded data. May throw+-- 'ProtocolException'+sendData :: Serialize a => Handle -> a -> IO ByteString++-- | Read the data from a handle and deserialize it.+-- May throw 'ProtocolException'+getData :: Serialize a => Handle -> IO a+```++Let's try to implement our handler in terms of those functions.++> cmdHandler :: Handle -> IO ()+> cmdHandler hndl = loop =<< getData hndl+> where+> loop Bye = hClose hndl+> loop cmd = evalCommand cmd >> getData hndl >>= loop++Our worker evaluates incoming commands until it hits 'Bye'.++Now let's implement the part of our program that would send commands to the worker:++> sendCommands :: Worker IOWorker -> [CommandW] -> IO ()+> sendCommands worker cmds = do+> hndl <- connectToWorker worker+> forM_ cmds (sendData hndl)+> sendData hndl Bye+> return ()++In the code above we are using 'connectToWorker' function from+'Worker.Internal', which simply connects to the active worker and+returns a handle for communication.++In the main function we delay for 80000 before killing the worker (or exiting) to make sure that the worker process has time to print its output.++> main :: IO ()+> main = do+> hSetBuffering stdin NoBuffering+> (worker,_) <- startIOWorker "Command Evaluator" def+> "/tmp/commandme.sock" cmdHandler+> sendCommands worker [Dance, Dance, AddNumbers 1 2, Echo "Hi", Dance]+> threadDelay 80000+> killWorker worker+> return ()++Sample output:++```+$ runhaskell CommandEvalProtocol.lhs+Starting worker "Command Evaluator"++♪ *dum* *dum* ♫+♪ *dum* *dum* ♫+3+Hi+♪ *dum* *dum* ♫+```
+ examples/EchoPool.lhs view
@@ -0,0 +1,119 @@+> {-# LANGUAGE OverloadedStrings #-}+> module Main where++In this example we'll take a look how to use 'System.Restricted.Worker.Pool' to create a+pool of EchoWokers. See also: 'EchoWorker.lhs'.++> import Control.Concurrent (forkIO, threadDelay)+> import Control.Monad (forever, void)+> import qualified Data.ByteString as BS+> import Data.Default (def)+> import Data.Monoid ((<>))+> import System.IO (Handle, stdin, hClose,+> hSetBuffering, BufferMode(..))+> +> import System.Restricted.Worker+> import System.Restricted.Worker.Pool++This is just the 'echoHandler' function from 'EchoWorker.lhs':++> echoHandler :: Handle -> IO ()+> echoHandler h = do+> hSetBuffering h LineBuffering+> s <- BS.hGetLine h+> BS.putStrLn $ "Got: " <> s+> BS.hPutStrLn h s+> hClose h++Let's examine the type of 'System.Restricted.Worker.Pool.mkPool':++```haskell+mkPool :: (Int -> IO (Worker IOWorker, RestartWorker IO IOWorker))+ -- ^ An action that creates a new worker. Takes a unique number as an argument+ -> Int+ -- ^ Maximum number of workers in the pool + -> Int+ -- ^ Restart rate (in seconds)+ -> IO (WorkersPool a)+```++Let's implement those parameters.+ +We allow a maximum of two workers being active concurrently++> maxWorkers :: Int+> maxWorkers = 2++Restart rate is how frequently we restart inactive workers. In this+case we restart inactive workers (workers not being currently used)+every 5 seconds. Normally you would specify a bigger number.++> restartRate :: Int+> restartRate = 5++Now we come to the more interesting part. Implementing an action that+creats a new worker. This is callback that gets called with a unique+worker id. No two live workers share the same id.++> newWorkerAct :: Int -> IO (Worker IOWorker, RestartWorker IO IOWorker)+> newWorkerAct uid = startIOWorker wname def wsock echoHandler+> where wname = "Worker" ++ show uid+> wsock = "/tmp/" ++ show uid ++ ".sock"++We are ready to write code for initializing our pool:++> main :: IO ()+> main = do+> hSetBuffering stdin NoBuffering+> pool <- mkPool newWorkerAct maxWorkers restartRate+> loop pool+ +Then we can write a simple control loop for out program.++When we recieve the "new" command from stdin, we take a worker from+the pool, wait for 8 seconds so the user can interact with it from+another shell and put the worker back.++> loop :: WorkersPool IOWorker -> IO ()+> loop pool = forever $ do+> ln <- getLine+> case ln of+> "new" -> void $ forkIO $ do +> withWorker pool $ \(w,_) -> do+> putStrLn $ "Got worker " +++> show (workerName w)+> threadDelay 8000000+> putStrLn $ "Putting back worker " +++> show (workerName w)+> _ -> putStrLn "Dunno"+++By running the program we can see that we can have up to two+simultaneously active workers:++```+$ ./EchoPool+new+Starting worker "Worker1"+Got worker "Worker1"++Putting back worker "Worker1"+Starting worker "Worker1"++Starting worker "Worker1"++Starting worker "Worker1"++Starting worker "Worker1"++new+Got worker "Worker1"+new+Starting worker "Worker2"+Got worker "Worker2"++new+Putting back worker "Worker1"+<....>+```+
+ examples/EchoWorker.lhs view
@@ -0,0 +1,100 @@+> {-# LANGUAGE OverloadedStrings #-}+> module Main where++In this example we will take a look at 'IOWorker' and how to use it in+your programs. We will create a simple worker that would echo back to+us whatever we sent it.++Let's start by importing some libraries++> import qualified Data.ByteString as BS+> import Data.Default (def)+> import Data.Monoid ((<>))+> import System.IO (BufferMode (..), Handle, hClose,+> hSetBuffering, stdin)+> +> import System.Restricted.Worker++Let's take a look at 'startIOWorker' function:++```haskell+startIOWorker :: String -- ^ Name+ -> LimitSettings -- ^ Restrictions+ -> FilePath -- ^ UNIX socket+ -> (Handle -> IO ()) -- ^ Callback+ -> IO (Worker IOWorker, RestartWorker IO IOWorker)+```++it takes a worker name, a UNIX socket filepath, callback. It returns+the worker itself together with the restarting function.++Firstly, let's implement our callback:++> echoHandler :: Handle -> IO ()+> echoHandler h = do+> hSetBuffering h LineBuffering+> s <- BS.hGetLine h+> BS.putStr $ "Got: " <> s <> "\n"+> BS.hPutStrLn h s+> hClose h++we are not doing anything fancy here, just getting one line of text,+printing it to stdout and sending it back.++Now we are going to write the main loop function that would take care+of restarting the worker:++> loop :: (Worker IOWorker, RestartWorker IO IOWorker) -> IO ()+> loop (w, restart) = do+> putStrLn "Press <ENTER> to restart the worker"+> _ <- getChar+> w' <- restart w+> loop (w', restart)+ ++and a main function:++> main :: IO ()+> main = do+> hSetBuffering stdin NoBuffering+> loop =<< startIOWorker "Testworker" def "/tmp/1.sock" echoHandler+> return ()++We can test it:++```+(In one shell)+$ ./EchoWorker+tarting worker "Testworker"+Press <ENTER> to restart the worker++(In another shell)+$ echo -n whatsup | nc -U /tmp/1.sock+whatsup+$ echo -n test | nc -U /tmp/1.sock+test+```++Now in the first window we can see the following:++```+Starting worker "Testworker"+Press <ENTER> to restart the worker++Got: whatsup+Got: test+```++We can press enter to restart the worker:++```+Starting worker "Testworker"+Press <ENTER> to restart the worker++Got: whatsup+Got: test++Starting worker "Testworker"+Press <ENTER> to restart the worker++```
+ examples/NewWorkerType.lhs view
@@ -0,0 +1,183 @@+> {-# LANGUAGE OverloadedStrings, TypeFamilies #-}+> module Main where++In this walkthrough we will learn how to make our worker types. Let's+consider the following motivational situation.++We want to have our worker perform some database operations based on+the commands it receives. The worker needs to connect to the database+on startup, based on some run-time information, store the connection+in memory and re-use this connection for every client that connects to+it.++We will use the 'sqlite-simple' library for the database access.++> import Control.Applicative ((<$>), (<*>))+> import Control.Monad (forever)+> import Database.SQLite.Simple+> import Data.Default +> import Network (accept, Socket)+> import System.IO (hGetLine, hClose)+> +> import System.Restricted.Types (LimitSettings)+> import System.Restricted.Worker+> import System.Restricted.Worker.Types++First of all, let's deal with abstractions for the underlying database.++Our database would have a table called 'accounts' with columns 'id',+'name' and 'age'. We can create such a database from a shell:++```+$ sqlite3 one.db "CREATE TABLE accounts (id INTEGER PRIMARY KEY, \+name text, age INTEGER);"+```++Some sqlite-simple boilerplate for representing our table in Haskell++> data Account = Account Int String Int+> deriving (Show)+> +> instance FromRow Account where+> fromRow = Account <$> field <*> field <*> field+> +> type Accounts = [Account]+++Now we are going to roll out or worker type and corresponding+WorkerData instance.+++> data DBWorker = DBWorker+> +> instance WorkerData DBWorker where+> type WData DBWorker = Connection+> type WMonad DBWorker = IO+ ++Our worker would run in the IO monad and it would store 'Connection'+between sessions. This information is provided via type families in+the 'WorkerData' typeclass.++The next thing we should provide is the runner+function for our worker. Let's examine (a simplified) 'startWorker'+function:++```haskell+{-|+ Start a general type of worker.++ The pre-forking action is a monadic action that will be run prior to+ calling 'forkWorker'. It might be some initialization code, running the+ DB query, anything you want. The resulting 'WData' will be passed to+ the callback.+-}+startWorker :: (WorkerData w, MonadIO (WMonad w))+ => String -- ^ Name+ -> FilePath -- ^ Socket+ -> Maybe (IO Handle) -- ^ Where to redirect stdout, stderr+ -> LimitSettings -- ^ Restrictions+ -> WMonad w (WData w) -- ^ Pre-forking action+ -> (WData w -> Socket -> IO ()) -- ^ Socket callback+ -> WMonad w (Worker w, RestartWorker (WMonad w) w)+```++Specified to 'DBWorker' the signature will be++```haskell+startWorker :: String -- ^ Name+ -> FilePath -- ^ Socket+ -> Maybe (IO Handle) -- ^ Where to redirect stdout, stderr+ -> LimitSettings -- ^ Restrictions+ -> IO Connection -- ^ Pre-forking action+ -> (Connection -> Socket -> IO ()) -- ^ Socket callback+ -> IO (Worker DBWorker, RestartWorker IO DBWorker)+```++First of all, let's provide a callback for our type of worker:++> dbWorkerCallb :: (String -> Query) -> (Connection -> Socket -> IO ())+> dbWorkerCallb mkQuery conn sock = forever $ do+> (hndl, _, _) <- accept sock+> ln <- hGetLine hndl+> r <- query_ conn (mkQuery ln) :: IO Accounts+> mapM_ print r+> putStrLn "--------------------------------------------------"+> hClose hndl++Our callback (more of a callback generator) takes a function which+converts String of incoming data to a query and performs it. The+socket that we receive is a server socket which accepts connections,+that's why we need a 'forever' above.++All is left is to write a general runDbWorker function.++> runDbWorker :: String -- ^ Name+> -> LimitSettings -- ^ Restrictions+> -> IO Connection -- ^ Action that gets the connection+> -> (String -> Query) -- ^ Query convertion function+> -> IO (Worker DBWorker, RestartWorker IO DBWorker)+> runDbWorker name settings initConn mkQuery =+> startWorker name path Nothing settings initConn callback +> where+> path = "/tmp/" ++ name ++ ".sock"+> callback = dbWorkerCallb mkQuery+++To test our newly crafted worker type let's make some dummy values.++> dummyInit :: IO Connection+> dummyInit = msg >> conn -- usually here we might want to consult a+> -- configuration file or whatnot to get the information during the+> -- run-time+> where conn = open "one.db"+> msg = putStrLn "Welcome to dummybot 2000"++> dummyCb :: String -> Query+> dummyCb "all" = "SELECT * FROM accounts;"+> dummyCb "adults" = "SELECT * FROM accounts WHERE (age >= 18);"+++At once we can go ahead and run our worker++> main :: IO ()+> main = do+> (dummyWorker, _) <- runDbWorker "dummy" def dummyInit dummyCb+> _ <- getLine+> killWorker dummyWorker+> return ()++Sample output:++```+$ sqlite3 one.db "INSERT INTO accounts (name, age) VALUES ('Bob Dole', 14)"+$ sqlite3 one.db "INSERT INTO accounts (name, age) VALUES ('Dob Bole', 15)"+$ sqlite3 one.db "INSERT INTO accounts (name, age) VALUES ('Stephen Colbert', 18)"+$ sqlite3 one.db "INSERT INTO accounts (name, age) VALUES ('Jon Steward', 24)"+$ ./NewWorkerType+Welcome to dummybot 2000++```++In another terminal window:++```+$ echo all | nc -U /tmp/dummy.sock+$ echo adults | nc -U /tmp/dummy.sock+```++Back to the first window:++```+<...>+Welcome to dummybot 2000++Account 1 "Bob Dole" 14+Account 2 "Dob Bole" 15+Account 3 "Stephen Colbert" 18+Account 4 "Jon Steward" 24+--------------------------------------------------+Account 3 "Stephen Colbert" 18+Account 4 "Jon Steward" 24+--------------------------------------------------+```
+ restricted-workers.cabal view
@@ -0,0 +1,83 @@+name: restricted-workers+synopsis: Running worker processes under system resource restrictions+version: 0.1.0++description: This library provides an abstract interface for+ running various kinds of workers under resource+ restrictions. It was originally developed as part+ of the interactive-diagrams+ (<http://github.com/co-dan/interactive-diagrams>)+ project. To read more about the idia behind the+ library check out my GSoC report:+ <http://parenz.wordpress.com/2013/07/15/interactive-diagrams-gsoc-progress-report/>.++ .+ + The library provides a convenient way of running worker processes,+ saving data obtained by the workers at start-up, a simple pool+ abstraction and a configurable security and resource limitations.++ Please consult+ <https://github.com/co-dan/interactive-diagrams/tree/master/restricted-workers/README.md>+ and+ <https://github.com/co-dan/interactive-diagrams/wiki/Restricted-Workers>+ for more details.+ + .+ + /Warning/: this library requires SELinux to+ function++homepage: https://github.com/co-dan/interactive-diagrams/wiki/Restricted-Workers+++author: Dan Frumin+maintainer: difrumin@gmail.com+license: BSD3+license-file: LICENSE+copyright: (c) 2013 ++build-type: Simple+cabal-version: >=1.10+category: System, Concurrency, Data++extra-source-files: examples/EchoWorker.lhs,+ examples/EchoPool.lhs,+ examples/CommandEvalProtocol.lhs,+ examples/NewWorkerType.lhs,+ README.md++library+ exposed-modules: System.Restricted.Limits,+ System.Restricted.Types,+ System.Restricted.Worker,+ System.Restricted.Worker.Internal,+ System.Restricted.Worker.Pool,+ System.Restricted.Worker.Protocol,+ System.Restricted.Worker.Types+ other-modules: SignalHandlers+ ghc-options: -Wall -fno-warn-orphans + build-depends: base >=4.5 && <4.8,+ async >=2.0 && <2.1,+ bytestring >= 0.10 && < 0.13,+ cereal >=0.3 && <0.4,+ data-default >= 0.5,+ directory >=1.2 && <1.3,+ either >= 3.4,+ filepath >= 1,+ monad-control >=0.3 && <0.4,+ mtl >=2.1 && <2.2,+ network >=2.4 && <2.5,+ selinux -any,+ stm >=2.4 && <2.5,+ text >= 0.10,+ transformers >=0.3 && <0.4,+ transformers-base >= 0.4 && <0.5,+ unix >=2.6 && <2.8 + hs-source-dirs: src+ default-language: Haskell2010++source-repository head+ type: git+ location: https://github.com/co-dan/interactive-diagrams+
+ src/SignalHandlers.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE CPP #-}++module SignalHandlers where++#if !defined(mingw32_HOST_OS)++import System.Posix.Signals++restoreHandlers :: IO ()+restoreHandlers = do+ mapM_ restore [sigQUIT, sigINT, sigHUP, sigTERM]+ putStrLn "" -- for some reason, restoring handlers often fails without this+ where+ restore s = installHandler s Default Nothing++#else++restoreHandlers :: IO ()+restoreHandlers = return ()++#endif
+ src/System/Restricted/Limits.hs view
@@ -0,0 +1,123 @@+{-# LANGUAGE ForeignFunctionInterface #-}+{-# LANGUAGE RecordWildCards #-}+-- | The implementation of security restrictions+module System.Restricted.Limits+ (+ -- * Apply restrictions+ setLimits+ -- * Individual limits+ , setRLimits+ , chroot+ , changeUserID+ , setCGroup+ , setupSELinuxCntx+ , processTimeout+ ) where++import Prelude hiding (mapM_)++import Control.Applicative ((<$>))+import Control.Concurrent (threadDelay)+import Control.Monad (when)+import Data.Foldable (mapM_)+import Data.List (intersperse)+import Data.Monoid (mconcat)+import Foreign.C+import Foreign.C.Types+import System.FilePath.Posix ((</>))+import System.Linux.SELinux (SecurityContext, getCon, setCon)+import System.Posix.Directory (changeWorkingDirectory)+import System.Posix.Process (nice)+import System.Posix.Resource (setResourceLimit)+import System.Posix.Resource (Resource (..))+import System.Posix.Signals (killProcess, signalProcess)+import System.Posix.Types (CUid (..), ProcessID, UserID)+import System.Posix.User (getEffectiveUserID, setEffectiveUserID,+ setUserID)++import SignalHandlers+import System.Restricted.Types+++-- | Waits for a certain period of time+-- and then kills the process+processTimeout :: ProcessID -- ^ ID of a process to be killed+ -> Int -- ^ Time limit (in seconds)+ -> IO ()+processTimeout pid lim = do+ threadDelay (lim * 1000000)+ signalProcess killProcess pid+ return ()+++foreign import ccall unsafe "unistd.h chroot"+ c_chroot :: CString -> IO CInt++-- | Set the chroot jail+chroot :: FilePath -> IO ()+chroot fp = do+ eid <- getEffectiveUserID+ setUserID (CUid 0)+ withCString fp $ \c_fp -> do+ _ <- throwErrnoIfMinus1 "chroot" (c_chroot c_fp)+ changeWorkingDirectory "/"+ return ()+ setEffectiveUserID eid++-- | Change the uid of the current process+changeUserID :: UserID -> IO ()+changeUserID uid = do+ setUserID (CUid 0) -- need to be root in order to setuid()+ setUserID uid++-- | Add a process to a cgroup+setCGroup :: LimitSettings+ -> ProcessID -- ^ The ID of a process to be added to the group+ -> IO ()+setCGroup LimitSettings{..} pid =+ mapM_ (\fp -> writeFile (fp </> "tasks") $ show pid) cgroupPath+++-- | Set rlimits using setrlimit syscall+setRLimits :: RLimits -> IO ()+setRLimits RLimits{..} = mapM_ (uncurry setResourceLimit) lims+ where lims = [ (ResourceCoreFileSize, coreFileSizeLimit)+ , (ResourceCPUTime, cpuTimeLimit)+ , (ResourceDataSize, dataSizeLimit)+ , (ResourceFileSize, fileSizeLimit)+ , (ResourceOpenFiles, openFilesLimit)+ -- , (ResourceStackSize, stackSizeLimit)+ , (ResourceTotalMemory, totalMemoryLimit) ]++-- | Set the security context.+-- To be more precise, it only sets up the type.+-- Example usage:+--+-- > setupSELinuxCntx "my_restricted_t"++-- SELinx context has the following format+-- user:role:type:level+-- we only modify the type part+setupSELinuxCntx :: SecurityContext -> IO ()+setupSELinuxCntx ty = do+ con <- splitBy (==':') <$> getCon+ when (length con < 4) $ error ("Bad context: " ++ mconcat (intersperse ":" con))+ setCon $ mconcat $ intersperse ":" [con !! 0, con !! 1, ty, con !! 3]++-- | @splitBy (==x)@ is an inverse of @'intersperse' [x]@+splitBy :: (a -> Bool) -> [a] -> [[a]]+splitBy _ [] = []+splitBy f (x:xs)+ | f x = splitBy f xs+ | otherwise = s : splitBy f s'+ where (s, s') = break f (x:xs)++-- | Apply the 'LimitSettings'+setLimits :: LimitSettings -> IO ()+setLimits LimitSettings{..} = do+ mapM_ setRLimits rlimits+ mapM_ setupSELinuxCntx secontext+ nice niceness+ mapM_ chroot chrootPath+ mapM_ changeUserID processUid+ restoreHandlers
+ src/System/Restricted/Types.hs view
@@ -0,0 +1,141 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE StandaloneDeriving #-}+module System.Restricted.Types+ (+ -- * Limit settings+ LimitSettings(..)+ , RLimits(..)+ , defaultLimits+ ) where+++import Data.Default+import Data.Serialize (Serialize)+import GHC.Generics+import System.Linux.SELinux (SecurityContext)+import System.Posix.Resource (Resource (..), ResourceLimit (..),+ ResourceLimits (..))+import System.Posix.Types (CUid (..), UserID)+++-- | Resource limits+data RLimits = RLimits+ { coreFileSizeLimit :: ResourceLimits+ , cpuTimeLimit :: ResourceLimits+ , dataSizeLimit :: ResourceLimits+ , fileSizeLimit :: ResourceLimits+ , openFilesLimit :: ResourceLimits+ , stackSizeLimit :: ResourceLimits+ , totalMemoryLimit :: ResourceLimits+ } deriving (Eq, Show, Generic)++deriving instance Show ResourceLimits+deriving instance Show ResourceLimit+deriving instance Show Resource+deriving instance Generic ResourceLimit+deriving instance Generic ResourceLimits+instance Serialize ResourceLimit+instance Serialize ResourceLimits+instance Serialize RLimits++-- | Datastructure that holds the information about restrictions and+-- limitations for the worker process+data LimitSettings = LimitSettings+ { -- | Maximum time for which the code is allowed to run+ -- (in seconds)+ timeout :: Int+ -- | Process priority for the 'nice' syscall.+ -- -20 is the highest, 20 is the lowest+ , niceness :: Int+ -- | Resource limits for the 'setrlimit' syscall+ , rlimits :: Maybe RLimits+ -- | The directory that the evaluator process will be 'chroot'ed+ -- into. Please note that if chroot is applied, all the pathes+ -- in 'EvalSettings' will be calculated relatively to this+ -- value.+ , chrootPath :: Maybe FilePath+ -- | The UID that will be set after the call to chroot.+ , processUid :: Maybe UserID+ -- | SELinux security context under which the worker+ -- process will be running.+ , secontext :: Maybe SecurityContext+ -- | A filepath to the 'tasks' file for the desired cgroup.+ --+ -- For example, if I have mounted the @cpu@ controller at+ -- @/cgroups/cpu/@ and I want the evaluator to be running in the+ -- cgroup 'idiaworkers' then the 'cgroupPath' would be+ -- @/cgroups/cpu/idiaworkers@+ , cgroupPath :: Maybe FilePath+ } deriving (Eq, Show, Generic)++deriving instance Generic CUid+instance Serialize CUid+instance Serialize LimitSettings++-- | Default 'LimitSettings'+defaultLimits :: LimitSettings+defaultLimits = LimitSettings+ { timeout = 3+ , niceness = 10+ , rlimits = Nothing+ , chrootPath = Nothing+ , processUid = Nothing+ , secontext = Nothing -- Just "idia_restricted_t"+ , cgroupPath = Nothing+ }++instance Default LimitSettings where+ def = defaultLimits++instance Default RLimits where+ def = RLimits+ { coreFileSizeLimit = mkLimits (coreSizeLimitSoft, coreSizeLimitHard)+ , cpuTimeLimit = mkLimits (cpuTimeLimitSoft, cpuTimeLimitHard)+ , dataSizeLimit = mkLimits (dataSizeLimitSoft, dataSizeLimitHard)+ , fileSizeLimit = mkLimits (fileSizeLimitSoft, fileSizeLimitHard)+ , openFilesLimit = mkLimits (openFilesLimitSoft, openFilesLimitHard)+ , stackSizeLimit = mkLimits (stackSizeLimitSoft, stackSizeLimitHard)+ , totalMemoryLimit = mkLimits (totalMemoryLimitSoft, totalMemoryLimitHard)+ }+++mkLimits :: (ResourceLimit, ResourceLimit) -> ResourceLimits+mkLimits = uncurry ResourceLimits++--- This snippet is taken from the mueval package+-- (c) Gwern Branwen+++-- | Set all the available rlimits.+-- These values have been determined through trial-and-error+stackSizeLimitSoft, stackSizeLimitHard, totalMemoryLimitSoft, totalMemoryLimitHard,+ dataSizeLimitSoft, openFilesLimitSoft, openFilesLimitHard, fileSizeLimitSoft, fileSizeLimitHard,+ dataSizeLimitHard, cpuTimeLimitSoft, cpuTimeLimitHard, coreSizeLimitSoft, coreSizeLimitHard, zero :: ResourceLimit+totalMemoryLimitSoft = dataSizeLimitSoft+totalMemoryLimitHard = dataSizeLimitHard+-- These limits seem to be useless?+stackSizeLimitSoft = ResourceLimitUnknown+stackSizeLimitHard = ResourceLimitUnknown+-- We allow a few files to be opened, such as package.conf, because they are necessary. This+-- doesn't seem to be security problem because it'll be opened at the module+-- stage, before code ever evaluates. I hope.+openFilesLimitSoft = ResourceLimit 20+openFilesLimitHard = ResourceLimit 50+-- TODO: It would be nice to set these to zero, but right now Hint gets around the+-- insecurity of the GHC API by writing stuff out to a file in /tmp, so we need+-- to allow our compiled binary to do file I/O... :( But at least we can still limit+-- how much we write out!+fileSizeLimitSoft = fileSizeLimitHard+fileSizeLimitHard = ResourceLimitUnknown+dataSizeLimitSoft = dataSizeLimitHard+dataSizeLimitHard = ResourceLimit $ 104857600 * 5 -- 100 * 5 mb+-- These should not be identical, to give the XCPU handler time to trigger+cpuTimeLimitSoft = ResourceLimit 4+cpuTimeLimitHard = ResourceLimit 5+coreSizeLimitSoft = coreSizeLimitHard+coreSizeLimitHard = zero++-- convenience+zero = ResourceLimit 0++
+ src/System/Restricted/Worker.hs view
@@ -0,0 +1,104 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MultiParamTypeClasses #-}+-- | Main entry point of the library+module System.Restricted.Worker+ (+ -- * Exposed modules + module System.Restricted.Worker.Types+ , module System.Restricted.Worker.Pool+ , module System.Restricted.Worker.Protocol+ -- * Creating workers+ , mkDefaultWorker+ , startWorker+ , startIOWorker+ -- * Quering and killing workers+ , killWorker+ , workerAlive+ , connectToWorker+ ) where++import Prelude hiding (mapM_)++import Control.Monad (forever)+import Control.Monad.Base (MonadBase (..))+import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.Foldable (mapM_)+import Data.Typeable ()+import Network (Socket, accept)+import System.IO (Handle)+import System.Posix.User (getEffectiveUserID,+ setEffectiveUserID)++import System.Restricted.Limits+import System.Restricted.Types+import System.Restricted.Worker.Internal+import System.Restricted.Worker.Pool+import System.Restricted.Worker.Protocol+import System.Restricted.Worker.Types++-- | Create an uninitialized worker+mkDefaultWorker :: String -> FilePath -> LimitSettings -> Worker a+mkDefaultWorker name sock set = Worker+ { workerName = name+ , workerSocket = sock+ , workerLimits = set + , workerPid = Nothing+ }+++{-|+ Start a general type of worker.++ The pre-forking action is a monadic action that will be run prior to+ calling 'forkWorker'. It might be some initialization code, running the+ DB query, anything you want. The resulting 'WData' will be passed to+ the callback.++ The socket that is passed to the callback is a server socket.+-}+startWorker :: (WorkerData w, MonadIO (WMonad w),+ MonadBase (WMonad w) m)+ => String -- ^ Name+ -> FilePath -- ^ Socket+ -> Maybe (IO Handle) -- ^ Where to redirect stdout, stderr+ -> LimitSettings -- ^ Restrictions+ -> WMonad w (WData w) -- ^ Pre-forking action+ -> (WData w -> Socket -> IO ()) -- ^ Socket callback+ -> WMonad w (Worker w, RestartWorker m w)+startWorker name sock out set pre cb = do+ let defW = mkDefaultWorker name sock set+ let restarter !w = do+ w' <- liftIO $ killWorker w+ oldId <- liftIO $ getEffectiveUserID+ liftIO $ mapM_ setEffectiveUserID (processUid set)+ -- this is necessary so that the control socket is accessible by+ -- non-root processes, probably a hack+ dat <- pre+ pid <- liftIO $ forkWorker w' out (cb dat)+ liftIO $ setEffectiveUserID oldId+ liftIO $ setCGroup set pid+ let w'' = w' { workerPid = Just pid }+ w'' `seq` return w''+ w' <- restarter defW+ return (w', liftBase . restarter)+++-- | Start a worker of type 'IOWorker'+-- The callback function is called every time a connectino is established+--+-- >>> startIOWorker "test" "/tmp/test.sock" $ \h -> hPutStrLn h "hello, world"+--+startIOWorker :: String -- ^ Name+ -> LimitSettings -- ^ Restrictions+ -> FilePath -- ^ UNIX socket+ -> (Handle -> IO ()) -- ^ Callback+ -> IO (Worker IOWorker, RestartWorker IO IOWorker)+startIOWorker name set sock callb = startWorker name sock out set preFork handle+ where handle () soc = forever $ do+ (hndl, _, _) <- accept soc+ callb hndl+ out = Nothing+ preFork = putStrLn ("Starting worker " ++ show name)+ >> return ()+
+ src/System/Restricted/Worker/Internal.hs view
@@ -0,0 +1,121 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+-- | Library exposing internal functions uses by 'Eval.Worker'+-- useful work writing your own workers+module System.Restricted.Worker.Internal+ (+ -- * Worker related+ killWorker+ , workerAlive+ , workerTimeout+ , forkWorker+ -- * Connection related+ , connectToWorker+ , mkSock+ -- * Useful util functions+ , removeFileIfExists+ , processAlive+ ) where++import Control.Concurrent (threadDelay)+import Control.Exception (IOException, catch, handle, throwIO)+import Control.Monad (void, when)+import Data.Maybe (fromJust)+import Network (PortID (..), Socket, connectTo, listenOn)+import Network.Socket (close)+import System.Directory (removeFile)+import System.IO (Handle)+import System.IO.Error (isDoesNotExistError, isPermissionError)+import System.Mem.Weak (addFinalizer)+import System.Posix.IO (dupTo, handleToFd)+import System.Posix.Process (forkProcess, getProcessStatus)+import System.Posix.Signals (Handler (..), installHandler,+ killProcess, processStatusChanged,+ setStoppedChildFlag, signalProcess)+import System.Posix.Types (Fd (..), ProcessID)++import System.Restricted.Limits+import System.Restricted.Worker.Types++-- | Connect to the worker's socket and return a handle+connectToWorker :: Worker a -> IO Handle+connectToWorker Worker{..} = connectTo "localhost" (UnixSocket workerSocket)++-- | Remove a file if it exists. Should be thread-safe.+removeFileIfExists :: FilePath -> IO ()+removeFileIfExists f = removeFile f `catch` handleE+ where handleE e+ | isDoesNotExistError e = return ()+ | isPermissionError e = return ()+ | otherwise = putStrLn ("removeFileIfExists " ++ show e)+ >> throwIO e++-- | Create a new unix socket+mkSock :: FilePath -> IO Socket+mkSock sf = do+ removeFileIfExists sf+ listenOn (UnixSocket sf)++-- | Fork a worker process+forkWorker :: Worker a+ -> Maybe (IO Handle) -- ^ Where to redirect stdout+ -> (Socket -> IO ()) -- ^ Callback funcion+ -> IO ProcessID+forkWorker (w@Worker{..}) out cb = do+ _ <- setStoppedChildFlag True+ _ <- installHandler processStatusChanged Ignore Nothing+ soc <- mkSock workerSocket+ addFinalizer w (close soc)+ forkProcess $ do+ _ <- setStoppedChildFlag False+ _ <- installHandler processStatusChanged Default Nothing+ setLimits workerLimits+ case out of+ Nothing -> return ()+ Just x -> do+ fd <- handleToFd =<< x+ void $ dupTo fd (Fd 1)+ cb soc++++-- | Kill a worker. Takes an initialized worker,+-- returns non-initialized one.+killWorker :: Worker a -> IO (Worker a)+killWorker w@Worker{..} = do+ when (initialized w) $ do+ alive <- processAlive (fromJust workerPid)+ when alive $ do+ signalProcess killProcess (fromJust workerPid)+ tc <- getProcessStatus False False (fromJust workerPid)+ case tc of+ Just _ -> return ()+ Nothing -> signalProcess killProcess (fromJust workerPid)+ return (w { workerPid = Nothing })++-- | Waits for a certain period of time+-- and then kills the worker+workerTimeout :: Worker a -- ^ ID of a process to be killed+ -> Int -- ^ Time limit (in seconds)+ -> IO (Worker a)+workerTimeout w lim = do+ threadDelay (lim * 1000000)+ killWorker w+++-----------------------++-- | Checks whether the process is alive+-- /hacky/+processAlive :: ProcessID -> IO Bool+processAlive pid = handle (\(_ :: IOException) -> return False) $ do+ _ <- getProcessStatus False False pid+ return True++-- | Checks whether the worker is alive+workerAlive :: Worker a -> IO Bool+workerAlive w = do+ case (workerPid w) of+ Nothing -> return False+ Just pid -> processAlive pid+
+ src/System/Restricted/Worker/Pool.hs view
@@ -0,0 +1,125 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE RecordWildCards #-}++-- | A non-stripped pooling abstraction that restarts workers+-- Some got has been taken from 'Data.Pool' by bos+module System.Restricted.Worker.Pool+ (+ -- * Workers Pool+ WorkersPool+ , mkPool+ -- * High-level operations on the pool+ , withWorker+ -- * Low-level operations on the pool+ , takeWorker+ , putWorker+ , destroyWorker+ ) where++import Control.Applicative ((<$>), (<*>))+import Control.Concurrent (forkIO, killThread, threadDelay)+import Control.Concurrent.STM+import Control.Exception (mask, onException)+import Control.Monad (forM, forever, join, when)+import Control.Monad.Base (MonadBase(..))+import Control.Monad.Trans.Control (MonadBaseControl, control)+import Control.Monad.IO.Class (MonadIO, liftIO) +import System.Mem.Weak (addFinalizer)++import System.Restricted.Worker.Internal+import System.Restricted.Worker.Types++-- | A simple pool for workers. Workers are restarted from time to time+data WorkersPool w = Pool+ { newWorker :: Int -> WMonad w (Worker w, RestartWorker IO w)+ -- ^ Action for creating a new worker+ , maxWorkers :: Int+ -- ^ Maximum number of initialized workers+ , activeWorkers :: TVar Int+ -- ^ Current number of active workers+ , workers :: TVar [(Worker w, RestartWorker IO w)]+ -- ^ A list of Workers+ , restartRate :: Int+ -- ^ How long we should wait before restarting the workers (in seconds)+ }+++-- | Create a new workers pool+mkPool :: (MonadIO (WMonad a))+ => (Int -> WMonad a (Worker a, RestartWorker IO a))+ -- ^ An action that creates a new worker. Takes a unique number as an argument+ -> Int+ -- ^ Maximum number of workers in the pool+ -> Int+ -- ^ Restart rate (in seconds)+ -> WMonad a (WorkersPool a)+mkPool newW maxW restartRate = do+ res <- liftIO $ atomically $ newTVar []+ num <- liftIO $ atomically $ newTVar (0 :: Int)+ reaperT <- liftIO $ forkIO $ reaper res restartRate+ let p = Pool newW maxW num res restartRate+ liftIO $ addFinalizer p (killThread reaperT)+ return p+++reaper :: TVar [(Worker a, RestartWorker IO a)] -> Int -> IO ()+reaper wrkrs t' = forever $ do+ let t = t' * 1000000+ threadDelay t+ workers <- readTVarIO wrkrs+ workers' <- forM workers $ \(w, rw) -> (,) <$> rw w <*> return rw+ atomically $ writeTVar wrkrs workers'+++-- | Take worker from the pool.+-- The caller is responsible for putting the worker back into the pool+-- or destroying it with 'destroyWorker'+takeWorker :: (MonadIO (WMonad a), MonadBaseControl IO (WMonad a))+ => WorkersPool a -> WMonad a (Worker a, RestartWorker IO a)+takeWorker Pool{..} = do+ res <- liftIO $ readTVarIO workers+ case res of+ ((w@Worker{..}, restartW):xs) -> liftIO $ do+ atomically $ writeTVar workers xs+ workerDead <- not <$> workerAlive w+ wrk <- if workerDead+ then do+ restartW w+ else return w+ return (wrk, restartW)+ [] -> join $ liftIO . atomically $ do+ activeRes <- readTVar activeWorkers+ when (activeRes >= maxWorkers) retry+ modifyTVar' activeWorkers (+1)+ return $ control $ \runIO ->+ runIO (newWorker (activeRes+1))+ `onException`+ atomically (modifyTVar' activeWorkers (subtract 1))++-- | Put the worker back in pool+putWorker :: WorkersPool a -> (Worker a, RestartWorker IO a) -> IO ()+putWorker Pool{..} w = atomically $+ modifyTVar' workers (w:)++-- | Destroy a worker. Frees up space in the pool+destroyWorker :: WorkersPool a -> Worker a -> IO ()+destroyWorker Pool{..} w = do+ _ <- killWorker w+ atomically $ modifyTVar' activeWorkers (subtract 1)+++-- | Like 'takeWorker' + 'putWorker' but takes care of the exception handling for you+withWorker :: (MonadBaseControl IO m, MonadBase (WMonad a) m,+ MonadBaseControl IO (WMonad a), MonadIO (WMonad a))+ => WorkersPool a+ -> ((Worker a, RestartWorker IO a) -> m b)+ -> m b+withWorker pool cb = do+ res <- liftBase $ takeWorker pool+ control $ \runIO -> mask $ \restore -> do+ ret <- restore (runIO (cb res))+ `onException` destroyWorker pool (fst res)+ putWorker pool res+ return ret+
+ src/System/Restricted/Worker/Protocol.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+-- | A simple protocol for sending serializable data over handles+--+-- Please note that this is a very simple implementation that works+-- fine for most of that data, however, the size of the data you might+-- send at one go is limited to MAX_WORD32 bytes. We use 'cereal' for+-- serialization.+module System.Restricted.Worker.Protocol+ (+ sendData+ , getData+ , getDataSafe+ , DecodeResult+ , ProtocolException(..)+ ) where++import Control.Applicative ((<$>))+import Control.Exception (IOException, catch, throw)+import Control.Monad.Trans (lift)+import Control.Monad.Trans.Either (hoistEither, left, runEitherT)+import Data.ByteString (ByteString, hGet, hPut)+import qualified Data.ByteString as BS+import Data.Serialize (Serialize, decode, encode)+import Data.Word (Word32)+import GHC.IO.Handle (Handle, hFlush)++import System.Restricted.Worker.Types++-- | Result of the deserialization+type DecodeResult a = Either String a++-- | Send some serialiazable data over a handle.+-- Returns 'ByteString' representing the encoded data. May throw+-- 'ProtocolException'+sendData :: Serialize a => Handle -> a -> IO ByteString+sendData h d = sendData' h d+ `catch` \(e :: IOException) ->+ throw (HandleException e)++-- | Read the data from a handle and deserialize it.+-- May throw 'ProtocolException'+getData :: Serialize a => Handle -> IO a+getData h = getData' h+ `catch` \(e :: IOException) ->+ throw (HandleException e)+++sendData' :: Serialize a => Handle -> a -> IO ByteString+sendData' hndl datum = do+ let encoded = encode datum+ let len = (fromIntegral . BS.length $ encoded) :: Word32+ hPut hndl (encode len)+ hFlush hndl+ hPut hndl encoded+ hFlush hndl+ return encoded++getData' :: Serialize a => Handle -> IO a+getData' hndl = do+ lenD :: DecodeResult Word32 <- decode <$> hGet hndl 4+ let len = case lenD of+ Right i -> fromIntegral i+ Left str -> throw (ConversionException $ "length\n" ++ str)+ res <- decode <$> hGet hndl len+ case res of+ Left str -> throw (ConversionException $ "Deserialization error:\n" ++ str)+ Right x -> return x++-- | Safe version of 'getData' that doesn't throw 'ProtocolException'+getDataSafe :: Serialize a => Handle -> IO (DecodeResult a)+getDataSafe hndl = runEitherT $ do+ lenD :: DecodeResult Word32 <- decode <$> lift (hGet hndl 4)+ case lenD of+ Left str -> left $ "Conversion error while reading length: " ++ str+ Right len ->+ hoistEither =<< decode <$> (lift (hGet hndl (fromIntegral len)))+
+ src/System/Restricted/Worker/Types.hs view
@@ -0,0 +1,102 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE EmptyDataDecls #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}+{-|+ Worker can be in one of three states++ [Uninitialized] Uninitialized worker is a worker that has a name,+ a socket, possibly 'WData' but has not been forker++ [Initialized] Initialized worker has an associated forker process.++ [Active] A worker is active if it's initialized and it's being used+ a client. Active/inactive workers are managed by a 'WorkersPool'.+-}+module System.Restricted.Worker.Types+ (+ -- * Workers+ Worker(..)+ , RestartWorker+ , WorkerData(..)+ , IOWorker+ -- * Other types+ , ProtocolException(..)+ -- * Helper functions+ , initialized+ ) where++import Control.Exception (Exception, IOException)+import Data.Maybe (isJust)+import Data.Serialize (Serialize)+import Data.Typeable+import GHC.Generics+import System.Posix.Types (CPid (..), ProcessID)++import System.Restricted.Types++-- | A worker restarting function+type RestartWorker m a = Worker a -> m (Worker a)++-- | A datatype representing a worker of type 'a'+data Worker a = Worker+ { -- | Name of the worker+ workerName :: String+ -- | A filepath to the Unix socket that will be+ -- used for communicating with the worker.+ -- If the file is already present it will be unliked+ -- during the initializatin step+ , workerSocket :: FilePath+ -- | Security restrictions for the worker+ , workerLimits :: LimitSettings+ -- | 'Just pid' if the worker's process ID is 'pid',+ -- Nothing' if the worker is not active/initialized+ , workerPid :: Maybe ProcessID+ } deriving (Show, Eq, Typeable, Generic)++deriving instance Generic CPid+instance Serialize CPid+instance Serialize (Worker a)++-- | Types of data attached to a worker.+-- This might be a configuration file, a size of the packet, session data, etc.+class WorkerData w where+ -- | Data that saves after restarts+ type WData w :: *+ -- | Monad in which the worker runs+ type WMonad w :: * -> *++{- | A simple type of worker that executes IO actions++The definition of the 'WorkerData' instance for IOWorker looks like this:++@+ instance WorkerData IOWorker where+ type WData IOWorker = ()+ type WMonad IOWorker = IO+@+-}+data IOWorker++instance WorkerData IOWorker where+ type WData IOWorker = ()+ type WMonad IOWorker = IO++-- | Check whether the worker is initialized+initialized :: Worker a -> Bool+initialized = isJust . workerPid++-- | An exception type used by 'Eval.Worker.Protocol'+data ProtocolException =+ -- | There has been an error during the conversion step+ ConversionException String+ -- | There has been an error while using the handler+ | HandleException IOException+ deriving (Typeable, Show)++instance Exception ProtocolException++