summaryrefslogtreecommitdiffstats
path: root/job.c
diff options
context:
space:
mode:
Diffstat (limited to 'job.c')
-rw-r--r--job.c415
1 files changed, 415 insertions, 0 deletions
diff --git a/job.c b/job.c
new file mode 100644
index 0000000..d2d9adb
--- /dev/null
+++ b/job.c
@@ -0,0 +1,415 @@
+/* $OpenBSD$ */
+
+/*
+ * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
+ * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+ * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "tmux.h"
+
+/*
+ * Job scheduling. Run queued commands in the background and record their
+ * output.
+ */
+
+static void job_read_callback(struct bufferevent *, void *);
+static void job_write_callback(struct bufferevent *, void *);
+static void job_error_callback(struct bufferevent *, short, void *);
+
+/* A single job. */
+struct job {
+ enum {
+ JOB_RUNNING,
+ JOB_DEAD,
+ JOB_CLOSED
+ } state;
+
+ int flags;
+
+ char *cmd;
+ pid_t pid;
+ char tty[TTY_NAME_MAX];
+ int status;
+
+ int fd;
+ struct bufferevent *event;
+
+ job_update_cb updatecb;
+ job_complete_cb completecb;
+ job_free_cb freecb;
+ void *data;
+
+ LIST_ENTRY(job) entry;
+};
+
+/* All jobs list. */
+static LIST_HEAD(joblist, job) all_jobs = LIST_HEAD_INITIALIZER(all_jobs);
+
+/* Start a job running. */
+struct job *
+job_run(const char *cmd, int argc, char **argv, struct environ *e, struct session *s,
+ const char *cwd, job_update_cb updatecb, job_complete_cb completecb,
+ job_free_cb freecb, void *data, int flags, int sx, int sy)
+{
+ struct job *job;
+ struct environ *env;
+ pid_t pid;
+ int nullfd, out[2], master;
+ const char *home;
+ sigset_t set, oldset;
+ struct winsize ws;
+ char **argvp, tty[TTY_NAME_MAX];
+
+ /*
+ * Do not set TERM during .tmux.conf, it is nice to be able to use
+ * if-shell to decide on default-terminal based on outside TERM.
+ */
+ env = environ_for_session(s, !cfg_finished);
+ if (e != NULL)
+ environ_copy(e, env);
+
+ sigfillset(&set);
+ sigprocmask(SIG_BLOCK, &set, &oldset);
+
+ if (flags & JOB_PTY) {
+ memset(&ws, 0, sizeof ws);
+ ws.ws_col = sx;
+ ws.ws_row = sy;
+ pid = fdforkpty(ptm_fd, &master, tty, NULL, &ws);
+ } else {
+ if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, out) != 0)
+ goto fail;
+ pid = fork();
+ }
+ if (cmd == NULL) {
+ cmd_log_argv(argc, argv, "%s:", __func__);
+ log_debug("%s: cwd=%s", __func__, cwd == NULL ? "" : cwd);
+ } else {
+ log_debug("%s: cmd=%s, cwd=%s", __func__, cmd,
+ cwd == NULL ? "" : cwd);
+ }
+
+ switch (pid) {
+ case -1:
+ if (~flags & JOB_PTY) {
+ close(out[0]);
+ close(out[1]);
+ }
+ goto fail;
+ case 0:
+ proc_clear_signals(server_proc, 1);
+ sigprocmask(SIG_SETMASK, &oldset, NULL);
+
+ if ((cwd == NULL || chdir(cwd) != 0) &&
+ ((home = find_home()) == NULL || chdir(home) != 0) &&
+ chdir("/") != 0)
+ fatal("chdir failed");
+
+ environ_push(env);
+ environ_free(env);
+
+ if (~flags & JOB_PTY) {
+ if (dup2(out[1], STDIN_FILENO) == -1)
+ fatal("dup2 failed");
+ if (dup2(out[1], STDOUT_FILENO) == -1)
+ fatal("dup2 failed");
+ if (out[1] != STDIN_FILENO && out[1] != STDOUT_FILENO)
+ close(out[1]);
+ close(out[0]);
+
+ nullfd = open(_PATH_DEVNULL, O_RDWR);
+ if (nullfd == -1)
+ fatal("open failed");
+ if (dup2(nullfd, STDERR_FILENO) == -1)
+ fatal("dup2 failed");
+ if (nullfd != STDERR_FILENO)
+ close(nullfd);
+ }
+ closefrom(STDERR_FILENO + 1);
+
+ if (cmd != NULL) {
+ execl(_PATH_BSHELL, "sh", "-c", cmd, (char *) NULL);
+ fatal("execl failed");
+ } else {
+ argvp = cmd_copy_argv(argc, argv);
+ execvp(argvp[0], argvp);
+ fatal("execvp failed");
+ }
+ }
+
+ sigprocmask(SIG_SETMASK, &oldset, NULL);
+ environ_free(env);
+
+ job = xmalloc(sizeof *job);
+ job->state = JOB_RUNNING;
+ job->flags = flags;
+
+ if (cmd != NULL)
+ job->cmd = xstrdup(cmd);
+ else
+ job->cmd = cmd_stringify_argv(argc, argv);
+ job->pid = pid;
+ strlcpy(job->tty, tty, sizeof job->tty);
+ job->status = 0;
+
+ LIST_INSERT_HEAD(&all_jobs, job, entry);
+
+ job->updatecb = updatecb;
+ job->completecb = completecb;
+ job->freecb = freecb;
+ job->data = data;
+
+ if (~flags & JOB_PTY) {
+ close(out[1]);
+ job->fd = out[0];
+ } else
+ job->fd = master;
+ setblocking(job->fd, 0);
+
+ job->event = bufferevent_new(job->fd, job_read_callback,
+ job_write_callback, job_error_callback, job);
+ if (job->event == NULL)
+ fatalx("out of memory");
+ bufferevent_enable(job->event, EV_READ|EV_WRITE);
+
+ log_debug("run job %p: %s, pid %ld", job, job->cmd, (long) job->pid);
+ return (job);
+
+fail:
+ sigprocmask(SIG_SETMASK, &oldset, NULL);
+ environ_free(env);
+ return (NULL);
+}
+
+/* Take job's file descriptor and free the job. */
+int
+job_transfer(struct job *job, pid_t *pid, char *tty, size_t ttylen)
+{
+ int fd = job->fd;
+
+ log_debug("transfer job %p: %s", job, job->cmd);
+
+ if (pid != NULL)
+ *pid = job->pid;
+ if (tty != NULL)
+ strlcpy(tty, job->tty, ttylen);
+
+ LIST_REMOVE(job, entry);
+ free(job->cmd);
+
+ if (job->freecb != NULL && job->data != NULL)
+ job->freecb(job->data);
+
+ if (job->event != NULL)
+ bufferevent_free(job->event);
+
+ free(job);
+ return (fd);
+}
+
+/* Kill and free an individual job. */
+void
+job_free(struct job *job)
+{
+ log_debug("free job %p: %s", job, job->cmd);
+
+ LIST_REMOVE(job, entry);
+ free(job->cmd);
+
+ if (job->freecb != NULL && job->data != NULL)
+ job->freecb(job->data);
+
+ if (job->pid != -1)
+ kill(job->pid, SIGTERM);
+ if (job->event != NULL)
+ bufferevent_free(job->event);
+ if (job->fd != -1)
+ close(job->fd);
+
+ free(job);
+}
+
+/* Resize job. */
+void
+job_resize(struct job *job, u_int sx, u_int sy)
+{
+ struct winsize ws;
+
+ if (job->fd == -1 || (~job->flags & JOB_PTY))
+ return;
+
+ log_debug("resize job %p: %ux%u", job, sx, sy);
+
+ memset(&ws, 0, sizeof ws);
+ ws.ws_col = sx;
+ ws.ws_row = sy;
+ if (ioctl(job->fd, TIOCSWINSZ, &ws) == -1)
+ fatal("ioctl failed");
+}
+
+/* Job buffer read callback. */
+static void
+job_read_callback(__unused struct bufferevent *bufev, void *data)
+{
+ struct job *job = data;
+
+ if (job->updatecb != NULL)
+ job->updatecb(job);
+}
+
+/*
+ * Job buffer write callback. Fired when the buffer falls below watermark
+ * (default is empty). If all the data has been written, disable the write
+ * event.
+ */
+static void
+job_write_callback(__unused struct bufferevent *bufev, void *data)
+{
+ struct job *job = data;
+ size_t len = EVBUFFER_LENGTH(EVBUFFER_OUTPUT(job->event));
+
+ log_debug("job write %p: %s, pid %ld, output left %zu", job, job->cmd,
+ (long) job->pid, len);
+
+ if (len == 0 && (~job->flags & JOB_KEEPWRITE)) {
+ shutdown(job->fd, SHUT_WR);
+ bufferevent_disable(job->event, EV_WRITE);
+ }
+}
+
+/* Job buffer error callback. */
+static void
+job_error_callback(__unused struct bufferevent *bufev, __unused short events,
+ void *data)
+{
+ struct job *job = data;
+
+ log_debug("job error %p: %s, pid %ld", job, job->cmd, (long) job->pid);
+
+ if (job->state == JOB_DEAD) {
+ if (job->completecb != NULL)
+ job->completecb(job);
+ job_free(job);
+ } else {
+ bufferevent_disable(job->event, EV_READ);
+ job->state = JOB_CLOSED;
+ }
+}
+
+/* Job died (waitpid() returned its pid). */
+void
+job_check_died(pid_t pid, int status)
+{
+ struct job *job;
+
+ LIST_FOREACH(job, &all_jobs, entry) {
+ if (pid == job->pid)
+ break;
+ }
+ if (job == NULL)
+ return;
+ if (WIFSTOPPED(status)) {
+ if (WSTOPSIG(status) == SIGTTIN || WSTOPSIG(status) == SIGTTOU)
+ return;
+ killpg(job->pid, SIGCONT);
+ return;
+ }
+ log_debug("job died %p: %s, pid %ld", job, job->cmd, (long) job->pid);
+
+ job->status = status;
+
+ if (job->state == JOB_CLOSED) {
+ if (job->completecb != NULL)
+ job->completecb(job);
+ job_free(job);
+ } else {
+ job->pid = -1;
+ job->state = JOB_DEAD;
+ }
+}
+
+/* Get job status. */
+int
+job_get_status(struct job *job)
+{
+ return (job->status);
+}
+
+/* Get job data. */
+void *
+job_get_data(struct job *job)
+{
+ return (job->data);
+}
+
+/* Get job event. */
+struct bufferevent *
+job_get_event(struct job *job)
+{
+ return (job->event);
+}
+
+/* Kill all jobs. */
+void
+job_kill_all(void)
+{
+ struct job *job;
+
+ LIST_FOREACH(job, &all_jobs, entry) {
+ if (job->pid != -1)
+ kill(job->pid, SIGTERM);
+ }
+}
+
+/* Are any jobs still running? */
+int
+job_still_running(void)
+{
+ struct job *job;
+
+ LIST_FOREACH(job, &all_jobs, entry) {
+ if ((~job->flags & JOB_NOWAIT) && job->state == JOB_RUNNING)
+ return (1);
+ }
+ return (0);
+}
+
+/* Print job summary. */
+void
+job_print_summary(struct cmdq_item *item, int blank)
+{
+ struct job *job;
+ u_int n = 0;
+
+ LIST_FOREACH(job, &all_jobs, entry) {
+ if (blank) {
+ cmdq_print(item, "%s", "");
+ blank = 0;
+ }
+ cmdq_print(item, "Job %u: %s [fd=%d, pid=%ld, status=%d]",
+ n, job->cmd, job->fd, (long)job->pid, job->status);
+ n++;
+ }
+}