packages feed

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