From 8ccf98e02167b31e6bca0566b398c349f0a45518 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Fri, 8 Nov 2019 10:59:51 -0500 Subject: [PATCH] Problem set 5 for 2019. --- pset5/.gitignore | 5 + pset5/AUTHORS.md | 17 + pset5/GNUmakefile | 33 ++ pset5/README.md | 14 + pset5/build/rules.mk | 104 ++++++ pset5/check.pl | 860 +++++++++++++++++++++++++++++++++++++++++++ pset5/helpers.cc | 136 +++++++ pset5/sh61.cc | 185 ++++++++++ pset5/sh61.hh | 53 +++ 9 files changed, 1407 insertions(+) create mode 100644 pset5/.gitignore create mode 100644 pset5/AUTHORS.md create mode 100644 pset5/GNUmakefile create mode 100644 pset5/README.md create mode 100644 pset5/build/rules.mk create mode 100755 pset5/check.pl create mode 100644 pset5/helpers.cc create mode 100644 pset5/sh61.cc create mode 100644 pset5/sh61.hh diff --git a/pset5/.gitignore b/pset5/.gitignore new file mode 100644 index 0000000..e6a2e3b --- /dev/null +++ b/pset5/.gitignore @@ -0,0 +1,5 @@ +*.o +.deps +cmdline +out +sh61 diff --git a/pset5/AUTHORS.md b/pset5/AUTHORS.md new file mode 100644 index 0000000..0fb1ade --- /dev/null +++ b/pset5/AUTHORS.md @@ -0,0 +1,17 @@ +Author and collaborators +======================== + +Primary student +--------------- +(Your name.) + + +Collaborators +------------- +(List any other collaborators and describe help you got from other students +in the class.) + + +Citations +--------- +(List any other sources consulted.) diff --git a/pset5/GNUmakefile b/pset5/GNUmakefile new file mode 100644 index 0000000..525e91f --- /dev/null +++ b/pset5/GNUmakefile @@ -0,0 +1,33 @@ +# Default optimization level +O ?= 2 + +all: sh61 + +-include build/rules.mk + +%.o: %.cc sh61.hh $(BUILDSTAMP) + $(call run,$(CXX) $(CPPFLAGS) $(CXXFLAGS) $(DEPCFLAGS) $(O) -o $@ -c,COMPILE,$<) + +sh61: sh61.o helpers.o + $(call run,$(CXX) $(CXXFLAGS) $(O) -o $@ $^ $(LDFLAGS) $(LIBS),LINK $@) + +sleep61: sleep61.cc + $(call run,$(CXX) $(CPPFLAGS) $(CXXFLAGS) $(DEPCFLAGS) $(O) -o $@ $^ $(LDFLAGS) $(LIBS),BUILD $@) + +ifneq ($(filter -fsanitize=leak,$(CXXFLAGS)),) +LEAKCHECK = --leak +endif + +check: sh61 + perl check.pl $(LEAKCHECK) + +check-%: sh61 + perl check.pl $(LEAKCHECK) $(subst check-,,$@) + +clean: clean-main +clean-main: + $(call run,rm -f sh61 *.o *~ *.bak core *.core,CLEAN) + $(call run,rm -rf out *.dSYM $(DEPSDIR)) + +.PRECIOUS: %.o +.PHONY: all clean clean-main distclean check check-% diff --git a/pset5/README.md b/pset5/README.md new file mode 100644 index 0000000..cca15a6 --- /dev/null +++ b/pset5/README.md @@ -0,0 +1,14 @@ +CS 61 Problem Set 5 +=================== + +**Fill out both this file and `AUTHORS.md` before submitting.** We grade +anonymously, so put all personally identifying information, including +collaborators, in `AUTHORS.md`. + +Grading notes (if any) +---------------------- + + + +Extra credit attempted (if any) +------------------------------- diff --git a/pset5/build/rules.mk b/pset5/build/rules.mk new file mode 100644 index 0000000..ab3e9a1 --- /dev/null +++ b/pset5/build/rules.mk @@ -0,0 +1,104 @@ +# are we using clang? +ISCLANG := $(shell if $(CC) --version | grep -e 'LLVM\|clang' >/dev/null; then echo 1; fi) +ISLINUX := $(if $(wildcard /usr/include/linux/*.h),1,) + +CFLAGS := -std=gnu11 -W -Wall -Wshadow -g $(DEFS) $(CFLAGS) +CXXFLAGS := -std=gnu++1z -W -Wall -Wshadow -g $(DEFS) $(CXXFLAGS) +O ?= -O3 +ifeq ($(filter 0 1 2 3 s,$(O)$(NOOVERRIDEO)),$(strip $(O))) +override O := -O$(O) +endif + +# sanitizer arguments +ifndef SAN +SAN := $(or $(SANITIZE),$(ASAN),$(LSAN),$(LEAKSAN),$(TSAN),$(UBSAN)) +endif +ifndef TSAN + ifeq ($(WANT_TSAN),1) +TSAN := $(SAN) + endif +endif + +check_for_sanitizer = $(if $(strip $(shell $(CC) -fsanitize=$(1) -x c -E /dev/null 2>&1 | grep sanitize=)),$(info ** WARNING: The `$(CC)` compiler does not support `-fsanitize=$(1)`.),1) +ifeq ($(TSAN),1) + ifeq ($(or $(ISCLANG),$(filter-out 3.% 4.% 5.% 6.%,$(shell $(CC) -dumpversion)),$(filter-out Linux%,$(shell uname))),) +$(info ** WARNING: If ThreadSanitizer fails, try `make SAN=1 CC=clang`.) + endif + ifeq ($(call check_for_sanitizer,thread),1) +CFLAGS += -fsanitize=thread +CXXFLAGS += -fsanitize=thread + endif +else + ifeq ($(or $(ASAN),$(SAN)),1) + ifeq ($(call check_for_sanitizer,address),1) +CFLAGS += -fsanitize=address +CXXFLAGS += -fsanitize=address + endif + endif + ifeq ($(or $(LSAN),$(LEAKSAN)),1) + ifeq ($(call check_for_sanitizer,leak),1) +CFLAGS += -fsanitize=leak +CXXFLAGS += -fsanitize=leak + endif + endif +endif +ifeq ($(or $(UBSAN),$(SAN)),1) + ifeq ($(call check_for_sanitizer,undefined),1) +CFLAGS += -fsanitize=undefined +CXXFLAGS += -fsanitize=undefined + endif +endif + +# profiling +ifeq ($(or $(PROFILE),$(PG)),1) +CFLAGS += -pg +CXXFLAGS += -pg +endif + +# these rules ensure dependencies are created +DEPCFLAGS = -MD -MF $(DEPSDIR)/$*.d -MP +DEPSDIR := .deps +BUILDSTAMP := $(DEPSDIR)/rebuildstamp +DEPFILES := $(wildcard $(DEPSDIR)/*.d) +ifneq ($(DEPFILES),) +include $(DEPFILES) +endif + +# when the C compiler or optimization flags change, rebuild all objects +ifneq ($(strip $(DEP_CC)),$(strip $(CC) $(CPPFLAGS) $(CFLAGS) $(O))) +DEP_CC := $(shell mkdir -p $(DEPSDIR); echo >$(BUILDSTAMP); echo "DEP_CC:=$(CC) $(CPPFLAGS) $(CFLAGS) $(O)" >$(DEPSDIR)/_cc.d) +endif +ifneq ($(strip $(DEP_CXX)),$(strip $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(O))) +DEP_CXX := $(shell mkdir -p $(DEPSDIR); echo >$(BUILDSTAMP); echo "DEP_CXX:=$(CXX) $(CPPFLAGS) $(CXXFLAGS) $(O)" >$(DEPSDIR)/_cxx.d) +endif + + +V = 0 +ifeq ($(V),1) +run = $(1) $(3) +xrun = /bin/echo "$(1) $(3)" && $(1) $(3) +else +run = @$(if $(2),/bin/echo " $(2) $(3)" &&,) $(1) $(3) +xrun = $(if $(2),/bin/echo " $(2) $(3)" &&,) $(1) $(3) +endif +runquiet = @$(1) $(3) + +# cancel implicit rules we don't want +%: %.c +%.o: %.c +%: %.cc +%.o: %.cc +%: %.o + +$(BUILDSTAMP): + @mkdir -p $(@D) + @echo >$@ + +always: + @: + +clean-hook: + @: + +.PHONY: always clean-hook +.PRECIOUS: %.o diff --git a/pset5/check.pl b/pset5/check.pl new file mode 100755 index 0000000..6e0254b --- /dev/null +++ b/pset5/check.pl @@ -0,0 +1,860 @@ +#! /usr/bin/perl -w +use Time::HiRes qw(gettimeofday); +use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK); +use POSIX; +use Scalar::Util qw(looks_like_number); +use List::Util qw(shuffle min max); +use Config; +sub first (@) { return $_[0]; } +my($CHECKSUM) = first(grep {-x $_} ("/usr/bin/md5sum", "/sbin/md5", + "/bin/false")); + +my($Red, $Redctx, $Green, $Cyan, $Off) = ("\x1b[01;31m", "\x1b[0;31m", "\x1b[01;32m", "\x1b[01;36m", "\x1b[0m"); +$Red = $Redctx = $Green = $Cyan = $Off = "" if !-t STDERR || !-t STDOUT; + + +$SIG{"CHLD"} = sub {}; +$SIG{"TTOU"} = "IGNORE"; +my($run61_pid); + +open(FOO, "sh61.cc") || die "Did you delete sh61.cc?"; +$lines = 0; +$lines++ while defined($_ = ); +close FOO; + +my $rev = 'rev'; +my $ALLOW_OPEN = 1; +my $ALLOW_SECRET = 0; +my @ALLOW_TESTS = (); + +sub CMD_INIT () { "CMD_INIT" } +sub CMD_CLEANUP () { "CMD_CLEANUP" } +sub CMD_CAREFUL_CLEANUP () { "CMD_CAREFUL_CLEANUP" } +sub CMD_FILE () { "CMD_FILE" } +sub CMD_OUTPUT_FILTER () { "CMD_OUTPUT_FILTER" } +sub CMD_INT_DELAY () { "CMD_INT_DELAY" } +sub CMD_SECRET () { "CMD_SECRET" } +sub CMD_CAT () { "CMD_CAT" } +sub CMD_MAX_TIME () { "CMD_MAX_TIME" } + +@tests = ( +# Execute + [ # 0. Test title + # 1. Test command + # 2. Expected test output (with newlines changed to spaces) + # 3. (optional) Setup command. This sets up the test environment, + # usually by creating input files. It's run by the normal shell, + # not your shell. + # 4. (optional) Cleanup command. This is run, by the normal shell, + # after your shell has finished. It usually examines output files. + # In the commands, the special syntax '%%' is replaced with the + # test number. + 'Test SIMPLE1', + 'echo Hooray', + 'Hooray' ], + + [ 'Test SIMPLE2', + 'echo Double Hooray', + 'Double Hooray' ], + + [ 'Test SIMPLE3', + 'cat f3.txt', + 'Triple Hooray', + CMD_INIT => 'echo Triple Hooray > f3.txt' ], + + [ 'Test SIMPLE4', + "echo Line 1\necho Line 2\necho Line 3", + 'Line 1 Line 2 Line 3' ], + + +# Background commands + [ 'Test BG1', + 'cp f%%a.txt f%%b.txt &', + 'Copied', + CMD_INIT => 'echo Copied > f%%a.txt; echo Original > f%%b.txt', + CMD_CLEANUP => 'sleep 0.1 && cat f%%b.txt' ], + + [ 'Test BG2', + 'sh -c "sleep 0.2; test -r f%%b.txt && rm -f f%%a.txt" &', + 'Still here', + CMD_INIT => 'echo Still here > f%%a.txt; echo > f%%b.txt', + CMD_CLEANUP => 'rm f%%b.txt && sleep 0.3 && cat f%%a.txt' ], + + # Check that background commands are run in the background + [ 'Test BG3', + 'sleep 2 &', + '1', + CMD_CLEANUP => 'ps T | grep sleep | grep -v grep | head -n 1 | wc -l' ], + + +# Command lists + [ 'Test LIST1', + 'echo Semi ;', + 'Semi' ], + + [ 'Test LIST2', + 'echo Semi ; echo Colon', + 'Semi Colon' ], + + [ 'Test LIST3', + 'echo Semi ; echo Colon ; echo Rocks', + 'Semi Colon Rocks' ], + + [ 'Test LIST4', + 'echo Hello ; echo There ; echo Who ; echo Are ; echo You ; echo ?', + 'Hello There Who Are You ?' ], + + [ 'Test LIST5', + 'rm -f f%%.txt ; echo Removed', + 'Removed', + CMD_INIT => 'echo > f%%.txt'], + + [ 'Test LIST6', + '../sh61 -q cmd%%.sh &', + 'Hello 1', + CMD_INIT => 'echo "echo Hello; sleep 0.4" > cmd%%.sh', + CMD_CLEANUP => 'sleep 0.2 ; ps T | grep sleep | grep -v grep | head -n 1 | wc -l'], + + [ 'Test LIST7', + '../sh61 -q cmd%%.sh', + 'Hello Bye 1', + CMD_INIT => 'echo "echo Hello; sleep 2& echo Bye" > cmd%%.sh', + CMD_CLEANUP => 'ps | grep sleep | grep -v grep | head -n 1 | wc -l'], + + [ 'Test LIST8', + 'sh -c "sleep 0.2; echo Second" & sh -c "sleep 0.15; echo First" & sleep 0.3', + 'First Second' ], + + +# Conditionals + [ 'Test COND1', + 'true && echo True', + 'True' ], + + [ 'Test COND2', + 'echo True || echo False', + 'True' ], + + [ 'Test COND3', + 'grep -cv NotThere ../sh61.cc && echo Wanted', + "$lines Wanted" ], + + [ 'Test COND4', + 'grep -c NotThere ../sh61.cc && echo Unwanted', + '0' ], + + [ 'Test COND5', + 'false || echo True', + 'True' ], + + [ 'Test COND6', + 'true && false || true && echo Good', + 'Good' ], + + [ 'Test COND7', + 'echo Start && false || false && echo Bad', + 'Start' ], + + [ 'Test COND8', + 'sleep 0.2 && echo Second & sleep 0.1 && echo First', + 'First Second', + CMD_CLEANUP => 'sleep 0.25'], + + [ 'Test COND9', + 'echo Start && false || false && false || echo End', + 'Start End' ], + + [ 'Test COND10', + 'false && echo no && echo no && echo no && echo no || echo yes', + 'yes' ], + + [ 'Test COND11', + 'true || echo no || echo no || echo no || echo no && echo yes', + 'yes' ], + + [ 'Test COND12', # Background conditionals + 'sleep 0.2 && echo second & echo first', + 'first second', + CMD_CLEANUP => 'sleep 0.2' ], + + [ 'Test COND13', + 'echo first && sleep 0.1 && echo third & sleep 0.05 ; echo second ; sleep 0.1 ; echo fourth', + 'first second third fourth' ], + + +# Pipelines + [ 'Test PIPE1', + 'echo Pipe | wc -c', + '5' ], + + [ 'Test PIPE2', + 'echo Good | grep -n G', + '1:Good' ], + + [ 'Test PIPE3', + 'echo Bad | grep -c G', + '0' ], + + [ 'Test PIPE4', + 'echo Line | cat | wc -l', + '1' ], + + [ 'Test PIPE5', + '../sh61 -q cmd%%.sh; ps | grep sleep | grep -v grep | head -n 1 | wc -l', + 'Hello Bye 1', + CMD_INIT => 'echo "echo Hello; sleep 2 & echo Bye" > cmd%%.sh'], + + [ 'Test PIPE6', + "echo GoHangASalamiImALasagnaHog | $rev | $rev | $rev", + 'goHangasaLAmIimalaSAgnaHoG' ], + + [ 'Test PIPE7', + "$rev f%%.txt | $rev", + 'goHangasaLAmIimalaSAgnaHoG', + CMD_INIT => 'echo goHangasaLAmIimalaSAgnaHoG > f%%.txt' ], + + [ 'Test PIPE8', + "cat f%%.txt | tr [A-Z] [a-z] | $CHECKSUM | tr -d -", + '8e21d03f7955611616bcd2337fe9eac1', + CMD_INIT => 'echo goHangasaLAmIimalaSAgnaHoG > f%%.txt' ], + + [ 'Test PIPE9', + "$rev f%%.txt | $CHECKSUM | tr [a-z] [A-Z] | tr -d -", + '502B109B37EC769342948826736FA063', + CMD_INIT => 'echo goHangasaLAmIimalaSAgnaHoG > f%%.txt' ], + + [ 'Test PIPE10', + 'sleep 2 & sleep 0.2; ps T | grep sleep | grep -v grep | head -n 1 | wc -l', + '1', + CMD_FILE => 1 ], + + [ 'Test PIPE11', + 'echo Sedi | tr d m ; echo Calan | tr a o', + 'Semi Colon' ], + + [ 'Test PIPE12', + 'echo Ignored | echo Desired', + 'Desired' ], + + [ 'Test PIPE13', + '../sh61 -q cmd%%.sh &', + 'Hello 1', + CMD_INIT => 'echo "echo Hello; sleep 0.4" > cmd%%.sh', + CMD_CLEANUP => 'echo "sleep 0.2 ; ps T | grep sleep | grep -v grep | head -n 1 | wc -l" > cmd%%b.sh; ../sh61 -q cmd%%b.sh', + CMD_CAREFUL_CLEANUP => 1, + CMD_FILE => 1 ], + + [ 'Test PIPE14', + 'true | true && echo True', + 'True' ], + + [ 'Test PIPE15', + 'true | echo True || echo False', + 'True' ], + + [ 'Test PIPE16', + 'false | echo True || echo False', + 'True' ], + + [ 'Test PIPE17', + 'echo Hello | grep -q X || echo NoXs', + 'NoXs' ], + + [ 'Test PIPE18', + 'echo Yes | grep -q Y && echo Ys', + 'Ys' ], + + [ 'Test PIPE19', + 'echo Hello | grep -q X || echo poqs | tr pq NX', + 'NoXs' ], + + [ 'Test PIPE20', + 'echo Yes | grep -q Y && echo fs | tr f Y', + 'Ys' ], + + [ 'Test PIPE21', + 'false && echo vnexpected | tr v u ; echo expected', + 'expected' ], + + [ 'Test PIPE22', + 'false && echo unexpected && echo vnexpected | tr v u ; echo expected', + 'expected' ], + + [ 'Test PIPE23', # actually a background test + 'sleep 0.2 | wc -c | sed s/0/Second/ & sleep 0.1 | wc -c | sed s/0/First/', + 'First Second', + CMD_CLEANUP => 'sleep 0.25'], + + [ 'Test PIPE24', + 'yes | head -n 5', + 'y y y y y' ], + + +# Zombies + [ 'Test ZOMBIE1', + "sleep 0.05 &\nsleep 0.1\nps T", + '', + CMD_OUTPUT_FILTER => 'grep defunct | grep -v grep'], + + [ 'Test ZOMBIE2', + "sleep 0.05 & sleep 0.05 & sleep 0.05 & sleep 0.05 &\nsleep 0.07\nsleep 0.07\nps T", + '', + CMD_OUTPUT_FILTER => 'grep defunct | grep -v grep'], + + +# Redirection + [ 'Test REDIR1', + 'echo Start ; echo File > f%%.txt', + 'Start File', + CMD_CLEANUP => 'cat f%%.txt'], + + [ 'Test REDIR2', + 'tr pq Fi < f%%.txt ; echo Done', + 'File Done', + CMD_INIT => 'echo pqle > f%%.txt'], + + [ 'Test REDIR3', + "perl -e 'print STDERR $$' 2> f%%.txt ; grep '^[1-9]' f%%.txt | wc -l ; rm -f f%%.txt", + '1', + CMD_INIT => 'echo File > f%%.txt' ], + + [ 'Test REDIR4', + "perl -e 'print STDERR $$; print STDOUT \"X\"' > f%%a.txt 2> f%%b.txt ; grep '^[1-9]' f%%a.txt | wc -l ; grep '^[1-9]' f%%b.txt | wc -l ; cmp -s f%%a.txt f%%b.txt || echo Different", + '0 1 Different', + CMD_INIT => 'echo File > f%%.txt' ], + + [ 'Test REDIR5', + 'tr hb HB < f%%.txt | sort | ../sh61 -q cmd%%.sh', + 'Bye Hello First Good', + CMD_INIT => 'echo "head -n 2 ; echo First && echo Good" > cmd%%.sh; (echo hello; echo bye) > f%%.txt'], + + [ 'Test REDIR6', + 'sort < f%%a.txt > f%%b.txt ; tail -n 2 f%%b.txt ; rm -f f%%a.txt f%%b.txt', + 'Bye Hello', + # (Remember, CMD_INIT is a normal shell command! For your shell, + # parentheses are extra credit.) + CMD_INIT => '(echo Hello; echo Bye) > f%%a.txt'], + + [ 'Test REDIR7', + 'echo > /tmp/directorydoesnotexist/foo', + 'No such file or directory', + CMD_CLEANUP => 'perl -pi -e "s,^.*:\s*,," out%%.txt' ], + + [ 'Test REDIR8', + 'echo > /tmp/directorydoesnotexist/foo && echo Unwanted', + 'No such file or directory', + CMD_CLEANUP => 'perl -pi -e "s,^.*:\s*,," out%%.txt' ], + + [ 'Test REDIR9', + 'echo > /tmp/directorydoesnotexist/foo || echo Wanted', + 'No such file or directory Wanted', + CMD_CLEANUP => 'perl -pi -e "s,^.*:\s*,," out%%.txt' ], + + [ 'Test REDIR10', + 'echo Hello < nonexistent%%.txt', + 'No such file or directory', + CMD_CLEANUP => 'perl -pi -e "s,^.*:\s*,," out%%.txt' ], + + [ 'Test REDIR11', + 'echo Hello < nonexistent%%.txt && echo Unwanted', + 'No such file or directory', + CMD_CLEANUP => 'perl -pi -e "s,^.*:\s*,," out%%.txt' ], + + [ 'Test REDIR12', + 'echo Hello < nonexistent%%.txt || echo Wanted', + 'No such file or directory Wanted', + CMD_CLEANUP => 'perl -pi -e "s,^.*:\s*,," out%%.txt' ], + + [ 'Test REDIR13', + 'cat unwanted.txt | cat < wanted.txt', + 'Wanted', + CMD_INIT => 'echo Unwanted > unwanted.txt; echo Wanted > wanted.txt' ], + + [ 'Test REDIR14', + 'cat < wanted.txt | cat > output.txt', + 'output.txt is Wanted', + CMD_INIT => 'echo Wanted > wanted.txt', + CMD_CLEANUP => 'echo output.txt is; cat output.txt' ], + + [ 'Test REDIR15', + 'cat < xoqted.txt | tr xoq Wan | cat > output.txt', + 'output.txt is Wanted', + CMD_INIT => 'echo xoqted > xoqted.txt', + CMD_CLEANUP => 'echo output.txt is; cat output.txt' ], + + [ 'Test REDIR16', + 'echo Ignored | cat < lower.txt | tr A-Z a-z', + 'lower', + CMD_INIT => 'echo LOWER > lower.txt' ], + + [ 'Test REDIR17', + 'echo > out.txt the redirection can occur anywhere && cat out.txt', + 'the redirection can occur anywhere' ], + + [ 'Test REDIR18', + 'echo the redirection > out.txt can really occur anywhere && cat out.txt', + 'the redirection can really occur anywhere' ], + + +# cd + [ 'Test CD1', + 'cd / ; pwd', + '/' ], + + [ 'Test CD2', + 'cd / ; cd /usr ; pwd', + '/usr' ], + +# cd without redirecting stdout + [ 'Test CD3', + 'cd / ; cd /doesnotexist 2> /dev/null ; pwd', + '/' ], + +# Fancy conditionals + [ 'Test CD4', + 'cd / && pwd', + '/' ], + + [ 'Test CD5', + 'echo go ; cd /doesnotexist 2> /dev/null > /dev/null && pwd', + 'go' ], + + [ 'Test CD6', + 'cd /doesnotexist 2> /dev/null > /dev/null || echo does not exist', + 'does not exist' ], + + [ 'Test CD7', + 'cd /tmp && cd / && pwd', + '/' ], + + [ 'Test CD8', + 'cd / ; cd /doesnotexist 2> /dev/null > /dev/null ; pwd', + '/' ], + + +# Interrupts + [ 'Test INTR1', + 'echo a && sleep 0.2 && echo b', + 'a', + CMD_INT_DELAY => 0.1 ], + + [ 'Test INTR2', + "echo start && sleep 0.2 && echo undesired \n echo end", + 'start end', + CMD_FILE => 1, + CMD_INT_DELAY => 0.1 ], + + [ 'Test INTR3', + 'sleep 0.3 && echo yes & sleep 0.2 && echo no', + 'yes', + CMD_CLEANUP => 'sleep 0.25', + CMD_INT_DELAY => 0.1 ], + + [ 'Test INTR4', + 'sleep 1', + '', + CMD_INT_DELAY => 0.1, + CMD_MAX_TIME => 0.15 ], + + [ 'Test INTR5', + '../sh61 -q cmd%%.sh', + '', + CMD_INIT => 'echo "sleep 1 && echo undesired" > cmd%%.sh', + CMD_INT_DELAY => 0.1, + CMD_MAX_TIME => 0.15 ] + + ); + +my($ntest) = 0; + +my($sh) = "./sh61"; +-d "out" || mkdir("out") || die "Cannot create 'out' directory\n"; +my($ntestfailed) = 0; + +# check for a ton of existing sh61 processes +$me = `id -un`; +chomp $me; +open(SH61, "ps uxww | grep '^$me.*sh61' | grep -v grep |"); +$nsh61 = 0; +$nsh61 += 1 while (defined($_ = )); +close SH61; +if ($nsh61 > 5) { + print STDERR "\n"; + print STDERR "${Red}**** Looks like $nsh61 ./sh61 processes are already running.\n"; + print STDERR "**** Do you want all those processes?\n"; + print STDERR "**** Run 'killall -9 sh61' to kill them.${Off}\n\n"; +} + +# remove output files +opendir(DIR, "out") || die "opendir: $!\n"; +foreach my $f (grep {/\.(?:txt|sh)$/} readdir(DIR)) { + unlink("out/$f"); +} +closedir(DIR); + +if (!-x $sh) { + $editsh = $sh; + $editsh =~ s,^\./,,; + print STDERR "${Red}$sh does not exist, so I can't run any tests!${Off}\n(Try running \"make $editsh\" to create $sh.)\n"; + exit(1); +} + +select STDOUT; +$| = 1; + +my($testsrun) = 0; +my($testindex) = 0; + +sub remove_files ($) { + my($testnumber) = @_; + opendir(DIR, "out"); + foreach my $f (grep {/$testnumber\.(?:sh|txt)$/} readdir(DIR)) { + unlink("out/$f"); + } + closedir(DIR); +} + +sub run_sh61_pipe ($$;$) { + my($text, $fd, $size) = @_; + my($n, $buf) = (0, ""); + return $text if !defined($fd); + while ((!defined($size) || length($text) <= $size) + && defined(($n = POSIX::read($fd, $buf, 8192))) + && $n > 0) { + $text .= substr($buf, 0, $n); + } + return $text; +} + +sub run_sh61 ($;%) { + my($command, %opt) = @_; + my($outfile) = exists($opt{"stdout"}) ? $opt{"stdout"} : undef; + my($size_limit_file) = exists($opt{"size_limit_file"}) ? $opt{"size_limit_file"} : $outfile; + $size_limit_file = [$size_limit_file] if $size_limit_file && !ref($size_limit_file); + my($size_limit) = exists($opt{"size_limit"}) ? $opt{"size_limit"} : undef; + my($dir) = exists($opt{"dir"}) ? $opt{"dir"} : undef; + if (defined($dir) && $size_limit_file) { + $dir =~ s{/+$}{}; + $size_limit_file = [map { m{^/} ? $_ : "$dir/$_" } @$size_limit_file]; + } + pipe(OR, OW) or die "pipe"; + fcntl(OR, F_SETFL, fcntl(OR, F_GETFL, 0) | O_NONBLOCK); + 1 while waitpid(-1, WNOHANG) > 0; + + open(TTY, "+<", "/dev/tty") or die "can't open /dev/tty: $!"; + + $run61_pid = fork(); + if ($run61_pid == 0) { + POSIX::setpgid(0, 0) or die("child setpgid: $!\n"); + defined($dir) && chdir($dir); + + my($fn) = defined($opt{"stdin"}) ? $opt{"stdin"} : "/dev/null"; + if (defined($fn) && $fn ne "/dev/stdin") { + my($fd) = POSIX::open($fn, O_RDONLY); + POSIX::dup2($fd, 0); + POSIX::close($fd) if $fd != 0; + fcntl(STDIN, F_SETFD, fcntl(STDIN, F_GETFD, 0) & ~FD_CLOEXEC); + } + + close(OR); + if (!defined($outfile) || $outfile ne "/dev/stdout") { + open(OW, ">", $outfile) || die if defined($outfile) && $outfile ne "pipe"; + POSIX::dup2(fileno(OW), 1); + POSIX::dup2(fileno(OW), 2); + close(OW) if fileno(OW) != 1 && fileno(OW) != 2; + fcntl(STDOUT, F_SETFD, fcntl(STDOUT, F_GETFD, 0) & ~FD_CLOEXEC); + fcntl(STDERR, F_SETFD, fcntl(STDERR, F_GETFD, 0) & ~FD_CLOEXEC); + } + + { exec($command) }; + print STDERR "error trying to run $command: $!\n"; + exit(1); + } + + POSIX::setpgid($run61_pid, $run61_pid) or die("setpgid: $!\n"); + POSIX::tcsetpgrp(fileno(TTY), $run61_pid) or die "tcsetpgrp: $!\n"; + + my($before) = Time::HiRes::time(); + my($died) = 0; + my($time_limit) = exists($opt{"time_limit"}) ? $opt{"time_limit"} : 0; + my($out, $buf, $nb) = ("", ""); + my($answer) = exists($opt{"answer"}) ? $opt{"answer"} : {}; + $answer->{"command"} = $command; + my($sigint_at) = defined($opt{"int_delay"}) ? $before + $opt{"int_delay"} : undef; + + close(OW); + + eval { + do { + my($delta) = 0.3; + if ($sigint_at) { + my($now) = Time::HiRes::time(); + $delta = min($delta, $sigint_at < $now + 0.02 ? 0.1 : $sigint_at - $now); + } + Time::HiRes::usleep($delta * 1e6) if $delta > 0; + + if (waitpid($run61_pid, WNOHANG) > 0) { + $answer->{"status"} = $?; + die "!"; + } + if ($sigint_at && Time::HiRes::time() >= $sigint_at) { + my($pgrp) = POSIX::tcgetpgrp(fileno(TTY)); + unless ($pgrp == getpgrp()) { + kill('-INT', $pgrp); + $sigint_at = undef; + } + } + if (defined($size_limit) && $size_limit_file && @$size_limit_file) { + my($len) = 0; + $out = run_sh61_pipe($out, fileno(OR), $size_limit); + foreach my $fname (@$size_limit_file) { + $len += ($fname eq "pipe" ? length($out) : -s $fname); + } + if ($len > $size_limit) { + $died = "output file size $len, expected <= $size_limit"; + die "!"; + } + } + } while (Time::HiRes::time() < $before + $time_limit); + $died = sprintf("timeout after %.2fs", $time_limit) + if waitpid($run61_pid, WNOHANG) <= 0; + }; + + my($delta) = Time::HiRes::time() - $before; + $answer->{"time"} = $delta; + + if (exists($answer->{"status"}) && exists($opt{"delay"}) && $opt{"delay"} > 0) { + Time::HiRes::usleep($opt{"delay"} * 1e6); + } + if (exists($opt{"nokill"})) { + $answer->{"pgrp"} = $run61_pid; + } else { + kill -9, $run61_pid; + } + $run61_pid = 0; + + if ($died) { + $answer->{"killed"} = $died; + close(OR); + return $answer; + } + + if (defined($outfile) && $outfile ne "pipe") { + $out = ""; + close(OR); + open(OR, "<", (defined($dir) ? "$dir/$outfile" : $outfile)); + } + $answer->{"output"} = run_sh61_pipe($out, fileno(OR), $size_limit); + close(OR); + + return $answer; +} + +sub test_runnable ($$) { + my($prefix, $number) = @_; + $prefix = lc($prefix); + return !@ALLOW_TESTS || grep { + $prefix eq $_->[0] + && ($_->[1] eq "" + || ($number >= $_->[1] + && ($_->[2] eq "-" || $number <= ($_->[2] eq "" ? $_->[1] : -$_->[2])))); + } @ALLOW_TESTS; +} + +sub kill_sleeps () { + open(PS, "ps T |"); + while (defined($_ = )) { + $_ =~ s/^\s+//; + my(@x) = split(/\s+/, $_); + if (@x && $x[0] =~ /\A\d+\z/ && $x[4] eq "sleep") { + kill("INT", $x[0]); + } + } + close(PS); +} + +sub disallowed_signal ($) { + my($s) = @_; + my(@sigs) = split(" ", $Config{sig_name}); + return "unknown signal $s" if $s >= @sigs; + return "illegal instruction" if $sigs[$s] eq "ILL"; + return "abort signal" if $sigs[$s] eq "ABRT"; + return "floating point exception" if $sigs[$s] eq "FPE"; + return "segmentation fault" if $sigs[$s] eq "SEGV"; + return "broken pipe" if $sigs[$s] eq "PIPE"; + return "SIG" . $sigs[$s]; +} + +sub run (@) { + my($testnumber); + if ($_[0] =~ /^Test (\w*?)(\d*)(\.\d+|[a-z]+|)(?=$|[.:\s])/i) { + $testnumber = $1 . $2 . $3; + return if !test_runnable($1, $2); + } else { + $testnumber = "x" . $testindex; + } + + for (my $i = 0; $i < @_; ++$i) { + $_[$i] =~ s/\%\%/$testnumber/g; + } + + my($desc, $in, $want) = @_; + my(%opts); + if (@_ > 3 && substr($_[3], 0, 4) ne "CMD_") { + print STDERR "Warning: old test format ", $desc, "\n"; + $opts{CMD_INIT} = $_[3] if $_[3]; + $opts{CMD_CLEANUP} = $_[4] if $_[4]; + } elsif (@_ > 3) { + %opts = @_[3..(@_ - 1)]; + } + return if $opts{CMD_SECRET} && !$ALLOW_SECRET; + return if !$opts{CMD_SECRET} && !$ALLOW_OPEN; + + $ntest++; + remove_files($testnumber); + kill_sleeps(); + system("cd out; " . $opts{CMD_INIT}) if $opts{CMD_INIT}; + + print OUT "$desc: "; + my($tempfile) = "main$testnumber.sh"; + my($outfile) = "out$testnumber.txt"; + open(F, ">out/$tempfile") || die; + print F $in, "\n"; + close(F); + + my($start) = Time::HiRes::time(); + my($cmd) = "../$sh -q" . ($opts{CMD_FILE} ? " $tempfile" : ""); + my($stdin) = $opts{CMD_FILE} ? "/dev/stdin" : $tempfile; + my($info) = run_sh61($cmd, "stdin" => $stdin, "stdout" => $outfile, "time_limit" => 10, "size_limit" => 1000, "dir" => "out", "nokill" => 1, "delay" => 0.05, "int_delay" => $opts{CMD_INT_DELAY}); + + if ($opts{CMD_CLEANUP}) { + if ($opts{CMD_CAREFUL_CLEANUP}) { + my($infox) = run_sh61("{ " . $opts{CMD_CLEANUP} . "; } >>$outfile 2>&1", "time_limit" => 5, "dir" => "out", "stdin" => "/dev/stdin", "stdout" => "/dev/stdout"); + $info->{killed} = "cleanup command killed" + if exists($infox->{killed}) && !exists($info->{killed}); + } else { + system("cd out; { " . $opts{CMD_CLEANUP} . "; } >>$outfile 2>&1"); + } + } + system("cd out; mv $outfile ${outfile}~; { " . $opts{CMD_OUTPUT_FILTER} . "; } <${outfile}~ >$outfile 2>&1") + if $opts{CMD_OUTPUT_FILTER}; + + kill -9, $info->{"pgrp"} if exists($info->{"pgrp"}); + + my($ok, $prefix, $sigdead) = (1, ""); + if (exists($info->{"status"}) + && ($info->{"status"} & 127) # died from signal + && ($sigdead = disallowed_signal($info->{"status"} & 127))) { + print OUT "${Red}KILLED${Redctx} by $sigdead${Off}\n"; + $ntestfailed += 1 if $ok; + $ok = 0; + $prefix = " "; + } + $result = `cat out/$outfile`; + # sanitization errors + my($sanitizera, $sanitizerb) = ("", ""); + if ($result =~ /\A([\s\S]*?)^(===+\s+==\d+==\s*ERROR[\s\S]*)\z/m) { + $result = $1; + $sanitizerb = $2; + } + while ($result =~ /\A([\s\S]*?)^(\S+\.cc:\d+:(?:\d+:)? runtime error.*(?:\n|\z)|=+\s+WARNING.*Sanitizer[\s\S]*?\n=+\n)([\s\S]*)\z/m) { + $result = $1 . $3; + $sanitizera .= $2; + } + my($sanitizer) = $sanitizera . $sanitizerb; + $result =~ s%^sh61[\[\]\d]*\$ %%m; + $result =~ s%sh61[\[\]\d]*\$ $%%m; + $result =~ s%^\[\d+\]\s+\d+$%%mg; + $result =~ s|\[\d+\]||g; + $result =~ s|^\s+||g; + $result =~ s|\s+| |g; + $result =~ s|\s+$||; + if (($result eq $want + || ($want eq 'Syntax error [NULL]' && $result eq '[NULL]')) + && !exists($info->{killed}) + && (!$opts{CMD_MAX_TIME} || $info->{time} <= $opts{CMD_MAX_TIME})) { + # remove all files unless @ARGV was set + print OUT "${Green}passed${Off}\n" if $ok; + remove_files($testnumber) if !@ARGV && $ok; + } else { + printf OUT "$prefix${Red}FAILED${Redctx} in %.3f sec${Off}\n", $info->{time}; + $in =~ s/\n/ \\n /g; + print OUT " command \`$in\`\n"; + if ($result eq $want) { + print OUT " output \`$want\`\n" if $want ne ""; + } else { + print OUT " expected \`$want\`\n"; + $result = substr($result, 0, 76) . "..." if length($result) > 76; + print OUT " got \`$result\`\n"; + } + if ($opts{CMD_MAX_TIME} && $info->{time} > $opts{CMD_MAX_TIME}) { + printf OUT " should have completed in %.3f sec\n", $opts{CMD_MAX_TIME}; + } + if (exists($info->{killed})) { + print OUT " ", $info->{killed}, "\n"; + } + $ntestfailed += 1 if $ok; + } + if ($sanitizer ne "") { + chomp $sanitizer; + $sanitizer = substr($sanitizer, 0, 1200) . "..." + if length($sanitizer) > 1200; + $sanitizer =~ s/\n/\n /g; + print OUT " ${Redctx}sanitizer reports errors:${Off}\n $sanitizer\n"; + } + + if (exists($opts{CMD_CAT})) { + print OUT "\n${Green}", $opts{CMD_CAT}, "\n==================${Off}\n"; + if (open(F, "<", "out/" . $opts{CMD_CAT})) { + print OUT $_ while (defined($_ = )); + close(F); + } else { + print OUT "${Red}out/", $opts{CMD_CAT}, ": $!${Off}\n"; + } + print OUT "\n"; + } +} + +open(OUT, ">&STDOUT"); +my($leak_check) = 0; + +while (@ARGV && $ARGV[0] =~ /^-/) { + if ($ARGV[0] eq "--leak" || $ARGV[0] eq "--leak-check") { + $leak_check = 1; + shift @ARGV; + next; + } elsif ($ARGV[0] =~ /\A--leak=(.*)\z/) { + $leak_check = ($1 eq "1" || $1 eq "yes"); + shift @ARGV; + next; + } elsif ($ARGV[0] eq "--ignore-sigint") { + $SIG{"INT"} = "IGNORE"; + shift @ARGV; + next; + } elsif ($ARGV[0] eq "--only") { + @ARGV = ($ARGV[1]); + last; + } + + print STDERR "Usage: check.pl TESTNUM...\n"; + exit(1); +} + +if (!$leak_check && !$ENV{"ASAN_OPTIONS"}) { + $ENV{"ASAN_OPTIONS"} = "detect_leaks=0"; +} + +foreach my $allowed_tests (@ARGV) { + while ($allowed_tests =~ m{(?:^|[\s,])(\w+?)-?(\d*)((?:-\d*)?)(?=[\s,]|$)}g) { + push(@ALLOW_TESTS, [lc($1), $2, $3]); + } +} + +foreach $test (@tests) { + ++$testindex; + run(@$test); +} + +my($ntestpassed) = $ntest - $ntestfailed; +print OUT "\r$ntestpassed of $ntest ", ($ntest == 1 ? "test" : "tests"), " passed\n" if $ntest > 1; +exit(0); diff --git a/pset5/helpers.cc b/pset5/helpers.cc new file mode 100644 index 0000000..b1daa29 --- /dev/null +++ b/pset5/helpers.cc @@ -0,0 +1,136 @@ +#include "sh61.hh" +#include +#include + +// isshellspecial(ch) +// Test if `ch` is a command that's special to the shell (that ends +// a command word). + +inline bool isshellspecial(int ch) { + return ch == '<' || ch == '>' || ch == '&' || ch == '|' || ch == ';' + || ch == '(' || ch == ')' || ch == '#'; +} + + +// parse_shell_token(str, type, token) +// Parse the next token from the shell command `str`. Stores the type of +// the token in `*type`; this is one of the TYPE_ constants. Stores the +// token itself in `*token`. Advances `str` to the next token and returns +// that pointer. +// +// At the end of the string, returns nullptr, sets `*type` to +// TYPE_SEQUENCE, and sets `*token` to an empty string. + +const char* parse_shell_token(const char* str, int* type, std::string* token) { + std::ostringstream buildtoken; + + // skip spaces; return nullptr and token ";" at end of line + while (str && isspace((unsigned char) *str)) { + ++str; + } + if (!str || !*str || *str == '#') { + *type = TYPE_SEQUENCE; + *token = std::string(); + return nullptr; + } + + // check for a redirection or special token + for (; isdigit((unsigned char) *str); ++str) { + buildtoken << *str; + } + if (*str == '<' || *str == '>') { + *type = TYPE_REDIRECTION; + buildtoken << *str; + if (str[1] == '>') { + buildtoken << str[1]; + ++str; + } else if (str[1] == '&' && isdigit((unsigned char) str[2])) { + buildtoken << str[1]; + for (str += 2; isdigit((unsigned char) *str); ++str) { + buildtoken << *str; + } + } + ++str; + } else if (buildtoken.tellp() == 0 + && (*str == '&' || *str == '|') + && str[1] == *str) { + *type = (*str == '&' ? TYPE_AND : TYPE_OR); + buildtoken << *str << str[1]; + str += 2; + } else if (buildtoken.tellp() == 0 + && isshellspecial((unsigned char) *str)) { + switch (*str) { + case ';': *type = TYPE_SEQUENCE; break; + case '&': *type = TYPE_BACKGROUND; break; + case '|': *type = TYPE_PIPE; break; + case '(': *type = TYPE_LPAREN; break; + case ')': *type = TYPE_RPAREN; break; + default: *type = TYPE_OTHER; break; + } + buildtoken << *str; + ++str; + } else { + // it's a normal token + *type = TYPE_NORMAL; + int quoted = 0; + // Read characters up to the end of the token. + while ((*str && quoted) + || (*str && !isspace((unsigned char) *str) + && !isshellspecial((unsigned char) *str))) { + if ((*str == '\"' || *str == '\'') && !quoted) { + quoted = *str; + } else if (*str == quoted) { + quoted = 0; + } else if (*str == '\\' && str[1] != '\0' && quoted != '\'') { + buildtoken << str[1]; + ++str; + } else { + buildtoken << *str; + } + ++str; + } + } + + // store new token and return the location of the next token + *token = buildtoken.str(); + return str; +} + + +// claim_foreground(pgid) +// Mark `pgid` as the current foreground process group for this terminal. +// This uses some ugly Unix warts, so we provide it for you. +int claim_foreground(pid_t pgid) { + // YOU DO NOT NEED TO UNDERSTAND THIS. + + // Initialize state first time we're called. + static int ttyfd = -1; + static int shell_owns_foreground = 0; + static pid_t shell_pgid = -1; + if (ttyfd < 0) { + // We need a fd for the current terminal, so open /dev/tty. + int fd = open("/dev/tty", O_RDWR); + assert(fd >= 0); + // Re-open to a large file descriptor (>=10) so that pipes and such + // use the expected small file descriptors. + ttyfd = fcntl(fd, F_DUPFD, 10); + assert(ttyfd >= 0); + close(fd); + // The /dev/tty file descriptor should be closed in child processes. + fcntl(ttyfd, F_SETFD, FD_CLOEXEC); + // Only mess with /dev/tty's controlling process group if the shell + // is in /dev/tty's controlling process group. + shell_pgid = getpgrp(); + shell_owns_foreground = (shell_pgid == tcgetpgrp(ttyfd)); + } + + // Set the terminal's controlling process group to `p` (so processes in + // group `p` can output to the screen, read from the keyboard, etc.). + if (shell_owns_foreground && pgid) { + return tcsetpgrp(ttyfd, pgid); + } else if (shell_owns_foreground) { + return tcsetpgrp(ttyfd, shell_pgid); + } else { + return 0; + } +} diff --git a/pset5/sh61.cc b/pset5/sh61.cc new file mode 100644 index 0000000..f2ef921 --- /dev/null +++ b/pset5/sh61.cc @@ -0,0 +1,185 @@ +#include "sh61.hh" +#include +#include +#include +#include +#include + + +// struct command +// Data structure describing a command. Add your own stuff. + +struct command { + std::vector args; + pid_t pid; // process ID running this command, -1 if none + + command(); + ~command(); + + pid_t make_child(pid_t pgid); +}; + + +// command::command() +// This constructor function initializes a `command` structure. You may +// add stuff to it as you grow the command structure. + +command::command() { + this->pid = -1; +} + + +// command::~command() +// This destructor function is called to delete a command. + +command::~command() { +} + + +// COMMAND EXECUTION + +// command::make_child(pgid) +// Create a single child process running the command in `this`. +// Sets `this->pid` to the pid of the child process and returns `this->pid`. +// +// PART 1: Fork a child process and run the command using `execvp`. +// This will require creating an array of `char*` arguments using +// `this->args[N].c_str()`. +// PART 5: Set up a pipeline if appropriate. This may require creating a +// new pipe (`pipe` system call), and/or replacing the child process's +// standard input/output with parts of the pipe (`dup2` and `close`). +// Draw pictures! +// PART 7: Handle redirections. +// PART 8: The child process should be in the process group `pgid`, or +// its own process group (if `pgid == 0`). To avoid race conditions, +// this will require TWO calls to `setpgid`. + +pid_t command::make_child(pid_t pgid) { + assert(this->args.size() > 0); + (void) pgid; // You won’t need `pgid` until part 8. + // Your code here! + + fprintf(stderr, "command::make_child not done yet\n"); + return pid; +} + + +// run(c) +// Run the command *list* starting at `c`. Initially this just calls +// `make_child` and `waitpid`; you’ll extend it to handle command lists, +// conditionals, and pipelines. +// +// PART 1: Start the single command `c` with `c->make_child(0)`, +// and wait for it to finish using `waitpid`. +// The remaining parts may require that you change `struct command` +// (e.g., to track whether a command is in the background) +// and write code in `run` (or in helper functions). +// PART 2: Treat background commands differently. +// PART 3: Introduce a loop to run all commands in the list. +// PART 4: Change the loop to handle conditionals. +// PART 5: Change the loop to handle pipelines. Start all processes in +// the pipeline in parallel. The status of a pipeline is the status of +// its LAST command. +// PART 8: - Choose a process group for each pipeline and pass it to +// `make_child`. +// - Call `claim_foreground(pgid)` before waiting for the pipeline. +// - Call `claim_foreground(0)` once the pipeline is complete. + +void run(command* c) { + c->make_child(0); + fprintf(stderr, "command::run not done yet\n"); +} + + +// parse_line(s) +// Parse the command list in `s` and return it. Returns `nullptr` if +// `s` is empty (only spaces). You’ll extend it to handle more token +// types. + +command* parse_line(const char* s) { + int type; + std::string token; + // Your code here! + + // build the command + // (The handout code treats every token as a normal command word. + // You'll add code to handle operators.) + command* c = nullptr; + while ((s = parse_shell_token(s, &type, &token)) != nullptr) { + if (!c) { + c = new command; + } + c->args.push_back(token); + } + return c; +} + + +int main(int argc, char* argv[]) { + FILE* command_file = stdin; + bool quiet = false; + + // Check for '-q' option: be quiet (print no prompts) + if (argc > 1 && strcmp(argv[1], "-q") == 0) { + quiet = true; + --argc, ++argv; + } + + // Check for filename option: read commands from file + if (argc > 1) { + command_file = fopen(argv[1], "rb"); + if (!command_file) { + perror(argv[1]); + exit(1); + } + } + + // - Put the shell into the foreground + // - Ignore the SIGTTOU signal, which is sent when the shell is put back + // into the foreground + claim_foreground(0); + set_signal_handler(SIGTTOU, SIG_IGN); + + char buf[BUFSIZ]; + int bufpos = 0; + bool needprompt = true; + + while (!feof(command_file)) { + // Print the prompt at the beginning of the line + if (needprompt && !quiet) { + printf("sh61[%d]$ ", getpid()); + fflush(stdout); + needprompt = false; + } + + // Read a string, checking for error or EOF + if (fgets(&buf[bufpos], BUFSIZ - bufpos, command_file) == nullptr) { + if (ferror(command_file) && errno == EINTR) { + // ignore EINTR errors + clearerr(command_file); + buf[bufpos] = 0; + } else { + if (ferror(command_file)) { + perror("sh61"); + } + break; + } + } + + // If a complete command line has been provided, run it + bufpos = strlen(buf); + if (bufpos == BUFSIZ - 1 || (bufpos > 0 && buf[bufpos - 1] == '\n')) { + if (command* c = parse_line(buf)) { + run(c); + delete c; + } + bufpos = 0; + needprompt = 1; + } + + // Handle zombie processes and/or interrupt requests + // Your code here! + } + + return 0; +} diff --git a/pset5/sh61.hh b/pset5/sh61.hh new file mode 100644 index 0000000..4633c0c --- /dev/null +++ b/pset5/sh61.hh @@ -0,0 +1,53 @@ +#ifndef SH61_HH +#define SH61_HH +#include +#include +#include +#include +#include +#include +#include + +#define TYPE_NORMAL 0 // normal command word +#define TYPE_REDIRECTION 1 // redirection operator (>, <, 2>) + +// All other tokens are control operators that terminate the current command. +#define TYPE_SEQUENCE 2 // `;` sequence operator +#define TYPE_BACKGROUND 3 // `&` background operator +#define TYPE_PIPE 4 // `|` pipe operator +#define TYPE_AND 5 // `&&` operator +#define TYPE_OR 6 // `||` operator + +// If you want to handle an extended shell syntax for extra credit, here are +// some other token types to get you started. +#define TYPE_LPAREN 7 // `(` operator +#define TYPE_RPAREN 8 // `)` operator +#define TYPE_OTHER -1 + +// parse_shell_token(str, type, token) +// Parse the next token from the shell command `str`. Stores the type of +// the token in `*type`; this is one of the TYPE_ constants. Stores the +// token itself in `*token`. Advances `str` to the next token and returns +// that pointer. +// +// At the end of the string, returns nullptr, sets `*type` to +// TYPE_SEQUENCE, and sets `*token` to en empty string. +const char* parse_shell_token(const char* str, int* type, std::string* token); + +// claim_foreground(pgid) +// Mark `pgid` as the current foreground process group. +int claim_foreground(pid_t pgid); + +// set_signal_handler(signo, handler) +// Install handler `handler` for signal `signo`. `handler` can be SIG_DFL +// to install the default handler, or SIG_IGN to ignore the signal. Return +// 0 on success, -1 on failure. See `man 2 sigaction` or `man 3 signal`. +inline int set_signal_handler(int signo, void (*handler)(int)) { + struct sigaction sa; + sa.sa_handler = handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + return sigaction(signo, &sa, NULL); +} + +#endif