packages feed

ipython-kernel-0.6.0.0: src/IHaskell/IPython/Stdin.hs

{-# 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