haskell-awk-1.0: src/System/Console/Hawk/Lock.hs
-- Copyright 2013 Mario Pastorelli (pastorelli.mario@gmail.com) Samuel Gélineau (gelisam@gmail.com)
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
module System.Console.Hawk.Lock
( withLock
, withTestLock
) where
import Control.Concurrent ( threadDelay )
import Control.Exception
import Control.Monad ( when, guard )
import Data.List ( elemIndex )
import GHC.IO.Exception
import GHC.IO.Handle ( Handle, hGetContents, hClose )
import Network.BSD ( getProtocolNumber ) -- still cross-platform, don't let the name fool you
import Network ( PortID (..), connectTo )
import Network.Socket
import Text.Printf
-- use a socket number as a lock indicator.
withSocketLock :: Bool -> IO a -> IO a
withSocketLock testing = withSocketsDo . bracket (lock testing) unlock . const
-- make sure GHC inlines withSocketLock in the following two functions
-- and optimizes the testing and non-testing versions separately.
{-# INLINE withSocketLock #-}
withLock :: IO a -> IO a
withLock = withSocketLock False
withTestLock :: IO a -> IO a
withTestLock = withSocketLock True
type Lock = Socket
lock :: Bool -> IO Lock
lock testing = catchJust isADDRINUSE openSocket $ \() -> do
-- open failed, the lock must be in use.
when testing $ putStrLn "** LOCKED **"
-- used to test an interleaving in which the socket is closed here,
-- between openSocket and waitForException.
when testing $ threadDelay 20000
-- wait for the other instance to signal that it is done with the lock.
catchJust isDisconnected waitForException $ \reason -> do
-- we were disconnected, the server must have released the lock!
when testing $ printf "** UNLOCKED (%s) **\n" reason
-- try again.
lock testing
unlock :: Lock -> IO ()
unlock = closeSocket
waitForException :: IO a
waitForException = bracket openHandle closeHandle $ \h -> do
s <- hGetContents h
length s `seq` return () -- blocks until EOF, which never comes
-- because the server never accepted the connection
error $ printf "port %s in use by a program other than hawk" $ show portNumber
isADDRINUSE :: IOError -> Maybe ()
isADDRINUSE = guard . (== "bind") . ioe_location
isDisconnected :: IOError -> Maybe String
isDisconnected = fmap (xs !!) . indexOf . ioe_location
where
xs = ["connect", "hGetContents"]
indexOf x = elemIndex x xs
openHandle :: IO Handle
openHandle = connectTo "localhost" $ PortNumber portNumber
closeHandle :: Handle -> IO ()
closeHandle = hClose
openSocket :: IO Socket
openSocket = listenOn portNumber
closeSocket :: Socket -> IO ()
closeSocket = sClose
-- the first few [0-9] characters of the sha1 of "hawk"
portNumber :: PortNumber
portNumber = 62243
-- from the source of Network.listenTo
listenOn :: PortNumber -> IO Socket
listenOn port = do
proto <- getProtocolNumber "tcp"
bracketOnError
(socket AF_INET Stream proto)
(sClose)
(\sock -> do
-- unlike the original listenOn, we do NOT set ReuseAddr.
-- this way the call will fail if another instance holds the lock.
--setSocketOption sock ReuseAddr 1
bindSocket sock (SockAddrInet port iNADDR_ANY)
listen sock maxListenQueue
return sock
)