cryptol/cryptol-server/Main.hs

387 lines
13 KiB
Haskell
Raw Normal View History

2015-07-21 21:52:17 +03:00
-- |
-- Module : $Header$
-- Copyright : (c) 2015-2016 Galois, Inc.
2015-07-21 21:52:17 +03:00
-- License : BSD3
-- Maintainer : cryptol@galois.com
-- Stability : provisional
-- Portability : portable
--
-- Alpha version of a Cryptol server that communicates via JSON over
-- ZeroMQ. This API is highly unstable and extremely likely to change
-- in the near future.
2015-07-09 04:01:22 +03:00
{-# LANGUAGE CPP #-}
{-# LANGUAGE ExtendedDefaultRules #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
2015-07-21 21:52:17 +03:00
{-# OPTIONS_GHC -Wall -fno-warn-type-defaults #-}
2015-07-09 04:01:22 +03:00
module Main where
import Control.Concurrent
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Control
import qualified Control.Exception as X
2015-07-09 04:01:22 +03:00
import Data.Aeson
import Data.Aeson.Encode.Pretty
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as BSL
import Data.Char
import Data.IORef
import Data.Map (Map)
2015-07-09 04:01:22 +03:00
import qualified Data.Map as Map
import Data.Text (Text)
import qualified Data.Text as T
import Data.Word
import Options.Applicative
2015-07-09 04:01:22 +03:00
import System.Environment
import System.Exit
import System.FilePath
import System.Posix.Signals
2015-07-09 04:01:22 +03:00
import System.ZMQ4
import Text.Read
import qualified Cryptol.Eval.Value as E
import Cryptol.REPL.Command
import Cryptol.REPL.Monad
import Cryptol.Symbolic (ProverResult(..))
import qualified Cryptol.Testing.Concrete as Test
2015-07-09 04:01:22 +03:00
import qualified Cryptol.TypeCheck.AST as T
import qualified Cryptol.ModuleSystem as M
import Cryptol.Utils.PP hiding ((<>))
2015-07-09 04:01:22 +03:00
import Cryptol.Aeson ()
2015-10-09 02:54:08 +03:00
import Prelude ()
import Prelude.Compat
2015-07-09 04:01:22 +03:00
data RCommand
= RCEvalExpr Text
| RCApplyFun FunHandle E.Value
| RCTypeOf Text
| RCSetOpt Text Text
| RCCheck Text
| RCExhaust Text
| RCProve Text
| RCSat Text
| RCLoadPrelude
2015-07-09 04:01:22 +03:00
| RCLoadModule FilePath
| RCDecls
| RCUnknownCmd Text
| RCExit
instance FromJSON RCommand where
parseJSON = withObject "RCommand" $ \o -> do
tag <- o .: "tag"
flip (withText "tag") tag $ \case
"evalExpr" -> RCEvalExpr <$> o .: "expr"
"applyFun" -> RCApplyFun <$> o .: "handle" <*> o .: "arg"
"typeOf" -> RCTypeOf <$> o .: "expr"
"setOpt" -> RCSetOpt <$> o .: "key" <*> o .: "value"
"check" -> RCCheck <$> o .: "expr"
"exhaust" -> RCExhaust <$> o .: "expr"
"prove" -> RCProve <$> o .: "expr"
"sat" -> RCSat <$> o .: "expr"
"loadPrelude" -> return RCLoadPrelude
"loadModule" -> RCLoadModule . T.unpack <$> o .: "filePath"
"browse" -> return RCDecls
"exit" -> return RCExit
unknown -> return (RCUnknownCmd unknown)
2015-07-09 04:01:22 +03:00
newtype FunHandle = FH Int
deriving (Eq, Ord, Enum, Bounded, Show)
instance ToJSON FunHandle where
toJSON (FH i) = toJSON i
instance FromJSON FunHandle where
parseJSON v = FH <$> parseJSON v
data RResult
= RRValue E.Value
| RRFunValue FunHandle T.Type
| RRType T.Schema String -- pretty-printed type
2015-07-09 04:01:22 +03:00
| RRDecls M.IfaceDecls
| RRCheck [Test.TestReport]
| RRExhaust [Test.TestReport]
| RRSat [[E.Value]]
-- ^ A list of satisfying assignments. Empty list means unsat, max
-- length determined by @satNum@ interpreter option
| RRProve (Maybe [E.Value])
-- ^ Counterexample if invalid or 'Nothing' if valid
| RRProverError String
| RRInteractiveError REPLException String -- pretty-printed exception
2015-07-09 04:01:22 +03:00
| RRUnknownCmd Text
| RRBadMessage BS.ByteString String
| RROk
| RRInterrupted
2015-07-09 04:01:22 +03:00
instance ToJSON RResult where
toJSON = \case
RRValue v -> object
[ "tag" .= "value", "value" .= v ]
RRFunValue fh t -> object
[ "tag" .= "funValue", "handle" .= fh, "type" .= t ]
RRType s pps -> object
[ "tag" .= "type", "value" .= s, "pp" .= pps ]
2015-07-09 04:01:22 +03:00
RRDecls ifds -> object
[ "tag" .= "decls", "decls" .= ifds ]
RRCheck out -> object
[ "tag" .= "check", "testReport" .= out ]
2015-07-09 04:01:22 +03:00
RRExhaust out -> object
[ "tag" .= "exhaust", "testReport" .= out ]
2015-07-09 04:01:22 +03:00
RRSat out -> object
[ "tag" .= "sat", "assignments" .= out ]
2015-07-09 04:01:22 +03:00
RRProve out -> object
[ "tag" .= "prove", "counterexample" .= out ]
RRProverError msg -> object
[ "tag" .= "proverError", "message" .= msg ]
RRInteractiveError err pps -> object
[ "tag" .= "interactiveError", "error" .= err, "pp" .= pps ]
2015-07-09 04:01:22 +03:00
RRUnknownCmd txt -> object
[ "tag" .= "unknownCommand", "command" .= txt ]
RRBadMessage msg err -> object
[ "tag" .= "badMessage", "message" .= BS.unpack msg, "error" .= err ]
RROk -> object
[ "tag" .= "ok" ]
RRInterrupted -> object
[ "tag" .= "interrupted" ]
2015-07-09 04:01:22 +03:00
data ControlMsg
= CMConnect
-- ^ Request a new Cryptol context and connection
| CMInterrupt Word16
-- ^ Request an interrupt of all current Cryptol contexts
2015-07-09 04:01:22 +03:00
| CMExit
-- ^ Request that the entire server shut down
2015-07-09 04:01:22 +03:00
| CMUnknown Text
-- ^ Unknown message
2015-07-09 04:01:22 +03:00
instance FromJSON ControlMsg where
parseJSON = withObject "ControlMsg" $ \o -> do
tag <- o .: "tag"
flip (withText "tag") tag $ \case
"connect" -> return CMConnect
"interrupt" -> CMInterrupt <$> o .: "port"
2015-07-09 04:01:22 +03:00
"exit" -> return CMExit
other -> return $ CMUnknown other
data ControlReply
= CRConnect Word16
-- ^ Return the port for a new connection
| CRInterrupted
-- ^ Acknowledge receipt of an interrupt command
| CRExiting
-- ^ Acknowledge receipt of an exit command
2015-07-09 04:01:22 +03:00
| CRBadMessage BS.ByteString String
-- ^ Acknowledge receipt of an ill-formed control message
2015-07-09 04:01:22 +03:00
instance ToJSON ControlReply where
toJSON = \case
CRConnect port -> object
[ "tag" .= "connect", "port" .= port ]
CRInterrupted -> object
[ "tag" .= "interrupted" ]
CRExiting -> object
[ "tag" .= "exiting" ]
2015-07-09 04:01:22 +03:00
CRBadMessage msg err -> object
[ "tag" .= "badMessage", "message" .= BS.unpack msg, "error" .= err ]
server :: Word16 -> IO ()
server port =
withContext $ \ctx ->
withSocket ctx Rep $ \rep -> do
let addr = "tcp://127.0.0.1:" ++ show port
putStrLn ("[cryptol-server] coming online at " ++ addr)
bind rep addr
workers <- newIORef Map.empty
2015-07-09 04:01:22 +03:00
let loop = do
msg <- receive rep
putStrLn "[cryptol-server] received message:"
case decodeStrict msg of
Nothing -> BS.putStrLn msg
Just js -> BSL.putStrLn (encodePretty (js :: Value))
case eitherDecodeStrict msg of
Left err -> reply rep $ CRBadMessage msg err
Right CMConnect -> do
putStrLn "[cryptol-server] handling new incoming connection"
newRep <- socket ctx Rep
bind newRep "tcp://127.0.0.1:*"
newAddr <- lastEndpoint newRep
let portStr = reverse . takeWhile isDigit . reverse $ newAddr
workerPort = read portStr
2015-07-09 04:01:22 +03:00
putStrLn ("[cryptol-server] starting worker on interface " ++ newAddr)
tid <- forkFinally (runRepl newRep) (removeWorker workers port)
addNewWorker workers workerPort tid
reply rep $ CRConnect workerPort
Right (CMInterrupt port') -> do
s <- readIORef workers
case Map.lookup port' s of
Nothing -> reply rep $ CRBadMessage msg "invalid worker port"
Just tid -> do
throwTo tid X.UserInterrupt
reply rep $ CRInterrupted
2015-07-09 04:01:22 +03:00
Right CMExit -> do
putStrLn "[cryptol-server] shutting down"
reply rep $ CRExiting
2015-07-09 04:01:22 +03:00
exitSuccess
Right (CMUnknown cmd) -> do
putStrLn ("[cryptol-server] unknown control command: " ++ T.unpack cmd)
reply rep $ CRBadMessage msg "unknown control command"
loop
loop
reply :: (ToJSON a, MonadIO m) => Socket Rep -> a -> m ()
reply rep msg = liftIO $ do
let bmsg = BS.concat . BSL.toChunks . encodePretty $ msg
putStrLn "[cryptol-server] sending response:"
BS.putStrLn bmsg
send rep [] bmsg
addNewWorker :: IORef (Map Word16 ThreadId) -> Word16 -> ThreadId -> IO ()
addNewWorker workers port tid =
atomicModifyIORef workers $ \s -> (Map.insert port tid s, ())
removeWorker :: IORef (Map Word16 ThreadId) -> Word16 -> a -> IO ()
removeWorker workers port _result =
atomicModifyIORef workers $ \s -> (Map.delete port s, ())
2015-07-09 04:01:22 +03:00
runRepl :: Socket Rep -> IO ()
runRepl rep = runREPL False stdoutLogger $ do -- TODO: batch mode?
2015-07-09 04:01:22 +03:00
mCryptolPath <- io $ lookupEnv "CRYPTOLPATH"
case mCryptolPath of
Nothing -> return ()
Just path -> prependSearchPath path'
#if defined(mingw32_HOST_OS) || defined(__MINGW32__)
-- Windows paths search from end to beginning
where path' = reverse (splitSearchPath path)
#else
where path' = splitSearchPath path
#endif
funHandles <- io $ newIORef (Map.empty, minBound :: FunHandle)
let handle err = reply rep (RRInteractiveError err (show (pp err)))
handleAsync :: X.AsyncException -> IO ()
handleAsync _int = reply rep RRInterrupted
loop = liftBaseWith $ \run -> X.handle handleAsync $ run $ do
2015-07-09 04:01:22 +03:00
msg <- io $ receive rep
io $ putStrLn "[cryptol-worker] received message:"
case decodeStrict msg of
Nothing -> io $ BS.putStrLn msg
Just js -> io $ BSL.putStrLn (encodePretty (js :: Value))
flip catch handle $ case eitherDecodeStrict msg of
Left cmdErr -> reply rep (RRBadMessage msg cmdErr)
Right rc -> case rc of
RCEvalExpr txt -> do
expr <- replParseExpr (T.unpack txt)
(val, ty) <- replEvalExpr expr
case val of
E.VFun f -> do
fh <- io $ atomicModifyIORef' funHandles $ \(m, fh) ->
let m' = Map.insert fh f m
fh' = succ fh
in ((m', fh'), fh)
reply rep (RRFunValue fh ty)
_ -> reply rep (RRValue val)
RCApplyFun fh arg -> do
(m, _) <- io $ readIORef funHandles
case Map.lookup fh m of
Nothing -> reply rep (RRBadMessage "invalid function handle" (show fh))
Just f -> do
case f arg of
E.VFun g -> do
gh <- io $ atomicModifyIORef' funHandles $ \(m', gh) ->
let m'' = Map.insert gh g m'
gh' = succ gh
in ((m'', gh'), gh)
-- TODO: bookkeeping to track the type of this value
reply rep (RRFunValue gh T.tZero)
val -> reply rep (RRValue val)
2015-07-09 04:01:22 +03:00
RCTypeOf txt -> do
expr <- replParseExpr (T.unpack txt)
(_expr, _def, sch) <- replCheckExpr expr
reply rep (RRType sch (show (pp sch)))
2015-07-09 04:01:22 +03:00
RCSetOpt key val -> do
setOptionCmd (T.unpack key ++ "=" ++ T.unpack val)
reply rep RROk
RCCheck expr -> do
reports <- qcCmd QCRandom (T.unpack expr)
reply rep (RRCheck reports)
2015-07-09 04:01:22 +03:00
RCExhaust expr -> do
reports <- qcCmd QCExhaust (T.unpack expr)
reply rep (RRExhaust reports)
RCProve expr -> do
result <- onlineProveSat False (T.unpack expr) Nothing
case result of
AllSatResult [cex] ->
reply rep (RRProve (Just (map (\(_,_,v) -> v) cex)))
ThmResult _ ->
reply rep (RRProve Nothing)
ProverError err ->
reply rep (RRProverError err)
_ ->
reply rep (RRProverError "unexpected prover result")
2015-07-09 04:01:22 +03:00
RCSat expr -> do
result <- onlineProveSat True (T.unpack expr) Nothing
case result of
AllSatResult sas ->
reply rep (RRSat (map (map (\(_,_,v) -> v)) sas))
ThmResult _ ->
reply rep (RRSat [])
ProverError err ->
reply rep (RRProverError err)
_ ->
reply rep (RRProverError "unexpected prover result")
RCLoadPrelude -> do
loadPrelude
reply rep RROk
2015-07-09 04:01:22 +03:00
RCLoadModule fp -> do
loadCmd fp
reply rep RROk
RCDecls -> do
2015-10-28 00:21:05 +03:00
(decls, _namingEnv, _nameDisp) <- getFocusedEnv
2015-07-09 04:01:22 +03:00
reply rep (RRDecls decls)
RCUnknownCmd cmd -> reply rep (RRUnknownCmd cmd)
RCExit -> do
reply rep RROk
io $ close rep
io $ putStrLn "[cryptol-worker] shutting down"
void $ forever loop
withCapturedOutput :: REPL a -> REPL (a, String)
withCapturedOutput m = do
old <- getPutStr
buf <- io $ newIORef ""
setPutStr $ \s -> modifyIORef' buf (++ s)
x <- m
s <- io $ readIORef buf
setPutStr old
return (x, s)
data Server = Server { serverPort :: Word16
, serverMaskSIGINT :: Bool }
deriving Show
2015-07-09 04:01:22 +03:00
main :: IO ()
main = execParser opts >>= mainWith
where
opts =
info (helper <*> serverOpts)
( fullDesc
<> progDesc "Run Cryptol as a server via ZeroMQ and JSON"
<> header "cryptol-server" )
serverOpts =
Server
<$> option auto
( long "port"
<> short 'p'
<> metavar "PORT"
<> value 5555
<> help "TCP port to bind" )
<*> switch
( long "mask-interrupts"
<> help "Suppress interrupt signals" )
mainWith Server {..} = do
when serverMaskSIGINT $ void $ installHandler sigINT Ignore Nothing
server serverPort