ipython-kernel 0.1.0.0 → 0.3.0.0
raw patch · 18 files changed
+1709/−1058 lines, 18 filesdep +directorydep +filepathdep +ipython-kerneldep ~aesondep ~basedep ~cerealnew-component:exe:simple-calc-examplebinary-added
Dependencies added: directory, filepath, ipython-kernel, mtl, parsec, tar, transformers
Dependency ranges changed: aeson, base, cereal
Files
- example-data/calc_profile.tar binary
- examples/Calc.hs +249/−0
- ipython-kernel.cabal +42/−11
- src/IHaskell/IPython/EasyKernel.hs +284/−0
- src/IHaskell/IPython/Kernel.hs +16/−0
- src/IHaskell/IPython/Message/Parser.hs +191/−0
- src/IHaskell/IPython/Message/UUID.hs +43/−0
- src/IHaskell/IPython/Message/Writer.hs +137/−0
- src/IHaskell/IPython/Stdin.hs +123/−0
- src/IHaskell/IPython/Types.hs +416/−0
- src/IHaskell/IPython/ZeroMQ.hs +208/−0
- src/IPython/Kernel.hs +0/−16
- src/IPython/Message/Parser.hs +0/−155
- src/IPython/Message/UUID.hs +0/−46
- src/IPython/Message/Writer.hs +0/−118
- src/IPython/Stdin.hs +0/−123
- src/IPython/Types.hs +0/−382
- src/IPython/ZeroMQ.hs +0/−207
+ example-data/calc_profile.tar view
binary file changed (absent → 168448 bytes)
+ examples/Calc.hs view
@@ -0,0 +1,249 @@+{-# LANGUAGE FlexibleContexts, FlexibleInstances, OverloadedStrings, MultiWayIf #-}+module Main where++import Control.Applicative+import Control.Arrow++import Control.Concurrent (MVar, newMVar, takeMVar, putMVar, threadDelay)+import Control.Monad (guard)+import Control.Monad.IO.Class (MonadIO(..))+import Control.Monad.State.Strict (StateT, get, modify, runStateT)++import Data.Char (isDigit)+import Data.List (isPrefixOf)+import Data.Monoid ((<>))+import qualified Data.Text as T++import IHaskell.IPython.Kernel+import IHaskell.IPython.EasyKernel (installProfile, easyKernel, KernelConfig(..))++import System.Environment (getArgs)+import System.FilePath ((</>))++import Text.Parsec (Parsec, ParseError, alphaNum, char, letter, oneOf, optionMaybe, runParser, (<?>))+import qualified Text.Parsec.Token as P++import qualified Paths_ipython_kernel as Paths++---------------------------------------------------------+-- Hutton's Razor, plus time delays, plus a global state+---------------------------------------------------------++-- | This language is Hutton's Razor with two added operations that+-- are needed to demonstrate the kernel features: a global state,+-- accessed and modified using Count, and a sleep operation.+data Razor = I Integer+ | Plus Razor Razor+ | SleepThen Double Razor+ | Count+ deriving (Read, Show, Eq)+++---------+-- Parser+---------++razorDef :: Monad m => P.GenLanguageDef String a m+razorDef = P.LanguageDef+ { P.commentStart = "(*"+ , P.commentEnd = "*)"+ , P.commentLine = "//"+ , P.nestedComments = True+ , P.identStart = letter <|> char '_'+ , P.identLetter = alphaNum <|> char '_'+ , P.opStart = oneOf "+"+ , P.opLetter = oneOf "+"+ , P.reservedNames = ["sleep", "then", "end", "count"]+ , P.reservedOpNames = []+ , P.caseSensitive = True+ }++lexer :: Monad m => P.GenTokenParser String a m+lexer = P.makeTokenParser razorDef++parens :: Parsec String a b -> Parsec String a b+parens = P.parens lexer++reserved :: String -> Parsec String a ()+reserved = P.reserved lexer++integer :: Parsec String a Integer+integer = P.integer lexer++float :: Parsec String a Double+float = P.float lexer++operator :: Parsec String a String+operator = P.operator lexer++keyword :: String -> Parsec String a ()+keyword kwd = reserved kwd <?> "the keyword \"" ++ kwd ++ "\""++literal :: Parsec String a Razor+literal = I <$> integer++sleepThen :: Parsec String a Razor+sleepThen = do keyword "sleep"+ delay <- float <?> "seconds"+ keyword "then"+ body <- expr+ keyword "end" <?> ""+ return $ SleepThen delay body++count :: Parsec String a Razor+count = keyword "count" >> return Count++expr :: Parsec String a Razor+expr = do one <- parens expr <|> literal <|> sleepThen <|> count+ rest <- optionMaybe (do op <- operator+ guard (op == "+")+ expr)+ case rest of+ Nothing -> return one+ Just other -> return $ Plus one other++parse :: String -> Either ParseError Razor+parse = runParser expr () "(input)"+++----------------------+-- Language operations+----------------------++-- | Completion+langCompletion :: T.Text -> T.Text -> Int -> Maybe ([T.Text], T.Text, T.Text)+langCompletion _code line col =+ let (before, _) = T.splitAt col line+ in fmap (\word -> (map T.pack . matchesFor $ T.unpack word, word, word))+ (lastMaybe (T.words before))+ where+ lastMaybe :: [a] -> Maybe a+ lastMaybe [] = Nothing+ lastMaybe [x] = Just x+ lastMaybe (_:xs) = lastMaybe xs+ matchesFor :: String -> [String]+ matchesFor input = filter (isPrefixOf input) available+ available = ["sleep", "then", "end", "count"] ++ map show [(-1000::Int)..1000]++-- | Documentation lookup+langInfo :: T.Text -> Maybe (T.Text, T.Text, T.Text)+langInfo obj =+ if | any (T.isPrefixOf obj) ["sleep", "then", "end"] ->+ Just (obj, sleepDocs, sleepType)+ | T.isPrefixOf obj "count" ->+ Just (obj, countDocs, countType)+ | obj == "+" -> Just (obj, plusDocs, plusType)+ | T.all isDigit obj -> Just (obj, intDocs obj, intType)+ | [x, y] <- T.splitOn "." obj,+ T.all isDigit x,+ T.all isDigit y -> Just (obj, floatDocs obj, floatType)+ | otherwise -> Nothing+ where+ sleepDocs = "sleep DURATION then VALUE end: sleep DURATION seconds, then eval VALUE"+ sleepType = "sleep FLOAT then INT end"+ plusDocs = "Perform addition"+ plusType = "INT + INT"+ intDocs i = "The integer " <> i+ intType = "INT"+ floatDocs f = "The floating point value " <> f+ floatType = "FLOAT"+ countDocs = "Increment and return the current counter"+ countType = "INT"++-- | Messages sent to the frontend during evaluation will be lists of trace elements+data IntermediateEvalRes = Got Razor Integer+ | Waiting Double+ deriving Show++-- | Cons for lists of trace elements - in this case, "sleeping"+-- messages should replace old ones to create a countdown effect.+consRes :: IntermediateEvalRes -> [IntermediateEvalRes] -> [IntermediateEvalRes]+consRes r@(Waiting _) (Waiting _ : s) = r:s+consRes r s = r:s++-- | Execute an expression.+execRazor :: MVar Integer -- ^ The global counter state+ -> Razor -- ^ The term to execute+ -> IO () -- ^ Callback to clear output so far+ -> ([IntermediateEvalRes] -> IO ()) -- ^ Callback for intermediate results+ -> StateT ([IntermediateEvalRes], T.Text) IO Integer+execRazor _ x@(I i) _ _ =+ modify (second (<> (T.pack (show x)))) >> return i+execRazor val tm@(Plus x y) clear send =+ do modify (second (<> (T.pack (show tm))))+ x' <- execRazor val x clear send+ modify (first $ consRes (Got x x'))+ sendState+ y' <- execRazor val y clear send+ modify (first $ consRes (Got y y'))+ sendState+ let res = x' + y'+ modify (first $ consRes (Got tm res))+ sendState+ return res+ where sendState = liftIO clear >> fst <$> get >>= liftIO . send+execRazor val (SleepThen delay body) clear send+ | delay <= 0.0 = execRazor val body clear send+ | delay > 0.1 = do modify (first $ consRes (Waiting delay))+ sendState+ liftIO $ threadDelay 100000+ execRazor val (SleepThen (delay - 0.1) body) clear send+ | otherwise = do modify (first $ consRes (Waiting 0))+ sendState+ liftIO $ threadDelay (floor (delay * 1000000))+ execRazor val body clear send+ where sendState = liftIO clear >> fst <$> get >>= liftIO . send+execRazor val Count clear send = do+ i <- liftIO $ takeMVar val+ modify (first $ consRes (Got Count i))+ sendState+ liftIO $ putMVar val (i+1)+ return i++ where sendState = liftIO clear >> fst <$> get >>= liftIO . send++-- | Generate a language configuration for some initial state+mkConfig :: MVar Integer -- ^ The internal state of the execution+ -> KernelConfig IO [IntermediateEvalRes] (Either ParseError Integer)+mkConfig var = KernelConfig+ { languageName = "expanded_huttons_razor"+ , languageVersion = [0,1,0]+ , profileSource = Just . (</> "calc_profile.tar") <$> Paths.getDataDir+ , displayResult = displayRes+ , displayOutput = displayOut+ , completion = langCompletion+ , objectInfo = langInfo+ , run = parseAndRun+ , debug = False+ }+ where+ displayRes (Left err) =+ [ DisplayData MimeHtml . T.pack $ "<em>" ++ show err ++ "</em>"+ , DisplayData PlainText . T.pack $ show err+ ]+ displayRes (Right x) =+ return . DisplayData MimeHtml . T.pack $+ "Answer: <strong>" ++ show x ++ "</strong>"+ displayOut out =+ let outLines = reverse (map (T.pack . show) out)+ in return (DisplayData PlainText (T.unlines outLines))+ parseAndRun code clear send =+ case parse (T.unpack code) of+ Left err -> return (Left err, Err, "")+ Right tm -> do+ (res, (_, pager)) <- runStateT (execRazor var tm clear send) ([], "")+ return (Right res, Ok, T.unpack pager)++main :: IO ()+main = do args <- getArgs+ val <- newMVar 1+ case args of+ ["kernel", profileFile] ->+ easyKernel profileFile (mkConfig val)+ ["setup"] -> do+ putStrLn "Installing profile..."+ installProfile (mkConfig val)+ _ -> do+ putStrLn "Usage:"+ putStrLn "simple-calc-example setup -- set up the profile"+ putStrLn "simple-calc-example kernel FILE -- run a kernel with FILE for communication with the frontend"
ipython-kernel.cabal view
@@ -1,5 +1,5 @@ name: ipython-kernel-version: 0.1.0.0+version: 0.3.0.0 synopsis: A library for creating kernels for IPython frontends description: ipython-kernel is a library for communicating with frontends for the interactive IPython framework. It is used extensively in IHaskell, the interactive Haskell environment.@@ -14,24 +14,55 @@ cabal-version: >=1.16 +data-dir: example-data+data-files: calc_profile.tar+++flag examples+ description: Build example programs+ default: False++ library- exposed-modules: IPython.Kernel- IPython.Types- IPython.ZeroMQ- IPython.Stdin- IPython.Message.Writer- IPython.Message.Parser- IPython.Message.UUID+ exposed-modules: IHaskell.IPython.Kernel+ IHaskell.IPython.Types+ IHaskell.IPython.ZeroMQ+ IHaskell.IPython.Stdin+ IHaskell.IPython.Message.Writer+ IHaskell.IPython.Message.Parser+ IHaskell.IPython.Message.UUID+ IHaskell.IPython.EasyKernel -- other-modules: other-extensions: OverloadedStrings hs-source-dirs: src default-language: Haskell2010- build-depends: base >=4.6 && <4.7,- aeson >=0.6 && <0.7,+ build-depends: base >=4.6 && < 4.8,+ aeson >=0.6 && < 0.9, bytestring >=0.10,- cereal ==0.3.*,+ cereal >=0.3, containers >=0.5,+ directory >=1.1,+ filepath >=1.2,+ mtl >=2.1,+ tar >=0.4.0.1, text >=0.11,+ transformers >=0.3, unix >=2.6, uuid >=1.3, zeromq4-haskell >=0.1+++-- Example program+executable simple-calc-example+ hs-source-dirs: examples+ main-is: Calc.hs+ build-depends: ipython-kernel,+ base >=4.6 && <4.8,+ filepath >=1.2,+ mtl >=2.1,+ parsec >=3.1,+ text >=0.11,+ transformers >=0.3+ + if !flag(examples)+ buildable: False
+ src/IHaskell/IPython/EasyKernel.hs view
@@ -0,0 +1,284 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Description : Easy IPython kernels+-- = Overview+-- This module provides automation for writing simple IPython+-- kernels. In particular, it provides a record type that defines+-- configurations and a function that interprets a configuration as an+-- action in some monad that can do IO.+--+-- The configuration consists primarily of functions that implement+-- the various features of a kernel, such as running code, looking up+-- documentation, and performing completion. An example for a simple+-- language that nevertheless has side effects, global state, and+-- timing effects is included in the examples directory.+--+-- = Profiles+-- To run your kernel, you will need an IPython profile that causes+-- the frontend to run it. To generate a fresh profile, run the command+--+-- > ipython profile create NAME+--+-- This will create a fresh IPython profile in @~\/.ipython\/profile_NAME@.+-- This profile must be modified in two ways:+--+-- 1. It needs to run your kernel instead of the default ipython+-- 2. It must have message signing turned off, because 'easyKernel' doesn't support it+--+-- == Setting the executable+-- To set the executable, modify the configuration object's+-- @KernelManager.kernel_cmd@ property. For example:+--+-- > c.KernelManager.kernel_cmd = ['my_kernel', '{connection_file}']+--+-- Your own main should arrange to parse command line arguments such+-- that the connection file is passed to easyKernel.+--+-- == Message signing+-- To turn off message signing, use the following snippet:+--+-- > c.Session.key = b''+-- > c.Session.keyfile = b''+--+-- == Further profile improvements+-- Consult the IPython documentation along with the generated profile+-- source code for further configuration of the frontend, including+-- syntax highlighting, logos, help text, and so forth.++module IHaskell.IPython.EasyKernel (easyKernel, installProfile, KernelConfig(..)) where++import Data.Aeson (decode)++import qualified Data.ByteString.Lazy as BL++import qualified Codec.Archive.Tar as Tar++import Control.Concurrent (MVar, readChan, writeChan, newMVar, readMVar, modifyMVar_)+import Control.Monad.IO.Class (MonadIO(..))+import Control.Monad (forever, when)++import qualified Data.Map as Map+import Data.Maybe (fromMaybe)+import qualified Data.Text as T++import IHaskell.IPython.Kernel+import IHaskell.IPython.Message.UUID as UUID++import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist, getHomeDirectory)+import System.FilePath ((</>))+import System.Exit (exitSuccess)+import System.IO (openFile, IOMode(ReadMode))++-- | The kernel configuration specifies the behavior that is specific+-- to your language. The type parameters provide the monad in which+-- your kernel will run, the type of intermediate outputs from running+-- cells, and the type of final results of cells, respectively.+data KernelConfig m output result = KernelConfig+ { languageName :: String+ -- ^ The name of the language. This field is used to calculate+ -- the name of the profile, so it should contain characters that+ -- are reasonable to have in file names.+ , languageVersion :: [Int] -- ^ The version of the language+ , profileSource :: IO (Maybe FilePath)+ -- ^ Determine the source of a profile to install using+ -- 'installProfile'. The source should be a tarball whose contents+ -- will be unpacked directly into the profile directory. For+ -- example, the file whose name is @ipython_config.py@ in the+ -- tar file for a language named @lang@ will end up in+ -- @~/.ipython/profile_lang/ipython_config.py@.+ , displayOutput :: output -> [DisplayData] -- ^ How to render intermediate output+ , displayResult :: result -> [DisplayData] -- ^ How to render final cell results+ , completion :: T.Text -> T.Text -> Int -> Maybe ([T.Text], T.Text, T.Text) + -- ^ Perform completion. The returned tuple consists of the matches,+ -- the matched text, and the completion text. The arguments are the+ -- code in the cell, the current line as text, and the column at+ -- which the cursor is placed.+ , objectInfo :: T.Text -> Maybe (T.Text, T.Text, T.Text)+ -- ^ Return the information or documentation for its argument. The+ -- returned tuple consists of the name, the documentation, and the+ -- type, respectively.+ , run :: T.Text -> IO () -> (output -> IO ()) -> m (result, ExecuteReplyStatus, String)+ -- ^ Execute a cell. The arguments are the contents of the cell, an+ -- IO action that will clear the current intermediate output, and an+ -- IO action that will add a new item to the intermediate+ -- output. The result consists of the actual result, the status to+ -- be sent to IPython, and the contents of the pager. Return the+ -- empty string to indicate that there is no pager output. Errors+ -- should be handled by defining an appropriate error constructor in+ -- your result type.+ , debug :: Bool -- ^ Whether to print extra debugging information to+ -- the console+ }++-- | Attempt to install the IPython profile from the .tar file+-- indicated by the 'profileSource' field of the configuration, if it+-- is not already installed.+installProfile :: MonadIO m => KernelConfig m output result -> m ()+installProfile config = do+ installed <- isInstalled+ when (not installed) $ do+ profSrc <- liftIO $ profileSource config+ case profSrc of+ Nothing -> liftIO (putStrLn "No IPython profile is installed or specified")+ Just tar -> do+ profExists <- liftIO $ doesFileExist tar+ profTgt <- profDir+ if profExists+ then do liftIO $ createDirectoryIfMissing True profTgt+ liftIO $ Tar.extract profTgt tar+ else liftIO . putStrLn $+ "The supplied profile source '" ++ tar ++ "' does not exist"++ where+ profDir = do+ home <- liftIO getHomeDirectory+ return $ home </> ".ipython" </> ("profile_" ++ languageName config)+ isInstalled = do+ prof <- profDir+ dirThere <- liftIO $ doesDirectoryExist prof+ isProf <- liftIO . doesFileExist $ prof </> "ipython_config.py"+ return $ dirThere && isProf++getProfile :: FilePath -> IO Profile+getProfile fn = do+ profData <- openFile fn ReadMode >>= BL.hGetContents+ case decode profData of+ Just prof -> return prof+ Nothing -> error "Invalid profile data"++createReplyHeader :: MonadIO m => MessageHeader -> m MessageHeader+createReplyHeader parent = do+ -- Generate a new message UUID.+ newMessageId <- liftIO UUID.random+ let repType = fromMaybe err (replyType $ msgType parent)+ err = error $ "No reply for message " ++ show (msgType parent)++ return MessageHeader {+ identifiers = identifiers parent,+ parentHeader = Just parent,+ metadata = Map.fromList [],+ messageId = newMessageId,+ sessionId = sessionId parent,+ username = username parent,+ msgType = repType+ }++++-- | Execute an IPython kernel for a config. Your 'main' action should+-- call this as the last thing it does.+easyKernel :: (MonadIO m)+ => FilePath -- ^ The connection file provided by the IPython frontend+ -> KernelConfig m output result -- ^ The kernel configuration specifying how to react to messages+ -> m ()+easyKernel profileFile config = do+ prof <- liftIO $ getProfile profileFile+ zmq@(Channels shellReqChan shellRepChan ctrlReqChan ctrlRepChan iopubChan) <-+ liftIO $ serveProfile prof+ execCount <- liftIO $ newMVar 0+ forever $ do+ req <- liftIO $ readChan shellReqChan+ repHeader <- createReplyHeader (header req)+ when (debug config) . liftIO $ print req+ reply <- replyTo config execCount zmq req repHeader+ liftIO $ writeChan shellRepChan reply+++replyTo :: MonadIO m+ => KernelConfig m output result+ -> MVar Integer+ -> ZeroMQInterface+ -> Message+ -> MessageHeader+ -> m Message+replyTo config _ _ KernelInfoRequest{} replyHeader =+ return KernelInfoReply+ { header = replyHeader+ , language = languageName config+ , versionList = languageVersion config+ }+replyTo config _ interface ShutdownRequest{restartPending=pending} replyHeader = do+ liftIO $ writeChan (shellReplyChannel interface) $ ShutdownReply replyHeader pending+ liftIO exitSuccess++replyTo config execCount interface req@ExecuteRequest { getCode = code } replyHeader = do+ let send msg = writeChan (iopubChannel interface) msg++ busyHeader <- dupHeader replyHeader StatusMessage+ liftIO . send $ PublishStatus busyHeader Busy++ outputHeader <- dupHeader replyHeader DisplayDataMessage+ (res, replyStatus, pagerOut) <-+ let clearOutput = do+ clearHeader <- dupHeader replyHeader ClearOutputMessage+ send $ ClearOutput clearHeader False+ sendOutput x =+ send $ PublishDisplayData outputHeader (languageName config)+ (displayOutput config x)+ in run config code clearOutput sendOutput+ liftIO . send $ PublishDisplayData outputHeader (languageName config) (displayResult config res)+++ idleHeader <- dupHeader replyHeader StatusMessage+ liftIO . send $ PublishStatus idleHeader Idle++ liftIO $ modifyMVar_ execCount (return . (+1))+ counter <- liftIO $ readMVar execCount++ return ExecuteReply+ { header = replyHeader+ , pagerOutput = pagerOut+ , executionCounter = fromIntegral counter+ , status = replyStatus+ }++replyTo config _ _ req@CompleteRequest{} replyHeader = do+ let code = getCode req+ line = getCodeLine req+ col = getCursorPos req++ return $ case completion config code line col of+ Nothing ->+ CompleteReply+ { header = replyHeader+ , completionMatches = []+ , completionMatchedText = ""+ , completionText = ""+ , completionStatus = False+ }+ Just (matches, matchedText, cmplText) ->+ CompleteReply+ { header = replyHeader+ , completionMatches = matches+ , completionMatchedText = matchedText+ , completionText = cmplText+ , completionStatus = True+ }++replyTo config _ _ ObjectInfoRequest { objectName = obj } replyHeader =+ return $ case objectInfo config obj of+ Just (name, docs, ty) -> ObjectInfoReply+ { header = replyHeader+ , objectName = obj+ , objectFound = True+ , objectTypeString = ty+ , objectDocString = docs+ }+ Nothing -> ObjectInfoReply+ { header = replyHeader+ , objectName = obj+ , objectFound = False+ , objectTypeString = ""+ , objectDocString = ""+ }++replyTo _ _ _ msg _ = do+ liftIO $ putStrLn "Unknown message: "+ liftIO $ print msg+ return msg+++dupHeader :: MonadIO m => MessageHeader -> MessageType -> m MessageHeader+dupHeader hdr mtype =+ do uuid <- liftIO UUID.random+ return hdr { messageId = uuid , msgType = mtype }
+ src/IHaskell/IPython/Kernel.hs view
@@ -0,0 +1,16 @@+-- | This module exports all the types and functions necessary to create an+-- IPython language kernel that supports the @ipython console@ and @ipython+-- notebook@ frontends. +module IHaskell.IPython.Kernel (+ module IHaskell.IPython.Types,+ module IHaskell.IPython.Message.Writer,+ module IHaskell.IPython.Message.Parser,+ module IHaskell.IPython.Message.UUID,+ module IHaskell.IPython.ZeroMQ,+ ) where++import IHaskell.IPython.Types+import IHaskell.IPython.Message.Writer+import IHaskell.IPython.Message.Parser+import IHaskell.IPython.Message.UUID+import IHaskell.IPython.ZeroMQ
+ src/IHaskell/IPython/Message/Parser.hs view
@@ -0,0 +1,191 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Description : Parsing messages received from IPython+--+-- This module is responsible for converting from low-level ByteStrings+-- obtained from the 0MQ sockets into Messages. The only exposed function is+-- `parseMessage`, which should only be used in the low-level 0MQ interface.+module IHaskell.IPython.Message.Parser (parseMessage) where++import Data.Aeson ((.:), decode, Result(..), Object)+import Control.Applicative ((<|>))+import Data.Aeson.Types (parse)+import Data.ByteString+import Data.Map (Map)+import Data.Text (Text)++import qualified Data.ByteString.Lazy as Lazy++import IHaskell.IPython.Types++type LByteString = Lazy.ByteString++----- External interface -----++-- | Parse a message from its ByteString components into a Message.+parseMessage :: [ByteString] -- ^ The list of identifiers sent with the message.+ -> ByteString -- ^ The header data.+ -> ByteString -- ^ The parent header, which is just "{}" if there is no header.+ -> ByteString -- ^ The metadata map, also "{}" for an empty map.+ -> ByteString -- ^ The message content.+ -> Message -- ^ A parsed message.+parseMessage idents headerData parentHeader metadata content = + let header = parseHeader idents headerData parentHeader metadata+ messageType = msgType header+ messageWithoutHeader = parser messageType $ Lazy.fromStrict content in+ messageWithoutHeader { header = header }++----- Module internals -----++-- | Parse a header from its ByteString components into a MessageHeader.+parseHeader :: [ByteString] -- ^ The list of identifiers.+ -> ByteString -- ^ The header data.+ -> ByteString -- ^ The parent header, or "{}" for Nothing.+ -> ByteString -- ^ The metadata, or "{}" for an empty map.+ -> MessageHeader -- The resulting message header.+parseHeader idents headerData parentHeader metadata = MessageHeader {+ identifiers = idents,+ parentHeader = parentResult,+ metadata = metadataMap,+ messageId = messageUUID,+ sessionId = sessionUUID,+ username = username,+ msgType = messageType+ } where+ -- Decode the header data and the parent header data into JSON objects.+ -- If the parent header data is absent, just have Nothing instead.+ Just result = decode $ Lazy.fromStrict headerData :: Maybe Object+ parentResult = if parentHeader == "{}"+ then Nothing+ else Just $ parseHeader idents parentHeader "{}" metadata++ -- Get the basic fields from the header.+ Success (messageType, username, messageUUID, sessionUUID) = flip parse result $ \obj -> do + messType <- obj .: "msg_type"+ username <- obj .: "username"+ message <- obj .: "msg_id"+ session <- obj .: "session"+ return (messType, username, message, session)++ -- Get metadata as a simple map.+ Just metadataMap = decode $ Lazy.fromStrict metadata :: Maybe (Map Text Text)++noHeader :: MessageHeader+noHeader = error "No header created"++parser :: MessageType -- ^ The message type being parsed.+ -> LByteString -> Message -- The parser that converts the body into a message.+ -- This message should have an undefined+ -- header.+parser KernelInfoRequestMessage = kernelInfoRequestParser+parser ExecuteRequestMessage = executeRequestParser+parser CompleteRequestMessage = completeRequestParser+parser ObjectInfoRequestMessage = objectInfoRequestParser+parser ShutdownRequestMessage = shutdownRequestParser+parser InputReplyMessage = inputReplyParser+parser CommOpenMessage = commOpenParser+parser CommDataMessage = commDataParser+parser CommCloseMessage = commCloseParser+parser other = error $ "Unknown message type " ++ show other++-- | Parse a kernel info request.+-- A kernel info request has no auxiliary information, so ignore the body.+kernelInfoRequestParser :: LByteString -> Message+kernelInfoRequestParser _ = KernelInfoRequest { header = noHeader }++-- | Parse an execute request.+-- Fields used are:+-- 1. "code": the code to execute.+-- 2. "silent": whether to execute silently.+-- 3. "store_history": whether to include this in history.+-- 4. "allow_stdin": whether to allow reading from stdin for this code.+executeRequestParser :: LByteString -> Message+executeRequestParser content = + let parser obj = do+ code <- obj .: "code"+ silent <- obj .: "silent"+ storeHistory <- obj .: "store_history"+ allowStdin <- obj .: "allow_stdin"++ return (code, silent, storeHistory, allowStdin)+ Just decoded = decode content+ Success (code, silent, storeHistory, allowStdin) = parse parser decoded in+ ExecuteRequest {+ header = noHeader,+ getCode = code,+ getSilent = silent,+ getAllowStdin = allowStdin,+ getStoreHistory = storeHistory,+ getUserVariables = [],+ getUserExpressions = []+ }++completeRequestParser :: LByteString -> Message+completeRequestParser content = parsed+ where+ Success parsed = flip parse decoded $ \ obj -> do+ code <- obj .: "block" <|> return ""+ codeLine <- obj .: "line"+ pos <- obj .: "cursor_pos"+ return $ CompleteRequest noHeader code codeLine pos++ Just decoded = decode content++objectInfoRequestParser :: LByteString -> Message+objectInfoRequestParser content = parsed+ where+ Success parsed = flip parse decoded $ \obj -> do+ oname <- obj .: "oname"+ dlevel <- obj .: "detail_level"+ return $ ObjectInfoRequest noHeader oname dlevel++ Just decoded = decode content+++shutdownRequestParser :: LByteString -> Message+shutdownRequestParser content = parsed+ where+ Success parsed = flip parse decoded $ \ obj -> do+ code <- obj .: "restart"+ return $ ShutdownRequest noHeader code++ Just decoded = decode content++inputReplyParser :: LByteString -> Message+inputReplyParser content = parsed+ where+ Success parsed = flip parse decoded $ \ obj -> do+ value <- obj .: "value"+ return $ InputReply noHeader value++ Just decoded = decode content++commOpenParser :: LByteString -> Message+commOpenParser content = parsed+ where+ Success parsed = flip parse decoded $ \ obj -> do+ uuid <- obj .: "comm_id"+ name <- obj .: "target_name"+ value <- obj .: "data"+ return $ CommOpen noHeader name uuid value++ Just decoded = decode content++commDataParser :: LByteString -> Message+commDataParser content = parsed+ where+ Success parsed = flip parse decoded $ \ obj -> do+ uuid <- obj .: "comm_id"+ value <- obj .: "data"+ return $ CommData noHeader uuid value++ Just decoded = decode content++commCloseParser :: LByteString -> Message+commCloseParser content = parsed+ where+ Success parsed = flip parse decoded $ \ obj -> do+ uuid <- obj .: "comm_id"+ value <- obj .: "data"+ return $ CommClose noHeader uuid value++ Just decoded = decode content
+ src/IHaskell/IPython/Message/UUID.hs view
@@ -0,0 +1,43 @@+-- | Description : UUID generator and data structure+--+-- Generate, parse, and pretty print UUIDs for use with IPython.+module IHaskell.IPython.Message.UUID (+ UUID,+ random, randoms,+ ) where++import Control.Monad (mzero, replicateM)+import Control.Applicative ((<$>))+import Data.Text (pack)+import Data.Aeson+import Data.UUID.V4 (nextRandom)++-- We use an internal string representation because for the purposes of+-- IPython, it matters whether the letters are uppercase or lowercase and+-- whether the dashes are present in the correct locations. For the+-- purposes of new UUIDs, it does not matter, but IPython expects UUIDs+-- passed to kernels to be returned unchanged, so we cannot actually parse+-- them.++-- | A UUID (universally unique identifier).+data UUID = UUID String deriving (Show, Read, Eq, Ord)++-- | Generate a list of random UUIDs.+randoms :: Int -- ^ Number of UUIDs to generate.+ -> IO [UUID]+randoms n = replicateM n random++-- | Generate a single random UUID.+random :: IO UUID+random = UUID <$> show <$> nextRandom++-- Allows reading and writing UUIDs as Strings in JSON. +instance FromJSON UUID where+ parseJSON val@(String _) = UUID <$> parseJSON val++ -- UUIDs must be Strings.+ parseJSON _ = mzero++instance ToJSON UUID where+ -- Extract the string from the UUID.+ toJSON (UUID str) = String $ pack str
+ src/IHaskell/IPython/Message/Writer.hs view
@@ -0,0 +1,137 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Description : @ToJSON@ for Messages+--+-- This module contains the @ToJSON@ instance for @Message@.+module IHaskell.IPython.Message.Writer (+ ToJSON(..)+) where++import Data.Aeson+import Data.Map (Map)+import Data.Text (Text, pack)+import Data.Monoid (mempty)++import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString as B+import Data.Text.Encoding++import IHaskell.IPython.Types++-- Convert message bodies into JSON.+instance ToJSON Message where+ toJSON KernelInfoReply{ versionList = vers, language = language } = object [+ "protocol_version" .= ints [4, 0], -- current protocol version, major and minor+ "language_version" .= vers,+ "language" .= language+ ]++ toJSON ExecuteReply{ status = status, executionCounter = counter, pagerOutput = pager} = object [+ "status" .= show status,+ "execution_count" .= counter,+ "payload" .= + if null pager+ then []+ else [object [+ "source" .= string "page",+ "text" .= pager+ ]],+ "user_variables" .= emptyMap,+ "user_expressions" .= emptyMap+ ]+ toJSON PublishStatus{ executionState = executionState } = object [+ "execution_state" .= executionState+ ]+ toJSON PublishStream{ streamType = streamType, streamContent = content } = object [+ "data" .= content,+ "name" .= streamType+ ]+ toJSON PublishDisplayData{ source = src, displayData = datas } = object [+ "source" .= src,+ "metadata" .= object [],+ "data" .= object (map displayDataToJson datas)+ ]++ toJSON PublishOutput{ executionCount = execCount, reprText = reprText } = object [+ "data" .= object ["text/plain" .= reprText],+ "execution_count" .= execCount,+ "metadata" .= object []+ ]+ toJSON PublishInput{ executionCount = execCount, inCode = code } = object [+ "execution_count" .= execCount,+ "code" .= code+ ]+ toJSON (CompleteReply _ m mt t s) = object [+ "matches" .= m,+ "matched_text" .= mt,+ "text" .= t,+ "status" .= if s then string "ok" else "error"+ ]+ toJSON o@ObjectInfoReply{} = object [+ "oname" .= objectName o,+ "found" .= objectFound o,+ "ismagic" .= False,+ "isalias" .= False,+ "type_name" .= objectTypeString o,+ "docstring" .= objectDocString o+ ]++ toJSON ShutdownReply{restartPending = restart} = object [+ "restart" .= restart+ ]++ toJSON ClearOutput{wait = wait} = object [+ "wait" .= wait+ ]++ toJSON RequestInput{inputPrompt = prompt} = object [+ "prompt" .= prompt+ ]++ toJSON req@CommOpen{} = object [+ "comm_id" .= commUuid req,+ "target_name" .= commTargetName req,+ "data" .= commData req+ ]++ toJSON req@CommData{} = object [+ "comm_id" .= commUuid req,+ "data" .= commData req+ ]++ toJSON req@CommClose{} = object [+ "comm_id" .= commUuid req,+ "data" .= commData req+ ]++ toJSON body = error $ "Do not know how to convert to JSON for message " ++ show body+++-- | Print an execution state as "busy", "idle", or "starting".+instance ToJSON ExecutionState where+ toJSON Busy = String "busy"+ toJSON Idle = String "idle"+ toJSON Starting = String "starting"++-- | Print a stream as "stdin" or "stdout" strings.+instance ToJSON StreamType where+ toJSON Stdin = String "stdin"+ toJSON Stdout = String "stdout"++-- | Convert a MIME type and value into a JSON dictionary pair.+displayDataToJson :: DisplayData -> (Text, Value) +displayDataToJson (DisplayData mimeType dataStr) =+ pack (show mimeType) .= String dataStr++----- Constants -----++emptyMap :: Map String String+emptyMap = mempty++emptyList :: [Int]+emptyList = []++ints :: [Int] -> [Int]+ints = id++string :: String -> String+string = id
+ src/IHaskell/IPython/Stdin.hs view
@@ -0,0 +1,123 @@+{-# LANGUAGE OverloadedStrings, DoAndIfThenElse #-}+-- | This module provides a way in which the Haskell standard input may be+-- forwarded to the IPython frontend and thus allows the notebook to use+-- the standard input.+--+-- This relies on the implementation of file handles in GHC, and is+-- generally unsafe and terrible. However, it is difficult to find another+-- way to do it, as file handles are generally meant to point to streams+-- and files, and not networked communication protocols.+--+-- In order to use this module, it must first be initialized with two+-- things. First of all, in order to know how to communicate with the+-- IPython frontend, it must know the kernel profile used for+-- communication. For this, use @recordKernelProfile@ once the profile is+-- known. Both this and @recordParentHeader@ take a directory name where+-- they can store this data.+--+-- Finally, the module must know what @execute_request@ message is+-- currently being replied to (which will request the input). Thus, every+-- time the language kernel receives an @execute_request@ message, it+-- should inform this module via @recordParentHeader@, so that the module+-- may generate messages with an appropriate parent header set. If this is+-- not done, the IPython frontends will not recognize the target of the+-- communication.+--+-- Finally, in order to activate this module, @fixStdin@ must be called+-- once. It must be passed the same directory name as @recordParentHeader@+-- and @recordKernelProfile@. Note that if this is being used from within+-- the GHC API, @fixStdin@ /must/ be called from within the GHC session+-- not from the host code.+module IHaskell.IPython.Stdin (+ fixStdin,+ recordParentHeader,+ recordKernelProfile+ ) where++import Control.Concurrent +import Control.Applicative ((<$>))+import Control.Concurrent.Chan+import Control.Monad+import GHC.IO.Handle+import GHC.IO.Handle.Types+import System.IO+import System.Posix.IO+import System.IO.Unsafe+import qualified Data.Map as Map++import IHaskell.IPython.Types+import IHaskell.IPython.ZeroMQ+import IHaskell.IPython.Message.UUID as UUID++stdinInterface :: MVar ZeroMQStdin+{-# NOINLINE stdinInterface #-}+stdinInterface = unsafePerformIO newEmptyMVar++-- | Manipulate standard input so that it is sourced from the IPython+-- frontend. This function is build on layers of deep magical hackery, so+-- be careful modifying it.+fixStdin :: String -> IO ()+fixStdin dir = do+ -- Initialize the stdin interface.+ profile <- read <$> readFile (dir ++ "/.kernel-profile")+ interface <- serveStdin profile+ putMVar stdinInterface interface+ void $ forkIO $ stdinOnce dir++stdinOnce :: String -> IO ()+stdinOnce dir = do+ -- Create a pipe using and turn it into handles.+ (readEnd, writeEnd) <- createPipe+ newStdin <- fdToHandle readEnd+ stdinInput <- fdToHandle writeEnd+ hSetBuffering newStdin NoBuffering+ hSetBuffering stdinInput NoBuffering++ -- Store old stdin and swap in new stdin.+ oldStdin <- hDuplicate stdin+ hDuplicateTo newStdin stdin++ loop stdinInput oldStdin newStdin+ where+ loop stdinInput oldStdin newStdin = do+ let FileHandle _ mvar = stdin+ threadDelay $ 150 * 1000+ empty <- isEmptyMVar mvar+ if not empty+ then loop stdinInput oldStdin newStdin+ else do+ line <- getInputLine dir+ hPutStr stdinInput $ line ++ "\n"+ loop stdinInput oldStdin newStdin++-- | Get a line of input from the IPython frontend.+getInputLine :: String -> IO String+getInputLine dir = do+ StdinChannel req rep <- readMVar stdinInterface++ -- Send a request for input.+ uuid <- UUID.random+ parentHeader <- read <$> readFile (dir ++ "/.last-req-header")+ let header = MessageHeader {+ username = username parentHeader,+ identifiers = identifiers parentHeader,+ parentHeader = Just parentHeader,+ messageId = uuid,+ sessionId = sessionId parentHeader,+ metadata = Map.fromList [],+ msgType = InputRequestMessage+ }+ let msg = RequestInput header ""+ writeChan req msg++ -- Get the reply.+ InputReply _ value <- readChan rep+ return value++recordParentHeader :: String -> MessageHeader -> IO ()+recordParentHeader dir header =+ writeFile (dir ++ "/.last-req-header") $ show header++recordKernelProfile :: String -> Profile -> IO ()+recordKernelProfile dir profile =+ writeFile (dir ++ "/.kernel-profile") $ show profile
+ src/IHaskell/IPython/Types.hs view
@@ -0,0 +1,416 @@+{-# LANGUAGE OverloadedStrings, DeriveDataTypeable, DeriveGeneric #-}+-- | This module contains all types used to create an IPython language+-- kernel.+module IHaskell.IPython.Types (+ -- * IPython kernel profile+ Profile(..),+ Transport(..),+ Port(..),+ IP(..),++ -- * IPython messaging protocol+ Message(..),+ MessageHeader(..),+ Username(..),+ Metadata(..),+ MessageType(..),+ Width(..), Height(..),+ StreamType(..),+ ExecutionState(..),+ ExecuteReplyStatus(..),+ replyType,++ -- ** IPython display data message+ DisplayData(..),+ MimeType(..),+ extractPlain+ + ) where++import Data.Aeson+import Control.Applicative ((<$>), (<*>))+import Data.ByteString (ByteString)+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text+import Data.Text (Text)+import Data.Serialize+import IHaskell.IPython.Message.UUID+import GHC.Generics (Generic)+import Data.Typeable+import Data.List (find)+import Data.Map (Map)++-------------------- IPython Kernel Profile Types ----------------------+-- | A TCP port.+type Port = Int++-- | An IP address.+type IP = String++-- | The transport mechanism used to communicate with the IPython frontend.+data Transport+ = TCP -- ^ Default transport mechanism via TCP.+ deriving (Show, Read)++-- | A kernel profile, specifying how the kernel communicates.+data Profile = Profile {+ ip :: IP, -- ^ The IP on which to listen.+ transport :: Transport, -- ^ The transport mechanism.+ stdinPort :: Port, -- ^ The stdin channel port. + controlPort :: Port, -- ^ The control channel port.+ hbPort :: Port, -- ^ The heartbeat channel port.+ shellPort :: Port, -- ^ The shell command port.+ iopubPort :: Port, -- ^ The IOPub port.+ key :: Text -- ^ The HMAC encryption key.+ } deriving (Show, Read)++-- Convert the kernel profile to and from JSON.+instance FromJSON Profile where+ parseJSON (Object v) = + Profile <$> v .: "ip"+ <*> v .: "transport"+ <*> v .: "stdin_port"+ <*> v .: "control_port"+ <*> v .: "hb_port"+ <*> v .: "shell_port"+ <*> v .: "iopub_port"+ <*> v .: "key"+ parseJSON _ = fail "Expecting JSON object."++instance ToJSON Profile where+ toJSON profile = object [+ "ip" .= ip profile,+ "transport" .= transport profile,+ "stdin_port" .= stdinPort profile,+ "control_port".= controlPort profile,+ "hb_port" .= hbPort profile,+ "shell_port" .= shellPort profile,+ "iopub_port" .= iopubPort profile,+ "key" .= key profile+ ]++instance FromJSON Transport where+ parseJSON (String mech) =+ case mech of+ "tcp" -> return TCP+ _ -> fail $ "Unknown transport mechanism " ++ Text.unpack mech+ parseJSON _ = fail "Expected JSON string as transport."++instance ToJSON Transport where+ toJSON TCP = String "tcp"+++-------------------- IPython Message Types ----------------------++-- | A message header with some metadata. +data MessageHeader = MessageHeader {+ identifiers :: [ByteString], -- ^ The identifiers sent with the message.+ parentHeader :: Maybe MessageHeader, -- ^ The parent header, if present.+ metadata :: Metadata, -- ^ A dict of metadata.+ messageId :: UUID, -- ^ A unique message UUID.+ sessionId :: UUID, -- ^ A unique session UUID.+ username :: Username, -- ^ The user who sent this message.+ msgType :: MessageType -- ^ The message type.+ } deriving (Show, Read)++-- Convert a message header into the JSON field for the header.+-- This field does not actually have all the record fields.+instance ToJSON MessageHeader where+ toJSON header = object [+ "msg_id" .= messageId header,+ "session" .= sessionId header,+ "username" .= username header,+ "msg_type" .= showMessageType (msgType header)+ ]++-- | A username for the source of a message.+type Username = Text++-- | A metadata dictionary.+type Metadata = Map Text Text++-- | The type of a message, corresponding to IPython message types.+data MessageType = KernelInfoReplyMessage+ | KernelInfoRequestMessage+ | ExecuteReplyMessage+ | ExecuteRequestMessage+ | StatusMessage+ | StreamMessage+ | DisplayDataMessage+ | OutputMessage+ | InputMessage+ | CompleteRequestMessage+ | CompleteReplyMessage+ | ObjectInfoRequestMessage+ | ObjectInfoReplyMessage+ | ShutdownRequestMessage+ | ShutdownReplyMessage+ | ClearOutputMessage+ | InputRequestMessage+ | InputReplyMessage+ | CommOpenMessage+ | CommDataMessage+ | CommCloseMessage+ deriving (Show, Read, Eq)++showMessageType :: MessageType -> String+showMessageType KernelInfoReplyMessage = "kernel_info_reply"+showMessageType KernelInfoRequestMessage = "kernel_info_request"+showMessageType ExecuteReplyMessage = "execute_reply"+showMessageType ExecuteRequestMessage = "execute_request"+showMessageType StatusMessage = "status"+showMessageType StreamMessage = "stream"+showMessageType DisplayDataMessage = "display_data"+showMessageType OutputMessage = "pyout"+showMessageType InputMessage = "pyin"+showMessageType CompleteRequestMessage = "complete_request"+showMessageType CompleteReplyMessage = "complete_reply"+showMessageType ObjectInfoRequestMessage = "object_info_request"+showMessageType ObjectInfoReplyMessage = "object_info_reply"+showMessageType ShutdownRequestMessage = "shutdown_request"+showMessageType ShutdownReplyMessage = "shutdown_reply"+showMessageType ClearOutputMessage = "clear_output"+showMessageType InputRequestMessage = "input_request"+showMessageType InputReplyMessage = "input_reply"+showMessageType CommOpenMessage = "comm_open" +showMessageType CommDataMessage = "comm_msg" +showMessageType CommCloseMessage = "comm_close" ++instance FromJSON MessageType where+ parseJSON (String s) = case s of+ "kernel_info_reply" -> return KernelInfoReplyMessage+ "kernel_info_request" -> return KernelInfoRequestMessage+ "execute_reply" -> return ExecuteReplyMessage+ "execute_request" -> return ExecuteRequestMessage+ "status" -> return StatusMessage+ "stream" -> return StreamMessage+ "display_data" -> return DisplayDataMessage+ "pyout" -> return OutputMessage+ "pyin" -> return InputMessage+ "complete_request" -> return CompleteRequestMessage+ "complete_reply" -> return CompleteReplyMessage+ "object_info_request" -> return ObjectInfoRequestMessage+ "object_info_reply" -> return ObjectInfoReplyMessage+ "shutdown_request" -> return ShutdownRequestMessage+ "shutdown_reply" -> return ShutdownReplyMessage+ "clear_output" -> return ClearOutputMessage+ "input_request" -> return InputRequestMessage+ "input_reply" -> return InputReplyMessage+ "comm_open" -> return CommOpenMessage+ "comm_msg" -> return CommDataMessage+ "comm_close" -> return CommCloseMessage++ _ -> fail ("Unknown message type: " ++ show s)+ parseJSON _ = fail "Must be a string."+++-- | A message used to communicate with the IPython frontend.+data Message + -- | A request from a frontend for information about the kernel.+ = KernelInfoRequest { header :: MessageHeader }+ -- | A response to a KernelInfoRequest.+ | KernelInfoReply { + header :: MessageHeader,+ versionList :: [Int], -- ^ The version of the language, e.g. [7, 6, 3] for GHC 7.6.3+ language :: String -- ^ The language name, e.g. "haskell"+ }+ + -- | A request from a frontend to execute some code.+ | ExecuteRequest {+ header :: MessageHeader,+ getCode :: Text, -- ^ The code string.+ getSilent :: Bool, -- ^ Whether this should be silently executed.+ getStoreHistory :: Bool, -- ^ Whether to store this in history.+ getAllowStdin :: Bool, -- ^ Whether this code can use stdin.++ getUserVariables :: [Text], -- ^ Unused.+ getUserExpressions :: [Text] -- ^ Unused.+ }++ -- | A reply to an execute request.+ | ExecuteReply {+ header :: MessageHeader,+ status :: ExecuteReplyStatus, -- ^ The status of the output.+ pagerOutput :: String, -- ^ The help string to show in the pager.+ executionCounter :: Int -- ^ The execution count, i.e. which output this is.+ }++ | PublishStatus {+ header :: MessageHeader,+ executionState :: ExecutionState -- ^ The execution state of the kernel.+ }++ | PublishStream {+ header :: MessageHeader,+ streamType :: StreamType, -- ^ Which stream to publish to.+ streamContent :: String -- ^ What to publish.+ }++ | PublishDisplayData {+ header :: MessageHeader,+ source :: String, -- ^ The name of the data source.+ displayData :: [DisplayData] -- ^ A list of data representations.+ }++ | PublishOutput {+ header :: MessageHeader,+ reprText :: String, -- ^ Printed output text.+ executionCount :: Int -- ^ Which output this is for.+ }++ | PublishInput {+ header :: MessageHeader,+ inCode :: String, -- ^ Submitted input code.+ executionCount :: Int -- ^ Which input this is.+ }++ | CompleteRequest {+ header :: MessageHeader,+ getCode :: Text, {- ^+ The entire block of text where the line is. This may be useful in the+ case of multiline completions where more context may be needed. Note: if+ in practice this field proves unnecessary, remove it to lighten the+ messages. json field @block@ -}+ getCodeLine :: Text, -- ^ just the line with the cursor. json field @line@+ getCursorPos :: Int -- ^ position of the cursor (index into the line?). json field @cursor_pos@++ }++ | CompleteReply {+ header :: MessageHeader,+ completionMatches :: [Text],+ completionMatchedText :: Text,+ completionText :: Text,+ completionStatus :: Bool+ }++ | ObjectInfoRequest {+ header :: MessageHeader, + objectName :: Text, -- ^ Name of object being searched for.+ detailLevel :: Int -- ^ Level of detail desired (defaults to 0).+ -- 0 is equivalent to foo?, 1 is equivalent+ -- to foo??.+ }++ | ObjectInfoReply {+ header :: MessageHeader, + objectName :: Text, -- ^ Name of object which was searched for.+ objectFound :: Bool, -- ^ Whether the object was found.+ objectTypeString :: Text, -- ^ Object type.+ objectDocString :: Text + }++ | ShutdownRequest {+ header :: MessageHeader,+ restartPending :: Bool -- ^ Whether this shutdown precedes a restart.+ }+ | ShutdownReply {+ header :: MessageHeader,+ restartPending :: Bool -- ^ Whether this shutdown precedes a restart.+ }++ | ClearOutput {+ header :: MessageHeader,+ wait :: Bool -- ^ Whether to wait to redraw until there is more output.+ }++ | RequestInput {+ header :: MessageHeader,+ inputPrompt :: String + }++ | InputReply {+ header :: MessageHeader,+ inputValue :: String + }++ | CommOpen {+ header :: MessageHeader,+ commTargetName :: String,+ commUuid :: UUID,+ commData :: Value+ }++ | CommData {+ header :: MessageHeader,+ commUuid :: UUID,+ commData :: Value+ }++ | CommClose {+ header :: MessageHeader,+ commUuid :: UUID,+ commData :: Value+ }++ | SendNothing -- Dummy message; nothing is sent.+ deriving Show++-- | Possible statuses in the execution reply messages.+data ExecuteReplyStatus = Ok | Err | Abort++instance Show ExecuteReplyStatus where+ show Ok = "ok"+ show Err = "error"+ show Abort = "abort"++-- | The execution state of the kernel.+data ExecutionState = Busy | Idle | Starting deriving Show++-- | Input and output streams.+data StreamType = Stdin | Stdout deriving Show++-- | Get the reply message type for a request message type.+replyType :: MessageType -> Maybe MessageType+replyType KernelInfoRequestMessage = Just KernelInfoReplyMessage+replyType ExecuteRequestMessage = Just ExecuteReplyMessage+replyType CompleteRequestMessage = Just CompleteReplyMessage+replyType ObjectInfoRequestMessage = Just ObjectInfoReplyMessage+replyType ShutdownRequestMessage = Just ShutdownReplyMessage+replyType _ = Nothing++-- | Data for display: a string with associated MIME type.+data DisplayData = DisplayData MimeType Text deriving (Typeable, Generic)++-- We can't print the actual data, otherwise this will be printed every+-- time it gets computed because of the way the evaluator is structured.+-- See how `displayExpr` is computed.+instance Show DisplayData where+ show _ = "DisplayData"++-- Allow DisplayData serialization+instance Serialize Text where+ put str = put (Text.encodeUtf8 str)+ get = Text.decodeUtf8 <$> get+instance Serialize DisplayData+instance Serialize MimeType++-- | Possible MIME types for the display data.+type Width = Int+type Height = Int+data MimeType = PlainText+ | MimeHtml+ | MimePng Width Height+ | MimeJpg Width Height+ | MimeSvg+ | MimeLatex+ | MimeJavascript+ deriving (Eq, Typeable, Generic)++-- Extract the plain text from a list of displays.+extractPlain :: [DisplayData] -> String+extractPlain disps =+ case find isPlain disps of+ Nothing -> ""+ Just (DisplayData PlainText bytestr) -> Text.unpack bytestr+ where+ isPlain (DisplayData mime _) = mime == PlainText++instance Show MimeType where+ show PlainText = "text/plain"+ show MimeHtml = "text/html"+ show (MimePng _ _) = "image/png" + show (MimeJpg _ _) = "image/jpeg"+ show MimeSvg = "image/svg+xml"+ show MimeLatex = "text/latex"+ show MimeJavascript = "application/javascript"
+ src/IHaskell/IPython/ZeroMQ.hs view
@@ -0,0 +1,208 @@+{-# LANGUAGE OverloadedStrings, DoAndIfThenElse #-}+-- | Description : Low-level ZeroMQ communication wrapper.+--+-- The "ZeroMQ" module abstracts away the low-level 0MQ based interface with IPython,+-- replacing it instead with a Haskell Channel based interface. The `serveProfile` function+-- takes a IPython profile specification and returns the channel interface to use.+module IHaskell.IPython.ZeroMQ (+ ZeroMQInterface (..),+ ZeroMQStdin(..),+ serveProfile,+ serveStdin,+ ) where++import qualified Data.ByteString.Lazy as ByteString+import Data.ByteString (ByteString)+import Control.Concurrent+import Control.Monad +import System.IO.Unsafe+import Data.Aeson (encode)+import System.ZMQ4 hiding (stdin)++import IHaskell.IPython.Types+import IHaskell.IPython.Message.Parser+import IHaskell.IPython.Message.Writer++-- | The channel interface to the ZeroMQ sockets. All communication is done via+-- Messages, which are encoded and decoded into a lower level form before being+-- transmitted to IPython. These channels should functionally serve as+-- high-level sockets which speak Messages instead of ByteStrings.+data ZeroMQInterface = Channels {+ shellRequestChannel :: Chan Message, -- ^ A channel populated with requests from the frontend.+ shellReplyChannel :: Chan Message, -- ^ Writing to this channel causes a reply to be sent to the frontend.+ controlRequestChannel :: Chan Message, -- ^ This channel is a duplicate of the shell request channel,+ -- ^ though using a different backend socket.+ controlReplyChannel :: Chan Message, -- ^ This channel is a duplicate of the shell reply channel,+ -- ^ though using a different backend socket.+ iopubChannel :: Chan Message -- ^ Writing to this channel sends an iopub message to the frontend.+ }++data ZeroMQStdin = StdinChannel {+ stdinRequestChannel :: Chan Message,+ stdinReplyChannel :: Chan Message+ }++-- | Start responding on all ZeroMQ channels used to communicate with IPython+-- | via the provided profile. Return a set of channels which can be used to+-- | communicate with IPython in a more structured manner. +serveProfile :: Profile -- ^ The profile specifying which ports and transport mechanisms to use.+ -> IO ZeroMQInterface -- ^ The Message-channel based interface to the sockets.+serveProfile profile = do+ -- Create all channels which will be used for higher level communication.+ shellReqChan <- newChan+ shellRepChan <- newChan+ controlReqChan <- dupChan shellReqChan+ controlRepChan <- dupChan shellRepChan+ iopubChan <- newChan+ let channels = Channels shellReqChan shellRepChan controlReqChan controlRepChan iopubChan++ -- Create the context in a separate thread that never finishes. If+ -- withContext or withSocket complete, the context or socket become invalid.+ forkIO $ withContext $ \context -> do+ -- Serve on all sockets.+ forkIO $ serveSocket context Rep (hbPort profile) $ heartbeat channels+ forkIO $ serveSocket context Router (controlPort profile) $ control channels+ forkIO $ serveSocket context Router (shellPort profile) $ shell channels++ -- The context is reference counted in this thread only. Thus, the last+ -- serveSocket cannot be asynchronous, because otherwise context would+ -- be garbage collectable - since it would only be used in other+ -- threads. Thus, keep the last serveSocket in this thread.+ serveSocket context Pub (iopubPort profile) $ iopub channels++ return channels++serveStdin :: Profile -> IO ZeroMQStdin +serveStdin profile = do+ reqChannel <- newChan+ repChannel <- newChan+ + -- Create the context in a separate thread that never finishes. If+ -- withContext or withSocket complete, the context or socket become invalid.+ forkIO $ withContext $ \context ->+ -- Serve on all sockets.+ serveSocket context Router (stdinPort profile) $ \socket -> do+ -- Read the request from the interface channel and send it.+ readChan reqChannel >>= sendMessage socket++ -- Receive a response and write it to the interface channel.+ receiveMessage socket >>= writeChan repChannel++ return $ StdinChannel reqChannel repChannel++-- | Serve on a given socket in a separate thread. Bind the socket in the+-- | given context and then loop the provided action, which should listen+-- | on the socket and respond to any events.+serveSocket :: SocketType a => Context -> a -> Port -> (Socket a -> IO b) -> IO ()+serveSocket context socketType port action = void $+ withSocket context socketType $ \socket -> do+ bind socket $ "tcp://127.0.0.1:" ++ show port+ forever $ action socket++-- | Listener on the heartbeat port. Echoes back any data it was sent.+heartbeat :: ZeroMQInterface -> Socket Rep -> IO ()+heartbeat _ socket = do+ -- Read some data.+ request <- receive socket++ -- Send it back.+ send socket [] request++-- | Listener on the shell port. Reads messages and writes them to+-- | the shell request channel. For each message, reads a response from the+-- | shell reply channel of the interface and sends it back to the frontend. +shell :: ZeroMQInterface -> Socket Router -> IO ()+shell channels socket = do+ -- Receive a message and write it to the interface channel.+ receiveMessage socket >>= writeChan requestChannel++ -- Read the reply from the interface channel and send it.+ readChan replyChannel >>= sendMessage socket++ where+ requestChannel = shellRequestChannel channels+ replyChannel = shellReplyChannel channels++-- | Listener on the shell port. Reads messages and writes them to+-- | the shell request channel. For each message, reads a response from the+-- | shell reply channel of the interface and sends it back to the frontend. +control :: ZeroMQInterface -> Socket Router -> IO ()+control channels socket = do+ -- Receive a message and write it to the interface channel.+ receiveMessage socket >>= writeChan requestChannel++ -- Read the reply from the interface channel and send it.+ readChan replyChannel >>= sendMessage socket++ where+ requestChannel = controlRequestChannel channels+ replyChannel = controlReplyChannel channels++-- | Send messages via the iopub channel.+-- | This reads messages from the ZeroMQ iopub interface channel +-- | and then writes the messages to the socket.+iopub :: ZeroMQInterface -> Socket Pub -> IO ()+iopub channels socket =+ readChan (iopubChannel channels) >>= sendMessage socket++-- | Receive and parse a message from a socket.+receiveMessage :: Receiver a => Socket a -> IO Message+receiveMessage socket = do+ -- Read all identifiers until the identifier/message delimiter.+ idents <- readUntil "<IDS|MSG>"++ -- Ignore the signature for now.+ void next++ headerData <- next+ parentHeader <- next+ metadata <- next+ content <- next++ let message = parseMessage idents headerData parentHeader metadata content+ return message++ where+ -- Receive the next piece of data from the socket.+ next = receive socket++ -- Read data from the socket until we hit an ending string.+ -- Return all data as a list, which does not include the ending string.+ readUntil str = do+ line <- next+ if line /= str+ then do+ remaining <- readUntil str+ return $ line : remaining+ else return []+ +-- | Encode a message in the IPython ZeroMQ communication protocol +-- | and send it through the provided socket.+sendMessage :: Sender a => Socket a -> Message -> IO ()+sendMessage _ SendNothing = return ()+sendMessage socket message = do+ let head = header message+ parentHeaderStr = maybe "{}" encodeStrict $ parentHeader head+ idents = identifiers head+ metadata = "{}"+ content = encodeStrict message+ headStr = encodeStrict head++ -- Send all pieces of the message.+ mapM_ sendPiece idents+ sendPiece "<IDS|MSG>"+ sendPiece ""+ sendPiece headStr+ sendPiece parentHeaderStr+ sendPiece metadata++ -- Conclude transmission with content.+ sendLast content++ where+ sendPiece = send socket [SendMore]+ sendLast = send socket []++ -- Encode to a strict bytestring.+ encodeStrict :: ToJSON a => a -> ByteString+ encodeStrict = ByteString.toStrict . encode
− src/IPython/Kernel.hs
@@ -1,16 +0,0 @@--- | This module exports all the types and functions necessary to create an--- IPython language kernel that supports the @ipython console@ and @ipython--- notebook@ frontends. -module IPython.Kernel (- module IPython.Types,- module IPython.Message.Writer,- module IPython.Message.Parser,- module IPython.Message.UUID,- module IPython.ZeroMQ,- ) where--import IPython.Types-import IPython.Message.Writer-import IPython.Message.Parser-import IPython.Message.UUID-import IPython.ZeroMQ
− src/IPython/Message/Parser.hs
@@ -1,155 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Description : Parsing messages received from IPython------ This module is responsible for converting from low-level ByteStrings--- obtained from the 0MQ sockets into Messages. The only exposed function is--- `parseMessage`, which should only be used in the low-level 0MQ interface.-module IPython.Message.Parser (parseMessage) where--import Data.Aeson ((.:), decode, Result(..), Object)-import Control.Applicative ((<|>))-import Data.Aeson.Types (parse)-import Data.ByteString-import qualified Data.ByteString.Lazy as Lazy-import Data.Map (Map)--import IPython.Types--type LByteString = Lazy.ByteString------- External interface --------- | Parse a message from its ByteString components into a Message.-parseMessage :: [ByteString] -- ^ The list of identifiers sent with the message.- -> ByteString -- ^ The header data.- -> ByteString -- ^ The parent header, which is just "{}" if there is no header.- -> ByteString -- ^ The metadata map, also "{}" for an empty map.- -> ByteString -- ^ The message content.- -> Message -- ^ A parsed message.-parseMessage idents headerData parentHeader metadata content = - let header = parseHeader idents headerData parentHeader metadata- messageType = msgType header- messageWithoutHeader = parser messageType $ Lazy.fromStrict content in- messageWithoutHeader { header = header }------- Module internals --------- | Parse a header from its ByteString components into a MessageHeader.-parseHeader :: [ByteString] -- ^ The list of identifiers.- -> ByteString -- ^ The header data.- -> ByteString -- ^ The parent header, or "{}" for Nothing.- -> ByteString -- ^ The metadata, or "{}" for an empty map.- -> MessageHeader -- The resulting message header.-parseHeader idents headerData parentHeader metadata = MessageHeader {- identifiers = idents,- parentHeader = parentResult,- metadata = metadataMap,- messageId = messageUUID,- sessionId = sessionUUID,- username = username,- msgType = messageType- } where- -- Decode the header data and the parent header data into JSON objects.- -- If the parent header data is absent, just have Nothing instead.- Just result = decode $ Lazy.fromStrict headerData :: Maybe Object- parentResult = if parentHeader == "{}"- then Nothing- else Just $ parseHeader idents parentHeader "{}" metadata-- -- Get the basic fields from the header.- Success (messageType, username, messageUUID, sessionUUID) = flip parse result $ \obj -> do - messType <- obj .: "msg_type"- username <- obj .: "username"- message <- obj .: "msg_id"- session <- obj .: "session"- return (messType, username, message, session)-- -- Get metadata as a simple map.- Just metadataMap = decode $ Lazy.fromStrict metadata :: Maybe (Map ByteString ByteString)--noHeader :: MessageHeader-noHeader = error "No header created"--parser :: MessageType -- ^ The message type being parsed.- -> LByteString -> Message -- The parser that converts the body into a message.- -- This message should have an undefined- -- header.-parser KernelInfoRequestMessage = kernelInfoRequestParser-parser ExecuteRequestMessage = executeRequestParser-parser CompleteRequestMessage = completeRequestParser-parser ObjectInfoRequestMessage = objectInfoRequestParser-parser ShutdownRequestMessage = shutdownRequestParser-parser InputReplyMessage = inputReplyParser-parser other = error $ "Unknown message type " ++ show other---- | Parse a kernel info request.--- A kernel info request has no auxiliary information, so ignore the body.-kernelInfoRequestParser :: LByteString -> Message-kernelInfoRequestParser _ = KernelInfoRequest { header = noHeader }---- | Parse an execute request.--- Fields used are:--- 1. "code": the code to execute.--- 2. "silent": whether to execute silently.--- 3. "store_history": whether to include this in history.--- 4. "allow_stdin": whether to allow reading from stdin for this code.-executeRequestParser :: LByteString -> Message-executeRequestParser content = - let parser obj = do- code <- obj .: "code"- silent <- obj .: "silent"- storeHistory <- obj .: "store_history"- allowStdin <- obj .: "allow_stdin"-- return (code, silent, storeHistory, allowStdin)- Just decoded = decode content- Success (code, silent, storeHistory, allowStdin) = parse parser decoded in- ExecuteRequest {- header = noHeader,- getCode = code,- getSilent = silent,- getAllowStdin = allowStdin,- getStoreHistory = storeHistory,- getUserVariables = [],- getUserExpressions = []- }--completeRequestParser :: LByteString -> Message-completeRequestParser content = parsed- where- Success parsed = flip parse decoded $ \ obj -> do- code <- obj .: "block" <|> return ""- codeLine <- obj .: "line"- pos <- obj .: "cursor_pos"- return $ CompleteRequest noHeader code codeLine pos-- Just decoded = decode content--objectInfoRequestParser :: LByteString -> Message-objectInfoRequestParser content = parsed- where- Success parsed = flip parse decoded $ \obj -> do- oname <- obj .: "oname"- dlevel <- obj .: "detail_level"- return $ ObjectInfoRequest noHeader oname dlevel-- Just decoded = decode content---shutdownRequestParser :: LByteString -> Message-shutdownRequestParser content = parsed- where- Success parsed = flip parse decoded $ \ obj -> do- code <- obj .: "restart"- return $ ShutdownRequest noHeader code-- Just decoded = decode content--inputReplyParser :: LByteString -> Message-inputReplyParser content = parsed- where- Success parsed = flip parse decoded $ \ obj -> do- value <- obj .: "value"- return $ InputReply noHeader value-- Just decoded = decode content
− src/IPython/Message/UUID.hs
@@ -1,46 +0,0 @@--- | Description : UUID generator and data structure------ Generate, parse, and pretty print UUIDs for use with IPython.-module IPython.Message.UUID (- UUID,- random, randoms,- ) where--import Control.Monad (mzero, replicateM)-import Control.Applicative ((<$>))-import Data.Text (pack)-import Data.Aeson-import Data.UUID.V4 (nextRandom)--import Text.Read as Read hiding (pfail, String)----- We use an internal string representation because for the purposes of--- IPython, it matters whether the letters are uppercase or lowercase and--- whether the dashes are present in the correct locations. For the--- purposes of new UUIDs, it does not matter, but IPython expects UUIDs--- passed to kernels to be returned unchanged, so we cannot actually parse--- them.---- | A UUID (universally unique identifier).-data UUID = UUID String deriving (Show, Read, Eq)---- | Generate a list of random UUIDs.-randoms :: Int -- ^ Number of UUIDs to generate.- -> IO [UUID]-randoms n = replicateM n random---- | Generate a single random UUID.-random :: IO UUID-random = UUID <$> show <$> nextRandom---- Allows reading and writing UUIDs as Strings in JSON. -instance FromJSON UUID where- parseJSON val@(String _) = UUID <$> parseJSON val-- -- UUIDs must be Strings.- parseJSON _ = mzero--instance ToJSON UUID where- -- Extract the string from the UUID.- toJSON (UUID str) = String $ pack str
− src/IPython/Message/Writer.hs
@@ -1,118 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--- | Description : @ToJSON@ for Messages------ This module contains the @ToJSON@ instance for @Message@.-module IPython.Message.Writer (- ToJSON(..)-) where--import Data.Aeson-import Data.Map (Map)-import Data.Text (Text, pack)-import Data.Monoid (mempty)---import IPython.Types---- Convert message bodies into JSON.-instance ToJSON Message where- toJSON KernelInfoReply{ versionList = vers, language = language } = object [- "protocol_version" .= ints [4, 0], -- current protocol version, major and minor- "language_version" .= vers,- "language" .= language- ]-- toJSON ExecuteReply{ status = status, executionCounter = counter, pagerOutput = pager} = object [- "status" .= show status,- "execution_count" .= counter,- "payload" .= - if null pager- then []- else [object [- "source" .= string "page",- "text" .= pager- ]],- "user_variables" .= emptyMap,- "user_expressions" .= emptyMap- ]- toJSON PublishStatus{ executionState = executionState } = object [- "execution_state" .= executionState- ]- toJSON PublishStream{ streamType = streamType, streamContent = content } = object [- "data" .= content,- "name" .= streamType- ]- toJSON PublishDisplayData{ source = src, displayData = datas } = object [- "source" .= src,- "metadata" .= object [],- "data" .= object (map displayDataToJson datas)- ]-- toJSON PublishOutput{ executionCount = execCount, reprText = reprText } = object [- "data" .= object ["text/plain" .= reprText],- "execution_count" .= execCount,- "metadata" .= object []- ]- toJSON PublishInput{ executionCount = execCount, inCode = code } = object [- "execution_count" .= execCount,- "code" .= code- ]- toJSON (CompleteReply _ m mt t s) = object [- "matches" .= m,- "matched_text" .= mt,- "text" .= t,- "status" .= if s then string "ok" else "error"- ]- toJSON o@ObjectInfoReply{} = object [- "oname" .= objectName o,- "found" .= objectFound o,- "ismagic" .= False,- "isalias" .= False,- "type_name" .= objectTypeString o,- "docstring" .= objectDocString o- ]-- toJSON ShutdownReply{restartPending = restart} = object [- "restart" .= restart- ]-- toJSON ClearOutput{wait = wait} = object [- "wait" .= wait- ]-- toJSON RequestInput{inputPrompt = prompt} = object [- "prompt" .= prompt- ]--- toJSON body = error $ "Do not know how to convert to JSON for message " ++ show body----- | Print an execution state as "busy", "idle", or "starting".-instance ToJSON ExecutionState where- toJSON Busy = String "busy"- toJSON Idle = String "idle"- toJSON Starting = String "starting"---- | Print a stream as "stdin" or "stdout" strings.-instance ToJSON StreamType where- toJSON Stdin = String "stdin"- toJSON Stdout = String "stdout"---- | Convert a MIME type and value into a JSON dictionary pair.-displayDataToJson :: DisplayData -> (Text, Value) -displayDataToJson (DisplayData mimeType dataStr) = pack (show mimeType) .= dataStr------- Constants -------emptyMap :: Map String String-emptyMap = mempty--emptyList :: [Int]-emptyList = []--ints :: [Int] -> [Int]-ints = id--string :: String -> String-string = id
− src/IPython/Stdin.hs
@@ -1,123 +0,0 @@-{-# LANGUAGE OverloadedStrings, DoAndIfThenElse #-}--- | This module provides a way in which the Haskell standard input may be--- forwarded to the IPython frontend and thus allows the notebook to use--- the standard input.------ This relies on the implementation of file handles in GHC, and is--- generally unsafe and terrible. However, it is difficult to find another--- way to do it, as file handles are generally meant to point to streams--- and files, and not networked communication protocols.------ In order to use this module, it must first be initialized with two--- things. First of all, in order to know how to communicate with the--- IPython frontend, it must know the kernel profile used for--- communication. For this, use @recordKernelProfile@ once the profile is--- known. Both this and @recordParentHeader@ take a directory name where--- they can store this data.------ Finally, the module must know what @execute_request@ message is--- currently being replied to (which will request the input). Thus, every--- time the language kernel receives an @execute_request@ message, it--- should inform this module via @recordParentHeader@, so that the module--- may generate messages with an appropriate parent header set. If this is--- not done, the IPython frontends will not recognize the target of the--- communication.------ Finally, in order to activate this module, @fixStdin@ must be called--- once. It must be passed the same directory name as @recordParentHeader@--- and @recordKernelProfile@. Note that if this is being used from within--- the GHC API, @fixStdin@ /must/ be called from within the GHC session--- not from the host code.-module IPython.Stdin (- fixStdin,- recordParentHeader,- recordKernelProfile- ) where--import Control.Concurrent -import Control.Applicative ((<$>))-import Control.Concurrent.Chan-import Control.Monad-import GHC.IO.Handle-import GHC.IO.Handle.Types-import System.IO-import System.Posix.IO-import System.IO.Unsafe-import qualified Data.Map as Map--import IPython.Types-import IPython.ZeroMQ-import IPython.Message.UUID as UUID--stdinInterface :: MVar ZeroMQStdin-{-# NOINLINE stdinInterface #-}-stdinInterface = unsafePerformIO newEmptyMVar---- | Manipulate standard input so that it is sourced from the IPython--- frontend. This function is build on layers of deep magical hackery, so--- be careful modifying it.-fixStdin :: String -> IO ()-fixStdin dir = do- -- Initialize the stdin interface.- profile <- read <$> readFile (dir ++ "/.kernel-profile")- interface <- serveStdin profile- putMVar stdinInterface interface- void $ forkIO $ stdinOnce dir--stdinOnce :: String -> IO ()-stdinOnce dir = do- -- Create a pipe using and turn it into handles.- (readEnd, writeEnd) <- createPipe- newStdin <- fdToHandle readEnd- stdinInput <- fdToHandle writeEnd- hSetBuffering newStdin NoBuffering- hSetBuffering stdinInput NoBuffering-- -- Store old stdin and swap in new stdin.- oldStdin <- hDuplicate stdin- hDuplicateTo newStdin stdin-- loop stdinInput oldStdin newStdin- where- loop stdinInput oldStdin newStdin = do- let FileHandle _ mvar = stdin- threadDelay $ 150 * 1000- empty <- isEmptyMVar mvar- if not empty- then loop stdinInput oldStdin newStdin- else do- line <- getInputLine dir- hPutStr stdinInput $ line ++ "\n"- loop stdinInput oldStdin newStdin---- | Get a line of input from the IPython frontend.-getInputLine :: String -> IO String-getInputLine dir = do- StdinChannel req rep <- readMVar stdinInterface-- -- Send a request for input.- uuid <- UUID.random- parentHeader <- read <$> readFile (dir ++ "/.last-req-header")- let header = MessageHeader {- username = username parentHeader,- identifiers = identifiers parentHeader,- parentHeader = Just parentHeader,- messageId = uuid,- sessionId = sessionId parentHeader,- metadata = Map.fromList [],- msgType = InputRequestMessage- }- let msg = RequestInput header ""- writeChan req msg-- -- Get the reply.- InputReply _ value <- readChan rep- return value--recordParentHeader :: String -> MessageHeader -> IO ()-recordParentHeader dir header =- writeFile (dir ++ "/.last-req-header") $ show header--recordKernelProfile :: String -> Profile -> IO ()-recordKernelProfile dir profile =- writeFile (dir ++ "/.kernel-profile") $ show profile
− src/IPython/Types.hs
@@ -1,382 +0,0 @@-{-# LANGUAGE OverloadedStrings, DeriveDataTypeable, DeriveGeneric #-}--- | This module contains all types used to create an IPython language--- kernel.-module IPython.Types (- -- * IPython kernel profile- Profile(..),- Transport(..),- Port(..),- IP(..),-- -- * IPython messaging protocol- Message(..),- MessageHeader(..),- Username(..),- Metadata(..),- MessageType(..),- Width(..), Height(..),- StreamType(..),- ExecutionState(..),- ExecuteReplyStatus(..),- replyType,-- -- ** IPython display data message- DisplayData(..),- MimeType(..),- extractPlain- - ) where--import Data.Aeson-import Control.Applicative ((<$>), (<*>))-import Data.ByteString (ByteString)-import qualified Data.ByteString.Char8 as Char-import qualified Data.Text as Text-import Data.Serialize-import IPython.Message.UUID-import GHC.Generics (Generic)-import Data.Typeable-import Data.List (find)-import Data.Map (Map)---------------------- IPython Kernel Profile Types ------------------------- | A TCP port.-type Port = Int---- | An IP address.-type IP = String---- | The transport mechanism used to communicate with the IPython frontend.-data Transport- = TCP -- ^ Default transport mechanism via TCP.- deriving (Show, Read)---- | A kernel profile, specifying how the kernel communicates.-data Profile = Profile {- ip :: IP, -- ^ The IP on which to listen.- transport :: Transport, -- ^ The transport mechanism.- stdinPort :: Port, -- ^ The stdin channel port. - controlPort :: Port, -- ^ The control channel port.- hbPort :: Port, -- ^ The heartbeat channel port.- shellPort :: Port, -- ^ The shell command port.- iopubPort :: Port, -- ^ The IOPub port.- key :: ByteString -- ^ The HMAC encryption key.- } deriving (Show, Read)---- Convert the kernel profile to and from JSON.-instance FromJSON Profile where- parseJSON (Object v) = - Profile <$> v .: "ip"- <*> v .: "transport"- <*> v .: "stdin_port"- <*> v .: "control_port"- <*> v .: "hb_port"- <*> v .: "shell_port"- <*> v .: "iopub_port"- <*> v .: "key"- parseJSON _ = fail "Expecting JSON object."--instance ToJSON Profile where- toJSON profile = object [- "ip" .= ip profile,- "transport" .= transport profile,- "stdin_port" .= stdinPort profile,- "control_port".= controlPort profile,- "hb_port" .= hbPort profile,- "shell_port" .= shellPort profile,- "iopub_port" .= iopubPort profile,- "key" .= key profile- ]--instance FromJSON Transport where- parseJSON (String str) = do- let mech = Text.unpack str- case mech of- "tcp" -> return TCP- _ -> fail $ "Unknown transport mechanism " ++ mech- parseJSON _ = fail "Expected JSON string as transport."--instance ToJSON Transport where- toJSON TCP = String "tcp"----------------------- IPython Message Types -------------------------- | A message header with some metadata. -data MessageHeader = MessageHeader {- identifiers :: [ByteString], -- ^ The identifiers sent with the message.- parentHeader :: Maybe MessageHeader, -- ^ The parent header, if present.- metadata :: Metadata, -- ^ A dict of metadata.- messageId :: UUID, -- ^ A unique message UUID.- sessionId :: UUID, -- ^ A unique session UUID.- username :: Username, -- ^ The user who sent this message.- msgType :: MessageType -- ^ The message type.- } deriving (Show, Read)---- Convert a message header into the JSON field for the header.--- This field does not actually have all the record fields.-instance ToJSON MessageHeader where- toJSON header = object [- "msg_id" .= messageId header,- "session" .= sessionId header,- "username" .= username header,- "msg_type" .= showMessageType (msgType header)- ]---- | A username for the source of a message.-type Username = ByteString---- | A metadata dictionary.-type Metadata = Map ByteString ByteString---- | The type of a message, corresponding to IPython message types.-data MessageType = KernelInfoReplyMessage- | KernelInfoRequestMessage- | ExecuteReplyMessage- | ExecuteRequestMessage- | StatusMessage- | StreamMessage- | DisplayDataMessage- | OutputMessage- | InputMessage- | CompleteRequestMessage- | CompleteReplyMessage- | ObjectInfoRequestMessage- | ObjectInfoReplyMessage- | ShutdownRequestMessage- | ShutdownReplyMessage- | ClearOutputMessage- | InputRequestMessage- | InputReplyMessage- deriving (Show, Read)--showMessageType :: MessageType -> String-showMessageType KernelInfoReplyMessage = "kernel_info_reply"-showMessageType KernelInfoRequestMessage = "kernel_info_request"-showMessageType ExecuteReplyMessage = "execute_reply"-showMessageType ExecuteRequestMessage = "execute_request"-showMessageType StatusMessage = "status"-showMessageType StreamMessage = "stream"-showMessageType DisplayDataMessage = "display_data"-showMessageType OutputMessage = "pyout"-showMessageType InputMessage = "pyin"-showMessageType CompleteRequestMessage = "complete_request"-showMessageType CompleteReplyMessage = "complete_reply"-showMessageType ObjectInfoRequestMessage = "object_info_request"-showMessageType ObjectInfoReplyMessage = "object_info_reply"-showMessageType ShutdownRequestMessage = "shutdown_request"-showMessageType ShutdownReplyMessage = "shutdown_reply"-showMessageType ClearOutputMessage = "clear_output"-showMessageType InputRequestMessage = "input_request"-showMessageType InputReplyMessage = "input_reply"--instance FromJSON MessageType where- parseJSON (String s) = case s of- "kernel_info_reply" -> return KernelInfoReplyMessage- "kernel_info_request" -> return KernelInfoRequestMessage- "execute_reply" -> return ExecuteReplyMessage- "execute_request" -> return ExecuteRequestMessage- "status" -> return StatusMessage- "stream" -> return StreamMessage- "display_data" -> return DisplayDataMessage- "pyout" -> return OutputMessage- "pyin" -> return InputMessage- "complete_request" -> return CompleteRequestMessage- "complete_reply" -> return CompleteReplyMessage- "object_info_request" -> return ObjectInfoRequestMessage- "object_info_reply" -> return ObjectInfoReplyMessage- "shutdown_request" -> return ShutdownRequestMessage- "shutdown_reply" -> return ShutdownReplyMessage- "clear_output" -> return ClearOutputMessage- "input_request" -> return InputRequestMessage- "input_reply" -> return InputReplyMessage-- _ -> fail ("Unknown message type: " ++ show s)- parseJSON _ = fail "Must be a string."----- | A message used to communicate with the IPython frontend.-data Message - -- | A request from a frontend for information about the kernel.- = KernelInfoRequest { header :: MessageHeader }- -- | A response to a KernelInfoRequest.- | KernelInfoReply { - header :: MessageHeader,- versionList :: [Int], -- ^ The version of the language, e.g. [7, 6, 3] for GHC 7.6.3- language :: String -- ^ The language name, e.g. "haskell"- }- - -- | A request from a frontend to execute some code.- | ExecuteRequest {- header :: MessageHeader,- getCode :: ByteString, -- ^ The code string.- getSilent :: Bool, -- ^ Whether this should be silently executed.- getStoreHistory :: Bool, -- ^ Whether to store this in history.- getAllowStdin :: Bool, -- ^ Whether this code can use stdin.-- getUserVariables :: [ByteString], -- ^ Unused.- getUserExpressions :: [ByteString] -- ^ Unused.- }-- -- | A reply to an execute request.- | ExecuteReply {- header :: MessageHeader,- status :: ExecuteReplyStatus, -- ^ The status of the output.- pagerOutput :: String, -- ^ The help string to show in the pager.- executionCounter :: Int -- ^ The execution count, i.e. which output this is.- }-- | PublishStatus {- header :: MessageHeader,- executionState :: ExecutionState -- ^ The execution state of the kernel.- }-- | PublishStream {- header :: MessageHeader,- streamType :: StreamType, -- ^ Which stream to publish to.- streamContent :: String -- ^ What to publish.- }-- | PublishDisplayData {- header :: MessageHeader,- source :: String, -- ^ The name of the data source.- displayData :: [DisplayData] -- ^ A list of data representations.- }-- | PublishOutput {- header :: MessageHeader,- reprText :: String, -- ^ Printed output text.- executionCount :: Int -- ^ Which output this is for.- }-- | PublishInput {- header :: MessageHeader,- inCode :: String, -- ^ Submitted input code.- executionCount :: Int -- ^ Which input this is.- }-- | CompleteRequest {- header :: MessageHeader,- getCode :: ByteString, {- ^- The entire block of text where the line is. This may be useful in the- case of multiline completions where more context may be needed. Note: if- in practice this field proves unnecessary, remove it to lighten the- messages. json field @block@ -}- getCodeLine :: ByteString, -- ^ just the line with the cursor. json field @line@- getCursorPos :: Int -- ^ position of the cursor (index into the line?). json field @cursor_pos@-- }-- | CompleteReply {- header :: MessageHeader,- completionMatches :: [ByteString],- completionMatchedText :: ByteString,- completionText :: ByteString,- completionStatus :: Bool- }-- | ObjectInfoRequest {- header :: MessageHeader, - objectName :: ByteString, -- ^ Name of object being searched for.- detailLevel :: Int -- ^ Level of detail desired (defaults to 0).- -- 0 is equivalent to foo?, 1 is equivalent- -- to foo??.- }-- | ObjectInfoReply {- header :: MessageHeader, - objectName :: ByteString, -- ^ Name of object which was searched for.- objectFound :: Bool, -- ^ Whether the object was found.- objectTypeString :: ByteString, -- ^ Object type.- objectDocString :: ByteString- }-- | ShutdownRequest {- header :: MessageHeader,- restartPending :: Bool -- ^ Whether this shutdown precedes a restart.- }- | ShutdownReply {- header :: MessageHeader,- restartPending :: Bool -- ^ Whether this shutdown precedes a restart.- }-- | ClearOutput {- header :: MessageHeader,- wait :: Bool -- ^ Whether to wait to redraw until there is more output.- }-- | RequestInput {- header :: MessageHeader,- inputPrompt :: String - }-- | InputReply {- header :: MessageHeader,- inputValue :: String - }-- deriving Show---- | Possible statuses in the execution reply messages.-data ExecuteReplyStatus = Ok | Err | Abort--instance Show ExecuteReplyStatus where- show Ok = "ok"- show Err = "error"- show Abort = "abort"---- | The execution state of the kernel.-data ExecutionState = Busy | Idle | Starting deriving Show---- | Input and output streams.-data StreamType = Stdin | Stdout deriving Show---- | Get the reply message type for a request message type.-replyType :: MessageType -> Maybe MessageType-replyType KernelInfoRequestMessage = Just KernelInfoReplyMessage-replyType ExecuteRequestMessage = Just ExecuteReplyMessage-replyType CompleteRequestMessage = Just CompleteReplyMessage-replyType ObjectInfoRequestMessage = Just ObjectInfoReplyMessage-replyType ShutdownRequestMessage = Just ShutdownReplyMessage-replyType _ = Nothing---- | Data for display: a string with associated MIME type.-data DisplayData = DisplayData MimeType ByteString deriving (Typeable, Generic)---- We can't print the actual data, otherwise this will be printed every--- time it gets computed because of the way the evaluator is structured.--- See how `displayExpr` is computed.-instance Show DisplayData where- show _ = "DisplayData"---- Allow DisplayData serialization-instance Serialize DisplayData-instance Serialize MimeType---- | Possible MIME types for the display data.-type Width = Int-type Height = Int-data MimeType = PlainText- | MimeHtml- | MimePng Width Height- | MimeJpg Width Height- | MimeSvg- | MimeLatex- deriving (Eq, Typeable, Generic)---- Extract the plain text from a list of displays.-extractPlain :: [DisplayData] -> String-extractPlain disps =- case find isPlain disps of- Nothing -> ""- Just (DisplayData PlainText bytestr) -> Char.unpack bytestr- where- isPlain (DisplayData mime _) = mime == PlainText--instance Show MimeType where- show PlainText = "text/plain"- show MimeHtml = "text/html"- show (MimePng _ _) = "image/png" - show (MimeJpg _ _) = "image/jpeg"- show MimeSvg = "image/svg+xml"- show MimeLatex = "text/latex"
− src/IPython/ZeroMQ.hs
@@ -1,207 +0,0 @@-{-# LANGUAGE OverloadedStrings, DoAndIfThenElse #-}--- | Description : Low-level ZeroMQ communication wrapper.------ The "ZeroMQ" module abstracts away the low-level 0MQ based interface with IPython,--- replacing it instead with a Haskell Channel based interface. The `serveProfile` function--- takes a IPython profile specification and returns the channel interface to use.-module IPython.ZeroMQ (- ZeroMQInterface (..),- ZeroMQStdin(..),- serveProfile,- serveStdin,- ) where--import qualified Data.ByteString.Lazy as ByteString-import Data.ByteString (ByteString)-import Control.Concurrent-import Control.Monad -import System.IO.Unsafe-import Data.Aeson (encode)-import System.ZMQ4 hiding (stdin)--import IPython.Types-import IPython.Message.Parser-import IPython.Message.Writer---- | The channel interface to the ZeroMQ sockets. All communication is done via--- Messages, which are encoded and decoded into a lower level form before being--- transmitted to IPython. These channels should functionally serve as--- high-level sockets which speak Messages instead of ByteStrings.-data ZeroMQInterface = Channels {- shellRequestChannel :: Chan Message, -- ^ A channel populated with requests from the frontend.- shellReplyChannel :: Chan Message, -- ^ Writing to this channel causes a reply to be sent to the frontend.- controlRequestChannel :: Chan Message, -- ^ This channel is a duplicate of the shell request channel,- -- ^ though using a different backend socket.- controlReplyChannel :: Chan Message, -- ^ This channel is a duplicate of the shell reply channel,- -- ^ though using a different backend socket.- iopubChannel :: Chan Message -- ^ Writing to this channel sends an iopub message to the frontend.- }--data ZeroMQStdin = StdinChannel {- stdinRequestChannel :: Chan Message,- stdinReplyChannel :: Chan Message- }---- | Start responding on all ZeroMQ channels used to communicate with IPython--- | via the provided profile. Return a set of channels which can be used to--- | communicate with IPython in a more structured manner. -serveProfile :: Profile -- ^ The profile specifying which ports and transport mechanisms to use.- -> IO ZeroMQInterface -- ^ The Message-channel based interface to the sockets.-serveProfile profile = do- -- Create all channels which will be used for higher level communication.- shellReqChan <- newChan- shellRepChan <- newChan- controlReqChan <- dupChan shellReqChan- controlRepChan <- dupChan shellRepChan- iopubChan <- newChan- let channels = Channels shellReqChan shellRepChan controlReqChan controlRepChan iopubChan-- -- Create the context in a separate thread that never finishes. If- -- withContext or withSocket complete, the context or socket become invalid.- forkIO $ withContext $ \context -> do- -- Serve on all sockets.- forkIO $ serveSocket context Rep (hbPort profile) $ heartbeat channels- forkIO $ serveSocket context Router (controlPort profile) $ control channels- forkIO $ serveSocket context Router (shellPort profile) $ shell channels-- -- The context is reference counted in this thread only. Thus, the last- -- serveSocket cannot be asynchronous, because otherwise context would- -- be garbage collectable - since it would only be used in other- -- threads. Thus, keep the last serveSocket in this thread.- serveSocket context Pub (iopubPort profile) $ iopub channels-- return channels--serveStdin :: Profile -> IO ZeroMQStdin -serveStdin profile = do- reqChannel <- newChan- repChannel <- newChan- - -- Create the context in a separate thread that never finishes. If- -- withContext or withSocket complete, the context or socket become invalid.- forkIO $ withContext $ \context ->- -- Serve on all sockets.- serveSocket context Router (stdinPort profile) $ \socket -> do- -- Read the request from the interface channel and send it.- readChan reqChannel >>= sendMessage socket-- -- Receive a response and write it to the interface channel.- receiveMessage socket >>= writeChan repChannel-- return $ StdinChannel reqChannel repChannel---- | Serve on a given socket in a separate thread. Bind the socket in the--- | given context and then loop the provided action, which should listen--- | on the socket and respond to any events.-serveSocket :: SocketType a => Context -> a -> Port -> (Socket a -> IO b) -> IO ()-serveSocket context socketType port action = void $- withSocket context socketType $ \socket -> do- bind socket $ "tcp://127.0.0.1:" ++ show port- forever $ action socket---- | Listener on the heartbeat port. Echoes back any data it was sent.-heartbeat :: ZeroMQInterface -> Socket Rep -> IO ()-heartbeat _ socket = do- -- Read some data.- request <- receive socket-- -- Send it back.- send socket [] request---- | Listener on the shell port. Reads messages and writes them to--- | the shell request channel. For each message, reads a response from the--- | shell reply channel of the interface and sends it back to the frontend. -shell :: ZeroMQInterface -> Socket Router -> IO ()-shell channels socket = do- -- Receive a message and write it to the interface channel.- receiveMessage socket >>= writeChan requestChannel-- -- Read the reply from the interface channel and send it.- readChan replyChannel >>= sendMessage socket-- where- requestChannel = shellRequestChannel channels- replyChannel = shellReplyChannel channels---- | Listener on the shell port. Reads messages and writes them to--- | the shell request channel. For each message, reads a response from the--- | shell reply channel of the interface and sends it back to the frontend. -control :: ZeroMQInterface -> Socket Router -> IO ()-control channels socket = do- -- Receive a message and write it to the interface channel.- receiveMessage socket >>= writeChan requestChannel-- -- Read the reply from the interface channel and send it.- readChan replyChannel >>= sendMessage socket-- where- requestChannel = controlRequestChannel channels- replyChannel = controlReplyChannel channels---- | Send messages via the iopub channel.--- | This reads messages from the ZeroMQ iopub interface channel --- | and then writes the messages to the socket.-iopub :: ZeroMQInterface -> Socket Pub -> IO ()-iopub channels socket =- readChan (iopubChannel channels) >>= sendMessage socket---- | Receive and parse a message from a socket.-receiveMessage :: Receiver a => Socket a -> IO Message-receiveMessage socket = do- -- Read all identifiers until the identifier/message delimiter.- idents <- readUntil "<IDS|MSG>"-- -- Ignore the signature for now.- void next-- headerData <- next- parentHeader <- next- metadata <- next- content <- next-- let message = parseMessage idents headerData parentHeader metadata content- return message-- where- -- Receive the next piece of data from the socket.- next = receive socket-- -- Read data from the socket until we hit an ending string.- -- Return all data as a list, which does not include the ending string.- readUntil str = do- line <- next- if line /= str- then do- remaining <- readUntil str- return $ line : remaining- else return []- --- | Encode a message in the IPython ZeroMQ communication protocol --- | and send it through the provided socket.-sendMessage :: Sender a => Socket a -> Message -> IO ()-sendMessage socket message = do- let head = header message- parentHeaderStr = maybe "{}" encodeStrict $ parentHeader head- idents = identifiers head- metadata = "{}"- content = encodeStrict message- headStr = encodeStrict head-- -- Send all pieces of the message.- mapM_ sendPiece idents- sendPiece "<IDS|MSG>"- sendPiece ""- sendPiece headStr- sendPiece parentHeaderStr- sendPiece metadata-- -- Conclude transmission with content.- sendLast content-- where- sendPiece = send socket [SendMore]- sendLast = send socket []-- -- Encode to a strict bytestring.- encodeStrict :: ToJSON a => a -> ByteString- encodeStrict = ByteString.toStrict . encode