packages feed

feedback-0.1.0.5: src/Feedback/Common/Process.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -fno-warn-unused-pattern-binds #-}

module Feedback.Common.Process where

import Data.Map as M
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Feedback.Common.OptParse
import Path
import Path.IO
import System.Environment as System (getEnvironment)
import System.Exit
import qualified System.Process as Process
import System.Process.Typed as Typed
import UnliftIO.IO.File

data ProcessHandle = ProcessHandle
  { processHandleProcess :: !P
  }

type P = Process () () ()

startProcessAndWait :: RunSettings -> IO ExitCode
startProcessAndWait runSettings = do
  processConfig <- makeProcessConfigFor runSettings
  startProcess processConfig >>= waitExitCode

startProcessHandle :: RunSettings -> IO ProcessHandle
startProcessHandle runSettings = do
  processConfig <- makeProcessConfigFor runSettings
  processHandleProcess <- startProcess processConfig
  pure ProcessHandle {..}

waitProcessHandle :: ProcessHandle -> IO ExitCode
waitProcessHandle ProcessHandle {..} = waitExitCode processHandleProcess

makeProcessConfigFor :: RunSettings -> IO (ProcessConfig () () ())
makeProcessConfigFor RunSettings {..} = do
  let RunSettings _ _ _ = undefined
  -- Set up the environment
  env <- System.getEnvironment
  let envForProcess = M.toList $ M.union runSettingExtraEnv (M.fromList env)
  let CommandScript script = runSettingCommand
  -- Set up the script file
  scriptFile <- do
    -- Write the script to a file
    systemTempDir <- getTempDir
    ensureDir systemTempDir
    tempDir <- createTempDir systemTempDir "feedback"
    scriptFile <- resolveFile tempDir "feedback-script.sh"
    writeBinaryFileDurableAtomic (fromAbsFile scriptFile) (TE.encodeUtf8 (T.pack script))

    -- Make the script executable
    oldPermissions <- getPermissions scriptFile
    let newPermissions = setOwnerExecutable True oldPermissions
    setPermissions scriptFile newPermissions

    pure $ fromAbsFile scriptFile

  pure
    $ setStdout inherit
      . setStderr inherit
      . setStdin nullStream -- TODO make this configurable?
      . setEnv envForProcess
      . setCreateGroup True -- See [ref:ProcessGroup]
      . maybe id (setWorkingDir . fromAbsDir) runSettingWorkingDir
    $ shell scriptFile

stopProcessHandle :: ProcessHandle -> IO ()
stopProcessHandle ProcessHandle {..} = do
  -- [tag:ProcessGroup]
  -- We create a new process group for the script we execute.
  -- The problem this solves is the following:
  -- When we execute a bash script, and want to stop it by sending a signal to
  -- it, bash does not propagate this signal to its subprocesses.
  -- To fix this, we put the bash script in a process group.
  -- Bash then creates subprocesses in the same process group.
  -- We can send signals to the entire group at once, which we do here.
  Process.interruptProcessGroupOf $ unsafeProcessHandle processHandleProcess
  stopProcess processHandleProcess
  -- No need to cancel the waiter thread.
  pure ()