packages feed

network-3.2.10.0: Network/Socket/Win32/Cancel.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}

#include "HsNetDef.h"

-- | Giving up on an operation on a socket, on Windows.
--
--   There is no readiness to wait for here -- completion ports report
--   that an operation has finished, not that one could be started -- so
--   the operation is run on a thread of its own and ended with
--   @CancelIoEx@.  Nothing else would end it: an asynchronous exception
--   does not reach a thread blocked in a socket call on Windows.
--
--   __These need the threaded runtime.__  The operation blocks in a
--   foreign call, which without @-threaded@ stops every other thread,
--   including the one which would cancel it.
module Network.Socket.Win32.Cancel (
    windowsViaSTM,
    windowsCancelPendingIo,
) where

import Control.Concurrent (forkIO)
import Control.Concurrent.STM
import qualified Control.Exception as E
import Foreign.Ptr (nullPtr)
import System.IO (hPutStrLn, stderr)

import Network.Socket.Imports
import Network.Socket.Types

-- | Running a blocking operation on the socket in a thread of its own
--   and handing its result over through STM, together with an action
--   cancelling it.  If the operation fails, the 'STM' action rethrows
--   the exception.
--
--   Cancelling waits for the operation to end, so a buffer it reads
--   into has to stay alive until either the 'STM' action completes or
--   the cancel returns.
windowsViaSTM :: Socket -> IO a -> IO (STM a, IO ())
windowsViaSTM s act = do
    var <- newTVarIO Nothing
    -- Masked so that the var is written whatever happens: a thread
    -- which died between the call returning and the write would leave
    -- the waiter retrying and the cancel below looping for ever.  The
    -- call itself is a foreign one, which no exception reaches anyway.
    tid <- forkIO $ E.mask_ $ E.try act >>= atomically . writeTVar var . Just
    let wait =
            readTVar var >>= \case
                Nothing -> retry
                Just (Left e) -> throwSTM (e :: E.SomeException)
                Just (Right x) -> return x
        ended = isJust <$> readTVar var
    return (wait, cancel s ended tid)
  where
    -- An asynchronous exception does not reach a thread blocked in a
    -- socket call here, so the call itself is cancelled.  CancelIoEx
    -- reaches an operation which is already in the kernel, and a call
    -- which has not got there yet is caught by trying again.  The
    -- socket is never shut down to end the call: the caller is entitled
    -- to keep using it, and a shutdown would take that away.
    --
    -- Uninterruptible because this must not be abandoned: the operation
    -- is still writing into the caller's buffer until it ends, and
    -- nothing but the end of it stops the loop.
    cancel sock ended _tid = E.uninterruptibleMask_ $ loop sayAfter
      where
        loop n = do
            windowsCancelPendingIo sock
            over <- endedWithin 1000
            unless over $ do
                when (n == 1) say
                loop (n - 1)
        -- Wait for the call to end rather than sleep through it:
        -- CancelIoEx usually works at once, and a sleep here costs a
        -- whole timer tick, which Windows measures in tens of ms.
        endedWithin us = do
            late <- registerDelay us
            atomically $
                (True <$ (check =<< ended))
                    `orElse` (False <$ (check =<< readTVar late))
    -- Rounds before saying something and carrying on.  A round which
    -- cancels nothing costs a timer tick, which Windows measures in tens
    -- of milliseconds, so this is seconds rather than the hundredth of a
    -- second it reads as.  Two rounds is the most ever measured.
    sayAfter :: Int
    sayAfter = 100
    -- Said from a thread of its own, and its failure ignored.  This is
    -- the path which has to finish before the caller's buffer can go,
    -- and writing to stderr can block on the handle's lock or on a pipe
    -- nobody is draining -- for ever, inside the mask above.
    say = void $ forkIO $ void (E.try warn :: IO (Either E.SomeException ()))
    warn =
        hPutStrLn stderr $
            "network: a socket operation given up on is not cancelling; "
                ++ "still trying.  Please report this."

-- | Cancelling whatever operation is pending on the socket, while
--   leaving the socket itself usable: unlike 'Network.Socket.shutdown'
--   this does not shut it down.  An operation which has not reached the
--   kernel yet is not cancelled, so the caller has to be ready to try
--   again.
windowsCancelPendingIo :: Socket -> IO ()
windowsCancelPendingIo s = void $ withFdSocket s $ \fd -> c_CancelIoEx fd nullPtr

-- BOOL CancelIoEx(HANDLE hFile, LPOVERLAPPED lpOverlapped)
-- A SOCKET is a kernel HANDLE.  A NULL lpOverlapped cancels all
-- pending I/O on the handle, whichever thread issued it.
foreign import CALLCONV unsafe "CancelIoEx"
  c_CancelIoEx :: CInt -> Ptr () -> IO CInt