README
lpty
a library that allows lua to start processes and control them via pty
Author: Gunnar Zötl , 2010-2023.
Released under MIT/X11 license. See file LICENSE for details.
Introduction
This is a simple interface to pty functionality, providing the ability to fork a process and run it under pty control. It does not try to mimic the posix API but instead focuses on the function of running and controlling a program. The interface is very bare bones, no additional functionality is provided, especially nothing like a full blown expect, which should be a different package. A rather simple version of expect is available, however.
This has been developed on Linux and tested on MacOS X. It should compile and run on any platform supporting the Unix 98 interface to pty’s. Support for additional Unixen might follow in the future, depending on demand (especially my own ;) ). No support for windows is planned, as ptys are a Unix thing. It might work with cygwin.
Installing
This uses only stuff that comes with your system, so you do not need to install any additional libraries to satisfy any dependencies.
There are 2 options for building and installing this module, the first and most obvious being luarocks. Normally, calling:
sudo luarocks install lpty
or when you have an unpacked source folder:
sudo luarocks make
should do the right thing.
There is also a Makefile in the distribution, that has been created for use with Linux and MacOS X. It may work for other platforms, or you may have to edit it by hand. Calling:
make && make install
will build the module and install it to some standard location for your version of lua.
Using
Load the module with:
lpty = require "lpty"
`
Note: since version 1.0, lpty no longer registers a global table lpty with lua.
Constructor
pty = lpty.new([options])
Creates and fully initializes the master side of a pty, and returns an object to interact with the pty. Options for the pty may be passed as key - value pairs in a table. These options are:throw_errors
- if true, errors are thrown, otherwise they are returned as a standard nil, “error message” pair. Default is false.no_local_echo
- If true, stuff sent to the parent side of the pty will not be echoed back to the parent side, otherwise it will be echoed back. Default is false.
Beware that this may not always work as expected.raw_mode
- If true, sets the terminal to raw mode. Default is false.use_path
- If true, the pty:startproc() method for the created pty will search for an executable file if the specified filename does not contain a slash (/) character. The search path is the path specified in the parents environment by the PATH variable, no default will be used.
If false, the path to the executable must be explicitly specified. Default is true.separate_stderr
- If true, then a separate channel will be opened to be used as the slaves’ stderr stream. You can then read data from this channel by using the readerr() method, and retrieve its file descriptor with geterrfd(). Default is false.
If you do not want to pass any options, you may omit the options table altogether.
Process handling
ok = pty:startproc(command, arg1, arg2, ...)
Starts a process with the slave side of the pty as its controlling terminal. The child processes stdin, stdout and stderr are set to use the slave side of th pty. The command and its arguments must be separate arguments to the startproc method. If the pty has been created withuse_path=true
(the default), then this uses the PATH environment variable of the calling process (the one using lpty) to find any executables, so in that case specifying a full path to the command is not always necessary. Returns true if the call tofork()
was successful. If anything goes wrong whilst starting the new process, an error is signalled (that is, thrown or returned as two values nil, “error message”, depending on the setting of the throw_errors flag.
If there is already an active process attached to the pty, the new process is not started and the method returns false.
Thepty:startproc()
method can not check whether running the command in the child process or basically anything that happens in the child process after the fork is successful. Ifpty:startproc()
returns true, this means that setting up the client side of the tty and fork()ing went well and yielded a running child process. You may want to check wether it is still running when you need it using thepty:hasproc()
method, especially if you’re dealing with an interactive program. If the program the child was supposed to start is not running when you expect it to, you will have to call thepty:exitstatus()
method and maybe even check the contents of the pty for the reason.pty:endproc([kill])
Terminates the process running with the pty as its controlling terminal. If the optional parameter kill is false or omitted, send the child process a SIGTERM, otherwise send a SIGKILL. It is not an error to call this on a pty object that has no active child processes. You do not need to call it when you have already terminated the child process using more civilized means (like, for example, sending it a quit command it understands). After the child process has been terminated, a new one can be started using the same PTY.ok = pty:hasproc()
returns true if the pty has an active child process, false if not (none has been created, the child has terminated).reason, code = pty:exitstatus()
Determines the reason why a previous child process has terminated. Returns a the reason for termination and the code. Possible combinations are:false, nil
- if the pty has an active child process or never had one,"exit", exitcode
- if the child process has exited normally,"sig", signum
- if the child process was terminated by a signal,"unk", 0
- if the reason for the death of the child process can not be determined any more.
The
pty:exitstatus()
method works by having a fixed length ringbuffer, in which the signal handler responsible for reaping child processes places whatwaitpid()
returns. Because of the fixed size, when you create a lot of processes, the ring buffer might wrap before you retrieve the exit status of your process, in which case the'unk', 0
values will be returned, ‘unk’ meaning “unknown”. If you hit this a lot, you may want to check the exit status earlier, or if that does not help, change the value forEXITSTATUS_BUFSIZ
in lpty.c.
Process Environments
env = pty:getenviron()
Returns a table containing the environment a child process of the pty will be executed in. If no custom environment has been set using thepty:setenviron()
method, this will be the environment the parent process is running in.pty:setenviron(env)
Sets a custom environment for the child process to be executed within the pty. The argument must be a table containing a complete environment for the child process. If you want to modify the current environment, obtain it with thepty:getenviron()
method, modify it and pass it into this method as a new environment.
Calling this function with nil as its argument resets the environment to the environment the parent process is running in.
Communication
Where the following functions have a timeout argument, the timeout is specified in seconds. The value can be fractional, i.e. you can specify a timeout of 0.1 seconds. The resolution depends on the underlying system.
ok = pty:readok([timeout])
returns true if the pty has data available to be read, false otherwise. If a timeout value is specified, the call will wait at most that amount of seconds for data to become available to read. No value is equivalent to a timeout of 0 seconds.data = pty:read([timeout])
reads data from the pty. This is a blocking read, so you can specify an optional timeout in seconds after which the read should return to the caller whether there was data or not. If the timeout is omitted, the read will block until there is something to read. Returns the data read, or nil if the request timed out or was interrupted (failed with EINTR or ECHILD). If an error occurred during the read attempt, it is signalled to lua.
Note: unless you specified no_local_echo=true when creating the pty, this reads data output from the client process as well as data sent to the client process from the controlling skript.
The pty:read() method reads its data into a buffer. The size of this buffer determines how much data can be returned from an individual read. It is set by a #define in the module source and by default is set to 4096. Considering the C-typical terminating zero, this will leave a maximum of 4095 bytes to be read in one go. In order to fetch all available data, just loop while pty:readok() returns true and concatenate the results.data = pty:readline([wantnl [, timeout]])
reads an individual line from the pty. A line in this case is defined as either everything that can be read up to and including the next newline character, or everything that can be read until there is nothing more to read from the pty. If you can read faster than the remote side writes, this may mean that your line is truncated.
If wantnl is specified and true, the closing newline character, if present, is returned at the end of the string, otherwise it will be removed.
Note that, as long as we actually can read stuff from the pty, the call will not time out, even if all the reading takes longer than the timeout value specifies.
The notes for pty:read() also apply here.data = pty:expect(pattern [, plain [, timeout]])
consumes all input until pattern is matched. If plain is true, pattern is taken as a literal string, otherwise it is a lua pattern. timeout is a timeout value, in seconds, for the expect operation. Returns the match, or if the pattern has captures, all of the captures, or nil on timeout.
Note that in any case, pty:expect() will have consumed as much input as it could get. However, as it reads from the pty line by line, only all lines up to and including the one with the match will be consumed, everything after that will still be available.
Also, as long as we actually can read stuff from the pty, the call will not time out, even if all the reading takes longer than the timeout value specifies.
Because expect() may read stuff faster than the remote side writes it, you may get spurious matches. It is probably a good idea to call expect with some timeout, unless you know your data is available.data = pty:readerr([timeout])
reads data from the clients’ stderr stream, if that was separated using the separate_stderr flag. Returns any data written to stderr by the client, or nil if there is no data available or if the stderr stream was not separated in the pty. If a timeout value is specified, the call will wait at most that many seconds for data to become available on the stderr stream. No value is equivalent to a timeout of 0 seconds. If the stderr channel has not been separated, then the timeout will be ignored and the method returns immediately.ok = pty:sendok([timeout])
returns true if the pty can accept data sent to it, false otherwise. If a timeout value is specified, the call will wait at most that many seconds for the pty to accept data, No value is equivalent to a timeout of 0 seconds.sent = pty:send(data [, timeout])
sends data to the pty. This will then be available to the child process on its stdin. As sending data may block, there is an optional argument for a timeout in seconds on the send operation. If the timeout argument is omitted, pty:send() will block until the pty can accept the data. Returns the number of bytes written, or nil if the send timed out or was interrupted (failed with EINTR or ECHILD). If an error occurred during the attempt to write it is signalled to lua.
The pty:send() method may not send all data you gave it in one go. For small amounts of data this is quite improbable, but in industrial strength applications you should probably check the return value of the pty:send() method against the length of the data you intended to send.pty:flush([mode])
flushes data from the pty. mode is an optional string argument. If mode is “i”, flush data received but not read. If mode is “o”, flush data written but not transmitted. If mode is anything else or omitted, flush both sides.
Note that the mode “o” is hardly ever useful, because when send() returns, the data it claims to have sent will already be transmitted to the client side.
Information
name = pty:ttyname()
return the tty name of the slave side of the pty.fd = pty:getfd()
return the file handle of the master side of the pty. This is intended for use with luasocket’s select() or luaposix’s poll() and similar functions.fd = pty:geterrfd()
return the file handle of the master side of the separated stderr stream. This will be u20131 if the stderr stream has not been separated.tbl = pty:getflags()
return a table with the flags and their values that were used on creation of the pty, as specifiable for lpty.new()ok = pty:setflag(flag, value)
set or reset any of the pty creation flags. The primary use for this is to re-enable no_local_echo or raw mode, if a controlled program has changed those settings, but you can also change all other flags.
Additional Details
The local echo thing
As written in the description of the no_local_echo flag, this may not always work as expected. Programs are free to set their terminals to whatever settings they like, and indeed some programs do, or even completely roll their own local echo policy. The most notable piece of code to do this is Gnu readline, though it seems that this does not happen for all versions of it. On Mac OS X, it certainly does, whereas on my Ubuntu Box it does not. So if you write something using lpty that is supposed to be used on different unixen, you better not rely on no_local_echo to do what you think it should.
Communication and process handling
If you have a pty without a running child process, you can still write to it. Anything you write to the pty will be available as input to a child process that you start later.
You can also read from a pty without a child process, but only what you have written to it - unless you specified no_local_echo=true for pty creation, in which case there is nothing to read from it. This means that if you (or the last active child process) have nothing written to it, a read with timeout will always time out, and a read without timeout will block. Also, if you read stuff back from the master side of a pty without a child, that you have written there, it will not be removed from a subsequent child process’s input.
Starting a process takes some time, so after you have called
pty:startproc()
it may take a while for data to become available from
the child process. So don’t be surprised if a pty:readok()
fails
directly after a pty:startproc()
.
Similar things are true for calls to pty:hasproc()
. If you call
pty:hasproc()
directly after pty:startproc()
, chances are it will
return true whatever the outcome of starting the child process was,
because it just may not have had enough time to crash. Calling
pty:hasproc()
will yield a much saner result if you do it after, for
example, a pty:read()
timed out when you didn’t expect it to, or
similar occurrences.
Garbage disposal
When a lpty object is garbage collected, its master side pty handle is closed, and if it has a child process, that is terminated with a SIGKILL. So it is reasonably safe to let pty’s with active processes become garbage, it just isn’t very friendly towards the child processes.
Example
A very simple example for lpty follows. In a real application you should probably do some checking for errors and stuff output by your child process. This uses bc to compute the sum of 2 numbers:
lpty = require "lpty"
p = lpty.new()
p:startproc("lua")
p:read() -- skip startup message
p:send("=111+234n")
r = p:read()
print("Result is "..r)
p:send("os.exit()n") -- terminate lua all friendly-like
print("Done.")
Also, see the samples folder in the distribution archive.
References
Read up on ptys on your local friendly linux system: man 7 pty and friends. This link is linux specific, but as lpty is implemented using Unix 98 pseudo ttys, which Linux is using, the information is probably valid whereever lpty runs.