4b0435e60a86d710080ab7aac5b7ba8b23b29928 — emersion 8 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);
  	}
  }