From a5a4707ac16fdffb9f0aaed2c28c59952830fc27 Mon Sep 17 00:00:00 2001
From: Tamar Christina <tamar@zhox.com>
Date: Sun, 6 Oct 2019 00:24:46 +0100
Subject: [PATCH] Inline atomic actions for Windows and write more detailed
 diagnostics from hsc2hs

---
 Common.hs          |  88 +++++++++++-----------------
 Compat/TempFile.hs | 142 +++++++++++++++++++++++++++++++++++++++++++++
 UtilsCodegen.hs    |   2 +-
 cbits/utils.c      |  76 ++++++++++++++++++++++++
 changelog.md       |   2 +
 hsc2hs.cabal       |   4 ++
 6 files changed, 260 insertions(+), 54 deletions(-)
 create mode 100644 Compat/TempFile.hs
 create mode 100644 cbits/utils.c

diff --git a/Common.hs b/Common.hs
index a79d485..c8bd297 100644
--- a/Common.hs
+++ b/Common.hs
@@ -2,16 +2,14 @@
 module Common where
 
 import qualified Control.Exception as Exception
+import qualified Compat.TempFile as Compat
 import Control.Monad            ( when )
 import Data.Char                ( isSpace )
 import Data.List                ( foldl' )
 import System.IO
 #if defined(mingw32_HOST_OS)
 import Control.Concurrent       ( threadDelay )
-import Data.Bits                ( xor )
 import System.IO.Error          ( isPermissionError )
-import System.CPUTime           ( getCPUTime )
-import System.FilePath          ( (</>) )
 #endif
 import System.Process           ( createProcess, waitForProcess
                                 , proc, CreateProcess(..), StdStream(..) )
@@ -33,47 +31,63 @@ writeBinaryFile fp str = withBinaryFile fp WriteMode $ \h -> hPutStr h str
 rawSystemL :: FilePath -> FilePath -> String -> Bool -> FilePath -> [String] -> IO ()
 rawSystemL outDir outBase action flg prog args = withResponseFile outDir outBase args $ \rspFile -> do
   let cmdLine = prog++" "++unwords args
-  when flg $ hPutStrLn stderr ("Executing: " ++ cmdLine)
-  (_,_,_,ph) <- createProcess (proc prog ['@':rspFile])
+  when flg $ hPutStrLn stderr ("Executing: (@" ++ rspFile ++ ") " ++ cmdLine)
+  (_ ,_ ,progerr ,ph) <- createProcess (proc prog ['@':rspFile])
   -- Because of the response files being written and removed after the process
   -- terminates we now need to use process jobs here to correctly wait for all
   -- child processes to terminate.  Not doing so would causes a race condition
   -- between the last child dieing and not holding a lock on the response file
   -- and the response file getting deleted.
-#if MIN_VERSION_process (1,5,0)
-    { use_process_jobs = True }
+    { std_err = CreatePipe
+#if MIN_VERSION_process(1,5,0)
+    , use_process_jobs = True
 #endif
+    }
   exitStatus <- waitForProcess ph
   case exitStatus of
-    ExitFailure exitCode -> die $ action ++ " failed "
-                               ++ "(exit code " ++ show exitCode ++ ")\n"
-                               ++ "command was: " ++ cmdLine ++ "\n"
+    ExitFailure exitCode ->
+      do errdata <- maybeReadHandle progerr
+         die $ action ++ " failed "
+                      ++ "(exit code "    ++ show exitCode ++ ")\n"
+                      ++ "rsp file was: " ++ show rspFile ++ "\n"
+                      ++ "command was: "  ++ cmdLine ++ "\n"
+                      ++ "error: "        ++ errdata ++ "\n"
     _                    -> return ()
 
 
 rawSystemWithStdOutL :: FilePath -> FilePath -> String -> Bool -> FilePath -> [String] -> FilePath -> IO ()
 rawSystemWithStdOutL outDir outBase action flg prog args outFile = withResponseFile outDir outBase args $ \rspFile -> do
   let cmdLine = prog++" "++unwords args++" >"++outFile
-  when flg (hPutStrLn stderr ("Executing: " ++ cmdLine))
+  when flg (hPutStrLn stderr ("Executing: (@" ++ rspFile ++ ") " ++ cmdLine))
   hOut <- openFile outFile WriteMode
-  (_ ,_ ,_ , process) <-
+  (_ ,_ ,progerr , process) <-
     -- We use createProcess here instead of runProcess since we need to specify
     -- a custom CreateProcess structure to turn on use_process_jobs when
     -- available.
     createProcess
-#if MIN_VERSION_process (1,5,0)
-      (proc prog ['@':rspFile]){ use_process_jobs = True, std_out = UseHandle  hOut }
-#else
-      (proc prog ['@':rspFile]){ std_out = UseHandle hOut }
+      (proc prog  ['@':rspFile])
+         { std_out = UseHandle hOut, std_err = CreatePipe
+#if MIN_VERSION_process(1,5,0)
+         , use_process_jobs = True
 #endif
+         }
   exitStatus <- waitForProcess process
   hClose hOut
   case exitStatus of
-    ExitFailure exitCode -> die $ action ++ " failed "
-                               ++ "(exit code " ++ show exitCode ++ ")\n"
-                               ++ "command was: " ++ cmdLine ++ "\n"
+    ExitFailure exitCode ->
+      do errdata <- maybeReadHandle progerr
+         die $ action ++ " failed "
+                      ++ "(exit code "    ++ show exitCode ++ ")\n"
+                      ++ "rsp file was: " ++ show rspFile ++ "\n"
+                      ++ "output file:"   ++ show outFile ++ "\n"
+                      ++ "command was: "  ++ cmdLine ++ "\n"
+                      ++ "error: "        ++ errdata ++ "\n"
     _                    -> return ()
 
+maybeReadHandle :: Maybe Handle -> IO String
+maybeReadHandle Nothing  = return "<no data>"
+maybeReadHandle (Just h) = hGetContents h
+
 -- delay the cleanup of generated files until the end; attempts to
 -- get around intermittent failure to delete files which has
 -- just been exec'ed by a sub-process (Win32 only.)
@@ -126,43 +140,11 @@ withTempFile :: FilePath -- ^ Temp dir to create the file in
              -> String   -- ^ Template for temp file
              -> Int      -- ^ Random seed for tmp name
              -> (FilePath -> Handle -> IO a) -> IO a
-#if !defined(mingw32_HOST_OS)
 withTempFile tmpDir _outBase template _seed action = do
   Exception.bracket
-    (openTempFile tmpDir template)
-    (\(name, handle) -> do hClose handle
-                           removeFile $ name)
+    (Compat.openTempFile tmpDir template)
+    (\(name, handle) -> finallyRemove name $ hClose handle)
     (uncurry action)
-#else
-withTempFile tmpDir outBase template seed action = do
-  -- openTempFile isn't atomic under Windows. This means it's unsuitable for
-  -- use on Windows for creating random temp files.  Instead we'll try to create
-  -- a reasonably random name based on the current outBase.  If the
-  -- Sanity check to see that nothing invalidated this assumption is violated
-  -- then we retry a few times otherwise an error is raised.
-  rspFile <- findTmp 5
-  Exception.bracket
-    (openFile rspFile ReadWriteMode)
-    (\handle -> finallyRemove rspFile $ hClose handle)
-    (action rspFile)
-    where findTmp :: Int -> IO FilePath
-          findTmp 0 = die "Could not find unallocated temp file\n"
-          findTmp n = do
-            -- Generate a reasonable random number for token to prevent clashes if this
-            -- function is used recursively.
-            cpuTime <- getCPUTime
-            let token = show $ (fromIntegral seed) `xor` cpuTime
-                file = tmpDir </> (outBase ++ token ++ template)
-            -- Because of the resolution of the CPU timers there exists a small
-            -- possibility that multiple nested calls to withTempFile get the
-            -- same "token".  To reduce the risk to almost zero we immediately
-            -- create the file to reserve it.  If the file already exists we try
-            -- again.
-            res <- Exception.try $ openFile file ReadMode
-            case (res :: Either Exception.SomeException Handle) of
-              Left  _ -> return file
-              Right h -> hClose h >> findTmp (n-1)
-#endif
 
 withResponseFile ::
      FilePath           -- ^ Working directory to create response file in.
diff --git a/Compat/TempFile.hs b/Compat/TempFile.hs
new file mode 100644
index 0000000..799484e
--- /dev/null
+++ b/Compat/TempFile.hs
@@ -0,0 +1,142 @@
+{-# LANGUAGE CPP #-}
+#if __GLASGOW_HASKELL__ >= 70
+{-# LANGUAGE Safe #-}
+#endif
+
+module Compat.TempFile (
+    openBinaryTempFile,
+    openTempFile
+  ) where
+
+#if defined(mingw32_HOST_OS)
+import Data.Bits
+import Foreign.C.Error
+import Foreign.C.String
+import Foreign.C.Types
+import Foreign.Ptr
+import Foreign.Marshal.Alloc
+import Foreign.Storable
+import GHC.IO.Encoding
+import GHC.IO.IOMode
+import qualified GHC.IO.FD as FD
+import qualified GHC.IO.Handle.FD as POSIX
+import System.Posix.Internals
+import System.Posix.Types
+#else
+import qualified System.IO as IOUtils
+#endif
+
+import GHC.IO.Handle
+
+-- | The function creates a temporary file in ReadWrite mode.
+-- The created file isn\'t deleted automatically, so you need to delete it manually.
+--
+-- The file is created with permissions such that only the current
+-- user can read\/write it.
+--
+-- With some exceptions (see below), the file will be created securely
+-- in the sense that an attacker should not be able to cause
+-- openTempFile to overwrite another file on the filesystem using your
+-- credentials, by putting symbolic links (on Unix) in the place where
+-- the temporary file is to be created.  On Unix the @O_CREAT@ and
+-- @O_EXCL@ flags are used to prevent this attack, but note that
+-- @O_EXCL@ is sometimes not supported on NFS filesystems, so if you
+-- rely on this behaviour it is best to use local filesystems only.
+--
+openTempFile :: FilePath   -- ^ Directory in which to create the file
+             -> String     -- ^ File name template. If the template is \"foo.ext\" then
+                           -- the created file will be \"fooXXX.ext\" where XXX is some
+                           -- random number. Note that this should not contain any path
+                           -- separator characters.
+             -> IO (FilePath, Handle)
+openTempFile tmp_dir template
+#if defined(mingw32_HOST_OS)
+    = openTempFile' "openTempFile" tmp_dir template False 0o600
+#else
+    = IOUtils.openTempFile tmp_dir template
+#endif
+
+-- | Like 'openTempFile', but opens the file in binary mode. See 'openBinaryFile' for more comments.
+openBinaryTempFile :: FilePath -> String -> IO (FilePath, Handle)
+openBinaryTempFile tmp_dir template
+#if defined(mingw32_HOST_OS)
+    = openTempFile' "openBinaryTempFile" tmp_dir template True 0o600
+#else
+    = IOUtils.openBinaryTempFile tmp_dir template
+#endif
+
+
+#if defined(mingw32_HOST_OS)
+openTempFile' :: String -> FilePath -> String -> Bool -> CMode
+              -> IO (FilePath, Handle)
+openTempFile' loc tmp_dir template binary mode
+    | pathSeparator template
+    = error $ "openTempFile': Template string must not contain path separator characters: "++template
+    | otherwise = findTempName
+  where
+    -- We split off the last extension, so we can use .foo.ext files
+    -- for temporary files (hidden on Unix OSes). Unfortunately we're
+    -- below filepath in the hierarchy here.
+    (prefix, suffix) =
+       case break (== '.') $ reverse template of
+         -- First case: template contains no '.'s. Just re-reverse it.
+         (rev_suffix, "")       -> (reverse rev_suffix, "")
+         -- Second case: template contains at least one '.'. Strip the
+         -- dot from the prefix and prepend it to the suffix (if we don't
+         -- do this, the unique number will get added after the '.' and
+         -- thus be part of the extension, which is wrong.)
+         (rev_suffix, '.':rest) -> (reverse rest, '.':reverse rev_suffix)
+         -- Otherwise, something is wrong, because (break (== '.')) should
+         -- always return a pair with either the empty string or a string
+         -- beginning with '.' as the second component.
+         _                      -> error "bug in System.IO.openTempFile"
+    findTempName = do
+      let label = if null prefix then "ghc" else prefix
+      withCWString tmp_dir $ \c_tmp_dir ->
+        withCWString label $ \c_template ->
+          withCWString suffix $ \c_suffix ->
+            -- FIXME: revisit this when new I/O manager in place and use a UUID
+            --       based one when we are no longer MAX_PATH bound.
+            allocaBytes (sizeOf (undefined :: CWchar) * 260) $ \c_str -> do
+            res <- c_getTempFileNameErrorNo c_tmp_dir c_template c_suffix 0
+                                            c_str
+            if not res
+               then do errno <- getErrno
+                       ioError (errnoToIOError loc errno Nothing (Just tmp_dir))
+               else do filename <- peekCWString c_str
+                       handleResults filename
+
+    handleResults filename = do
+      let oflags1 = rw_flags .|. o_EXCL
+          binary_flags
+              | binary    = o_BINARY
+              | otherwise = 0
+          oflags = oflags1 .|. binary_flags
+      fd <- withFilePath filename $ \ f -> c_open f oflags mode
+      case fd < 0 of
+        True -> do errno <- getErrno
+                   ioError (errnoToIOError loc errno Nothing (Just tmp_dir))
+        False ->
+          do (fD,fd_type) <- FD.mkFD fd ReadWriteMode Nothing{-no stat-}
+                                     False{-is_socket-}
+                                     True{-is_nonblock-}
+
+             enc <- getLocaleEncoding
+             h <- POSIX.mkHandleFromFD fD fd_type filename ReadWriteMode
+                                 False{-set non-block-} (Just enc)
+
+             return (filename, h)
+
+foreign import ccall "__get_temp_file_name" c_getTempFileNameErrorNo
+  :: CWString -> CWString -> CWString -> CUInt -> Ptr CWchar -> IO Bool
+
+pathSeparator :: String -> Bool
+pathSeparator template = any (\x-> x == '/' || x == '\\') template
+
+output_flags = std_flags
+
+-- XXX Copied from GHC.Handle
+std_flags, output_flags, rw_flags :: CInt
+std_flags    = o_NONBLOCK   .|. o_NOCTTY
+rw_flags     = output_flags .|. o_RDWR
+#endif /* mingw32_HOST_OS */
\ No newline at end of file
diff --git a/UtilsCodegen.hs b/UtilsCodegen.hs
index 0e35614..36305f3 100644
--- a/UtilsCodegen.hs
+++ b/UtilsCodegen.hs
@@ -79,7 +79,7 @@ withUtilsObject config outDir outBase f = do
 
         possiblyRemove oUtilsName $ do
            unless (cNoCompile config) $
-               rawSystemL outDir outBase ("compiling " ++ cUtilsName)
+               rawSystemL outDir (outBase ++ "_utils") ("compiling " ++ cUtilsName)
                           beVerbose
                           (cCompiler config)
                           (["-c", cUtilsName, "-o", oUtilsName] ++
diff --git a/cbits/utils.c b/cbits/utils.c
new file mode 100644
index 0000000..d9f9461
--- /dev/null
+++ b/cbits/utils.c
@@ -0,0 +1,76 @@
+/* ----------------------------------------------------------------------------
+   (c) The University of Glasgow 2006, Lifted from Bases
+
+   Useful Win32 bits
+   ------------------------------------------------------------------------- */
+
+#if defined(_WIN32)
+
+#include "HsBase.h"
+#include <stdbool.h>
+#include <stdint.h>
+/* Using Secure APIs */
+#define MINGW_HAS_SECURE_API 1
+#include <wchar.h>
+#include <windows.h>
+
+/* Copied from getTempFileNameErrorNo in base's cbits/Win32Utils.c in GHC 8.10.
+   Check there for any bugfixes first and please keep in sync when making
+   changes.  */
+
+bool __get_temp_file_name (wchar_t* pathName, wchar_t* prefix,
+                           wchar_t* suffix, uint32_t uUnique,
+                           wchar_t* tempFileName)
+{
+  int retry = 5;
+  bool success = false;
+  while (retry > 0 && !success)
+    {
+      // TODO: This needs to handle long file names.
+      if (!GetTempFileNameW(pathName, prefix, uUnique, tempFileName))
+        {
+          maperrno();
+          return false;
+        }
+
+      wchar_t* drive = malloc (sizeof(wchar_t) * _MAX_DRIVE);
+      wchar_t* dir   = malloc (sizeof(wchar_t) * _MAX_DIR);
+      wchar_t* fname = malloc (sizeof(wchar_t) * _MAX_FNAME);
+      if (_wsplitpath_s (tempFileName, drive, _MAX_DRIVE, dir, _MAX_DIR,
+                         fname, _MAX_FNAME, NULL, 0) != 0)
+        {
+          success = false;
+          maperrno ();
+        }
+      else
+        {
+          wchar_t* temp = _wcsdup (tempFileName);
+          if (wcsnlen(drive, _MAX_DRIVE) == 0)
+            swprintf_s(tempFileName, MAX_PATH, L"%s\%s%s",
+                      dir, fname, suffix);
+          else
+            swprintf_s(tempFileName, MAX_PATH, L"%s\%s\%s%s",
+                      drive, dir, fname, suffix);
+          success
+             = MoveFileExW(temp, tempFileName, MOVEFILE_WRITE_THROUGH
+                                               | MOVEFILE_COPY_ALLOWED) != 0;
+          errno = 0;
+          if (!success && (GetLastError () != ERROR_FILE_EXISTS || --retry < 0))
+            {
+              success = false;
+              maperrno ();
+              DeleteFileW (temp);
+            }
+
+
+          free(temp);
+        }
+
+      free(drive);
+      free(dir);
+      free(fname);
+    }
+
+  return success;
+}
+#endif
\ No newline at end of file
diff --git a/changelog.md b/changelog.md
index 100c2c2..495e6a6 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,6 +1,8 @@
 ## 0.68.7
 
  - Fix race condition when using response files (#30)
+ - Add extra diagnostics when hsc2hs sub-process fails
+   and make TempFile creation fully atomic on Windows. See (#33)
 
 ## 0.68.6
 
diff --git a/hsc2hs.cabal b/hsc2hs.cabal
index f87b757..a2dfd3f 100644
--- a/hsc2hs.cabal
+++ b/hsc2hs.cabal
@@ -52,8 +52,12 @@ Executable hsc2hs
         ATTParser
         UtilsCodegen
         Compat.ResponseFile
+        Compat.TempFile
         Paths_hsc2hs
 
+    c-sources:
+        cbits/utils.c
+
     Other-Extensions: CPP, NoMonomorphismRestriction
 
     Build-Depends: base       >= 4.3.0 && < 4.14,
-- 
GitLab