Skip to content

Commit

Permalink
job: Add options to spawn jobs connected to pseudo terminals
Browse files Browse the repository at this point in the history
  • Loading branch information
tarruda committed Feb 23, 2015
1 parent 16fa725 commit c3fbf48
Show file tree
Hide file tree
Showing 10 changed files with 407 additions and 8 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ if(BUSTED_PRG)
-P ${PROJECT_SOURCE_DIR}/cmake/RunTests.cmake
DEPENDS nvim-test unittest-headers)

add_subdirectory(test/functional/job) # compile pty test program

add_custom_target(functionaltest
COMMAND ${CMAKE_COMMAND}
-DBUSTED_PRG=${BUSTED_PRG}
Expand Down
1 change: 1 addition & 0 deletions src/nvim/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ list(APPEND NVIM_LINK_LIBRARIES
${LIBTERMKEY_LIBRARIES}
${LIBUNIBILIUM_LIBRARIES}
m
util
${CMAKE_THREAD_LIBS_INIT}
)

Expand Down
23 changes: 20 additions & 3 deletions src/nvim/eval.c
Original file line number Diff line number Diff line change
Expand Up @@ -6489,7 +6489,7 @@ static struct fst {
{"islocked", 1, 1, f_islocked},
{"items", 1, 1, f_items},
{"jobsend", 2, 2, f_jobsend},
{"jobstart", 2, 3, f_jobstart},
{"jobstart", 2, 4, f_jobstart},
{"jobstop", 1, 1, f_jobstop},
{"join", 1, 2, f_join},
{"keys", 1, 1, f_keys},
Expand Down Expand Up @@ -10682,8 +10682,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv)

if (argvars[0].v_type != VAR_STRING
|| argvars[1].v_type != VAR_STRING
|| (argvars[2].v_type != VAR_LIST
&& argvars[2].v_type != VAR_UNKNOWN)) {
|| (argvars[2].v_type != VAR_LIST && argvars[2].v_type != VAR_UNKNOWN)) {
// Wrong argument types
EMSG(_(e_invarg));
return;
Expand Down Expand Up @@ -10731,6 +10730,24 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv)
opts.stdout_cb = on_job_stdout;
opts.stderr_cb = on_job_stderr;
opts.exit_cb = on_job_exit;

if (argvars[3].v_type == VAR_DICT) {
dict_T *job_opts = argvars[3].vval.v_dict;
opts.pty = true;
uint16_t width = get_dict_number(job_opts, (uint8_t *)"width");
if (width > 0) {
opts.width = width;
}
uint16_t height = get_dict_number(job_opts, (uint8_t *)"height");
if (height > 0) {
opts.height = height;
}
char *term = (char *)get_dict_string(job_opts, (uint8_t *)"TERM", true);
if (term) {
opts.term_name = term;
}
}

job_start(opts, &rettv->vval.v_number);

if (rettv->vval.v_number <= 0) {
Expand Down
13 changes: 12 additions & 1 deletion src/nvim/os/job_defs.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ typedef struct {
job_exit_cb exit_cb;
// Maximum memory used by the job's WStream
size_t maxmem;
// Connect the job to a pseudo terminal
bool pty;
// Initial window dimensions if the job is connected to a pseudo terminal
uint16_t width, height;
// Value for the $TERM environment variable. A default value of "ansi" is
// assumed if NULL
char *term_name;
} JobOptions;

#define JOB_OPTIONS_INIT ((JobOptions) { \
Expand All @@ -46,6 +53,10 @@ typedef struct {
.stdout_cb = NULL, \
.stderr_cb = NULL, \
.exit_cb = NULL, \
.maxmem = 0 \
.maxmem = 0, \
.pty = false, \
.width = 80, \
.height = 24, \
.term_name = NULL \
})
#endif // NVIM_OS_JOB_DEFS_H
21 changes: 17 additions & 4 deletions src/nvim/os/job_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "nvim/os/rstream_defs.h"
#include "nvim/os/wstream_defs.h"
#include "nvim/os/pipe_process.h"
#include "nvim/os/pty_process.h"
#include "nvim/os/shell.h"
#include "nvim/log.h"

Expand Down Expand Up @@ -43,22 +44,34 @@ extern uv_timer_t job_stop_timer;

static inline bool process_spawn(Job *job)
{
return pipe_process_spawn(job);
return job->opts.pty ? pty_process_spawn(job) : pipe_process_spawn(job);
}

static inline void process_init(Job *job)
{
pipe_process_init(job);
if (job->opts.pty) {
pty_process_init(job);
} else {
pipe_process_init(job);
}
}

static inline void process_close(Job *job)
{
pipe_process_close(job);
if (job->opts.pty) {
pty_process_close(job);
} else {
pipe_process_close(job);
}
}

static inline void process_destroy(Job *job)
{
pipe_process_destroy(job);
if (job->opts.pty) {
pty_process_destroy(job);
} else {
pipe_process_destroy(job);
}
}

static inline void job_exit_callback(Job *job)
Expand Down
200 changes: 200 additions & 0 deletions src/nvim/os/pty_process.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Some of the code came from st(http://st.suckless.org/) and libuv
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/wait.h>

// openpty is not in POSIX, so headers are platform-specific
#if defined(__linux)
#include <pty.h>
#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__)
#include <util.h>
#elif defined(__FreeBSD__) || defined(__DragonFly__)
#include <libutil.h>
#endif

#include <uv.h>

#include "nvim/os/uv_helpers.h"
#include "nvim/os/job.h"
#include "nvim/os/job_defs.h"
#include "nvim/os/job_private.h"
#include "nvim/os/pty_process.h"
#include "nvim/memory.h"

#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "os/pty_process.c.generated.h"
#endif

typedef struct {
struct winsize winsize;
uv_pipe_t proc_stdin, proc_stdout, proc_stderr;
uv_signal_t schld;
} PtyProcess;

void pty_process_init(Job *job)
{
PtyProcess *ptyproc = xmalloc(sizeof(PtyProcess));

if (job->opts.writable) {
uv_pipe_init(uv_default_loop(), &ptyproc->proc_stdin, 0);
ptyproc->proc_stdin.data = NULL;
}

if (job->opts.stdout_cb) {
uv_pipe_init(uv_default_loop(), &ptyproc->proc_stdout, 0);
ptyproc->proc_stdout.data = NULL;
}

if (job->opts.stderr_cb) {
uv_pipe_init(uv_default_loop(), &ptyproc->proc_stderr, 0);
ptyproc->proc_stderr.data = NULL;
}

job->proc_stdin = (uv_stream_t *)&ptyproc->proc_stdin;
job->proc_stdout = (uv_stream_t *)&ptyproc->proc_stdout;
job->proc_stderr = (uv_stream_t *)&ptyproc->proc_stderr;
job->process = ptyproc;
}

void pty_process_destroy(Job *job)
{
free(job->opts.term_name);
free(job->process);
job->process = NULL;
}

bool pty_process_spawn(Job *job)
{
int master, slave;
PtyProcess *ptyproc = job->process;
ptyproc->winsize = (struct winsize){job->opts.height, job->opts.width, 0, 0};

if (openpty(&master, &slave, NULL, NULL, &ptyproc->winsize)) {
return false;
}

int pid = fork();

if (pid < 0) {
return false;
} else if (pid == 0) {
close(master);
init_child(job, slave);
abort();
}

close(slave);
// make sure the master file descriptor is non blocking
fcntl(master, F_SETFL, fcntl(master, F_GETFL) | O_NONBLOCK);

if (job->opts.writable) {
uv_pipe_open(&ptyproc->proc_stdin, dup(master));
}

if (job->opts.stdout_cb) {
uv_pipe_open(&ptyproc->proc_stdout, dup(master));
}

if (job->opts.stderr_cb) {
uv_pipe_open(&ptyproc->proc_stderr, dup(master));
}

close(master);
uv_signal_init(uv_default_loop(), &ptyproc->schld);
uv_signal_start(&ptyproc->schld, chld_handler, SIGCHLD);
ptyproc->schld.data = job;
job->pid = pid;
return true;
}

void pty_process_close(Job *job)
{
job_close_streams(job);
job_decref(job);
}

static void init_child(Job *job, int slave)
{
// child, become session leader
setsid();
// close stdin/stdout/stderr
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// and reopen connected to the slave side of the pty or to /dev/null if
// it should be ignored
if (job->opts.writable) {
dup(slave);
} else {
open("/dev/null", O_RDONLY);
}
if (job->opts.stdout_cb) {
dup(slave);
} else {
open("/dev/null", O_RDWR);
}
if (job->opts.stderr_cb) {
dup(slave);
} else {
open("/dev/null", O_RDWR);
}

if (ioctl(slave, TIOCSCTTY, NULL) < 0) {
fprintf(stderr, "ioctl TIOCSTTY failed: %s\n", strerror(errno));
abort();
}

close(slave);
// Call uv_close on every active handle
uv_walk(uv_default_loop(), walk_cb, NULL);
// Run the event loop until all handles are successfully closed
while (uv_loop_close(uv_default_loop())) {
uv_run(uv_default_loop(), UV_RUN_ONCE);
}

unsetenv("COLUMNS");
unsetenv("LINES");
unsetenv("TERMCAP");
unsetenv("COLORTERM");
unsetenv("COLORFGBG");

signal(SIGCHLD, SIG_DFL);
signal(SIGHUP, SIG_DFL);
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
signal(SIGTERM, SIG_DFL);
signal(SIGALRM, SIG_DFL);

setenv("TERM", job->opts.term_name ? job->opts.term_name : "ansi", 1);
execvp(job->opts.argv[0], job->opts.argv);
fprintf(stderr, "execvp failed: %s\n", strerror(errno));
abort();
}

static void walk_cb(uv_handle_t *handle, void *arg) {
if (!uv_is_closing(handle)) {
uv_close(handle, NULL);
}
}

static void chld_handler(uv_signal_t *handle, int signum)
{
Job *job = handle->data;
int stat = 0;

if (waitpid(job->pid, &stat, 0) < 0) {
fprintf(stderr, "Waiting for pid %d failed: %s\n", job->pid,
strerror(errno));
return;
}

if (WIFEXITED(stat)) {
job->status = WEXITSTATUS(stat);
pty_process_close(job);
}
}
7 changes: 7 additions & 0 deletions src/nvim/os/pty_process.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#ifndef NVIM_OS_PTY_PROCESS_H
#define NVIM_OS_PTY_PROCESS_H

#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "os/pty_process.h.generated.h"
#endif
#endif // NVIM_OS_PTY_PROCESS_H
2 changes: 2 additions & 0 deletions test/functional/job/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
add_executable(tty-test tty-test.c)
target_link_libraries(tty-test ${LIBUV_LIBRARIES})
32 changes: 32 additions & 0 deletions test/functional/job/job_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,36 @@ describe('jobs', function()
eq({'notification', 'j', {{jobid, 'stdout', {'abcdef'}}}}, next_message())
eq({'notification', 'j', {{jobid, 'exit'}}}, next_message())
end)

describe('running tty-test program', function()
local function next_chunk()
local msg = next_message()
local data = msg[3][1]
for i = 1, #data do
data[i] = data[i]:gsub('\n', '\000')
end
return table.concat(data, '\n')
end

local function send(str)
nvim('command', 'call jobsend(j, "'..str..'")')
end

before_each(function()
nvim('command', notify_str('v:job_data[1]', 'get(v:job_data, 2)'))
end)

it('ok when pty options are passed', function()
nvim('command', "let j = jobstart('xxx', 'build/bin/tty-test', [], {})")
eq('tty ready\r', next_chunk())
send('test')
-- the tty driver will echo input by default
eq('test', next_chunk())
send('\\<c-c>')
eq('^Cinterrupt received, press again to exit\r', next_chunk())
send('\\<c-c>')
eq('^Ctty done\r', next_chunk())
eq({'notification', 'exit', {0}}, next_message())
end)
end)
end)

0 comments on commit c3fbf48

Please sign in to comment.