hsc3-process-0.7.0: Sound/SC3/Server/Process.hs
{-# LANGUAGE ExistentialQuantification
, ScopedTypeVariables #-}
-- | This module includes utilities for spawning an external scsynth process,
-- either for realtime or non-realtime execution.
module Sound.SC3.Server.Process
( module Sound.SC3.Server.Process.Options
, OutputHandler(..)
, defaultOutputHandler
, NetworkTransport
, withTransport
, withSynth
, withNRT ) where
import Control.Applicative
import Control.Concurrent
import Control.Exception
import Control.Monad (unless)
import Prelude hiding (catch)
import Data.List (isPrefixOf)
import Sound.OpenSoundControl (Transport(..))
import qualified Sound.OpenSoundControl as OSC
import Sound.SC3 (quit)
import Sound.SC3.Server.Process.CommandLine
import Sound.SC3.Server.Process.Options
import System.Exit (ExitCode(..))
import System.IO (Handle, hFlush, hGetLine, hIsEOF, hPutStrLn, stderr, stdout)
import System.Process (runInteractiveProcess, waitForProcess)
localhost :: String
localhost = "127.0.0.1"
-- | Check wether a network port is within the valid range (0, 65535]
checkPort :: String -> Int -> Int
checkPort tag p | p <= 0 || p > 65535 = error ("Invalid " ++ tag ++ " port " ++ show p)
checkPort _ p = p
-- | Network transport wrapper.
data NetworkTransport = forall t . Transport t => NetworkTransport t
instance Transport NetworkTransport where
recv (NetworkTransport t) = recv t
send (NetworkTransport t) = send t
close (NetworkTransport t) = close t
-- | Open a network transport connected to a network port.
openTransport :: NetworkPort -> IO NetworkTransport
openTransport (UDPPort p) = NetworkTransport <$> OSC.openUDP localhost (checkPort "UDP" p)
openTransport (TCPPort p) = NetworkTransport <$> OSC.openTCP localhost (checkPort "TCP" p)
-- ====================================================================
-- * Output handler
-- | Handle output of external @scsynth@ processes.
data OutputHandler = OutputHandler {
onPutString :: String -> IO () -- ^ Handle one line of normal output
, onPutError :: String -> IO () -- ^ Handle one line of error output
}
-- | Default IO handler, writing to stdout and stderr, respectively.
defaultOutputHandler :: OutputHandler
defaultOutputHandler = OutputHandler {
onPutString = \s -> hPutStrLn stdout s >> hFlush stdout
, onPutError = \s -> hPutStrLn stderr s >> hFlush stderr
}
-- ====================================================================
-- Process helpers
pipeOutput :: (String -> IO ()) -> Handle -> IO ()
pipeOutput f h = hIsEOF h >>= flip unless (hGetLine h >>= f >> pipeOutput f h)
-- ====================================================================
-- * Realtime scsynth execution
withTransport ::
ServerOptions
-> RTOptions
-> (NetworkTransport -> IO a)
-> IO a
withTransport _ rtOptions = bracket (openTransport (networkPort rtOptions)) OSC.close
-- | Execute a realtime instance of @scsynth@ with 'Transport' t.
--
-- The spawned @scsynth@ is sent a @\/quit@ message after the supplied action
-- returns.
--
-- /NOTE/: When compiling executables with GHC, the @-threaded@ option should
-- be passed, otherwise the I\/O handlers will not work correctly.
withSynth ::
ServerOptions
-> RTOptions
-> OutputHandler
-> (NetworkTransport -> IO a)
-> IO a
withSynth serverOptions rtOptions handler action = do
(_, hOut, hErr, hProc) <- runInteractiveProcess exe args Nothing Nothing
forkPipe onPutError hErr
exitCode <- newEmptyMVar
forkIO $ waitForProcess hProc >>= putMVar exitCode
a <- loop hOut
e <- takeMVar exitCode
case e of
ExitSuccess -> return a
ExitFailure _ -> throw e
where
(exe:args) = rtCommandLine serverOptions rtOptions
loop h = do
l <- hGetLine h
onPutString handler l
if "SuperCollider 3 server ready" `isPrefixOf` l
then cont h
else loop h
cont h = do
forkPipe onPutString h
fd <- openTransport (networkPort rtOptions)
action fd `finally` OSC.send fd quit
forkPipe f = forkIO . pipeOutput (f handler)
-- ====================================================================
-- * Non-Realtime scsynth execution
-- | Execute a non-realtime instance of @scsynth@ and return 'ExitCode' when
-- the process exists.
withNRT ::
ServerOptions
-> NRTOptions
-> OutputHandler
-> (Handle -> IO a)
-> IO a
withNRT serverOptions nrtOptions handler action = do
(hIn, hOut, hErr, hProc) <- runInteractiveProcess exe args Nothing Nothing
forkIO $ putStdout hOut
forkIO $ putStderr hErr
result <- newEmptyMVar
thread <- forkIO $ do
a <- try (action hIn)
case a of
Left (ex :: SomeException) -> putMVar result (Left ex)
_ -> putMVar result a
exitCode <- waitForProcess hProc
case exitCode of
ExitSuccess -> do
a <- readMVar result
case a of
Left e -> throw e
Right a -> return a
ExitFailure _ -> do
killThread thread
throw (toException exitCode)
where
(exe:args) = nrtCommandLine serverOptions nrtOptions { commandFilePath = Nothing }
putStdout = pipeOutput (onPutString handler)
putStderr = pipeOutput (onPutString handler)
-- EOF