Skip to content

Commit a4a0464

Browse files
committed
Merge branch 'cloexec'
2 parents d2b5ed2 + 8296101 commit a4a0464

File tree

8 files changed

+372
-222
lines changed

8 files changed

+372
-222
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Revision history for file-io
22

3+
## 0.1.2 -- 2024-05-27
4+
5+
* expose internals via `.Internal` modules
6+
* add `openFileWithCloseOnExec` and `openExistingFileWithCloseOnExec` to `.Internal` modules wrt [#21](https://github.com/hasufell/file-io/issues/21)
7+
38
## 0.1.1 -- 2024-01-20
49

510
* fix a severe bug on windows, where `readFile` may create a missing file, wrt [#14](https://github.com/hasufell/file-io/issues/14)

System/File/OsPath.hs

Lines changed: 12 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
{-# LANGUAGE TypeApplications #-}
2-
{-# LANGUAGE BangPatterns #-}
1+
{- |
2+
Module : System.File.OsPath
3+
Copyright : (c) Julian Ospald 2023-2024
4+
License : BSD3
35
6+
Maintainer : [email protected]
7+
Stability : stable
8+
Portability : portable
9+
10+
This module mimics base API wrt file IO, but using 'OsPath'.
11+
-}
412
module System.File.OsPath (
513
openBinaryFile
614
, withFile
@@ -18,154 +26,6 @@ module System.File.OsPath (
1826
) where
1927

2028

21-
import qualified System.File.Platform as P
22-
23-
import Prelude ((.), ($), String, IO, ioError, pure, either, const, flip, Maybe(..), fmap, (<$>), id, Bool(..), FilePath, (++), return, show, (>>=))
24-
import GHC.IO (catchException)
25-
import GHC.IO.Exception (IOException(..))
26-
import GHC.IO.Handle (hClose_help)
27-
import GHC.IO.Handle.Internals (debugIO)
28-
import GHC.IO.Handle.Types (Handle__, Handle(..))
29-
import Control.Concurrent.MVar
30-
import Control.Monad (void, when)
31-
import Control.DeepSeq (force)
32-
import Control.Exception (SomeException, try, evaluate, mask, onException)
33-
import System.IO (IOMode(..), hSetBinaryMode, hClose)
34-
import System.IO.Unsafe (unsafePerformIO)
35-
import System.OsPath as OSP
36-
import System.OsString.Internal.Types
37-
38-
import qualified Data.ByteString as BS
39-
import qualified Data.ByteString.Lazy as BSL
40-
41-
-- | Like 'openFile', but open the file in binary mode.
42-
-- On Windows, reading a file in text mode (which is the default)
43-
-- will translate CRLF to LF, and writing will translate LF to CRLF.
44-
-- This is usually what you want with text files. With binary files
45-
-- this is undesirable; also, as usual under Microsoft operating systems,
46-
-- text mode treats control-Z as EOF. Binary mode turns off all special
47-
-- treatment of end-of-line and end-of-file characters.
48-
-- (See also 'System.IO.hSetBinaryMode'.)
49-
50-
-- On POSIX systems, 'openBinaryFile' is an /interruptible operation/ as
51-
-- described in "Control.Exception".
52-
openBinaryFile :: OsPath -> IOMode -> IO Handle
53-
openBinaryFile osfp iomode = augmentError "openBinaryFile" osfp $ withOpenFile' osfp iomode True False pure False
54-
55-
56-
-- | Run an action on a file.
57-
--
58-
-- The 'Handle' is automatically closed afther the action.
59-
withFile :: OsPath -> IOMode -> (Handle -> IO r) -> IO r
60-
withFile osfp iomode act = (augmentError "withFile" osfp
61-
$ withOpenFile' osfp iomode False False (try . act) True)
62-
>>= either ioError pure
63-
64-
withBinaryFile :: OsPath -> IOMode -> (Handle -> IO r) -> IO r
65-
withBinaryFile osfp iomode act = (augmentError "withBinaryFile" osfp
66-
$ withOpenFile' osfp iomode True False (try . act) True)
67-
>>= either ioError pure
68-
69-
-- | Run an action on a file.
70-
--
71-
-- The 'Handle' is not automatically closed to allow lazy IO. Use this
72-
-- with caution.
73-
withFile'
74-
:: OsPath -> IOMode -> (Handle -> IO r) -> IO r
75-
withFile' osfp iomode act = (augmentError "withFile'" osfp
76-
$ withOpenFile' osfp iomode False False (try . act) False)
77-
>>= either ioError pure
78-
79-
withBinaryFile'
80-
:: OsPath -> IOMode -> (Handle -> IO r) -> IO r
81-
withBinaryFile' osfp iomode act = (augmentError "withBinaryFile'" osfp
82-
$ withOpenFile' osfp iomode True False (try . act) False)
83-
>>= either ioError pure
84-
85-
-- | The 'readFile' function reads a file and returns the contents of the file
86-
-- as a 'ByteString'. The file is read lazily, on demand.
87-
readFile :: OsPath -> IO BSL.ByteString
88-
readFile fp = withFile' fp ReadMode BSL.hGetContents
89-
90-
-- | The 'readFile'' function reads a file and returns the contents of the file
91-
-- as a 'ByteString'. The file is fully read before being returned.
92-
readFile'
93-
:: OsPath -> IO BS.ByteString
94-
readFile' fp = withFile fp ReadMode BS.hGetContents
95-
96-
-- | The computation 'writeFile' @file str@ function writes the lazy 'ByteString' @str@,
97-
-- to the file @file@.
98-
writeFile :: OsPath -> BSL.ByteString -> IO ()
99-
writeFile fp contents = withFile fp WriteMode (`BSL.hPut` contents)
100-
101-
-- | The computation 'writeFile' @file str@ function writes the strict 'ByteString' @str@,
102-
-- to the file @file@.
103-
writeFile'
104-
:: OsPath -> BS.ByteString -> IO ()
105-
writeFile' fp contents = withFile fp WriteMode (`BS.hPut` contents)
106-
107-
-- | The computation 'appendFile' @file str@ function appends the lazy 'ByteString' @str@,
108-
-- to the file @file@.
109-
appendFile :: OsPath -> BSL.ByteString -> IO ()
110-
appendFile fp contents = withFile fp AppendMode (`BSL.hPut` contents)
111-
112-
-- | The computation 'appendFile' @file str@ function appends the strict 'ByteString' @str@,
113-
-- to the file @file@.
114-
appendFile'
115-
:: OsPath -> BS.ByteString -> IO ()
116-
appendFile' fp contents = withFile fp AppendMode (`BS.hPut` contents)
117-
118-
-- | Open a file and return the 'Handle'.
119-
openFile :: OsPath -> IOMode -> IO Handle
120-
openFile osfp iomode = augmentError "openFile" osfp $ withOpenFile' osfp iomode False False pure False
121-
122-
123-
-- | Open an existing file and return the 'Handle'.
124-
openExistingFile :: OsPath -> IOMode -> IO Handle
125-
openExistingFile osfp iomode = augmentError "openExistingFile" osfp $ withOpenFile' osfp iomode False True pure False
126-
127-
128-
-- ---------------------------------------------------------------------------
129-
-- Internals
130-
131-
handleFinalizer :: FilePath -> MVar Handle__ -> IO ()
132-
handleFinalizer _fp m = do
133-
handle_ <- takeMVar m
134-
(handle_', _) <- hClose_help handle_
135-
putMVar m handle_'
136-
return ()
137-
138-
type HandleFinalizer = FilePath -> MVar Handle__ -> IO ()
139-
140-
-- | Add a finalizer to a 'Handle'. Specifically, the finalizer
141-
-- will be added to the 'MVar' of a file handle or the write-side
142-
-- 'MVar' of a duplex handle. See Handle Finalizers for details.
143-
addHandleFinalizer :: Handle -> HandleFinalizer -> IO ()
144-
addHandleFinalizer hndl finalizer = do
145-
debugIO $ "Registering finalizer: " ++ show filepath
146-
void $ mkWeakMVar mv (finalizer filepath mv)
147-
where
148-
!(filepath, !mv) = case hndl of
149-
FileHandle fp m -> (fp, m)
150-
DuplexHandle fp _ write_m -> (fp, write_m)
151-
152-
withOpenFile' :: OsPath -> IOMode -> Bool -> Bool -> (Handle -> IO r) -> Bool -> IO r
153-
withOpenFile' (OsString fp) iomode binary existing action close_finally = mask $ \restore -> do
154-
hndl <- if existing
155-
then P.openExistingFile fp iomode
156-
else P.openFile fp iomode
157-
addHandleFinalizer hndl handleFinalizer
158-
when binary $ hSetBinaryMode hndl True
159-
r <- restore (action hndl) `onException` hClose hndl
160-
when close_finally $ hClose hndl
161-
pure r
162-
163-
addFilePathToIOError :: String -> OsPath -> IOException -> IOException
164-
addFilePathToIOError fun fp ioe = unsafePerformIO $ do
165-
fp' <- either (const (fmap OSP.toChar . OSP.unpack $ fp)) id <$> try @SomeException (OSP.decodeFS fp)
166-
fp'' <- evaluate $ force fp'
167-
pure $ ioe{ ioe_location = fun, ioe_filename = Just fp'' }
168-
169-
augmentError :: String -> OsPath -> IO a -> IO a
170-
augmentError str osfp = flip catchException (ioError . addFilePathToIOError str osfp)
29+
import System.File.OsPath.Internal
30+
import Prelude ()
17131

System/File/OsPath/Internal.hs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
{-# LANGUAGE TypeApplications #-}
2+
{-# LANGUAGE BangPatterns #-}
3+
4+
module System.File.OsPath.Internal where
5+
6+
7+
import qualified System.File.Platform as P
8+
9+
import Prelude ((.), ($), String, IO, ioError, pure, either, const, flip, Maybe(..), fmap, (<$>), id, Bool(..), FilePath, (++), return, show, (>>=))
10+
import GHC.IO (catchException)
11+
import GHC.IO.Exception (IOException(..))
12+
import GHC.IO.Handle (hClose_help)
13+
import GHC.IO.Handle.Internals (debugIO)
14+
import GHC.IO.Handle.Types (Handle__, Handle(..))
15+
import Control.Concurrent.MVar
16+
import Control.Monad (void, when)
17+
import Control.DeepSeq (force)
18+
import Control.Exception (SomeException, try, evaluate, mask, onException)
19+
import System.IO (IOMode(..), hSetBinaryMode, hClose)
20+
import System.IO.Unsafe (unsafePerformIO)
21+
import System.OsPath as OSP
22+
import System.OsString.Internal.Types
23+
24+
import qualified Data.ByteString as BS
25+
import qualified Data.ByteString.Lazy as BSL
26+
27+
-- | Like 'openFile', but open the file in binary mode.
28+
-- On Windows, reading a file in text mode (which is the default)
29+
-- will translate CRLF to LF, and writing will translate LF to CRLF.
30+
-- This is usually what you want with text files. With binary files
31+
-- this is undesirable; also, as usual under Microsoft operating systems,
32+
-- text mode treats control-Z as EOF. Binary mode turns off all special
33+
-- treatment of end-of-line and end-of-file characters.
34+
-- (See also 'System.IO.hSetBinaryMode'.)
35+
36+
-- On POSIX systems, 'openBinaryFile' is an /interruptible operation/ as
37+
-- described in "Control.Exception".
38+
openBinaryFile :: OsPath -> IOMode -> IO Handle
39+
openBinaryFile osfp iomode = augmentError "openBinaryFile" osfp $ withOpenFile' osfp iomode True False False pure False
40+
41+
42+
-- | Run an action on a file.
43+
--
44+
-- The 'Handle' is automatically closed afther the action.
45+
withFile :: OsPath -> IOMode -> (Handle -> IO r) -> IO r
46+
withFile osfp iomode act = (augmentError "withFile" osfp
47+
$ withOpenFile' osfp iomode False False False (try . act) True)
48+
>>= either ioError pure
49+
50+
withBinaryFile :: OsPath -> IOMode -> (Handle -> IO r) -> IO r
51+
withBinaryFile osfp iomode act = (augmentError "withBinaryFile" osfp
52+
$ withOpenFile' osfp iomode True False False (try . act) True)
53+
>>= either ioError pure
54+
55+
-- | Run an action on a file.
56+
--
57+
-- The 'Handle' is not automatically closed to allow lazy IO. Use this
58+
-- with caution.
59+
withFile'
60+
:: OsPath -> IOMode -> (Handle -> IO r) -> IO r
61+
withFile' osfp iomode act = (augmentError "withFile'" osfp
62+
$ withOpenFile' osfp iomode False False False (try . act) False)
63+
>>= either ioError pure
64+
65+
withBinaryFile'
66+
:: OsPath -> IOMode -> (Handle -> IO r) -> IO r
67+
withBinaryFile' osfp iomode act = (augmentError "withBinaryFile'" osfp
68+
$ withOpenFile' osfp iomode True False False (try . act) False)
69+
>>= either ioError pure
70+
71+
-- | The 'readFile' function reads a file and returns the contents of the file
72+
-- as a 'ByteString'. The file is read lazily, on demand.
73+
readFile :: OsPath -> IO BSL.ByteString
74+
readFile fp = withFile' fp ReadMode BSL.hGetContents
75+
76+
-- | The 'readFile'' function reads a file and returns the contents of the file
77+
-- as a 'ByteString'. The file is fully read before being returned.
78+
readFile'
79+
:: OsPath -> IO BS.ByteString
80+
readFile' fp = withFile fp ReadMode BS.hGetContents
81+
82+
-- | The computation 'writeFile' @file str@ function writes the lazy 'ByteString' @str@,
83+
-- to the file @file@.
84+
writeFile :: OsPath -> BSL.ByteString -> IO ()
85+
writeFile fp contents = withFile fp WriteMode (`BSL.hPut` contents)
86+
87+
-- | The computation 'writeFile' @file str@ function writes the strict 'ByteString' @str@,
88+
-- to the file @file@.
89+
writeFile'
90+
:: OsPath -> BS.ByteString -> IO ()
91+
writeFile' fp contents = withFile fp WriteMode (`BS.hPut` contents)
92+
93+
-- | The computation 'appendFile' @file str@ function appends the lazy 'ByteString' @str@,
94+
-- to the file @file@.
95+
appendFile :: OsPath -> BSL.ByteString -> IO ()
96+
appendFile fp contents = withFile fp AppendMode (`BSL.hPut` contents)
97+
98+
-- | The computation 'appendFile' @file str@ function appends the strict 'ByteString' @str@,
99+
-- to the file @file@.
100+
appendFile'
101+
:: OsPath -> BS.ByteString -> IO ()
102+
appendFile' fp contents = withFile fp AppendMode (`BS.hPut` contents)
103+
104+
-- | Open a file and return the 'Handle'.
105+
openFile :: OsPath -> IOMode -> IO Handle
106+
openFile osfp iomode = augmentError "openFile" osfp $ withOpenFile' osfp iomode False False False pure False
107+
108+
109+
-- | Open an existing file and return the 'Handle'.
110+
openExistingFile :: OsPath -> IOMode -> IO Handle
111+
openExistingFile osfp iomode = augmentError "openExistingFile" osfp $ withOpenFile' osfp iomode False True False pure False
112+
113+
-- | Open a file and return the 'Handle'.
114+
--
115+
-- Sets @O_CLOEXEC@ on posix.
116+
--
117+
-- @since 0.1.2
118+
openFileWithCloseOnExec :: OsPath -> IOMode -> IO Handle
119+
openFileWithCloseOnExec osfp iomode = augmentError "openFileWithCloseOnExec" osfp $ withOpenFile' osfp iomode False False True pure False
120+
121+
122+
-- | Open an existing file and return the 'Handle'.
123+
--
124+
-- Sets @O_CLOEXEC@ on posix.
125+
--
126+
-- @since 0.1.2
127+
openExistingFileWithCloseOnExec :: OsPath -> IOMode -> IO Handle
128+
openExistingFileWithCloseOnExec osfp iomode = augmentError "openExistingFileWithCloseOnExec" osfp $ withOpenFile' osfp iomode False True True pure False
129+
130+
-- ---------------------------------------------------------------------------
131+
-- Internals
132+
133+
handleFinalizer :: FilePath -> MVar Handle__ -> IO ()
134+
handleFinalizer _fp m = do
135+
handle_ <- takeMVar m
136+
(handle_', _) <- hClose_help handle_
137+
putMVar m handle_'
138+
return ()
139+
140+
type HandleFinalizer = FilePath -> MVar Handle__ -> IO ()
141+
142+
-- | Add a finalizer to a 'Handle'. Specifically, the finalizer
143+
-- will be added to the 'MVar' of a file handle or the write-side
144+
-- 'MVar' of a duplex handle. See Handle Finalizers for details.
145+
addHandleFinalizer :: Handle -> HandleFinalizer -> IO ()
146+
addHandleFinalizer hndl finalizer = do
147+
debugIO $ "Registering finalizer: " ++ show filepath
148+
void $ mkWeakMVar mv (finalizer filepath mv)
149+
where
150+
!(filepath, !mv) = case hndl of
151+
FileHandle fp m -> (fp, m)
152+
DuplexHandle fp _ write_m -> (fp, write_m)
153+
154+
withOpenFile' :: OsPath -> IOMode -> Bool -> Bool -> Bool -> (Handle -> IO r) -> Bool -> IO r
155+
withOpenFile' (OsString fp) iomode binary existing cloExec action close_finally = mask $ \restore -> do
156+
hndl <- case (existing, cloExec) of
157+
(True, False) -> P.openExistingFile fp iomode
158+
(False, False) -> P.openFile fp iomode
159+
(True, True) -> P.openExistingFileWithCloseOnExec fp iomode
160+
(False, True) -> P.openFileWithCloseOnExec fp iomode
161+
addHandleFinalizer hndl handleFinalizer
162+
when binary $ hSetBinaryMode hndl True
163+
r <- restore (action hndl) `onException` hClose hndl
164+
when close_finally $ hClose hndl
165+
pure r
166+
167+
addFilePathToIOError :: String -> OsPath -> IOException -> IOException
168+
addFilePathToIOError fun fp ioe = unsafePerformIO $ do
169+
fp' <- either (const (fmap OSP.toChar . OSP.unpack $ fp)) id <$> try @SomeException (OSP.decodeFS fp)
170+
fp'' <- evaluate $ force fp'
171+
pure $ ioe{ ioe_location = fun, ioe_filename = Just fp'' }
172+
173+
augmentError :: String -> OsPath -> IO a -> IO a
174+
augmentError str osfp = flip catchException (ioError . addFilePathToIOError str osfp)
175+

0 commit comments

Comments
 (0)