packages feed

libiserv-9.0.1: src/Remote/Slave.hs

{-# LANGUAGE ForeignFunctionInterface, GADTs, LambdaCase #-}
module Remote.Slave where

import Network.Socket

import Lib (serv)
import Remote.Message

import System.IO
import Control.Exception
import Control.Concurrent
import Control.Monad (when, forever)
import System.Directory
import System.FilePath (takeDirectory, (</>), dropTrailingPathSeparator,
                        isAbsolute, joinPath, splitPath)
import GHCi.ResolvedBCO

import Data.IORef
import GHCi.Message (Pipe(..), Msg(..), Message(..), readPipe, writePipe)

import Foreign.C.String

import Data.Binary
import GHC.Fingerprint (getFileHash)

import qualified Data.ByteString as BS

import Text.Printf
import System.Environment (getProgName)

trace :: String -> IO ()
trace s = getProgName >>= \name -> printf "[%20s] %s\n" name s

dropLeadingPathSeparator :: FilePath -> FilePath
dropLeadingPathSeparator p | isAbsolute p = joinPath (drop 1 (splitPath p))
                           | otherwise    = p

-- | Path concatenation that prevents a double path separator to appear in the
-- final path. "/foo/bar/" <//> "/baz/quux" == "/foo/bar/baz/quux"
(<//>) :: FilePath -> FilePath -> FilePath
lhs <//> rhs = dropTrailingPathSeparator lhs </> dropLeadingPathSeparator rhs
infixr 5 <//>

foreign export ccall startSlave :: Bool -> Int -> CString -> IO ()

-- | @startSlave@ is the exported slave function, that the
-- hosting application on the target needs to invoce to
-- start the slave process, and runs iserv.
startSlave :: Bool -> Int -> CString -> IO ()
startSlave verbose port s = do
  base_path <- peekCString s
  trace $ "DocRoot: " ++ base_path
  _ <- forkIO $ startSlave' verbose base_path (toEnum port)
  return ()

-- | @startSlave'@ provdes a blocking haskell interface, that
-- the hosting application on the target can use to start the
-- slave process.
startSlave' :: Bool -> String -> PortNumber -> IO ()
startSlave' verbose base_path port = do
  hSetBuffering stdin LineBuffering
  hSetBuffering stdout LineBuffering

  sock <- openSocket port

  forever $ do
    when verbose $ trace "Opening socket"
    pipe <- acceptSocket sock >>= socketToPipe
    putStrLn $ "Listening on port " ++ show port
    when verbose $ trace "Starting serv"
    uninterruptibleMask $ serv verbose (hook verbose base_path pipe) pipe
    when verbose $ trace "serv ended"
    return ()

-- | The iserv library may need access to files, specifically
-- archives and object files to be linked. If ghc and the slave
-- are on the same host, this is trivial, as the underlying
-- filestorage is the same.  If however the slave does not run
-- on the same host, the filestorage is not identical and we
-- need to request data from the host where ghc runs on.
--
-- If we however already have the requested file we need to make
-- sure that this file is the same one ghc sees. Hence we
-- calculate the Fingerprint of the file and send it back to the
-- host for comparison. The proxy will then send back either @Nothing@
-- indicating that the file on the host has the same Fingerprint, or
-- Maybe ByteString containing the payload to replace the existing
-- file with.
handleLoad :: Pipe -> FilePath -> FilePath -> IO ()
handleLoad pipe path localPath = do
  exists <- doesFileExist localPath
  if exists
    then getFileHash localPath >>= \hash -> proxyCall (Have path hash) >>= \case
      Nothing -> return ()
      Just bs -> BS.writeFile localPath bs
    else do
      createDirectoryIfMissing True (takeDirectory localPath)
      resp <- proxyCall (Missing path)
      BS.writeFile localPath resp

  proxyCall Done
  where
    proxyCall :: (Binary a, Show a) => SlaveMessage a -> IO a
    proxyCall msg = do
      writePipe pipe (putSlaveMessage msg)
      readPipe pipe get

-- | The hook we install in the @serv@ function from the
-- iserv library, to request archives over the wire.
hook :: Bool -> String -> Pipe -> Msg -> IO Msg
hook verbose base_path pipe m = case m of
  Msg (AddLibrarySearchPath p) -> do
    when verbose $ putStrLn ("Need Path: " ++ (base_path <//> p))
    createDirectoryIfMissing True (base_path <//> p)
    return $ Msg (AddLibrarySearchPath (base_path <//> p))
  Msg (LoadObj path) -> do
    when verbose $ putStrLn ("Need Obj: " ++ (base_path <//> path))
    handleLoad pipe path (base_path <//> path)
    return $ Msg (LoadObj (base_path <//> path))
  Msg (LoadArchive path) -> do
    handleLoad pipe path (base_path <//> path)
    return $ Msg (LoadArchive (base_path <//> path))
  -- when loading DLLs (.so, .dylib, .dll, ...) and these are provided
  -- as relative paths, the intention is to load a pre-existing system library,
  -- therefore we hook the LoadDLL call only for absolute paths to ship the
  -- dll from the host to the target.  On windows we assume that we don't
  -- want to copy libraries that are referenced in C:\ these are usually
  -- system libraries.
  Msg (LoadDLL path@('C':':':_)) -> do
    return m
  Msg (LoadDLL path) | isAbsolute path -> do
    when verbose $ trace ("Need DLL: " ++ (base_path <//> path))
    handleLoad pipe path (base_path <//> path)
    return $ Msg (LoadDLL (base_path <//> path))
  _other -> return m

--------------------------------------------------------------------------------
-- socket to pipe briding logic.
socketToPipe :: Socket -> IO Pipe
socketToPipe sock = do
  hdl <- socketToHandle sock ReadWriteMode
  hSetBuffering hdl NoBuffering

  lo_ref <- newIORef Nothing
  pure Pipe{ pipeRead = hdl, pipeWrite = hdl, pipeLeftovers = lo_ref }

openSocket :: PortNumber -> IO Socket
openSocket port = do
  sock <- socket AF_INET Stream 0
  setSocketOption sock ReuseAddr 1
  bind sock (SockAddrInet port iNADDR_ANY)
  listen sock 1
  return sock

acceptSocket :: Socket -> IO Socket
acceptSocket = fmap fst . accept