~emersion/mrsh

4b0435e60a86d710080ab7aac5b7ba8b23b29928 — emersion 1 year, 7 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 @@ static const struct builtin builtins[] = {
	{ ".", 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 @@ typedef int (*mrsh_builtin_func_t)(struct mrsh_state *state,
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_eval(struct mrsh_state *state, int argc, char *argv[]);
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_state {
	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 @@ void mrsh_pop_args(struct mrsh_state *state);
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 main(int argc, char *argv[]) {
		}
	}

	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 @@ int main(int argc, char *argv[]) {
			}

			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 @@ int main(int argc, char *argv[]) {
	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 @@ lib_mrsh = library(
		'ast.c',
		'buffer.c',
		'builtin/alias.c',
		'builtin/bg.c',
		'builtin/builtin.c',
		'builtin/cd.c',
		'builtin/colon.c',


@@ 71,6 72,7 @@ lib_mrsh = library(
		'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 @@ lib_mrsh = library(
		'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 @@ static struct mrsh_array running_processes = {0};
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 @@ void process_notify(pid_t pid, int stat) {
	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 @@ static bool task_process_start(struct task_command *tc, struct context *ctx) {
		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 @@ static bool task_process_start(struct task_command *tc, struct context *ctx) {
	}

	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 @@ int task_run(struct task *task, struct context *ctx) {
			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);
	}
}