/*
* linx_tty -- the NIF backing `Linx.Tty`.
*
* Wraps the small set of termios(3) and tty ioctl(2) syscalls
* `Linx.Tty` exposes:
*
* - open(/dev/tty) + cfmakeraw + tcsetattr (raw-mode entry)
* - tcsetattr + close (restore exit)
* - ioctl(TIOCGWINSZ) / ioctl(TIOCSWINSZ) (window size)
*
* The work is short and syscall-shaped, so a plain (non-dirty) NIF is
* the right tool -- no scheduler-blocking concern. fd lifetimes are
* caller-controlled: the NIF returns the integer fd, the Elixir side
* wraps it (`:erlang.open_port({:fd, _, _}, _)`) and hands it back to
* the close/restore call.
*
* ERROR SHAPE
* -----------
* Every fallible function returns either the success shape (`:ok`,
* `{:ok, ...}`) or `{:error, {Stage::atom, ErrnoAtom | ErrnoInt}}`.
* Common Linux errnos are mapped to POSIX-style atoms (`:enxio`,
* `:enotty`, `:eperm`, ...) so callers can pattern-match cleanly; any
* errno we don't recognise falls back to the raw integer.
*
* The TERMIOS BLOB
* ----------------
* `open_controlling_raw/0` returns the saved `struct termios` as a
* binary -- its bytes are an opaque token, not part of the public API.
* `restore_and_close/2` accepts the same shape unchanged. The Elixir
* side wraps it in `%Linx.Tty.Saved{}` so the type is visible at call
* sites; the binary itself is not meant for inspection or mutation.
*/
#include <erl_nif.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <termios.h>
#include <unistd.h>
#define LINX_TTY_VERSION "linx_tty"
/* --- errno -> atom ------------------------------------------------------- */
static const char *errno_atom(int err)
{
switch (err) {
case EACCES: return "eacces";
case EBADF: return "ebadf";
case EBUSY: return "ebusy";
case EINTR: return "eintr";
case EINVAL: return "einval";
case EIO: return "eio";
case ENOENT: return "enoent";
case ENOMEM: return "enomem";
case ENOTTY: return "enotty";
case ENXIO: return "enxio";
case EPERM: return "eperm";
default: return NULL;
}
}
static ERL_NIF_TERM make_errno(ErlNifEnv *env, int err)
{
const char *a = errno_atom(err);
if (a)
return enif_make_atom(env, a);
return enif_make_int(env, err);
}
/* {error, {Stage::atom, ErrnoAtom | ErrnoInt}} */
static ERL_NIF_TERM make_error(ErlNifEnv *env, const char *stage, int err)
{
return enif_make_tuple2(
env,
enif_make_atom(env, "error"),
enif_make_tuple2(env,
enif_make_atom(env, stage),
make_errno(env, err)));
}
/* --- version ------------------------------------------------------------ */
static ERL_NIF_TERM version(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
(void)argc;
(void)argv;
ErlNifBinary bin;
size_t len = strlen(LINX_TTY_VERSION);
if (!enif_alloc_binary(len, &bin))
return enif_make_badarg(env);
memcpy(bin.data, LINX_TTY_VERSION, len);
return enif_make_binary(env, &bin);
}
/* --- open_controlling_raw/0 ---------------------------------------------- */
/* Open /dev/tty, save the current termios, switch to raw mode.
*
* Returns {:ok, Fd::int, Saved::binary} or {:error, {Stage, Errno}}
* where Stage is one of :open, :tcgetattr, :tcsetattr. The common
* "no controlling terminal" case (process detached from a tty)
* surfaces as {:error, {:open, :enxio}}.
*
* The fd is opened O_RDWR | O_NOCTTY | O_CLOEXEC -- the agent does
* not want /dev/tty to become its *new* controlling tty (it already
* has one if we got here at all), and CLOEXEC keeps the fd from
* leaking into any child the BEAM later forks. */
static ERL_NIF_TERM open_controlling_raw(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[])
{
(void)argc;
(void)argv;
int fd = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC);
if (fd < 0)
return make_error(env, "open", errno);
struct termios saved;
if (tcgetattr(fd, &saved) < 0) {
int e = errno;
close(fd);
return make_error(env, "tcgetattr", e);
}
struct termios raw = saved;
cfmakeraw(&raw);
if (tcsetattr(fd, TCSANOW, &raw) < 0) {
int e = errno;
close(fd);
return make_error(env, "tcsetattr", e);
}
/* The saved termios travels back to Elixir as an opaque binary. */
ErlNifBinary bin;
if (!enif_alloc_binary(sizeof saved, &bin)) {
close(fd);
return make_error(env, "alloc", ENOMEM);
}
memcpy(bin.data, &saved, sizeof saved);
return enif_make_tuple3(env,
enif_make_atom(env, "ok"),
enif_make_int(env, fd),
enif_make_binary(env, &bin));
}
/* --- restore_and_close/2 ------------------------------------------------- */
/* Restore the saved termios on `fd` and close it.
*
* Idempotent against already-closed fds: EBADF from either tcsetattr
* or close is treated as success, so a double-restore from an `after`
* block stacked on top of an early failure path is safe. */
static ERL_NIF_TERM restore_and_close(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[])
{
(void)argc;
int fd;
if (!enif_get_int(env, argv[0], &fd))
return enif_make_badarg(env);
ErlNifBinary bin;
if (!enif_inspect_binary(env, argv[1], &bin))
return enif_make_badarg(env);
if (bin.size != sizeof(struct termios))
return enif_make_badarg(env);
struct termios saved;
memcpy(&saved, bin.data, sizeof saved);
int rc = tcsetattr(fd, TCSANOW, &saved);
int tc_errno = (rc < 0) ? errno : 0;
int closed = close(fd);
int close_errno = (closed < 0) ? errno : 0;
if (tc_errno && tc_errno != EBADF)
return make_error(env, "tcsetattr", tc_errno);
if (close_errno && close_errno != EBADF)
return make_error(env, "close", close_errno);
return enif_make_atom(env, "ok");
}
/* --- window_size/1, set_window_size/2 ------------------------------------ */
static ERL_NIF_TERM window_size(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[])
{
(void)argc;
int fd;
if (!enif_get_int(env, argv[0], &fd))
return enif_make_badarg(env);
struct winsize ws;
if (ioctl(fd, TIOCGWINSZ, &ws) < 0)
return make_error(env, "ioctl", errno);
return enif_make_tuple2(
env,
enif_make_atom(env, "ok"),
enif_make_tuple4(env,
enif_make_uint(env, ws.ws_row),
enif_make_uint(env, ws.ws_col),
enif_make_uint(env, ws.ws_xpixel),
enif_make_uint(env, ws.ws_ypixel)));
}
static ERL_NIF_TERM set_window_size(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[])
{
(void)argc;
int fd;
if (!enif_get_int(env, argv[0], &fd))
return enif_make_badarg(env);
int arity;
const ERL_NIF_TERM *tuple;
if (!enif_get_tuple(env, argv[1], &arity, &tuple) || arity != 4)
return enif_make_badarg(env);
unsigned rows, cols, xpix, ypix;
if (!enif_get_uint(env, tuple[0], &rows) ||
!enif_get_uint(env, tuple[1], &cols) ||
!enif_get_uint(env, tuple[2], &xpix) ||
!enif_get_uint(env, tuple[3], &ypix))
return enif_make_badarg(env);
/* struct winsize uses unsigned short for each field -- the kernel
* silently truncates a wider value. Reject obviously-too-big
* values up front so the caller sees an EINVAL-shaped error
* rather than a confused kernel call. */
if (rows > 0xFFFF || cols > 0xFFFF || xpix > 0xFFFF || ypix > 0xFFFF)
return make_error(env, "ioctl", EINVAL);
struct winsize ws = {
.ws_row = (unsigned short)rows,
.ws_col = (unsigned short)cols,
.ws_xpixel = (unsigned short)xpix,
.ws_ypixel = (unsigned short)ypix,
};
if (ioctl(fd, TIOCSWINSZ, &ws) < 0)
return make_error(env, "ioctl", errno);
return enif_make_atom(env, "ok");
}
/* --- socketpair/0 ------------------------------------------------------- */
/* Create a connected AF_UNIX SOCK_STREAM pair (CLOEXEC on both ends).
*
* The two fds together act as an in-memory tube: bytes written to one
* appear readable on the other. Primarily here so `Linx.Tty.attach/2`
* tests can stand in for a real `/dev/tty` -- one end wrapped as the
* "user side" port the test drives, the other wrapped as the port the
* attach loop reads from. Useful enough as a general primitive that
* we expose it (under @doc false on the Elixir side).
*
* Returns {:ok, {Fd1, Fd2}} or {:error, {:socketpair, ErrnoAtom}}.
* Both fds belong to the caller -- close(2) them when done. */
static ERL_NIF_TERM nif_socketpair(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[])
{
(void)argc;
(void)argv;
int sv[2];
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sv) < 0)
return make_error(env, "socketpair", errno);
return enif_make_tuple2(
env,
enif_make_atom(env, "ok"),
enif_make_tuple2(env,
enif_make_int(env, sv[0]),
enif_make_int(env, sv[1])));
}
/* --- close/1 ------------------------------------------------------------ */
/* close(fd). Used by tests + by callers who own an fd handed back by
* `socketpair/0`. EBADF is swallowed (idempotent close). */
static ERL_NIF_TERM nif_close(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[])
{
(void)argc;
int fd;
if (!enif_get_int(env, argv[0], &fd))
return enif_make_badarg(env);
if (close(fd) < 0 && errno != EBADF)
return make_error(env, "close", errno);
return enif_make_atom(env, "ok");
}
/* --- NIF init ----------------------------------------------------------- */
static ErlNifFunc nif_funcs[] = {
{ "version", 0, version, 0 },
{ "open_controlling_raw", 0, open_controlling_raw, 0 },
{ "restore_and_close", 2, restore_and_close, 0 },
{ "window_size", 1, window_size, 0 },
{ "set_window_size", 2, set_window_size, 0 },
{ "socketpair", 0, nif_socketpair, 0 },
{ "close", 1, nif_close, 0 },
};
ERL_NIF_INIT(Elixir.Linx.Tty.Native, nif_funcs, NULL, NULL, NULL, NULL)