git-annex/Annex/ExternalAddonProcess.hs
Joey Hess 7403aeb95f
use Annex.ExternalAddonProcess for P2P.Generic processes
These are another sort of external addon process, and this makes several
things work including shell scripts on windows. And it makes for nicer
error messages when the command is not in the path.

Note that the refactored startExternalAddonProcess used by this
does not use propGitEnv to set git environment variables in the
environment. Unlike startExternalAddonProcessProtocol which does.
This is because it runs in IO and does not have access to that
information. But also, I don't think that P2P.Generic processes need
that.
2025-07-30 14:46:37 -04:00

120 lines
3.9 KiB
Haskell

{- External addon processes for special remotes and backends.
-
- Copyright 2013-2024 Joey Hess <id@joeyh.name>
-
- Licensed under the GNU AGPL version 3 or higher.
-}
{-# LANGUAGE OverloadedStrings #-}
module Annex.ExternalAddonProcess where
import qualified Annex
import Annex.Common
import Git.Env
import Utility.Shell
import Messages.Progress
import Control.Concurrent.Async
data ExternalAddonProcess = ExternalAddonProcess
{ externalSend :: Handle
, externalReceive :: Handle
-- Shut down the process. With True, it's forced to stop
-- immediately.
, externalShutdown :: Bool -> IO ()
, externalPid :: ExternalAddonPID
, externalProgram :: String
}
type ExternalAddonPID = Int
data ExternalAddonStartError
= ProgramNotInstalled String
| ProgramFailure String
externalAddonStartErr :: Maybe OsPath -> String -> IO ExternalAddonStartError
externalAddonStartErr (Just cmd) _ =
return $ ProgramFailure $
"Cannot run " ++ fromOsPath cmd ++ " -- Make sure it's executable and that its dependencies are installed."
externalAddonStartErr Nothing basecmd = do
path <- intercalate ":" . map fromOsPath <$> getSearchPath
return $ ProgramNotInstalled $
"Cannot run " ++ basecmd ++ " -- It is not installed in PATH (" ++ path ++ ")"
startExternalAddonProcess
:: (CreateProcess -> CreateProcess)
-> String
-> [CommandParam]
-> IO (Either ExternalAddonStartError (String, (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)))
startExternalAddonProcess f basecmd ps = do
cmdpath <- searchPath basecmd
startExternalAddonProcess' cmdpath f basecmd ps
startExternalAddonProcess'
:: Maybe OsPath
-> (CreateProcess -> CreateProcess)
-> String
-> [CommandParam]
-> IO (Either ExternalAddonStartError (String, (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)))
startExternalAddonProcess' cmdpath mkproc basecmd ps = do
(cmd, cmdps) <- maybe (pure (basecmd, [])) findShellCommand cmdpath
let p = mkproc (proc cmd (toCommand (cmdps ++ ps)))
tryNonAsync (createProcess p) >>= \case
Right v -> return (Right (cmd, v))
Left _ -> Left <$> externalAddonStartErr cmdpath basecmd
-- | Starts an external addon process that speaks a protocol over stdio.
startExternalAddonProcessProtocol :: String -> [CommandParam] -> ExternalAddonPID -> Annex (Either ExternalAddonStartError ExternalAddonProcess)
startExternalAddonProcessProtocol basecmd ps pid = do
errrelayer <- mkStderrRelayer
g <- Annex.gitRepo
cmdpath <- liftIO $ searchPath basecmd
liftIO $ start errrelayer g cmdpath
where
start errrelayer g cmdpath = do
environ <- propGitEnv g
let mkproc = \p -> p
{ std_in = CreatePipe
, std_out = CreatePipe
, std_err = CreatePipe
, env = Just environ
}
startExternalAddonProcess' cmdpath mkproc basecmd ps >>= \case
Right (cmd, v) -> (Right <$> started cmd errrelayer v)
`catchNonAsync` const (Left <$> externalAddonStartErr cmdpath basecmd)
Left err -> return (Left err)
started cmd errrelayer pall@(Just hin, Just hout, Just herr, ph) = do
stderrelay <- async $ errrelayer ph herr
let shutdown forcestop = do
-- Close the process's stdin, to let it know there
-- are no more requests, so it will exit.
hClose hout
-- Close the procces's stdout as we're not going to
-- process any more output from it.
hClose hin
if forcestop
then cleanupProcess pall
else void (waitForProcess ph)
`onException` cleanupProcess pall
-- This thread will exit after consuming any
-- remaining stderr from the process.
() <- wait stderrelay
hClose herr
return $ ExternalAddonProcess
{ externalSend = hin
, externalReceive = hout
, externalPid = pid
, externalShutdown = shutdown
, externalProgram = cmd
}
started _ _ _ = giveup "internal"
protocolDebug :: ExternalAddonProcess -> Bool -> String -> IO ()
protocolDebug external sendto line = debug "Annex.ExternalAddonProcess" $ unwords
[ externalProgram external ++
"[" ++ show (externalPid external) ++ "]"
, if sendto then "<--" else "-->"
, line
]