git-annex/Utility/MagicWormhole.hs
Joey Hess f1c85ac11b
fix breakage in wormhole's sendFile
Commit ff0927bde9 broke this, it made it
try to read all of the input before looking for the code. But, wormhole
keeps running until it sends the file, so that caused a deadlock. Oops.

Sponsored-by: Luke Shumaker on Patreon
2022-09-26 15:26:29 -04:00

177 lines
4.8 KiB
Haskell

{- Magic Wormhole integration
-
- Copyright 2016 Joey Hess <id@joeyh.name>
-
- License: BSD-2-clause
-}
module Utility.MagicWormhole (
Code,
mkCode,
toCode,
fromCode,
validCode,
CodeObserver,
CodeProducer,
mkCodeObserver,
mkCodeProducer,
waitCode,
sendCode,
WormHoleParams,
appId,
sendFile,
receiveFile,
isInstalled,
) where
import Utility.Process
import Utility.SafeCommand
import Utility.Monad
import Utility.Misc
import Utility.Env
import Utility.Path
import Utility.Exception
import System.IO
import System.Exit
import Control.Concurrent
import Control.Concurrent.Async
import Data.Char
import Data.List
import Control.Applicative
import Prelude
-- | A Magic Wormhole code.
newtype Code = Code String
deriving (Eq, Show)
-- | Smart constructor for Code
mkCode :: String -> Maybe Code
mkCode s
| validCode s = Just (Code s)
| otherwise = Nothing
-- | Tries to fix up some common mistakes in a homan-entered code.
toCode :: String -> Maybe Code
toCode s = mkCode $ intercalate "-" $ words s
fromCode :: Code -> String
fromCode (Code s) = s
-- | Codes have the form number-word-word and may contain 2 or more words.
validCode :: String -> Bool
validCode s =
let (n, r) = separate (== '-') s
(w1, w2) = separate (== '-') r
in and
[ not (null n)
, all isDigit n
, not (null w1)
, not (null w2)
, not $ any isSpace s
]
newtype CodeObserver = CodeObserver (MVar Code)
newtype CodeProducer = CodeProducer (MVar Code)
mkCodeObserver :: IO CodeObserver
mkCodeObserver = CodeObserver <$> newEmptyMVar
mkCodeProducer :: IO CodeProducer
mkCodeProducer = CodeProducer <$> newEmptyMVar
waitCode :: CodeObserver -> IO Code
waitCode (CodeObserver o) = readMVar o
sendCode :: CodeProducer -> Code -> IO ()
sendCode (CodeProducer p) = putMVar p
type WormHoleParams = [CommandParam]
-- | An appid should be provided when using wormhole in an app, to avoid
-- using the same channel space as ad-hoc wormhole users.
appId :: String -> WormHoleParams
appId s = [Param "--appid", Param s]
-- | Sends a file. Once the send is underway, and the Code has been
-- generated, it will be sent to the CodeObserver. (This may not happen,
-- eg if there's a network problem).
--
-- Currently this has to parse the output of wormhole to find the code.
-- To make this as robust as possible, avoids looking for any particular
-- output strings, and only looks for the form of a wormhole code
-- (number-word-word).
--
-- Note that, if the filename looks like "foo 1-wormhole-code bar", when
-- that is output by wormhole, it will look like it's output a wormhole code.
--
-- A request to make the code available in machine-parsable form is here:
-- https://github.com/warner/magic-wormhole/issues/104
sendFile :: FilePath -> CodeObserver -> WormHoleParams -> IO Bool
sendFile f (CodeObserver observer) ps = do
-- Work around stupid stdout buffering behavior of python.
-- See https://github.com/warner/magic-wormhole/issues/108
environ <- addEntry "PYTHONUNBUFFERED" "1" <$> getEnvironment
runWormHoleProcess p { env = Just environ} $ \_hin hout herr ph -> do
(inout, inerr) <- concurrently
(findcode ph hout)
(findcode ph herr)
return (inout || inerr)
where
p = wormHoleProcess (ps ++ [Param "send", File f])
findcode ph h = hGetLineUntilExitOrEOF ph h >>= \case
Nothing -> return False
Just l -> ifM (findcode' (words l))
( drain ph h >> return True
, findcode ph h
)
findcode' [] = return False
findcode' (w:ws) = case mkCode w of
Just code -> do
_ <- tryPutMVar observer code
return True
Nothing -> findcode' ws
drain ph h = hGetLineUntilExitOrEOF ph h >>= \case
Just _ -> drain ph h
Nothing -> return ()
-- | Receives a file. Once the receive is under way, the Code will be
-- read from the CodeProducer, and fed to wormhole on stdin.
receiveFile :: FilePath -> CodeProducer -> WormHoleParams -> IO Bool
receiveFile f (CodeProducer producer) ps = runWormHoleProcess p $ \hin _hout _herr _ph -> do
Code c <- readMVar producer
hPutStrLn hin c
hFlush hin
return True
where
p = wormHoleProcess $ ps ++
[ Param "receive"
, Param "--accept-file"
, Param "--output-file"
, File f
]
wormHoleProcess :: WormHoleParams -> CreateProcess
wormHoleProcess = proc "wormhole" . toCommand
runWormHoleProcess :: CreateProcess -> (Handle -> Handle -> Handle -> ProcessHandle -> IO Bool) -> IO Bool
runWormHoleProcess p consumer =
withCreateProcess p' go `catchNonAsync` const (return False)
where
p' = p
{ std_in = CreatePipe
, std_out = CreatePipe
, std_err = CreatePipe
}
go (Just hin) (Just hout) (Just herr) pid =
consumer hin hout herr pid <&&> waitbool pid
go _ _ _ _ = error "internal"
waitbool pid = do
r <- waitForProcess pid
return $ case r of
ExitSuccess -> True
ExitFailure _ -> False
isInstalled :: IO Bool
isInstalled = inSearchPath "wormhole"