diff --git a/userland/testbin/Makefile b/userland/testbin/Makefile index 5627997..44d1787 100644 --- a/userland/testbin/Makefile +++ b/userland/testbin/Makefile @@ -10,7 +10,7 @@ SUBDIRS=add argtest badcall bigexec bigfile bigfork bigseek bloat conman \ filetest fileonlytest forkbomb forktest frack guzzle hash hog huge kitchen \ malloctest matmult multiexec palin parallelvm poisondisk psort \ quinthuge quintmat quintsort randcall redirect rmdirtest rmtest \ - sbrktest schedpong sink sort sparsefile spinner sty tail tictac \ + sbrktest schedpong shll sink sort sparsefile spinner sty tail tictac \ triplehuge triplemat triplesort usemtest waiter zero # But not: diff --git a/userland/testbin/shll/Makefile b/userland/testbin/shll/Makefile new file mode 100644 index 0000000..fbbb44d --- /dev/null +++ b/userland/testbin/shll/Makefile @@ -0,0 +1,11 @@ +# Makefile for shll + +TOP=../../.. +.include "$(TOP)/mk/os161.config.mk" + +PROG=shll +SRCS=shll.c +BINDIR=/testbin + + +.include "$(TOP)/mk/os161.prog.mk" diff --git a/userland/testbin/shll/shll.c b/userland/testbin/shll/shll.c new file mode 100644 index 0000000..de54379 --- /dev/null +++ b/userland/testbin/shll/shll.c @@ -0,0 +1,573 @@ +/* + * shll - sh that drops characters for automation testing + * + * Usage: + * shll [-p percent] + * -p percent of characters to drop (default 5) + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HOST +#include "hostcompat.h" +#endif + +#ifndef NARG_MAX +/* no NARG_MAX on most unixes */ +#define NARG_MAX 1024 +#endif + +/* avoid making this unreasonably large; causes problems under dumbvm */ +#define CMDLINE_MAX 4096 + +/* struct to (portably) hold exit info */ +struct exitinfo { + unsigned val:8, + signaled:1, + stopped:1, + coredump:1; +}; + +/* set to nonzero if __time syscall seems to work */ +static int timing = 0; + +/* array of backgrounded jobs (allows "foregrounding") */ +#define MAXBG 128 +static pid_t bgpids[MAXBG]; + +/* percent of characters to drop */ +static int percent = 5; + +/* + * can_bg + * just checks for an open slot. + */ +static +int +can_bg(void) +{ + int i; + + for (i = 0; i < MAXBG; i++) { + if (bgpids[i] == 0) { + return 1; + } + } + + return 0; +} + +/* + * remember_bg + * sticks the pid in an open slot in the background array. note the assert -- + * better check can_bg before calling this. + */ +static +void +remember_bg(pid_t pid) +{ + int i; + for (i = 0; i < MAXBG; i++) { + if (bgpids[i] == 0) { + bgpids[i] = pid; + return; + } + } + assert(0); +} + +/* + * constructor for exitinfo + */ +static +void +exitinfo_exit(struct exitinfo *ei, int code) +{ + ei->val = code; + ei->signaled = 0; + ei->stopped = 0; + ei->coredump = 0; +} + +/* + * readstatus + * unpack results from wait + */ +static +void +readstatus(int status, struct exitinfo *ei) +{ + if (WIFEXITED(status)) { + ei->val = WEXITSTATUS(status); + ei->signaled = 0; + ei->stopped = 0; + ei->coredump = 0; + } + else if (WIFSIGNALED(status) && WCOREDUMP(status)) { + ei->val = WTERMSIG(status); + ei->signaled = 1; + ei->stopped = 0; + ei->coredump = 1; + } + else if (WIFSIGNALED(status)) { + ei->val = WTERMSIG(status); + ei->signaled = 1; + ei->stopped = 0; + ei->coredump = 0; + } + else if (WIFSTOPPED(status)) { + ei->val = WSTOPSIG(status); + ei->signaled = 0; + ei->stopped = 1; + ei->coredump = 0; + } + else { + printf("Invalid status code %d", status); + ei->val = status; + ei->signaled = 0; + ei->stopped = 0; + ei->coredump = 0; + } + +} + +/* + * printstatus + * print results from wait + */ +static +void +printstatus(const struct exitinfo *ei, int printexitzero) +{ + if (ei->signaled && ei->coredump) { + printf("Signal %d (core dumped)\n", ei->val); + } + else if (ei->signaled) { + printf("Signal %d\n", ei->val); + } + else if (ei->stopped) { + printf("Stopped on signal %d\n", ei->val); + } + else if (printexitzero || ei->val != 0) { + printf("Exit %d\n", ei->val); + } +} + +/* + * dowait + * just does a waitpid. + */ +static +void +dowait(pid_t pid) +{ + struct exitinfo ei; + int status; + + if (waitpid(pid, &status, 0) < 0) { + warn("pid %d", pid); + } + else { + printf("pid %d: ", pid); + readstatus(status, &ei); + printstatus(&ei, 1); + } +} + +#ifdef WNOHANG +/* + * dowaitpoll + * like dowait, but uses WNOHANG. returns true if we got something. + */ +static +int +dowaitpoll(pid_t pid) +{ + struct exitinfo ei; + pid_t foundpid; + int status; + + foundpid = waitpid(pid, &status, WNOHANG); + if (foundpid < 0) { + warn("pid %d", pid); + } + else if (foundpid != 0) { + printf("pid %d: ", pid); + readstatus(status, &ei); + printstatus(&ei, 1); + return 1; + } + return 0; +} + +/* + * waitpoll + * poll all background jobs for having exited. + */ +static +void +waitpoll(void) +{ + int i; + for (i=0; i < MAXBG; i++) { + if (bgpids[i] != 0) { + if (dowaitpoll(bgpids[i])) { + bgpids[i] = 0; + } + } + } +} +#endif /* WNOHANG */ + +/* + * wait + * allows the user to "foreground" a process by waiting on it. without ps to + * know the pids, this is a little tough to use with an arg, but without an + * arg it will wait for all the background jobs. + */ +static +void +cmd_wait(int ac, char *av[], struct exitinfo *ei) +{ + int i; + pid_t pid; + + if (ac == 2) { + pid = atoi(av[1]); + dowait(pid); + for (i = 0; i < MAXBG; i++) { + if (bgpids[i]==pid) { + bgpids[i] = 0; + } + } + exitinfo_exit(ei, 0); + return; + } + else if (ac == 1) { + for (i=0; i < MAXBG; i++) { + if (bgpids[i] != 0) { + dowait(bgpids[i]); + bgpids[i] = 0; + } + } + exitinfo_exit(ei, 0); + return; + } + printf("Usage: wait [pid]\n"); + exitinfo_exit(ei, 1); +} + +/* + * chdir + * just an interface to the system call. no concept of home directory, so + * require the directory. + */ +static +void +cmd_chdir(int ac, char *av[], struct exitinfo *ei) +{ + if (ac == 2) { + if (chdir(av[1])) { + warn("chdir: %s", av[1]); + exitinfo_exit(ei, 1); + return; + } + exitinfo_exit(ei, 0); + return; + } + printf("Usage: chdir dir\n"); + exitinfo_exit(ei, 1); +} + +/* + * exit + * pretty simple. allow the user to choose the exit code if they want, + * otherwise default to 0 (success). + */ +static +void +cmd_exit(int ac, char *av[], struct exitinfo *ei) +{ + int code; + + if (ac == 1) { + code = 0; + } + else if (ac == 2) { + code = atoi(av[1]); + } + else { + printf("Usage: exit [code]\n"); + exitinfo_exit(ei, 1); + return; + } + + exit(code); +} + +/* + * a struct of the builtins associates the builtin name with the function that + * executes it. they must all take an argc and argv. + */ +static struct { + const char *name; + void (*func)(int, char **, struct exitinfo *); +} builtins[] = { + { "cd", cmd_chdir }, + { "chdir", cmd_chdir }, + { "exit", cmd_exit }, + { "wait", cmd_wait }, + { NULL, NULL } +}; + +/* + * docommand + * tokenizes the command line using strtok. if there aren't any commands, + * simply returns. checks to see if it's a builtin, running it if it is. + * otherwise, it's a standard command. check for the '&', try to background + * the job if possible, otherwise just run it and wait on it. + */ +static +void +docommand(char *buf, struct exitinfo *ei) +{ + char *args[NARG_MAX + 1]; + int nargs, i; + char *s; + pid_t pid; + int status; + int bg=0; + time_t startsecs, endsecs; + unsigned long startnsecs, endnsecs; + + nargs = 0; + for (s = strtok(buf, " \t\r\n"); s; s = strtok(NULL, " \t\r\n")) { + if (nargs >= NARG_MAX) { + printf("%s: Too many arguments " + "(exceeds system limit)\n", + args[0]); + exitinfo_exit(ei, 1); + return; + } + args[nargs++] = s; + } + args[nargs] = NULL; + + if (nargs==0) { + /* empty line */ + exitinfo_exit(ei, 0); + return; + } + + for (i=0; builtins[i].name; i++) { + if (!strcmp(builtins[i].name, args[0])) { + builtins[i].func(nargs, args, ei); + return; + } + } + + /* Not a builtin; run it */ + + if (nargs > 0 && !strcmp(args[nargs-1], "&")) { + /* background */ + if (!can_bg()) { + printf("%s: Too many background jobs; wait for " + "some to finish before starting more\n", + args[0]); + exitinfo_exit(ei, 1); + return; + } + nargs--; + args[nargs] = NULL; + bg = 1; + } + + if (timing) { + __time(&startsecs, &startnsecs); + } + + pid = fork(); + switch (pid) { + case -1: + /* error */ + warn("fork"); + exitinfo_exit(ei, 255); + return; + case 0: + /* child */ + execvp(args[0], args); + warn("%s", args[0]); + /* + * Use _exit() instead of exit() in the child + * process to avoid calling atexit() functions, + * which would cause hostcompat (if present) to + * reset the tty state and mess up our input + * handling. + */ + _exit(1); + default: + break; + } + + /* parent */ + if (bg) { + /* background this command */ + remember_bg(pid); + printf("[%d] %s ... &\n", pid, args[0]); + exitinfo_exit(ei, 0); + return; + } + + if (waitpid(pid, &status, 0) < 0) { + warn("waitpid"); + exitinfo_exit(ei, 255); + } + else { + readstatus(status, ei); + } + + if (timing) { + __time(&endsecs, &endnsecs); + if (endnsecs < startnsecs) { + endnsecs += 1000000000; + endsecs--; + } + endnsecs -= startnsecs; + endsecs -= startsecs; + warnx("subprocess time: %lu.%09lu seconds", + (unsigned long) endsecs, (unsigned long) endnsecs); + } +} + +/* + * getcmd + * pulls valid characters off the console, filling the buffer. + * backspace deletes a character, simply by moving the position back. + * a newline or carriage return breaks the loop, which terminates + * the string and returns. + * + * if there's an invalid character or a backspace when there's nothing + * in the buffer, putchars an alert (bell). + */ +static +void +getcmd(char *buf, size_t len) +{ + size_t pos = 0; + int done=0, ch; + + /* + * In the absence of a , assume input is 7-bit ASCII. + */ + + while (!done) { + ch = getchar(); + if (random() % 100 < percent) { + continue; + } + if ((ch == '\b' || ch == 127) && pos > 0) { + putchar('\b'); + putchar(' '); + putchar('\b'); + pos--; + } + else if (ch == '\r' || ch == '\n') { + putchar('\r'); + putchar('\n'); + done = 1; + } + else if (ch >= 32 && ch < 127 && pos < len-1) { + buf[pos++] = ch; + putchar(ch); + } + else { + /* alert (bell) character */ + putchar('\a'); + } + } + buf[pos] = 0; +} + +/* + * interactive + * runs the interactive shell. basically, just infinitely loops, grabbing + * commands and running them (and printing the exit status if it's not + * success.) + */ +static +void +interactive(void) +{ + char buf[CMDLINE_MAX]; + struct exitinfo ei; + + while (1) { + printsf("OS/161$ "); + getcmd(buf, sizeof(buf)); + docommand(buf, &ei); + printstatus(&ei, 0); +#ifdef WNOHANG + waitpoll(); +#endif + } +} + +static +void +check_timing(void) +{ + time_t secs; + unsigned long nsecs; + if (__time(&secs, &nsecs) != -1) { + timing = 1; + warnx("Timing enabled."); + } +} + +static +void +usage(void) +{ + printf("Usage: shll [-p percent]\n"); + printf(" -p percent of characters to drop (default 5)\n"); + exit(1); +} + +/* + * main + * if there are no arguments, run interactively, otherwise, run a program + * from within the shell, but immediately exit. + */ + +int +main(int argc, char *argv[]) +{ +#ifdef HOST + hostcompat_init(argc, argv); +#endif + check_timing(); + + /* + * Allow argc to be 0 in case we're running on a broken kernel, + * or one that doesn't set argv when starting the first shell. + */ + + if (argc == 0 || argc == 1) { + interactive(); + } else if (argc == 3 && !strcmp(argv[1], "-p")) { + percent = atoi(argv[2]); + interactive(); + } else { + usage(); + } + return 0; +}