metro-socket-0.1.0.0: src/Metro/Socket.hs
module Metro.Socket
( Socket
, close
, listen
, connect
, getHost
, getService
-- udp
, bindTo
, getDatagramAddr
) where
import Control.Exception (bracketOnError, throwIO)
import Control.Monad (when)
import Data.List (isPrefixOf)
import Data.Maybe (listToMaybe)
import Network.Socket hiding (bind, connect, listen)
import qualified Network.Socket as S (bind, connect, listen)
import System.Directory (doesFileExist, removeFile)
import System.Exit (exitFailure)
import UnliftIO (tryIO)
-- Returns the first action from a list which does not throw an exception.
-- If all the actions throw exceptions (and the list of actions is not empty),
-- the last exception is thrown.
-- The operations are run outside of the catchIO cleanup handler because
-- catchIO masks asynchronous exceptions in the cleanup handler.
-- In the case of complete failure, the last exception is actually thrown.
firstSuccessful :: [IO a] -> IO a
firstSuccessful = go Nothing
where
-- Attempt the next operation, remember exception on failure
go _ (p:ps) =
do r <- tryIO p
case r of
Right x -> return x
Left e -> go (Just e) ps
-- All operations failed, throw error if one exists
go Nothing [] = error "firstSuccessful: empty list"
go (Just e) [] = throwIO e
connectTo :: Maybe HostName -> Maybe ServiceName -> IO Socket
connectTo host serv = do
let hints = defaultHints { addrFlags = [AI_ADDRCONFIG]
, addrSocketType = Stream }
addrs <- getAddrInfo (Just hints) host serv
firstSuccessful $ map tryToConnect addrs
where
tryToConnect addr =
bracketOnError
(socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))
close -- only done if there's an error
(\sock -> do
S.connect sock (addrAddress addr)
return sock
)
connectToFile :: FilePath -> IO Socket
connectToFile path =
bracketOnError
(socket AF_UNIX Stream 0)
close
(\sock -> do
S.connect sock (SockAddrUnix path)
return sock
)
listenOnFile :: FilePath -> IO Socket
listenOnFile path =
bracketOnError
(socket AF_UNIX Stream 0)
close
(\sock -> do
setSocketOption sock ReuseAddr 1
S.bind sock (SockAddrUnix path)
S.listen sock maxListenQueue
return sock
)
listenOn :: Maybe HostName -> Maybe ServiceName -> IO Socket
listenOn host serv = do
-- We should probably specify addrFamily = AF_INET6 and the filter
-- code below should be removed. AI_ADDRCONFIG is probably not
-- necessary. But this code is well-tested. So, let's keep it.
let hints = defaultHints { addrFlags = [AI_ADDRCONFIG, AI_PASSIVE]
, addrSocketType = Stream
}
addrs <- getAddrInfo (Just hints) host serv
-- Choose an IPv6 socket if exists. This ensures the socket can
-- handle both IPv4 and IPv6 if v6only is false.
let addrs' = filter (\x -> addrFamily x == AF_INET6) addrs
addr = if null addrs' then head addrs else head addrs'
bracketOnError
(socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))
close
(\sock -> do
setSocketOption sock ReuseAddr 1
setSocketOption sock NoDelay 1
S.bind sock (addrAddress addr)
S.listen sock maxListenQueue
return sock
)
listen :: String -> IO Socket
listen port =
if "tcp" `isPrefixOf` port then
listenOn (getHost port) (getService port)
else do
let sockFile = dropS port
exists <- doesFileExist sockFile
when exists $ do
e <- tryIO $ connectToFile sockFile
case e of
Left _ -> removeFile sockFile
Right _ -> do
putStrLn "periodicd: bind: resource busy (Address already in use)"
exitFailure
listenOnFile sockFile
connect :: String -> IO Socket
connect h | "tcp" `isPrefixOf` h = connectTo (getHost h) (getService h)
| otherwise = connectToFile (dropS h)
getDatagramAddrList :: String -> IO [AddrInfo]
getDatagramAddrList hostPort = getAddrInfo (Just hints) host port
where hints = defaultHints
{ addrFlags = [AI_PASSIVE]
, addrSocketType = Datagram
}
host = getHost hostPort
port = getService hostPort
getDatagramAddr :: String -> IO (Maybe AddrInfo)
getDatagramAddr hostPort = listToMaybe <$> getDatagramAddrList hostPort
bindTo :: String -> IO Socket
bindTo hostPort = do
addrs <- getDatagramAddrList hostPort
firstSuccessful $ map tryToConnect addrs
where
tryToConnect addr =
bracketOnError
(socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))
close -- only done if there's an error
(\sock -> do
setSocketOption sock ReuseAddr 1
S.bind sock $ addrAddress addr
return sock
)
-- ipv6 fe80::1046:372a:8c3b:94b8%en0:80
countColon :: String -> Int
countColon = length . filter (==':')
-- ipv6 fe80::1046:372a:8c3b:94b8%en0:80
-- ipv6 fe80::1046:372a:8c3b:94b8%en0
-- ipv4 127.0.0.1:80
-- ipv4 127.0.0.1
-- only port :80
splitHostPort :: String -> (Maybe String, Maybe String)
splitHostPort hostPort
| colon == 0 = (Just hostPort, Nothing)
| colon == 1 = (takeFst id hostPort, takeSnd id hostPort)
| colon == 5 = (Just hostPort, Nothing)
| colon == 6 = (takeSnd reverse hostPort, takeFst reverse hostPort )
| otherwise = (Nothing, Nothing)
where colon = countColon hostPort
takeFst f = toMaybe . f . takeWhile (/=':') . f
takeSnd f = toMaybe . f . drop 1 . dropWhile (/=':') . f
dropS :: String -> String
dropS = drop 3 . dropWhile (/= ':')
toMaybe :: String -> Maybe String
toMaybe [] = Nothing
toMaybe xs = Just xs
getHost :: String -> Maybe String
getHost = fst . splitHostPort . dropS
getService :: String -> Maybe String
getService = snd . splitHostPort . dropS