Use the SIO monad in the GF shell

+ The restrictions on arbitrary IO when GF is running in restricted mode is now
  enforced in the types.
+ This hopefully also solves an intermittent problem when accessing the GF
  shell through the web API provided by gf -server. This was visible in the
  Simple Translation Tool and probably caused by some low-level bug in the
  GHC IO libraries.
This commit is contained in:
hallgren
2012-09-25 19:08:33 +00:00
parent 1adc0ed9f7
commit 43d5016996
6 changed files with 71 additions and 71 deletions

View File

@@ -112,7 +112,8 @@ executable gf
parallel parallel
ghc-options: -threaded ghc-options: -threaded
if flag(server) if flag(server)
build-depends: httpd-shed, network, silently, utf8-string, json, cgi build-depends: httpd-shed, network, utf8-string, json, cgi
-- ,silently
cpp-options: -DSERVER_MODE cpp-options: -DSERVER_MODE
other-modules: GFServer other-modules: GFServer
hs-source-dirs: src/server src/server/transfer src/example-based hs-source-dirs: src/server src/server/transfer src/example-based

View File

@@ -10,6 +10,7 @@ module GF.Command.Commands (
CommandInfo, CommandInfo,
CommandOutput CommandOutput
) where ) where
import Prelude hiding (putStrLn)
import PGF import PGF
import PGF.CId import PGF.CId
@@ -27,7 +28,8 @@ import GF.Compile.Export
import GF.Compile.ToAPI import GF.Compile.ToAPI
import GF.Compile.ExampleBased import GF.Compile.ExampleBased
import GF.Infra.Option (noOptions, readOutputFormat, outputFormatsExpl) import GF.Infra.Option (noOptions, readOutputFormat, outputFormatsExpl)
import GF.Infra.UseIO import GF.Infra.UseIO(writeUTF8File)
import GF.Infra.SIO
import GF.Data.ErrM ---- import GF.Data.ErrM ----
import GF.Command.Abstract import GF.Command.Abstract
import GF.Command.Messages import GF.Command.Messages
@@ -48,12 +50,12 @@ import qualified Data.Map as Map
import Text.PrettyPrint import Text.PrettyPrint
import Data.List (sort) import Data.List (sort)
import Debug.Trace import Debug.Trace
import System.Random (newStdGen) ---- --import System.Random (newStdGen) ----
type CommandOutput = ([Expr],String) ---- errors, etc type CommandOutput = ([Expr],String) ---- errors, etc
data CommandInfo = CommandInfo { data CommandInfo = CommandInfo {
exec :: PGFEnv -> [Option] -> [Expr] -> IO CommandOutput, exec :: PGFEnv -> [Option] -> [Expr] -> SIO CommandOutput,
synopsis :: String, synopsis :: String,
syntax :: String, syntax :: String,
explanation :: String, explanation :: String,
@@ -350,7 +352,7 @@ allCommands = Map.fromList [
pgf <- optProbs opts pgf pgf <- optProbs opts pgf
let printer = if (isOpt "api" opts) then exprToAPI else (showExpr []) let printer = if (isOpt "api" opts) then exprToAPI else (showExpr [])
let conf = configureExBased pgf (optMorpho env opts) (optLang pgf opts) printer let conf = configureExBased pgf (optMorpho env opts) (optLang pgf opts) printer
(file',ws) <- parseExamplesInGrammar conf file (file',ws) <- restricted $ parseExamplesInGrammar conf file
if null ws then return () else putStrLn ("unknown words: " ++ unwords ws) if null ws then return () else putStrLn ("unknown words: " ++ unwords ws)
return (fromString ("wrote " ++ file')), return (fromString ("wrote " ++ file')),
needsTypeCheck = False needsTypeCheck = False
@@ -544,7 +546,7 @@ allCommands = Map.fromList [
let typ = optType pgf opts let typ = optType pgf opts
pgf <- optProbs opts pgf pgf <- optProbs opts pgf
let mt = mexp xs let mt = mexp xs
morphologyQuiz mt pgf lang typ restricted $ morphologyQuiz mt pgf lang typ
return void, return void,
flags = [ flags = [
("lang","language of the quiz"), ("lang","language of the quiz"),
@@ -721,7 +723,7 @@ allCommands = Map.fromList [
(es, err) | null es -> return ([], render (err $$ text "no trees found")) (es, err) | null es -> return ([], render (err $$ text "no trees found"))
| otherwise -> return (es, render err) | otherwise -> return (es, render err)
s <- readFile file s <- restricted $ readFile file
case opts of case opts of
_ | isOpt "lines" opts && isOpt "tree" opts -> _ | isOpt "lines" opts && isOpt "tree" opts ->
returnFromLines (zip [1..] (lines s)) returnFromLines (zip [1..] (lines s))
@@ -768,7 +770,7 @@ allCommands = Map.fromList [
let typ = optType pgf opts let typ = optType pgf opts
let mt = mexp xs let mt = mexp xs
pgf <- optProbs opts pgf pgf <- optProbs opts pgf
translationQuiz mt pgf from to typ restricted $ translationQuiz mt pgf from to typ
return void, return void,
flags = [ flags = [
("from","translate from this language"), ("from","translate from this language"),
@@ -824,7 +826,7 @@ allCommands = Map.fromList [
restricted $ writeFile tmpi $ toString arg restricted $ writeFile tmpi $ toString arg
let syst = optComm opts ++ " " ++ tmpi let syst = optComm opts ++ " " ++ tmpi
restrictedSystem $ syst ++ " <" ++ tmpi ++ " >" ++ tmpo restrictedSystem $ syst ++ " <" ++ tmpi ++ " >" ++ tmpo
s <- readFile tmpo s <- restricted $ readFile tmpo
return $ fromString s, return $ fromString s,
flags = [ flags = [
("command","the system command applied to the argument") ("command","the system command applied to the argument")
@@ -911,7 +913,7 @@ allCommands = Map.fromList [
let outp = valStrOpts "output" "dot" opts let outp = valStrOpts "output" "dot" opts
mlab <- case file of mlab <- case file of
"" -> return Nothing "" -> return Nothing
_ -> readFile file >>= return . Just . getDepLabels . lines _ -> restricted (readFile file) >>= return . Just . getDepLabels . lines
let lang = optLang pgf opts let lang = optLang pgf opts
let grphs = unlines $ map (graphvizDependencyTree outp debug mlab Nothing pgf lang) es let grphs = unlines $ map (graphvizDependencyTree outp debug mlab Nothing pgf lang) es
if isFlag "view" opts || isFlag "format" opts then do if isFlag "view" opts || isFlag "format" opts then do
@@ -1172,16 +1174,16 @@ allCommands = Map.fromList [
optProbs opts pgf = case valStrOpts "probs" "" opts of optProbs opts pgf = case valStrOpts "probs" "" opts of
"" -> return pgf "" -> return pgf
file -> do file -> do
probs <- readProbabilitiesFromFile file pgf probs <- restricted $ readProbabilitiesFromFile file pgf
return (setProbabilities probs pgf) return (setProbabilities probs pgf)
optTranslit opts = case (valStrOpts "to" "" opts, valStrOpts "from" "" opts) of optTranslit opts = case (valStrOpts "to" "" opts, valStrOpts "from" "" opts) of
("","") -> return id ("","") -> return id
(file,"") -> do (file,"") -> do
src <- readFile file src <- restricted $ readFile file
return $ transliterateWithFile file src False return $ transliterateWithFile file src False
(_,file) -> do (_,file) -> do
src <- readFile file src <- restricted $ readFile file
return $ transliterateWithFile file src True return $ transliterateWithFile file src True
optFile opts = valStrOpts "file" "_gftmp" opts optFile opts = valStrOpts "file" "_gftmp" opts
@@ -1230,7 +1232,7 @@ allCommands = Map.fromList [
| isOpt "pgf" opts = do | isOpt "pgf" opts = do
let pgf1 = if isOpt "opt" opts then optimizePGF pgf else pgf let pgf1 = if isOpt "opt" opts then optimizePGF pgf else pgf
let outfile = valStrOpts "file" (showCId (abstractName pgf) ++ ".pgf") opts let outfile = valStrOpts "file" (showCId (abstractName pgf) ++ ".pgf") opts
encodeFile outfile pgf1 restricted $ encodeFile outfile pgf1
putStrLn $ "wrote file " ++ outfile putStrLn $ "wrote file " ++ outfile
return void return void
| isOpt "cats" opts = return $ fromString $ unwords $ map showCId $ categories pgf | isOpt "cats" opts = return $ fromString $ unwords $ map showCId $ categories pgf
@@ -1345,7 +1347,7 @@ prMorphoAnalysis (w,lps) =
-- This function is to be excuted when the command 'tok' is parsed -- This function is to be excuted when the command 'tok' is parsed
execToktok :: PGFEnv -> [Option] -> [Expr] -> IO CommandOutput execToktok :: Monad m => PGFEnv -> [Option] -> [Expr] -> m CommandOutput
execToktok (pgf, _) opts exprs = do execToktok (pgf, _) opts exprs = do
let tokenizers = Map.fromList [ (l, mkTokenizer pgf l) | l <- languages pgf] let tokenizers = Map.fromList [ (l, mkTokenizer pgf l) | l <- languages pgf]
case getLang opts of case getLang opts of

View File

@@ -6,6 +6,7 @@ module GF.Command.Interpreter (
interpretPipe, interpretPipe,
getCommandOp getCommandOp
) where ) where
import Prelude hiding (putStrLn)
import GF.Command.Commands import GF.Command.Commands
import GF.Command.Abstract import GF.Command.Abstract
@@ -14,7 +15,7 @@ import PGF
import PGF.Data import PGF.Data
import PGF.Morphology import PGF.Morphology
import GF.System.Signal import GF.System.Signal
import GF.Infra.UseIO import GF.Infra.SIO
import GF.Infra.Option import GF.Infra.Option
import Text.PrettyPrint import Text.PrettyPrint
@@ -38,7 +39,7 @@ mkCommandEnv pgf =
emptyCommandEnv :: CommandEnv emptyCommandEnv :: CommandEnv
emptyCommandEnv = mkCommandEnv emptyPGF emptyCommandEnv = mkCommandEnv emptyPGF
interpretCommandLine :: CommandEnv -> String -> IO () interpretCommandLine :: CommandEnv -> String -> SIO ()
interpretCommandLine env line = interpretCommandLine env line =
case readCommandLine line of case readCommandLine line of
Just [] -> return () Just [] -> return ()
@@ -82,7 +83,7 @@ appCommand xs c@(Command i os arg) = case arg of
EFun x -> EFun x EFun x -> EFun x
-- return the trees to be sent in pipe, and the output possibly printed -- return the trees to be sent in pipe, and the output possibly printed
interpret :: CommandEnv -> [Expr] -> Command -> IO CommandOutput interpret :: CommandEnv -> [Expr] -> Command -> SIO CommandOutput
interpret env trees comm = interpret env trees comm =
case getCommand env trees comm of case getCommand env trees comm of
Left msg -> do putStrLn ('\n':msg) Left msg -> do putStrLn ('\n':msg)

View File

@@ -197,16 +197,6 @@ writeUTF8File fpath content = do
readBinaryFile path = hGetContents =<< openBinaryFile path ReadMode readBinaryFile path = hGetContents =<< openBinaryFile path ReadMode
writeBinaryFile path s = withBinaryFile path WriteMode (flip hPutStr s) writeBinaryFile path s = withBinaryFile path WriteMode (flip hPutStr s)
-- * Functions to limit acesss to arbitrary IO and system commands
restricted io =
either (const io) (const $ fail message) =<< try (getEnv "GF_RESTRICTED")
where
message =
"This operation is not allowed when GF is running in restricted mode."
restrictedSystem = restricted . system
-- Because GHC adds the confusing text "user error" for failures cased by -- Because GHC adds the confusing text "user error" for failures cased by
-- calls to fail. -- calls to fail.
ioErrorText e = if isUserError e ioErrorText e = if isUserError e

View File

@@ -1,11 +1,13 @@
{-# LANGUAGE ScopedTypeVariables, CPP #-} {-# LANGUAGE ScopedTypeVariables, CPP #-}
-- | GF interactive mode
module GFI (mainGFI,mainRunGFI,mainServerGFI) where module GFI (mainGFI,mainRunGFI,mainServerGFI) where
import Prelude hiding (putStrLn,print)
import GF.Command.Interpreter import qualified Prelude as P(putStrLn)
import GF.Command.Importing import GF.Command.Interpreter(CommandEnv(..),commands,mkCommandEnv,emptyCommandEnv,interpretCommandLine)
import GF.Command.Commands --import GF.Command.Importing(importSource,importGrammar)
import GF.Command.Commands(flags,options)
import GF.Command.Abstract import GF.Command.Abstract
import GF.Command.Parse import GF.Command.Parse(readCommandLine,pCommand)
import GF.Data.ErrM import GF.Data.ErrM
import GF.Data.Operations (chunks,err) import GF.Data.Operations (chunks,err)
import GF.Grammar hiding (Ident) import GF.Grammar hiding (Ident)
@@ -14,19 +16,19 @@ import GF.Grammar.Parser (runP, pExp)
import GF.Grammar.Printer (ppGrammar, ppModule) import GF.Grammar.Printer (ppGrammar, ppModule)
import GF.Grammar.ShowTerm import GF.Grammar.ShowTerm
import GF.Grammar.Lookup (allOpers,allOpersTo) import GF.Grammar.Lookup (allOpers,allOpersTo)
import GF.Compile.Rename import GF.Compile.Rename(renameSourceTerm)
import GF.Compile.Compute.Concrete (computeConcrete,checkPredefError) import GF.Compile.Compute.Concrete (computeConcrete,checkPredefError)
import GF.Compile.TypeCheck.Concrete (inferLType,ppType) import GF.Compile.TypeCheck.Concrete (inferLType,ppType)
import GF.Infra.Dependencies import GF.Infra.Dependencies(depGraph)
import GF.Infra.CheckM import GF.Infra.CheckM
import GF.Infra.UseIO import GF.Infra.UseIO(ioErrorText)
import GF.Infra.SIO
import GF.Infra.Option import GF.Infra.Option
import GF.Infra.Ident (showIdent) import GF.Infra.Ident (showIdent)
import GF.Infra.BuildInfo (buildInfo)
import qualified System.Console.Haskeline as Haskeline import qualified System.Console.Haskeline as Haskeline
import GF.Text.Coding import GF.Text.Coding(decodeUnicode,encodeUnicode)
import GF.Compile.Coding import GF.Compile.Coding(codeTerm)
import PGF import PGF
import PGF.Data import PGF.Data
@@ -38,14 +40,13 @@ import Data.List(nub,isPrefixOf,isInfixOf,partition)
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import qualified Text.ParserCombinators.ReadP as RP import qualified Text.ParserCombinators.ReadP as RP
import System.IO import System.IO(utf8,mkTextEncoding,hSetEncoding,stdin,stdout,stderr)
import System.CPUTime --import System.CPUTime(getCPUTime)
import System.Directory import System.Directory({-getCurrentDirectory,-}getAppUserDataDirectory)
import Control.Exception import Control.Exception(SomeException,fromException,evaluate,try)
import Control.Monad import Control.Monad
import Data.Version
import Text.PrettyPrint (render) import Text.PrettyPrint (render)
import GF.System.Signal import qualified GF.System.Signal as IO(runInterruptibly)
#ifdef SERVER_MODE #ifdef SERVER_MODE
import GFServer(server) import GFServer(server)
#endif #endif
@@ -54,7 +55,9 @@ import System.Win32.Console
import System.Win32.NLS import System.Win32.NLS
#endif #endif
import Paths_gf import GF.Infra.BuildInfo(buildInfo)
import Data.Version(showVersion)
import Paths_gf(version)
mainRunGFI :: Options -> [FilePath] -> IO () mainRunGFI :: Options -> [FilePath] -> IO ()
mainRunGFI opts files = shell (beQuiet opts) files mainRunGFI opts files = shell (beQuiet opts) files
@@ -63,14 +66,14 @@ beQuiet = addOptions (modifyFlags (\f -> f{optVerbosity=Quiet}))
mainGFI :: Options -> [FilePath] -> IO () mainGFI :: Options -> [FilePath] -> IO ()
mainGFI opts files = do mainGFI opts files = do
putStrLn welcome P.putStrLn welcome
shell opts files shell opts files
shell opts files = loop opts =<< importInEnv emptyGFEnv opts files shell opts files = loop opts =<< runSIO (importInEnv emptyGFEnv opts files)
#ifdef SERVER_MODE #ifdef SERVER_MODE
mainServerGFI opts0 port files = mainServerGFI opts0 port files =
server port (execute1 opts) =<< importInEnv emptyGFEnv opts files server port (execute1 opts) =<< runSIO (importInEnv emptyGFEnv opts files)
where opts = beQuiet opts0 where opts = beQuiet opts0
#else #else
mainServerGFI opts files = mainServerGFI opts files =
@@ -84,7 +87,8 @@ loop opts gfenv = maybe (return ()) (loop opts) =<< readAndExecute1 opts gfenv
-- | Read and execute one command, returning Just an updated environment for -- | Read and execute one command, returning Just an updated environment for
-- | the next command, or Nothing when it is time to quit -- | the next command, or Nothing when it is time to quit
readAndExecute1 :: Options -> GFEnv -> IO (Maybe GFEnv) readAndExecute1 :: Options -> GFEnv -> IO (Maybe GFEnv)
readAndExecute1 opts gfenv = execute1 opts gfenv =<< readCommand opts gfenv readAndExecute1 opts gfenv =
runSIO . execute1 opts gfenv =<< readCommand opts gfenv
-- | Read a command -- | Read a command
readCommand :: Options -> GFEnv -> IO String readCommand :: Options -> GFEnv -> IO String
@@ -94,7 +98,7 @@ readCommand opts gfenv0 =
_ -> fetchCommand gfenv0 _ -> fetchCommand gfenv0
-- | Optionally show how much CPU time was used to run an IO action -- | Optionally show how much CPU time was used to run an IO action
optionallyShowCPUTime :: Options -> IO a -> IO a optionallyShowCPUTime :: Options -> SIO a -> SIO a
optionallyShowCPUTime opts act optionallyShowCPUTime opts act
| not (verbAtLeast opts Normal) = act | not (verbAtLeast opts Normal) = act
| otherwise = do t0 <- getCPUTime | otherwise = do t0 <- getCPUTime
@@ -115,7 +119,7 @@ loopOptNewCPU opts gfenv'
-- | Execute a given command, returning Just an updated environment for -- | Execute a given command, returning Just an updated environment for
-- | the next command, or Nothing when it is time to quit -- | the next command, or Nothing when it is time to quit
execute1 :: Options -> GFEnv -> String -> IO (Maybe GFEnv) execute1 :: Options -> GFEnv -> String -> SIO (Maybe GFEnv)
execute1 opts gfenv0 s0 = execute1 opts gfenv0 s0 =
interruptible $ optionallyShowCPUTime opts $ interruptible $ optionallyShowCPUTime opts $
case pwords s0 of case pwords s0 of
@@ -239,7 +243,7 @@ execute1 opts gfenv0 s0 =
[showIdent j ++ "\t" ++ show (fst k) | (j,k) <- snd sz] [showIdent j ++ "\t" ++ show (fst k) | (j,k) <- snd sz]
_ | elem "-save" os -> mapM_ _ | elem "-save" os -> mapM_
(\ m@(i,_) -> let file = (showIdent i ++ ".gfh") in (\ m@(i,_) -> let file = (showIdent i ++ ".gfh") in
writeFile file (render (ppModule Qualified m)) >> putStrLn ("wrote " ++ file)) restricted $ writeFile file (render (ppModule Qualified m)) >> P.putStrLn ("wrote " ++ file))
(modules mygr) (modules mygr)
_ -> putStrLn $ render $ ppGrammar mygr _ -> putStrLn $ render $ ppGrammar mygr
continue gfenv continue gfenv
@@ -253,7 +257,7 @@ execute1 opts gfenv0 s0 =
continue gfenv continue gfenv
eh [w] = -- Ehhh? Reads commands from a file, but does not execute them eh [w] = -- Ehhh? Reads commands from a file, but does not execute them
do cs <- readFile w >>= return . map (interpretCommandLine env) . lines do cs <- restricted (readFile w) >>= return . map (interpretCommandLine env) . lines
continue gfenv continue gfenv
eh _ = do putStrLn "eh command not parsed" eh _ = do putStrLn "eh command not parsed"
continue gfenv continue gfenv
@@ -311,20 +315,21 @@ execute1 opts gfenv0 s0 =
set_encoding [c] = set_encoding [c] =
do let cod = renameEncoding c do let cod = renameEncoding c
restricted $ do
#ifdef mingw32_HOST_OS #ifdef mingw32_HOST_OS
case cod of case cod of
'C':'P':c -> case reads c of 'C':'P':c -> case reads c of
[(cp,"")] -> do setConsoleCP cp [(cp,"")] -> do setConsoleCP cp
setConsoleOutputCP cp setConsoleOutputCP cp
_ -> return () _ -> return ()
"UTF-8" -> do setConsoleCP 65001 "UTF-8" -> do setConsoleCP 65001
setConsoleOutputCP 65001 setConsoleOutputCP 65001
_ -> return () _ -> return ()
#endif #endif
enc <- mkTextEncoding cod enc <- mkTextEncoding cod
hSetEncoding stdin enc hSetEncoding stdin enc
hSetEncoding stdout enc hSetEncoding stdout enc
hSetEncoding stderr enc hSetEncoding stderr enc
continue gfenv continue gfenv
set_encoding _ = putStrLn "se command not parsed" >> continue gfenv set_encoding _ = putStrLn "se command not parsed" >> continue gfenv
@@ -347,13 +352,13 @@ fetchCommand gfenv = do
Haskeline.historyFile = Just path, Haskeline.historyFile = Just path,
Haskeline.autoAddHistory = True Haskeline.autoAddHistory = True
} }
res <- runInterruptibly $ Haskeline.runInputT settings (Haskeline.getInputLine (prompt (commandenv gfenv))) res <- IO.runInterruptibly $ Haskeline.runInputT settings (Haskeline.getInputLine (prompt (commandenv gfenv)))
case res of case res of
Left _ -> return "" Left _ -> return ""
Right Nothing -> return "q" Right Nothing -> return "q"
Right (Just s) -> return s Right (Just s) -> return s
importInEnv :: GFEnv -> Options -> [FilePath] -> IO GFEnv importInEnv :: GFEnv -> Options -> [FilePath] -> SIO GFEnv
importInEnv gfenv opts files importInEnv gfenv opts files
| flag optRetainResource opts = | flag optRetainResource opts =
do src <- importSource (sourcegrammar gfenv) opts files do src <- importSource (sourcegrammar gfenv) opts files

View File

@@ -27,11 +27,12 @@ import Network.Shed.Httpd(initServer,Request(..),Response(..),queryToArguments,
import Network.CGI(handleErrors,liftIO) import Network.CGI(handleErrors,liftIO)
import FastCGIUtils(outputJSONP,handleCGIErrors,stderrToFile) import FastCGIUtils(outputJSONP,handleCGIErrors,stderrToFile)
import Text.JSON(encode,showJSON,makeObj) import Text.JSON(encode,showJSON,makeObj)
import System.IO.Silently(hCapture) --import System.IO.Silently(hCapture)
import System.Process(readProcessWithExitCode) import System.Process(readProcessWithExitCode)
import System.Exit(ExitCode(..)) import System.Exit(ExitCode(..))
import Codec.Binary.UTF8.String(decodeString,encodeString) import Codec.Binary.UTF8.String(decodeString,encodeString)
import GF.Infra.UseIO(readBinaryFile,writeBinaryFile) import GF.Infra.UseIO(readBinaryFile,writeBinaryFile)
import GF.Infra.SIO(captureSIO)
import qualified PGFService as PS import qualified PGFService as PS
import qualified ExampleService as ES import qualified ExampleService as ES
import Data.Version(showVersion) import Data.Version(showVersion)
@@ -171,7 +172,7 @@ handle state0 cache execute1
case b of case b of
Left _ -> err $ resp404 dir Left _ -> err $ resp404 dir
Right dir' -> cd dir' Right dir' -> cd dir'
Right _ -> do logPutStrLn $ "cd "++dir Right _ -> do --logPutStrLn $ "cd "++dir
r <- hmtry (ok dir) r <- hmtry (ok dir)
liftIO $ setCurrentDirectory cwd liftIO $ setCurrentDirectory cwd
either (either (liftIO . ioError) err) return r either (either (liftIO . ioError) err) return r
@@ -183,7 +184,7 @@ handle state0 cache execute1
do cmd <- look "command" do cmd <- look "command"
state <- get_state state <- get_state
let st = maybe state0 id $ M.lookup dir state let st = maybe state0 id $ M.lookup dir state
(output,st') <- liftIO $ hCapture [stdout,stderr] (execute1 st cmd) (output,st') <- liftIO $ captureSIO $ execute1 st cmd
let state' = maybe state (flip (M.insert dir) state) st' let state' = maybe state (flip (M.insert dir) state) st'
put_state state' put_state state'
return $ ok200 output return $ ok200 output