/
Pty.hs
372 lines (319 loc) · 13.6 KB
/
Pty.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
{-# LANGUAGE CApiFFI #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE Trustworthy #-}
{-# LANGUAGE ViewPatterns #-}
-------------------------------------------------------------------------------
-- |
-- Module : System.Posix.Pty
-- Copyright : (C) 2013 Merijn Verstraaten
-- License : BSD-style (see the file LICENSE)
-- Maintainer : Merijn Verstraaten <merijn@inconsistent.nl>
-- Stability : experimental
-- Portability : haha
--
-- A module for interacting with subprocesses through a pseudo terminal (pty).
-- Provides functions for reading from, writing to and resizing pseudo
-- terminals. Re-exports most of "System.Posix.Terminal", providing wrappers
-- that work with the 'Pty' type where necessary.
-------------------------------------------------------------------------------
module System.Posix.Pty (
-- * Subprocess Creation
spawnWithPty
-- * Data Structures
, Pty
, PtyControlCode (..)
-- * Pty Interaction Functions
, createPty
, closePty
, tryReadPty
, readPty
, writePty
, resizePty
, ptyDimensions
-- * Blocking on 'Pty's
, threadWaitReadPty
, threadWaitWritePty
, threadWaitReadPtySTM
, threadWaitWritePtySTM
-- * Re-exports of "System.Posix.Terminal"
-- $posix-reexport
, getTerminalAttributes
, setTerminalAttributes
, sendBreak
, drainOutput
, discardData
, controlFlow
, getTerminalProcessGroupID
, getTerminalName
, getSlaveTerminalName
, module System.Posix.Terminal
) where
#if __GLASGOW_HASKELL__ < 710
import Control.Applicative
#endif
import Control.Concurrent (withMVar)
import Control.Exception (bracket, throwIO, ErrorCall(..))
import Control.Monad (when)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BS (createAndTrim)
import qualified Data.ByteString.Unsafe as BS (unsafeUseAsCString)
import GHC.Conc (STM)
import GHC.Conc.IO (threadWaitRead, threadWaitWrite,
threadWaitReadSTM, threadWaitWriteSTM)
import Foreign
import Foreign.C.Error (throwErrnoIfMinus1Retry, throwErrnoIfMinus1Retry_)
import Foreign.C.String (CString, newCString)
import Foreign.C.Types
import System.IO.Error (mkIOError, eofErrorType)
import System.Posix.IO (fdReadBuf, fdWriteBuf,closeFd)
import System.Posix.Types
import System.Process.Internals (mkProcessHandle, runInteractiveProcess_lock, ProcessHandle)
import qualified System.Posix.Terminal as T
import System.Posix.Terminal hiding
( getTerminalAttributes
, setTerminalAttributes
, sendBreak
, drainOutput
, discardData
, controlFlow
, getTerminalProcessGroupID
, setTerminalProcessGroupID
, queryTerminal
, getTerminalName
, openPseudoTerminal
, getSlaveTerminalName)
-- | Abstract pseudo terminal type.
newtype Pty = Pty Fd
-- | Pseudo terminal control information.
--
-- [Terminal read queue] The terminal read queue contains the data that was
-- written from the master terminal to the slave terminal, which was not read
-- from the slave yet.
--
-- [Terminal write queue] The terminal write queue contains the data that was
-- written from the slave terminal, which was not sent to the master yet.
data PtyControlCode = FlushRead -- ^ Terminal read queue was flushed.
| FlushWrite -- ^ Terminal write queue was flushed.
| OutputStopped -- ^ Terminal output was stopped.
| OutputStarted -- ^ Terminal output was restarted.
| DoStop -- ^ Terminal stop and start characters are
-- @^S@ and @^Q@ respectively.
| NoStop -- ^ Terminal stop and start characters are
-- NOT @^S@ and @^Q@.
deriving (Eq, Read, Show)
-- | Produces a 'Pty' if the file descriptor is associated with a terminal and
-- Nothing if not.
createPty :: Fd -> IO (Maybe Pty)
createPty fd = do
isTerminal <- T.queryTerminal fd
let result | isTerminal = Just (Pty fd)
| otherwise = Nothing
return result
-- | Close this pseudo terminal.
closePty :: Pty -> IO ()
closePty (Pty fd) = closeFd fd
-- | Attempt to read data from a pseudo terminal. Produces either the data read
-- or a list of 'PtyControlCode'@s@ indicating which control status events that
-- have happened on the slave terminal.
--
-- Throws an 'IOError' of type 'eofErrorType' when the terminal has been
-- closed, for example when the subprocess has terminated.
tryReadPty :: Pty -> IO (Either [PtyControlCode] ByteString)
tryReadPty (Pty fd) = do
result <- readBS 1024
case BS.uncons result of
Nothing -> ioError ptyClosed
Just (byte, rest)
| byte == 0 -> return (Right rest)
| BS.null rest -> return $ Left (byteToControlCode byte)
| otherwise -> ioError can'tHappen
where
ptyClosed :: IOError
ptyClosed = mkIOError eofErrorType "pty terminated" Nothing Nothing
can'tHappen :: IOError
can'tHappen = userError "Uh-oh! Something different went horribly wrong!"
readBS :: ByteCount -> IO ByteString
readBS n
| n <= 0 = return BS.empty
| overflow = throwIO (ErrorCall "invalid size for read")
| otherwise = BS.createAndTrim (fromIntegral n) $
fmap fromIntegral . fillBuf
where
overflow :: Bool
overflow = n >= fromIntegral (maxBound :: Int)
fillBuf :: Ptr Word8 -> IO ByteCount
fillBuf buf = throwErrnoIfMinus1Retry "read failed" $
fdReadBuf fd buf n
-- | The same as 'tryReadPty', but discards any control status events.
readPty :: Pty -> IO ByteString
readPty pty = tryReadPty pty >>= \case
Left _ -> readPty pty
Right bs -> return bs
-- | Write a 'ByteString' to the pseudo terminal, throws an 'IOError' when the
-- terminal has been closed, for example when the subprocess has terminated.
writePty :: Pty -> ByteString -> IO ()
writePty (Pty fd) bs =
BS.unsafeUseAsCString bs $ write (fromIntegral (BS.length bs)) . castPtr
where
write :: ByteCount -> Ptr Word8 -> IO ()
write len buf = do
res <- throwErrnoIfMinus1Retry "write failed" $ fdWriteBuf fd buf len
when (res < len) $ do
write (len - res) $ plusPtr buf (fromIntegral res)
-- | Set the pseudo terminal's dimensions to the specified width and height.
resizePty :: Pty -> (Int, Int) -> IO ()
resizePty (Pty fd) (x, y) =
throwErrnoIfMinus1Retry_ "unable to set pty dimensions" $ set_pty_size fd x y
-- | Produces the pseudo terminal's current dimensions.
ptyDimensions :: Pty -> IO (Int, Int)
ptyDimensions (Pty fd) = alloca $ \x -> alloca $ \y -> do
throwErrnoIfMinus1Retry_ "unable to get pty size" $ get_pty_size fd x y
(,) <$> peek x <*> peek y
-- | Create a new process that is connected to the current process through a
-- pseudo terminal. If an environment is specified, then only the specified
-- environment variables will be set. If no environment is specified the
-- process will inherit its environment from the current process. Example:
--
-- > pty <- spawnWithPty (Just [("SHELL", "tcsh")]) True "ls" ["-l"] (20, 10)
--
-- This searches the user's PATH for a binary called @ls@, then runs this
-- binary with the commandline argument @-l@ in a terminal that is 20
-- characters wide and 10 characters high. The environment of @ls@ will
-- contains one variable, SHELL, which will be set to the value \"tcsh\".
spawnWithPty :: Maybe [(String, String)] -- ^ Optional environment for the
-- new process.
-> Bool -- ^ Search for the executable in
-- PATH?
-> FilePath -- ^ Program's name.
-> [String] -- ^ Command line arguments for the
-- program.
-> (Int, Int) -- ^ Initial dimensions for the
-- pseudo terminal.
-> IO (Pty, ProcessHandle)
spawnWithPty env' (fromBool -> search) path' argv' (x, y) = do
bracket allocStrings cleanupStrings $ \(path, argvList, envList) -> do
let allocLists = do
argv <- newArray0 nullPtr (path : argvList)
env <- case envList of
[] -> return nullPtr
_ -> newArray0 nullPtr envList
return (argv, env)
cleanupLists (argv, env) = free argv >> free env
bracket allocLists cleanupLists $ \(argv, env) -> do
alloca $ \pidPtr -> do
fd <- throwErrnoIfMinus1Retry "failed to fork or open pty" $
withMVar runInteractiveProcess_lock $ \_ ->
fork_exec_with_pty x y search path argv env pidPtr
pid <- peek pidPtr
handle <- mkProcessHandle (fromIntegral pid) True
return (Pty fd, handle)
where
fuse :: (String, String) -> IO CString
fuse (key, val) = newCString (key ++ "=" ++ val)
allocStrings :: IO (CString, [CString], [CString])
allocStrings = do
path <- newCString path'
argv <- mapM newCString argv'
env <- maybe (return []) (mapM fuse) env'
return (path, argv, env)
cleanupStrings :: (CString, [CString], [CString]) -> IO ()
cleanupStrings (path, argv, env) = do
free path
mapM_ free argv
mapM_ free env
-- Module internal functions
getFd :: Pty -> Fd
getFd (Pty fd) = fd
byteToControlCode :: Word8 -> [PtyControlCode]
byteToControlCode i = map snd $ filter ((/=0) . (.&.i) . fst) codeMapping
where codeMapping :: [(Word8, PtyControlCode)]
codeMapping =
[ (tiocPktFlushRead, FlushRead)
, (tiocPktFlushWrite, FlushWrite)
, (tiocPktStop, OutputStopped)
, (tiocPktStart, OutputStarted)
, (tiocPktDoStop, DoStop)
, (tiocPktNoStop, NoStop)
]
-- Foreign imports
foreign import capi unsafe "sys/ioctl.h value TIOCPKT_FLUSHREAD"
tiocPktFlushRead :: Word8
foreign import capi unsafe "sys/ioctl.h value TIOCPKT_FLUSHWRITE"
tiocPktFlushWrite :: Word8
foreign import capi unsafe "sys/ioctl.h value TIOCPKT_STOP"
tiocPktStop :: Word8
foreign import capi unsafe "sys/ioctl.h value TIOCPKT_START"
tiocPktStart :: Word8
foreign import capi unsafe "sys/ioctl.h value TIOCPKT_DOSTOP"
tiocPktDoStop :: Word8
foreign import capi unsafe "sys/ioctl.h value TIOCPKT_NOSTOP"
tiocPktNoStop :: Word8
foreign import ccall "pty_size.h"
set_pty_size :: Fd -> Int -> Int -> IO CInt
foreign import ccall "pty_size.h"
get_pty_size :: Fd -> Ptr Int -> Ptr Int -> IO CInt
foreign import ccall "fork_exec_with_pty.h"
fork_exec_with_pty :: Int
-> Int
-> CInt
-> CString
-> Ptr CString
-> Ptr CString
-> Ptr Int
-> IO Fd
-- Pty specialised versions of GHC.Conc.IO
-- | Equivalent to 'threadWaitRead'.
threadWaitReadPty :: Pty -> IO ()
threadWaitReadPty = threadWaitRead . getFd
-- | Equivalent to 'threadWaitWrite'.
threadWaitWritePty :: Pty -> IO ()
threadWaitWritePty = threadWaitWrite . getFd
-- | Equivalent to 'threadWaitReadSTM'.
threadWaitReadPtySTM :: Pty -> IO (STM (), IO ())
threadWaitReadPtySTM = threadWaitReadSTM . getFd
-- | Equivalent to 'threadWaitWriteSTM'.
threadWaitWritePtySTM :: Pty -> IO (STM (), IO ())
threadWaitWritePtySTM = threadWaitWriteSTM . getFd
-- Pty specialised re-exports of System.Posix.Terminal
{- $posix-reexport
This module re-exports the entirety of "System.Posix.Terminal", with the
exception of the following functions:
[setTerminalProcessGroupID] This function can't be used after a process using
the slave terminal has been created, rendering it mostly useless for working
with 'Pty'@s@ created by this module.
[queryTerminal] Useless, 'Pty' is always a terminal.
[openPseudoTerminal] Only useful for the kind of tasks this module is supposed
abstract away.
In addition, some functions from "System.Posix.Terminal" work directly with
'Fd'@s@, these have been hidden and instead the following replacements working
on 'Pty'@s@ are exported.
-}
-- | See 'System.Posix.Terminal.getTerminalAttributes'.
getTerminalAttributes :: Pty -> IO TerminalAttributes
getTerminalAttributes = T.getTerminalAttributes . getFd
-- | See 'System.Posix.Terminal.setTerminalAttributes'.
setTerminalAttributes :: Pty -> TerminalAttributes -> TerminalState -> IO ()
setTerminalAttributes = T.setTerminalAttributes . getFd
-- | See 'System.Posix.Terminal.sendBreak'.
sendBreak :: Pty -> Int -> IO ()
sendBreak = T.sendBreak . getFd
-- | See 'System.Posix.Terminal.drainOutput'.
drainOutput :: Pty -> IO ()
drainOutput = T.drainOutput . getFd
-- | See 'System.Posix.Terminal.discardData'.
discardData :: Pty -> QueueSelector -> IO ()
discardData = T.discardData . getFd
-- | See 'System.Posix.Terminal.controlFlow'.
controlFlow :: Pty -> FlowAction -> IO ()
controlFlow = T.controlFlow . getFd
-- | See 'System.Posix.Terminal.getTerminalProcessGroupID'.
getTerminalProcessGroupID :: Pty -> IO ProcessGroupID
getTerminalProcessGroupID = T.getTerminalProcessGroupID . getFd
-- | See 'System.Posix.Terminal.getTerminalName'.
getTerminalName :: Pty -> IO FilePath
getTerminalName = T.getTerminalName . getFd
-- | See 'System.Posix.Terminal.getSlaveTerminalName'.
getSlaveTerminalName :: Pty -> IO FilePath
getSlaveTerminalName = T.getSlaveTerminalName . getFd