Practical Haskell: shell scripting with error handling and privilege separation
Shell scripts are often a quick, dirty way to get the job done. You glue
together external tools, maybe do a little error checking and process
all data as strings.
This is great for some very simple problems but as requirements change and more
is demanded from the code shell scripts become unwieldy and fragile. When they
get large, they become slow and difficult to maintain. If you need to write
robust code then shell is not the way to go.
At the other extreme we have Haskell. Haskell is about as far from shell
programming as you can get: its full of abstractions, its designed for robust
error and exception handling, is strongly statically typed (you'd be shot if
you represented all data as strings). Fortunately, it is also rather concise,
like shell code.
So it makes sense then for Haskell to be used in a number of ``scripting''
situations where robustness and correctness are important. For example, large,
critical tools, such as the package management infrastructure in the Linspire
linux distro, are written in Haskell.
This article looks at how to use Haskell for a scripting task.
By refining the semantics of the problem domain, employing abstract, we produce
shorter and more robust code. Finally, as a highlight, we'll use type checking
to statically separate code that requires root privileges from user code.
== The spec ==
I have a variable frequency cpu in my laptop. The frequency of the clock life
is greatly extended, and the machine stays a lot cooler. At the highest level,
my code runs a faster.
There exist tools for all common operating systems to automatically
scale up and down the clock based on load. However, I usually don't care
about scaling -- I either explicitly want the clock all the way up, or all
the way down. In particular, when I do benchmarking I want to keep the
cpu clocked up all the way.
So we'll develop a simple program that acts as a toggle, flipping the cpu speed
up or down, and printing some strings about the current state. It should behave
like this:
$ cpuperf
cpu: 0 -> 100
clock: 1.6 Ghz
$ cpuperf
cpu: 100 -> 0
clock: 0.6 Ghz
== Operating details ==
First let's look at how we'd typically do this in the shell.
I use the OpenBSD operating system. Rather than using a /proc filesystem as on
linux, tuning kernel variables in OpenBSD is done via sysctls. The userland
sysctl program let's you get or set kernel values:
For example, the OS type:
$ sysctl kern.ostype
kern.ostype=OpenBSD
The current clock speed:
$ sysctl hw.cpuspeed
hw.cpuspeed=600
The current performance level (between 0 and 100):
$ sysctl hw.setperf
hw.setperf=0
We'll use these latter two sysctls to tweak the clock speed. Note that to set a
sysctl value we need root privileges (via sudo).
== An implementation in shell ==
Implementing the specification in shell:
#!/bin/sh
s=`sysctl hw.setperf`
old=`echo $s | sed 's/.*=//'`
if [ "100" = $old ] ; then
new=0
else
new=100
fi
sudo sysctl -w hw.setperf=$new > /dev/null
printf "cpu: %d -> %d\n" $old $new
speed=`sysctl hw.cpuspeed`
clock=`echo $speed | sed 's/.*=//'`
clock=`bc -l -e "$clock / 1000" -e quit`
printf "clock: %0.1f Ghz\n" $clock
Note that we assume you've made the sysctl command accessible through sudo.
For example:
$ visudo
...
dons mymachine = NOPASSWD: /sbin/sysctl -w hw.setperf=0
dons mymachine = NOPASSWD: /sbin/sysctl -w hw.setperf=100
...
The script is short and does no error handling. Does it work?
$ sh naive.sh
cpu: 0 -> 100
clock: 1.6 Ghz
$ sh naive.sh
cpu: 100 -> 0
clock: 0.6 Ghz
$ sh naive.sh
cpu: 0 -> 100
clock: 1.6 Ghz
Great! The performance is toggled between 0 and 100, clocking up and down the
cpu. Some interesting things to note;
* we use regular expressions for parsing
* we don't check for failure
* strings are treated as numbers
* floating point math is a little hard
* we take root privileges in the middle of the code
== An Haskell translation ==
We can directly translate this code into Haskell:
import Text.Printf
import Process
main :: IO ()
main = do
s <- run "sysctl hw.setperf"
let old = clean s
new = if old == 100 then 0 else 100 :: Integer
run $ "sudo sysctl -w hw.setperf=" ++ show new
printf "cpu: %d -> %d\n" old new
s <- run "sysctl hw.cpuspeed"
let clock = fromIntegral (clean s) / 1000
printf "clock: %f Ghz\n" (clock :: Double)
where
clean :: String -> Integer
clean = read . init . tail . dropWhile (/='=')
We replace the regular expression with some list processing, failure is
translated to unhandled exceptions, IO is interleaved with pure actions (like
the math), just as in shell. One difference is that we explicitly treat strings
as Integers and Doubles.
Running the code in the bytecode interpreter:
$ runhaskell naive.hs
cpu: 100 -> 0
clock: 0.6 Ghz
$ runhaskell naive.hs
cpu: 0 -> 100
clock: 1.6 Ghz
Of course, this being Haskell, we can compile to native code:
$ ghc -O --make naive.hs -o cpuperf
[1 of 2] Compiling Process ( Process.hs, Process.o )
[2 of 2] Compiling Main ( naive.hs, naive.o )
Linking cpuperf ...
$ ./cpuperf
cpu: 100 -> 0
clock: 0.6 Ghz
Which does run quite a bit faster than bytecode (and faster than the sh code).
This code uses the Process module, a
small wrapper over System.Process.
== Doing a better job ==
This is all very nice, but the code feels a bit icky. There's something
unsatisfying: we haven't really captured the sysctl abstraction at all, so
there's no easy reuse of this code for other purposes. Neither have we looked
at error handling, and finally, we've played fast and loose with sudo. In a
larger application, we'd want to be far more careful about taking root
privileges.
== Domain specific shell code ==
The first thing to clean this code up is to notice that the sysctl values
behave like mutable boxes who's contents change (these are known as 'variables'
in some cultures). A nice interface to mutable boxes is the get/set/modify api,
which goes something like this:
get :: box -> m a
set :: box -> a -> m ()
modify :: box -> (a -> a) -> m (a,a)
The 'get' function retrieves a value from a mutable box. The set function
writes a new value into one. The most convenient function is `modify', a higher
order function which takes a box, and a function modifying the contents, and
applies that to the current contents, mutating the contents. It returns the old
and new values of the box.
Since sysctls act as mutable boxes of integers keyed by strings names our
abstract api can be specified concretely as:
get :: String -> IO Integer
set :: String -> Integer -> Priv ()
modify :: String -> (Integer -> Integer) -> IO (Integer, Integer)
We can implement the semantics of the 'sysctl' command as a small domain
specific set of functions in Haskell:
get s = do
v <- run ("sysctl " ++ s)
readM (parse v)
where
parse = tail . dropWhile (/= '=') . init
set s v = run $ printf "sysctl -w %s=%s" s (show v)
and our nice 'modify' function combines the two:
modify s f = do
v <- get s
let u = f v
set s u
return (v,u)
This let's us simplify the main function:
main = do
(old,new) <- modify "hw.setperf" toggle
clock <- get "hw.cpuspeed"
printf "cpu: %d -> %d\n" old new
printf "clock: %f Ghz\n" (fromIntegral clock / 1000 :: Double)
toggle v = if v == 100 then 0 else 100
Which is really pretty nice. By getting closer to the semantics of the problem,
we find the right api, and the code becomes simpler and cleaner.
So our code now more closely matches the spec of:
* modify the hw.setperf value based on its current value
* print the current cpu speed
== Improving error handling ==
In the current code exceptions aren't caught (if they're noticed at all).
We can introduce a bug to see the problem:
parse = read -- . init . tail . dropWhile (/='=')
Now the Haskell code dies with the unhelpful error message:
$ cpuperf
*** Exception: user error (Prelude.read: no parse)
We really should handle the possibility of 'read' failing. Currently, any error
results in a call to the default ioError action in the IO monad.
However, this being Haskell, we can implement our own error monad to provide
custom error handling. This situation is exactly what the ErrorT
monad transformer. was designed for. So how to use it?
The first step is to replace read with a version lifted into a generic error
monad, MonadError:
readM :: (MonadError String m, Read a) => String -> m a
readM s | [x] <- parse = return x
| otherwise = throwError $ "Failed parse: " ++ show s
where
parse = [x | (x,t) <- reads s]
Now should a parse fail it will call the 'throwError' function in whatever
monad we happen to be using -- the code is polymorphic in its monad type.
For particular types, we can see how throwError is defined:
instance MonadError IOError IO where
throwError = ioError
instance (Error e) => MonadError e (Either e) where
throwError = Left
That is, for IO, throwError corresponds to a normal io error (which will throw
an exception). If we're in the Either monad, instead our result will be marked
as an error (with no exception thrown).
But, even with this nice 'read' function, we still have a problem checking errors.
Functions like 'get' or 'set' might fail. One way to handle errors like this is
to check every functions' result (this style is encouraged in some cultures).
We can tag any error and then check the result after each function call using
the Either type:
data Either a b = Left a | Right b
A value of 'Right x' is a good value, anything of the form 'Left e' is an error.
Assuming we then wrap 'get' and 'set' to return 'Left's in the case of errors, we can
obfuscate our 'modify' function with error handling boilerplate like so:
modify :: String -> (Integer -> Integer) -> IO (Either String (Integer,Integer))
modify s f = do
ev <- get s
case ev of
Left e -> return (Left e)
Right v -> do
let u = f v
ev <- set s u
case ev of
Left e -> return (Left e)
Right _ -> return (v,u)
Urgh .. boilerplate! Note the common pattern: after each evaluation step: we
perform a particular check, and then optionally propagate results further down.
All good Haskellers reading should immediately recognise the pattern:
* we have a particular operation we need to run between each step of our code
This kind of boilerplate can be abstracted perfectly with a monad (of course).
== Scrap your error handling boilerplate ==
But which monad? Well, Either is itself an monad: the Error monad:
instance (Error e) => Monad (Either e) where
return = Right
Left l >>= _ = Left l
Right r >>= k = k r
If you recall from the dozens of other monad tutorials out there, a monad gives
us a programmable ';' (the semicolon statement terminator from the imperative
world). With a custom monad we can specify precisely what happens at the end of
each statement in our code.
in this case, we want any 'Left' value to immediately terminate the
computation, and any 'Right' value to produce a result we feed to the rest of
the code. Since we need to use IO as well, we'll actually need an ErrorT
monad transformer, which wraps an underlying monad with error handling
capabilities:
newtype ErrorT e m a = ErrorT { runErrorT :: m (Either e a) }
Note that body of 'ErrorT' is exactly the type of our explicit boilerplate full
code:
IO (Either String (Integer,Integer))
where
m = IO
e = String
a = (Integer,Integer)
We can thus scrap our boilerplate, and rewrite modify to run in a new ErrorT monad.
We replace the use of IO and Either with a new monad, Shell, with its own
MonadError instance:
newtype Shell a = Shell { runShell :: ErrorT String IO a }
deriving (Functor, Monad, MonadIO)
In this way any errors thrown will be translated to useful strings in the Shell
monad. We can now implement a custom 'throwError' for our Shell monad:
instance MonadError String Shell where
throwError = error . ("Shell failed: "++)
running a fragment of Shell code is achieved with:
shell :: Shell a -> IO (Either String a)
shell = runErrorT . runShell
And our 'modify' function has its boilerplate entirely moved into the ';' :
modify :: String -> (Integer -> Integer) -> Shell (Integer, Integer)
modify s f = do {
v <- get s;
let u = f v;
set s u;
return (v,u);
}
Of course, since this is Haskell, we can scrap our (programmable) semicolons
too, and just specify which ';' to use in the type:
modify :: String -> (Integer -> Integer) -> Shell (Integer, Integer)
modify s f = do
v <- get s
let u = f v
set s u
return (v,u)
Finally, running this code, we get the much nicer, and more specific, error
output:
cpuperf: Shell failed: Failed parse: "hw.setperf=0\n"
The error handling boilerplate is hidden by the error handling monad, inside
the invisible, programmable ';'.
== Adding privilege separation ==
One slightly icky thing at the moment is the use of sudo directly in the code
to obtain root privileges. In larger software the use and abuse of root
privileges can be a source of security problems. Some projects got to great
length to precisely control the scope of code that has root privileges using
privilege separation.
This kind of property is the kind of thing we can lean on the type system for:
to implement statically checked privilege separation.
To do this we need to introduce a new type for actions that run with root privileges:
newtype Priv a = Priv { priv :: Shell a }
deriving (Functor, Monad, MonadIO)
Yes! Another monad! It's really just the Shell monad dressed as a new type, so
we can distinguish the two in the type checker. Note how we lean heavily on
GHC's newtype deriving to automatically generate boilerplate code implementing
the basic type classes for our type.
Now we add a custom error message for any code that fails in privileged mode:
instance MonadError String Priv where
throwError = error . ("Priv failed: "++)
The key step is to abstract out the taking of root ops into a combinator, and then hiding
the Priv constructor:
runPriv :: String -> Priv String
runPriv = Priv . run . ("/usr/bin/sudo " ++)
Now the only way to get Priv status in your types is to actually run the code
through 'sudo'. So the type 'Priv' means 'this code will be checked by sudo'.
Our set sysctl code becomes:
set :: String -> Integer -> Priv String
set s v = runPriv $ printf "sysctl -w %s=%s" s (show v)
and we explicitly state in the type of 'set' that it runs in the Priv monad,
not the normal Shell monad.
The cool thing is that we can ask the typechecker now to audit our code for all
uses of priv commands that are unchecked. Compiling the old code, we get:
Main.hs:66:4:
Couldn't match expected type `Shell t'
against inferred type `Priv String'
Great! On line 66 we use a program requiring root privileges as if it was a
normal user command, the 'set' call in 'modify'. So now we can check that
that is indeed a place we should be taking root ops, and then tag it as safe
with 'priv':
modify :: String -> (Integer -> Integer) -> Shell (Integer, Integer)
modify s f = do
v <- get s
let u = f v
priv (set s u)
return (v,u)
which evaluates runs a fragment of Shell code in the Priv monad. So, if in
doubt, embed the problem domain in the type system.
== Summary ==
The final code, with error handling and privilege separation on the type level
boils down to:
import Shell
import Text.Printf
main = shell $ do
(old,new) <- modify "hw.setperf" toggle
clock <- get "hw.cpuspeed"
io $ do printf "cpu: %d -> %d\n" old new
printf "clock: %f Ghz\n" (fromIntegral clock / 1000 :: Double)
toggle v = if v == 100 then 0 else 100
All the rest is library code. For binding to 'sysctl' nicely:
--
-- Read a sysctl value from the shell
--
get :: String -> Shell Integer
get s = readM . parse =<< run ("sysctl " ++ s)
where
parse = tail . dropWhile (/= '=') . init
--
-- Set a sysctl value. Runs in the Priv monad, and requires root privledges.
-- Will prompt for a password.
--
set :: String -> Integer -> Priv ()
set s v = do runPriv $ printf "sysctl -w %s=%s" s (show v)
return ()
--
-- Modify a particular sysctl value, using a function applied to the
-- current value, yielding a new value. Both the old and new values are
-- returned.
--
modify :: String -> (Integer -> Integer) -> Shell (Integer, Integer)
modify s f = do
v <- get s
let u = f v
priv (set s u) -- root
return (v,u)
And the Shell and Priv monads are implemented as:
{-# OPTIONS -fglasgow-exts #-}
module Shell where
import qualified Process
import System.IO
import System.Exit
import Text.Printf
import Control.Monad.Error
import Control.Exception
newtype Shell a = Shell { runShell :: ErrorT String IO a }
deriving (Functor, Monad, MonadIO)
newtype Priv a = Priv { priv :: Shell a }
deriving (Functor, Monad, MonadIO)
instance MonadError String Shell where
throwError = error . ("Shell failed: "++)
instance MonadError String Priv where
throwError = error . ("Priv failed: "++)
shell :: Shell a -> IO (Either String a)
shell = runErrorT . runShell
runPriv :: String -> Priv String
runPriv = Priv . run . ("/usr/bin/sudo " ++)
io :: IO a -> Shell a
io = liftIO
run :: String -> Shell String
run = io . Process.run
The entire program is packaged up by Cabal, and available online from
Hackage,
the central repository of new haskell code and libraries.
Running the damn thing:
$ cpuperf
cpu: 100 -> 0
clock: 0.6 Ghz
$ cpuperf
cpu: 0 -> 100
clock: 1.6 Ghz
$ cpuperf
cpu: 100 -> 0
clock: 0.6 Ghz
$ cpuperf
cpu: 0 -> 100
clock: 1.6 Ghz
The final act is to bind the Haskell program to my ThinkPad's "Access IBM" hotkey:
tpb -d -t /home/dons/bin/cpuperf
So hitting 'Access IBM' now runs the cpu clock scaling Haskell program.