{- Copyright 2017 Joey Hess - - Licensed under the GNU AGPL version 3 or higher. -} {-# LANGUAGE DeriveGeneric #-} -- | debug-me session control unix socket module ControlSocket where import Types import DotDir import JSON import System.IO import System.Posix import System.FilePath import Control.Concurrent.Async import Control.Concurrent.STM import Control.Concurrent.STM.TMChan import Control.Exception import qualified Network.Socket as S import qualified Data.ByteString.Lazy as L import Data.Char data ControlInput = ControlInputAction ControlAction | ControlWindowMessage String deriving (Show, Generic) data ControlOutput = ControlOutputAction ControlAction | ControlWindowOpened | ControlWindowRequestedImmediateQuit deriving (Show, Generic) instance ToJSON ControlInput instance FromJSON ControlInput instance ToJSON ControlOutput instance FromJSON ControlOutput defaultSocketFile :: IO FilePath defaultSocketFile = ( "control") <$> dotDir bindSocket :: FilePath -> IO S.Socket bindSocket socketfile = do -- Delete any existing socket file. _ <- try (removeLink socketfile) :: IO (Either IOException ()) soc <- S.socket S.AF_UNIX S.Stream S.defaultProtocol S.bind soc (S.SockAddrUnix socketfile) setFileMode socketfile (unionFileModes ownerWriteMode ownerReadMode) S.listen soc 2 return soc -- | Serve connections to the control socket, feeding data between it and -- the TMChans. -- -- Returns once either of the TMChans is closed. serveControlSocket :: S.Socket -> TMChan ControlInput -> TMChan ControlOutput -> IO () serveControlSocket soc ichan ochan = do _ <- serve `race` waitclose return () where serve = do (sconn, _) <- S.accept soc conn <- S.socketToHandle sconn ReadWriteMode hSetBinaryMode conn True _ <- async $ sendToConn conn ichan `race` receiveFromConn conn ochan serve waitclose = atomically $ do ic <- isClosedTMChan ichan oc <- isClosedTMChan ochan if ic || oc then return () else retry -- | Connects to the control socket and feeds data between it and the -- TMChans. -- -- Returns when the socket server exits or the TMChan ControlInput is -- closed. connectControlSocket :: FilePath -> TMChan ControlInput -> TMChan ControlOutput -> IO () connectControlSocket socketfile ichan ochan = bracket setup cleanup connected where setup = do soc <- S.socket S.AF_UNIX S.Stream S.defaultProtocol S.connect soc (S.SockAddrUnix socketfile) conn <- S.socketToHandle soc ReadWriteMode hSetBinaryMode conn True return conn cleanup conn = do hClose conn atomically $ do closeTMChan ichan closeTMChan ochan connected conn = do _ <- sendToConn conn ochan `race` receiveFromConn conn ichan return () sendToConn :: ToJSON a => Handle -> TMChan a -> IO () sendToConn conn chan = go =<< atomically (readTMChan chan) where go Nothing = return () go (Just v) = do L.hPut conn (encode v) hPutStr conn "\n" hFlush conn sendToConn conn chan receiveFromConn :: FromJSON a => Handle -> TMChan a -> IO () receiveFromConn conn chan = withLines conn go where go [] = return () go (l:ls) | L.null l = go ls | otherwise = case decode l of Nothing -> error "internal control message parse error" Just v -> do atomically $ writeTMChan chan v go ls withLines :: Handle -> ([L.ByteString] -> IO a) -> IO a withLines conn a = do ls <- L.split nl <$> L.hGetContents conn a ls where nl = fromIntegral (ord '\n')