4b0435e60a86d710080ab7aac5b7ba8b23b29928 — emersion 10 months ago 1c6cbcf job-control
wip: job control
A builtin/bg.c => builtin/bg.c +43 -0
@@ 0,0 1,43 @@
+#include <mrsh/array.h>
+#include <mrsh/getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include "builtin.h"
+#include "shell/job.h"
+
+// TODO: bg [job_id]
+static const char bg_usage[] = "usage: bg\n";
+
+int builtin_bg(struct mrsh_state *state, int argc, char *argv[]) {
+	mrsh_optind = 1;
+	int opt;
+	while ((opt = mrsh_getopt(argc, argv, ":")) != -1) {
+		switch (opt) {
+		default:
+			fprintf(stderr, "bg: unknown option -- %c\n", mrsh_optopt);
+			fprintf(stderr, bg_usage);
+			return EXIT_FAILURE;
+		}
+	}
+	if (mrsh_optind < argc) {
+		fprintf(stderr, bg_usage);
+		return EXIT_FAILURE;
+	}
+
+	struct job *bg = NULL;
+	for (ssize_t i = jobs.len - 1; i >= 0; --i) {
+		struct job *j = jobs.data[i];
+		if (j != job_foreground()) {
+			bg = j;
+			break;
+		}
+	}
+	if (bg == NULL) {
+		fprintf(stderr, "bg: no current job");
+		return EXIT_FAILURE;
+	}
+
+	job_continue(bg);
+
+	return EXIT_SUCCESS;
+}

M builtin/builtin.c => builtin/builtin.c +2 -0
@@ 17,11 17,13 @@
 	{ ".", builtin_dot, true },
 	{ ":", builtin_colon, true },
 	{ "alias", builtin_alias, false },
+	{ "bg", builtin_bg, false },
 	{ "cd", builtin_cd, false },
 	{ "eval", builtin_eval, true },
 	{ "exit", builtin_exit, true },
 	{ "export", builtin_export, true },
 	{ "false", builtin_false, false },
+	{ "fg", builtin_fg, false },
 	{ "getopts", builtin_getopts, false },
 	{ "pwd", builtin_pwd, false },
 	{ "read", builtin_read, false },

A builtin/fg.c => builtin/fg.c +44 -0
@@ 0,0 1,44 @@
+#include <mrsh/array.h>
+#include <mrsh/getopt.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include "builtin.h"
+#include "shell/job.h"
+
+// TODO: fg [job_id]
+static const char fg_usage[] = "usage: fg\n";
+
+int builtin_fg(struct mrsh_state *state, int argc, char *argv[]) {
+	mrsh_optind = 1;
+	int opt;
+	while ((opt = mrsh_getopt(argc, argv, ":")) != -1) {
+		switch (opt) {
+		default:
+			fprintf(stderr, "fg: unknown option -- %c\n", mrsh_optopt);
+			fprintf(stderr, fg_usage);
+			return EXIT_FAILURE;
+		}
+	}
+	if (mrsh_optind < argc) {
+		fprintf(stderr, fg_usage);
+		return EXIT_FAILURE;
+	}
+
+	struct job *bg = NULL;
+	for (ssize_t i = jobs.len - 1; i >= 0; --i) {
+		struct job *j = jobs.data[i];
+		if (j != job_foreground()) {
+			bg = j;
+			break;
+		}
+	}
+	if (bg == NULL) {
+		fprintf(stderr, "fg: no current job");
+		return EXIT_FAILURE;
+	}
+
+	job_set_foreground(bg, true);
+	job_continue(bg);
+
+	return EXIT_SUCCESS;
+}

M include/builtin.h => include/builtin.h +2 -0
@@ 9,6 9,7 @@
 void print_escaped(const char *value);
 
 int builtin_alias(struct mrsh_state *state, int argc, char *argv[]);
+int builtin_bg(struct mrsh_state *state, int argc, char *argv[]);
 int builtin_cd(struct mrsh_state *state, int argc, char *argv[]);
 int builtin_colon(struct mrsh_state *state, int argc, char *argv[]);
 int builtin_dot(struct mrsh_state *state, int argc, char *argv[]);


@@ 16,6 17,7 @@
 int builtin_exit(struct mrsh_state *state, int argc, char *argv[]);
 int builtin_export(struct mrsh_state *state, int argc, char *argv[]);
 int builtin_false(struct mrsh_state *state, int argc, char *argv[]);
+int builtin_fg(struct mrsh_state *state, int argc, char *argv[]);
 int builtin_getopts(struct mrsh_state *state, int argc, char *argv[]);
 int builtin_pwd(struct mrsh_state *state, int argc, char *argv[]);
 int builtin_read(struct mrsh_state *state, int argc, char *argv[]);

M include/mrsh/shell.h => include/mrsh/shell.h +5 -0
@@ 6,6 6,7 @@
 #include <mrsh/hashtable.h>
 #include <stdint.h>
 #include <stdio.h>
+#include <termios.h>
 
 enum mrsh_option {
 	// -a: When this option is on, the export attribute shall be set for each


@@ 88,6 89,9 @@
 	struct mrsh_hashtable aliases; // char *
 	struct mrsh_hashtable functions; // mrsh_function *
 	int last_status;
+	pid_t pgid;
+	int fd;
+	struct termios term_modes;
 };
 
 void mrsh_function_destroy(struct mrsh_function *fn);


@@ 104,5 108,6 @@
 int mrsh_run_program(struct mrsh_state *state, struct mrsh_program *prog);
 int mrsh_run_word(struct mrsh_state *state, struct mrsh_word **word);
 bool mrsh_run_arithm_expr(struct mrsh_arithm_expr *expr, long *result);
+bool mrsh_set_job_control(struct mrsh_state *state, bool enabled);
 
 #endif

A include/shell/job.h => include/shell/job.h +38 -0
@@ 0,0 1,38 @@
+#ifndef SHELL_JOB_H
+#define SHELL_JOB_H
+
+#include <stdbool.h>
+#include <sys/types.h>
+#include <termios.h>
+
+/**
+ * A job is a group of processes. When job control is enabled, jobs can be:
+ *
+ * - Put in the foreground or in the background
+ * - Stopped and continued
+ *
+ * The shell will typically wait for the foreground job to finish or to stop
+ * before displaying its prompt.
+ */
+struct job {
+	pid_t pgid;
+	struct mrsh_state *state;
+	struct termios term_modes;
+	// TODO: list of processes
+};
+
+// TODO: these shouldn't be globals
+extern struct mrsh_array jobs; // struct job *
+
+struct job *job_foreground(void);
+struct job *job_create(struct mrsh_state *state);
+void job_destroy(struct job *job);
+void job_add_process(struct job *job, pid_t pid);
+bool job_set_foreground(struct job *job, bool foreground);
+bool job_continue(struct job *job);
+bool job_wait(struct job *job);
+
+bool job_child_init(struct mrsh_state *state, bool foreground);
+void job_notify(pid_t pid, int stat);
+
+#endif

M include/shell/process.h => include/shell/process.h +2 -1
@@ 5,10 5,11 @@
 #include <sys/types.h>
 
 /**
- * This struct is used to track child processes.
+ * A child process.
  */
 struct process {
 	pid_t pid;
+	bool stopped;
 	bool finished;
 	int stat;
 };

M main.c => main.c +11 -3
@@ 130,17 130,19 @@
 		}
 	}
 
-	int fd = -1;
+	state.fd = -1;
 	struct mrsh_buffer parser_buffer = {0};
 	struct mrsh_parser *parser;
 	if (state.interactive) {
 		interactive_init(&state);
 		parser = mrsh_parser_with_buffer(&parser_buffer);
+		state.fd = STDIN_FILENO;
 	} else {
 		if (init_args.command_str) {
 			parser = mrsh_parser_with_data(init_args.command_str,
 				strlen(init_args.command_str));
 		} else {
+			int fd;
 			if (init_args.command_file) {
 				fd = open(init_args.command_file, O_RDONLY | O_CLOEXEC);
 				if (fd < 0) {


@@ 153,10 155,16 @@
 			}
 
 			parser = mrsh_parser_with_fd(fd);
+			state.fd = fd;
 		}
 	}
+
 	mrsh_parser_set_alias(parser, get_alias, &state);
 
+	if (state.interactive) {
+		mrsh_set_job_control(&state, true);
+	}
+
 	struct mrsh_buffer read_buffer = {0};
 	while (state.exit == -1) {
 		if (state.interactive) {


@@ 226,8 234,8 @@
 	mrsh_parser_destroy(parser);
 	mrsh_buffer_finish(&parser_buffer);
 	mrsh_state_finish(&state);
-	if (fd >= 0) {
-		close(fd);
+	if (state.fd >= 0) {
+		close(state.fd);
 	}
 
 	return state.exit;

M meson.build => meson.build +3 -0
@@ 63,6 63,7 @@
 		'ast.c',
 		'buffer.c',
 		'builtin/alias.c',
+		'builtin/bg.c',
 		'builtin/builtin.c',
 		'builtin/cd.c',
 		'builtin/colon.c',


@@ 71,6 72,7 @@
 		'builtin/exit.c',
 		'builtin/export.c',
 		'builtin/false.c',
+		'builtin/fg.c',
 		'builtin/getopts.c',
 		'builtin/pwd.c',
 		'builtin/read.c',


@@ 91,6 93,7 @@
 		'parser/program.c',
 		'parser/word.c',
 		'shell/arithm.c',
+		'shell/job.c',
 		'shell/path.c',
 		'shell/process.c',
 		'shell/redir.c',

A shell/job.c => shell/job.c +226 -0
@@ 0,0 1,226 @@
+#define _POSIX_C_SOURCE 1
+#include <assert.h>
+#include <errno.h>
+#include <mrsh/array.h>
+#include <mrsh/shell.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include "shell/job.h"
+#include "shell/process.h"
+
+static const int ignored_signals[] = {
+	SIGINT,
+	SIGQUIT,
+	SIGTSTP,
+	SIGTTIN,
+	SIGTTOU,
+};
+
+static const size_t IGNORED_SIGNALS_LEN =
+		sizeof(ignored_signals) / sizeof(ignored_signals[0]);
+
+// TODO: these shouldn't be globals
+struct mrsh_array jobs = { 0 };
+static bool job_control_enabled = false;
+static struct job *foreground_job = NULL;
+
+bool mrsh_set_job_control(struct mrsh_state *state, bool enabled) {
+	assert(state->fd >= 0);
+
+	if (job_control_enabled == enabled) {
+		return true;
+	}
+
+	if (enabled) {
+		// Loop until we are in the foreground
+		while (true) {
+			pid_t pgid = getpgrp();
+			if (tcgetpgrp(state->fd) == pgid) {
+				break;
+			}
+			kill(-pgid, SIGTTIN);
+		}
+
+		// Ignore interactive and job-control signals
+		struct sigaction sa = { .sa_handler = SIG_IGN };
+		sigemptyset(&sa.sa_mask);
+		for (size_t i = 0; i < IGNORED_SIGNALS_LEN; ++i) {
+			sigaction(ignored_signals[i], &sa, NULL);
+		}
+
+		// Put ourselves in our own process group
+		state->pgid = getpid();
+		if (setpgid(state->pgid, state->pgid) < 0) {
+			return false;
+		}
+
+		// Grab control of the terminal
+		tcsetpgrp(state->fd, state->pgid);
+		// Save default terminal attributes for shell
+		tcgetattr(state->fd, &state->term_modes);
+	} else {
+		return false; // TODO
+	}
+
+	job_control_enabled = enabled;
+	return true;
+}
+
+bool job_child_init(struct mrsh_state *state, bool foreground) {
+	// TODO: don't always create a new process group
+	pid_t pgid = getpid();
+	if (setpgid(pgid, pgid) < 0) {
+		return false;
+	}
+
+	if (foreground) {
+		tcsetpgrp(state->fd, pgid);
+	}
+
+	struct sigaction sa = { .sa_handler = SIG_DFL };
+	sigemptyset(&sa.sa_mask);
+	for (size_t i = 0; i < IGNORED_SIGNALS_LEN; ++i) {
+		sigaction(ignored_signals[i], &sa, NULL);
+	}
+
+	return true;
+}
+
+struct job *job_create(struct mrsh_state *state) {
+	struct job *job = calloc(1, sizeof(struct job));
+	job->state = state;
+	job->pgid = 0;
+	mrsh_array_add(&jobs, job);
+	return job;
+}
+
+static void array_remove(struct mrsh_array *array, size_t i) {
+	memmove(&array->data[i], &array->data[i + 1],
+			(array->len - i - 1) * sizeof(void *));
+	--array->len;
+}
+
+static void job_remove(struct job *job) {
+	assert(foreground_job != job);
+
+	for (size_t i = 0; i < jobs.len; ++i) {
+		if (jobs.data[i] == job) {
+			array_remove(&jobs, i);
+			break;
+		}
+	}
+}
+
+void job_destroy(struct job *job) {
+	if (job == NULL) {
+		return;
+	}
+	job_set_foreground(job, false);
+	job_remove(job);
+	free(job);
+}
+
+void job_add_process(struct job *job, pid_t pid) {
+	if (job->pgid == 0) {
+		job->pgid = pid;
+	}
+
+	setpgid(pid, job->pgid);
+}
+
+static struct job *job_from_pid(pid_t pid) {
+	for (size_t i = 0; i < jobs.len; ++i) {
+		struct job *job = jobs.data[i];
+		// TODO: search in process list
+		if (job->pgid == pid) {
+			return job;
+		}
+	}
+	return NULL;
+}
+
+bool job_set_foreground(struct job *job, bool foreground) {
+	struct mrsh_state *state = job->state;
+
+	bool is_foreground = foreground_job == job;
+	if (is_foreground == foreground) {
+		return true;
+	}
+
+	if (foreground) {
+		if (foreground_job != NULL) {
+			job_set_foreground(foreground_job, false);
+		}
+
+		tcsetpgrp(state->fd, job->pgid);
+
+		// TODO: only do this if we want to continue this job
+		//tcsetattr(state->fd, TCSADRAIN, &job->term_modes);
+		foreground_job = job;
+	} else {
+		if (tcgetpgrp(state->fd) == job->pgid) {
+			tcsetpgrp(state->fd, state->pgid);
+
+			tcgetattr(state->fd, &job->term_modes);
+			tcsetattr(state->fd, TCSADRAIN, &state->term_modes);
+		}
+
+		if (foreground_job == job) {
+			foreground_job = NULL;
+		}
+	}
+
+	return true;
+}
+
+bool job_continue(struct job *job) {
+	// TODO: mark all processes as non-stopped
+	return kill(-job->pgid, SIGCONT) >= 0;
+}
+
+struct job *job_foreground(void) {
+	return foreground_job;
+}
+
+bool job_wait(struct job *job) {
+	while (true) {
+		int stat;
+		pid_t pid;
+		do {
+			pid = waitpid(-1, &stat, WUNTRACED);
+		} while (pid == -1 && errno == EINTR);
+
+		if (pid == -1) {
+			fprintf(stderr, "failed to waitpid(): %s\n", strerror(errno));
+			return false;
+		}
+		printf("waitpid() = %d, %d, WIFSTOPPED=%d, WIFEXITED=%d, WIFSIGNALED=%d\n",
+			pid, stat, WIFSTOPPED(stat), WIFEXITED(stat), WIFSIGNALED(stat)); // TODO: remove me
+
+		process_notify(pid, stat);
+
+		struct job *waited_job = job_from_pid(pid);
+		if (waited_job == NULL) {
+			continue;
+		}
+
+		// TODO: only works if job has only one process
+		// TODO: figure out when to destroy jobs
+		if (WIFSTOPPED(stat)) {
+			job_set_foreground(job, false);
+		} else if (WIFEXITED(stat) || WIFSIGNALED(stat)) {
+			job_set_foreground(job, false);
+			job_remove(job);
+		} else {
+			assert(false);
+		}
+
+		if (job == waited_job) {
+			return true;
+		}
+	}
+}

M shell/process.c => shell/process.c +9 -4
@@ 1,3 1,4 @@
+#include <assert.h>
 #include <mrsh/array.h>
 #include <stdbool.h>
 #include <string.h>


@@ 11,8 12,6 @@
 void process_init(struct process *proc, pid_t pid) {
 	mrsh_array_add(&running_processes, proc);
 	proc->pid = pid;
-	proc->finished = false;
-	proc->stat = 0;
 }
 
 int process_poll(struct process *proc) {


@@ 45,9 44,15 @@
 	for (size_t i = 0; i < running_processes.len; ++i) {
 		struct process *proc = running_processes.data[i];
 		if (proc->pid == pid) {
-			proc->finished = true;
 			proc->stat = stat;
-			process_remove(proc);
+			if (WIFSTOPPED(stat)) {
+				proc->stopped = true;
+			} else if (WIFEXITED(stat) || WIFSIGNALED(stat)) {
+				proc->finished = true;
+				process_remove(proc);
+			} else {
+				assert(false);
+			}
 			break;
 		}
 	}

M shell/task/command_process.c => shell/task/command_process.c +10 -0
@@ 3,6 3,7 @@
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
+#include "shell/job.h"
 #include "shell/path.h"
 #include "shell/redir.h"
 #include "shell/task_command.h"


@@ 28,6 29,8 @@
 		fprintf(stderr, "failed to fork(): %s\n", strerror(errno));
 		return false;
 	} else if (pid == 0) {
+		job_child_init(ctx->state, true); // TODO: foreground=false
+
 		for (size_t i = 0; i < sc->assignments.len; ++i) {
 			struct mrsh_assignment *assign = sc->assignments.data[i];
 			uint32_t prev_attribs;


@@ 75,6 78,13 @@
 	}
 
 	process_init(&tc->process, pid);
+
+	// TODO: don't always create a new job
+	struct job *job = job_create(ctx->state);
+	job_add_process(job, pid);
+	// TODO: don't always put in foreground
+	job_set_foreground(job, true);
+
 	return true;
 }
 

M shell/task/task.c => shell/task/task.c +6 -11
@@ 4,7 4,7 @@
 #include <stdlib.h>
 #include <string.h>
 #include <sys/wait.h>
-#include "shell/process.h"
+#include "shell/job.h"
 #include "shell/task.h"
 
 void task_init(struct task *task, const struct task_interface *impl) {


@@ 44,17 44,12 @@
 			return ret;
 		}
 
-		errno = 0;
-		int stat;
-		pid_t pid = waitpid(0, &stat, 0);
-		if (pid == -1) {
-			if (errno == EINTR) {
-				continue;
-			}
-			fprintf(stderr, "failed to waitpid(): %s\n", strerror(errno));
+		struct job *job = job_foreground();
+		if (job == NULL) {
+			return EXIT_SUCCESS;
+		}
+		if (!job_wait(job)) {
 			return -1;
 		}
-
-		process_notify(pid, stat);
 	}
 }