packages feed

winio-0.1: System/Windows/IO.hs

{-# LANGUAGE ForeignFunctionInterface #-}
{- |
   Module      : System.Windows.IO
   Copyright   : 2007-2009 Felix Martini
   License     : BSD-style (see LICENSE for license terms)

   Maintainer  : felix@felixmartini.com
   Stability   : Experimental

   Low-level I/O functions using the Windows API.
-}

module System.Windows.IO
    ( HANDLE
    , ShareMode(..)
    
    , openFile
    , openTempFile
    , getFileSize
    , setFileSize
    , getPosition
    , setPosition

    , associateWithPort
    , associateSocketWithPort

    , read
    , write
    , send
    , sendTo
    , recv
    , recvFrom
    , readBS
    , writeBS
    , sendBS
    , sendToBS
    , recvBS
    , recvFromBS
    
    , closeHandle
    ) where

import Prelude hiding (read)

import Control.Concurrent (ThreadId)
import Control.Concurrent.MVar (MVar)
import Control.Monad (when)
import Data.Bits ((.|.))
import Data.ByteString.Internal (ByteString(..))
import Data.Int (Int64)
import Data.IORef (IORef)
import Data.Word (Word8, Word32, Word64)
import Foreign.C.String (CWString)
import Foreign.C.Types (CInt)
import Foreign.Ptr (Ptr)
import Foreign.StablePtr (StablePtr)
import Network.Socket (Socket(..), SockAddr)
import System.IO (IOMode(..), SeekMode(..), FilePath)
import System.IO.Unsafe (unsafePerformIO)
import System.Windows.Error

import qualified Control.Concurrent as Concur
import qualified Control.Concurrent.MVar as MVar
import qualified Control.Exception.Extensible as Ex
import qualified Data.ByteString.Internal as B
import qualified Data.IORef as IORef
import qualified Network.Socket as Socket
import qualified Network.Socket.Internal as Socket
import qualified Foreign.C.String as CString
import qualified Foreign.Ptr as Ptr
import qualified Foreign.ForeignPtr as ForeignPtr
import qualified Foreign.Marshal as Marshal
import qualified Foreign.StablePtr as StablePtr
import qualified Foreign.Storable as Storable
import qualified System.IO.Error as IOErr

-- -------------
-- General types

-- | A Windows handle.
type HANDLE = Ptr ()

-- | File share mode. Determines if other processes can access the
-- open file.
data ShareMode = ShareRead
               | ShareWrite
               | ShareReadWrite
               | NotShared
    deriving (Show)

-- ---------------------
-- Windows API constants

-- File pointer
fileBegin   = 0 :: Word32
fileCurrent = 1 :: Word32
fileEnd     = 2 :: Word32

-- Access rights
genericRead    = 0x80000000 :: Word32
genericWrite   = 0x40000000 :: Word32
genericExecute = 0x20000000 :: Word32
genericAll     = 0x10000000 :: Word32
fileAppendData = 0x00000004 :: Word32

-- Share modes
fileShareRead   = 0x00000001 :: Word32
fileShareWrite  = 0x00000002 :: Word32
fileShareDelete = 0x00000004 :: Word32

-- Attributes and flags
fileAttributeReadonly  = 0x00000001 :: Word32
fileAttributeNormal    = 0x00000080 :: Word32
fileAttributeTemporary = 0x00000100 :: Word32
fileFlagOverlapped     = 0x40000000 :: Word32

-- Creation disposition
createNew        = 1 :: Word32
createAlways     = 2 :: Word32
openExisting     = 3 :: Word32
openAlways       = 4 :: Word32
truncateExisting = 5 :: Word32

-- Infinite timeout
infinite :: Word32
infinite = 0xFFFFFFFF

-- --------------
-- File functions

-- | Open a file. The share mode determines if other processes can
-- access the open file.
openFile :: FilePath -> IOMode -> ShareMode -> IO HANDLE
openFile filepath iomode sharemode = do
    let (access, create) =
            case iomode of
                ReadMode      -> (genericRead, openExisting)
                WriteMode     -> (genericWrite, createAlways)
                ReadWriteMode -> (genericRead .|. genericWrite, openAlways)
                AppendMode    -> (fileAppendData, openAlways)
    let share =
            case sharemode of
                ShareRead      -> fileShareRead
                ShareWrite     -> fileShareWrite
                ShareReadWrite -> fileShareRead .|. fileShareWrite
                NotShared      -> 0
    hdl <- CString.withCWString filepath $ \fp ->
        throwWinErrorIfInvalidHandle $
            c_CreateFile fp
                         access
                         share
                         Ptr.nullPtr
                         create
                         fileFlagOverlapped
                         Ptr.nullPtr
    associateWithPort hdl
    return hdl

-- | Get the file size.
getFileSize :: HANDLE -> IO Int64
getFileSize hdl = Marshal.alloca $ \s -> do
    throwWinErrorIfFalse $ c_GetFileSize hdl s
    Storable.peek s

-- | Set a new file size.
setFileSize :: HANDLE -> Int64 -> IO ()
setFileSize hdl s = do
    fp <- getPosition hdl
    setPosition hdl s AbsoluteSeek
    throwWinErrorIfFalse $ c_SetEndOfFile hdl
    setPosition hdl fp AbsoluteSeek

-- | Get the current file position.
getPosition :: HANDLE -> IO Int64
getPosition hdl = Marshal.alloca $ \pos -> do
    throwWinErrorIfFalse $ c_GetFilePointer hdl pos
    Storable.peek pos

-- | Set a new file position.
setPosition :: HANDLE -> Int64 -> SeekMode -> IO ()
setPosition hdl offset mode = do
    let method = case mode of
                  AbsoluteSeek -> fileBegin
                  RelativeSeek -> fileCurrent
                  SeekFromEnd -> fileEnd
    throwWinErrorIfFalse $ c_SetFilePointer hdl offset method

-- ---------------
-- Temporary Files

-- Maximum allowed path length.
maxPath :: Int
maxPath = 260

-- | Get the default temporary directory.
getTempPath :: IO FilePath
getTempPath =
    -- 16-bit chars
    Marshal.allocaBytes (fromIntegral (2 * maxPath)) $ \tmp_path -> do
        throwWinErrorIf (== 0) $
            c_GetTempPath (fromIntegral maxPath) tmp_path
        CString.peekCWString tmp_path

-- | Get a unique temporary file name. No more than three characters of
-- the prefix string are used. An empty prefix string is allowed.
getTempFileName :: FilePath -> String -> IO FilePath
getTempFileName dir prefix = do
    when (length dir > maxPath - 14) $
        ioError $ IOErr.mkIOError
            IOErr.illegalOperationErrorType
            "System.Windows.IO.getTempFileName: \
            \Path length is too long."
            Nothing (Just dir)
    dir' <- if null dir then getTempPath else return dir
    let prefix3 = take 3 prefix
    Marshal.allocaBytes (2 * maxPath) $ \filename ->
        CString.withCWString dir' $ \d ->
            CString.withCWString prefix3 $ \p3 -> do
                throwWinErrorIf (== 0) $
                    c_GetTempFileName d p3 0 filename
                CString.peekCWString filename

-- | Open a temporary file with a filename prefix. If the directory
-- path is an empty string then the default temporary directory will be
-- used. No more than three characters of the prefix string are used.
-- An empty prefix string is allowed.
openTempFile :: FilePath -> String -> IO (FilePath, HANDLE)
openTempFile dir prefix = do
    filename <- getTempFileName dir prefix
    hdl <- CString.withCWString filename $ \file -> do
        throwWinErrorIfInvalidHandle $
            c_CreateFile file
                         (genericRead .|. genericWrite)
                         0
                         Ptr.nullPtr
                         createAlways
                         (fileFlagOverlapped .|. fileAttributeTemporary)
                         Ptr.nullPtr
    return (filename, hdl)

-- -------------------------
-- I/O manager (threaded RTS)

-- | I/O completion port.
completionPort :: IORef HANDLE
{-# NOINLINE completionPort #-}
completionPort = unsafePerformIO $ do
    hdl <- throwWinErrorIfInvalidHandle (c_NewCompletionPort 1)
    startIOManager hdl
    IORef.newIORef hdl

-- | Associate a Windows handle with the I/O completion port.
associateWithPort :: HANDLE -> IO ()
associateWithPort hdl = do
    port <- IORef.readIORef completionPort
    throwWinErrorIfFalse (c_AssociateHandleWithPort hdl port)

-- | Associate a socket with the I/O completion port.
associateSocketWithPort :: Socket -> IO ()
associateSocketWithPort sock = do
    port <- IORef.readIORef completionPort
    throwWinErrorIfFalse $
        c_AssociateSocketWithPort (Socket.fdSocket sock) port

-- | Start the IO manager.
startIOManager :: HANDLE -> IO ThreadId
startIOManager port = Concur.forkIO (handleCompletions port)

-- | Handle completed I/O actions obtained from an I/O completion port.
handleCompletions :: HANDLE -> IO ()
handleCompletions port = do
    Marshal.alloca $ \num_bytes_ptr -> Marshal.alloca $ \userdata_ptr -> do
        res <- c_GetCompletion port num_bytes_ptr userdata_ptr infinite
        err <- if not res then getWinError else return errorSuccess
        
        if userdata_ptr == Ptr.nullPtr
          then Ex.throwIO err
          else do
            mvar_stable_ptr <- Storable.peek userdata_ptr
            mvar <- StablePtr.deRefStablePtr mvar_stable_ptr
            if err /= errorSuccess
              then do
                success <- MVar.tryPutMVar mvar (Left err)
                when (not success) $ fail
                    "System.Windows.IO.handleCompletions: \
                    \MVar is not empty."
              else do
                num_bytes <-
                    fmap fromIntegral $! Storable.peek num_bytes_ptr :: IO Int
                success <- MVar.tryPutMVar mvar (Right num_bytes)
                when (not success) $ fail
                    "System.Windows.IO.handleCompletions: \
                    \MVar is not empty."
    handleCompletions port

-- ---------------
-- Asynchronous I/O

errorIOPending :: WinError
errorIOPending = WinError 997

waitForCompletion :: (StablePtr (MVar (Either WinError Int)) -> IO CInt)
                  -> IO Int
waitForCompletion async_op = do
    wait_var <- MVar.newEmptyMVar
    Ex.bracket (StablePtr.newStablePtr wait_var) StablePtr.freeStablePtr $
               \wait_var_ptr -> do
        res <- async_op wait_var_ptr
        if res == (-1)
          then do
            err <- getWinError
            if err == errorIOPending
              then do
                -- The operation is initiated asynchronously.
                -- We wait for the completion of the operation.
                res' <- MVar.takeMVar wait_var
                case res' of
                    Right num_bytes -> return num_bytes
                    Left e | e == errorHandleEOF -> return 0
                           | otherwise -> Ex.throwIO e
              else
                -- An error occurred.
                if err == errorHandleEOF then return 0 else Ex.throwIO err
          else
            -- The operation is completed synchronously.
            return $ fromIntegral res

-- | Read bytes from a handle into a buffer. The actual number of bytes
-- read depends on the handle and may be less than the number of bytes
-- requested. The function returns zero if the end of input has been
-- reached.
read :: HANDLE -> Int64 -> Ptr Word8 -> Int -> IO Int
read hdl pos buf buf_size =
    waitForCompletion $
        c_AsyncRead hdl (fromIntegral pos) buf (fromIntegral buf_size)

-- | Write bytes from a buffer to a handle.
write :: HANDLE -> Int64 -> Ptr Word8 -> Int -> IO Int
write hdl pos buf buf_size =
    waitForCompletion $
        c_AsyncWrite hdl (fromIntegral pos) buf (fromIntegral buf_size)

-- | Send bytes from a buffer.
send :: Socket -> Ptr Word8 -> Int -> IO Int
send (MkSocket sd _ _ _ _) buf buf_size =
    waitForCompletion $
        c_AsyncSend sd buf (fromIntegral buf_size)

-- | Send bytes from a buffer to a remote address.
sendTo :: Socket -> Ptr Word8 -> Int -> SockAddr -> IO Int
sendTo (MkSocket sd _ _ _ _) buf buf_size addr =
    let addr_size = Socket.sizeOfSockAddr addr in
    Marshal.allocaBytes addr_size $ \addr_ptr -> do
        Socket.pokeSockAddr addr_ptr addr
        waitForCompletion $
            c_AsyncSendTo sd buf (fromIntegral buf_size)
                          addr_ptr (fromIntegral addr_size)

-- | Receive bytes from a socket into a buffer. The actual number of
-- bytes received may be less than the number of bytes requested. The
-- function returns zero if the end of input has been reached.
recv :: Socket -> Ptr Word8 -> Int -> IO Int
recv (MkSocket sd _ _ _ _) buf buf_size  =
    waitForCompletion $
        c_AsyncRecv sd buf (fromIntegral buf_size)

-- | Receive bytes from a socket into a buffer. The actual number of
-- bytes received may be less than the number of bytes requested. The
-- function returns zero if the end of input has been reached.
recvFrom :: Socket -> Ptr a -> Int -> IO (Int, SockAddr)
recvFrom sock@(MkSocket sd family _ _ _) buf buf_size =
    let addr_size = Socket.sizeOfSockAddrByFamily family in
    Marshal.allocaBytes addr_size $ \addr_ptr ->
        Marshal.alloca $ \addr_size_ptr -> do
            Storable.poke addr_size_ptr (fromIntegral addr_size)
            num <- waitForCompletion $
                c_AsyncRecvFrom sd buf (fromIntegral buf_size) addr_ptr
                                addr_size_ptr
            -- N.B. WSARecvFrom does not provide the source address for
            -- connection-oriented sockets.
            connected <- Socket.sIsConnected sock
            maybe_addr <- if connected
                              then Socket.getPeerName sock
                              else Socket.peekSockAddr addr_ptr
            return (num, maybe_addr)

-- --------------------------
-- strict ByteString versions

-- | Read bytes from a handle into a strict ByteString. The actual
-- number of bytes read depends on the handle and may be less
-- than the number of bytes requested. The function returns an empty
-- ByteString if the end of input has been reached.
readBS :: HANDLE -> Int64 -> Int -> IO ByteString
readBS hdl pos num_bytes =
    B.createAndTrim num_bytes
        (\buf -> read hdl pos buf num_bytes)

-- | Write bytes from a strict ByteString to a handle.
writeBS :: HANDLE -> Int64 -> ByteString -> IO Int
writeBS hdl pos (PS fp off len) =
    ForeignPtr.withForeignPtr fp $ \p ->
        write hdl pos (p `Ptr.plusPtr` off) len

-- | Send bytes from a strict ByteString.
sendBS :: Socket -> ByteString -> IO Int
sendBS sock (PS fp off len) =
    ForeignPtr.withForeignPtr fp $ \p ->
        send sock (p `Ptr.plusPtr` off) len

-- | Send bytes from a strict ByteString to a remote address.
sendToBS :: Socket -> ByteString -> SockAddr -> IO Int
sendToBS sock (PS fp off len) addr =
    ForeignPtr.withForeignPtr fp $ \p ->
        sendTo sock (p `Ptr.plusPtr` off) len addr

-- | Receive bytes into a strict ByteString. The actual number of bytes
-- received may be less than the number of bytes requested. The
-- function returns an empty ByteString if the end of input has been
-- reached.
recvBS :: Socket -> Int -> IO ByteString
recvBS sock num_bytes =
    B.createAndTrim num_bytes (\buf -> recv sock buf num_bytes)

-- | Receive bytes into a strict ByteString. The actual number of bytes
-- received may be less than the number of bytes requested. The
-- function returns an empty ByteString if the end of input has been
-- reached.
recvFromBS :: Socket -> Int -> IO (ByteString, SockAddr)
recvFromBS sock@(MkSocket sd family _ _ _) num_bytes =
    B.createAndTrim' num_bytes $ \buf -> do
        (num_recvd, addr) <- recvFrom sock buf num_bytes
        return (0, num_recvd, addr)
            
-- -----------------------
-- Miscellaneous functions

-- | Close a Windows handle.
closeHandle :: HANDLE -> IO ()
closeHandle hdl = throwWinErrorIfFalse $ c_CloseHandle hdl

-- -----------------
-- Foreign functions

foreign import stdcall unsafe "CloseHandle"
    c_CloseHandle :: HANDLE -> IO Bool

foreign import ccall unsafe "HsNewCompletionPort"
    c_NewCompletionPort :: Word32 -> IO HANDLE

foreign import ccall unsafe "HsAssociateHandleWithPort"
    c_AssociateHandleWithPort :: HANDLE -> HANDLE -> IO Bool

foreign import ccall unsafe "HsAssociateSocketWithPort"
    c_AssociateSocketWithPort :: CInt -> HANDLE -> IO Bool

foreign import ccall safe "HsGetCompletion"
    c_GetCompletion :: HANDLE -> Ptr Word32 -> Ptr (StablePtr a)
                    -> Word32 -> IO Bool

foreign import ccall unsafe "HsAsyncRead"
    c_AsyncRead :: HANDLE -> Word64 -> Ptr a -> CInt -> StablePtr b
                -> IO CInt

foreign import ccall unsafe "HsAsyncWrite"
    c_AsyncWrite :: HANDLE -> Word64 -> Ptr a -> CInt -> StablePtr b
                 -> IO CInt

foreign import ccall unsafe "HsAsyncRecv"
    c_AsyncRecv :: CInt -> Ptr a -> CInt -> StablePtr b -> IO CInt

foreign import ccall unsafe "HsAsyncRecvFrom"
    c_AsyncRecvFrom :: CInt -> Ptr a -> CInt  -> Ptr SockAddr
                    -> Ptr CInt -> StablePtr b -> IO CInt

foreign import ccall unsafe "HsAsyncSend"
    c_AsyncSend :: CInt -> Ptr a -> CInt -> StablePtr b -> IO CInt

foreign import ccall unsafe "HsAsyncSendTo"
    c_AsyncSendTo :: CInt -> Ptr a -> CInt -> Ptr SockAddr
                  -> CInt -> StablePtr b -> IO CInt

foreign import stdcall unsafe "CreateFileW"
    c_CreateFile :: CWString -> Word32 -> Word32 -> Ptr ()
                 -> Word32 -> Word32 -> HANDLE -> IO HANDLE

foreign import ccall unsafe "HsGetFileSize"
    c_GetFileSize :: HANDLE -> Ptr Int64 -> IO Bool

foreign import stdcall unsafe "SetEndOfFile"
    c_SetEndOfFile :: HANDLE -> IO Bool

foreign import ccall unsafe "HsGetFilePointer"
    c_GetFilePointer :: HANDLE -> Ptr Int64 -> IO Bool

foreign import ccall unsafe "HsSetFilePointer"
    c_SetFilePointer :: HANDLE -> Int64 -> Word32 -> IO Bool

foreign import stdcall unsafe "GetTempPathW"
    c_GetTempPath :: Word32 -> CWString -> IO Word32

foreign import stdcall unsafe "GetTempFileNameW"
    c_GetTempFileName :: CWString -> CWString -> Word32 -> CWString
                      -> IO Word32