module IHP.IDE.PortConfig ( PortConfig (..) , defaultAppPort , findAvailablePortConfig , isPortAvailable ) where import ClassyPrelude import qualified Network.Socket as Socket import qualified UnliftIO.Exception as Exception import Foreign.C.Error (Errno (..), eCONNREFUSED) import Control.Exception.Safe (IOException(..)) import GHC.IO.Exception (ioe_errno) import IHP.FrameworkConfig (defaultPort) -- | Port configuration used for starting the different app services data PortConfig = PortConfig { appPort :: !Socket.PortNumber , toolServerPort :: !Socket.PortNumber } deriving (Show, Eq) defaultAppPort :: Socket.PortNumber defaultAppPort = fromIntegral defaultPort allPorts :: PortConfig -> [Socket.PortNumber] allPorts PortConfig { .. } = [appPort, toolServerPort] instance Enum PortConfig where fromEnum PortConfig { .. } = fromIntegral $ toInteger (appPort - defaultAppPort) toEnum i = PortConfig { .. } where port = fromIntegral i appPort = port + defaultAppPort toolServerPort = port + defaultAppPort + 1 -- | Returns True when the given port looks to be free. -- Used to e.g. detect which port the dev server should use. isPortAvailable :: Socket.PortNumber -> IO Bool isPortAvailable port = do let address = Socket.SockAddrInet port (Socket.tupleToHostAddress (127, 0, 0, 1)) Exception.bracket (Socket.socket Socket.AF_INET Socket.Stream 6) Socket.close' $ \socket -> do res <- Exception.try (Socket.connect socket address) case res of Left e -> if (Errno <$> ioe_errno e) == Just eCONNREFUSED then pure True else throwIO e Right _ -> pure False -- | Returns True when all ports in port config are available. -- -- Example: -- -- >>> let portConfig = PortConfig { appPort = 8000, toolServerPort = 8001 } -- >>> isPortConfigAvailable portConfig -- True isPortConfigAvailable :: PortConfig -> IO Bool isPortConfigAvailable portConfig = do available <- mapM isPortAvailable (allPorts portConfig) pure (and available) -- | Returns a port config where all ports are available -- -- When e.g. port 8000 and 8001 are not used: -- -- >>> portConfig <- findAvailablePortConfig -- PortConfig { appPort = 8000, toolServerPort = 8001 } findAvailablePortConfig :: IO PortConfig findAvailablePortConfig = do let portConfigs :: [PortConfig] = take 100 (map toEnum [0..]) go portConfigs where go (portConfig : rest) = do available <- isPortConfigAvailable portConfig if available then pure portConfig else go rest go [] = error "findAvailablePortConfig: No port configuration found"