diff -Pur sshell/Makefile rsshell/Makefile --- sshell/Makefile 2026-02-01 22:12:01.844916156 -0500 +++ rsshell/Makefile 2026-02-16 11:51:54.442227657 -0500 @@ -1,4 +1,4 @@ -## Part of the solution for Assignment 1 (CS 464/564) +## Part of the solution for Assignment 2 (CS 464/564) ## by Stefan Bruda. ## Uncomment this line (and comment out the following line) to produce @@ -6,7 +6,7 @@ #CXXFLAGS = -g -Wall -pedantic -DDEBUG CXXFLAGS = -g -Wall -pedantic -all: sshell +all: rsshell tcp-utils.o: tcp-utils.h tcp-utils.cc $(CXX) $(CXXFLAGS) -c -o tcp-utils.o tcp-utils.cc @@ -17,8 +17,8 @@ sshell.o: tcp-utils.h tokenize.h sshell.cc $(CXX) $(CXXFLAGS) -c -o sshell.o sshell.cc -sshell: sshell.o tcp-utils.o tokenize.o - $(CXX) $(CXXFLAGS) -o sshell sshell.o tcp-utils.o tokenize.o +rsshell: sshell.o tcp-utils.o tokenize.o + $(CXX) $(CXXFLAGS) -o rsshell sshell.o tcp-utils.o tokenize.o clean: - rm -f sshell *~ *.o *.bak core \#* + rm -f rsshell *~ *.o *.bak core \#* diff -Pur sshell/README rsshell/README --- sshell/README 2026-02-01 22:14:13.212875404 -0500 +++ rsshell/README 2026-02-16 11:51:47.235119063 -0500 @@ -1,7 +1,7 @@ Stefan Bruda stefan@bruda.ca -Solution to Assignment 1 (CS 464/564) +Solution to Assignment 2 (CS 464/564) Content @@ -50,6 +50,10 @@ handler as the behaviour of setting SIG_IGN as handler for SIGCHLD is undefined according to the POSIX standard.) +External commands are implemented as per the handout. We use a pipe +to communicate back with the master process the event of socket +closure in a child, background process. + Tests @@ -65,10 +69,15 @@ o error conditions for the configuration, such as unreadable or garbled configuration file; in particular, negative terminal dimensions were provided in the configuration file -o special cases for input such as empty commands and EOF. - -Tests show a correctly working program, except for the problems -below. +o special cases for input such as empty commands and EOF +o various external commands including SMTP interactions with a Postfix + server; tests with and without keepalive have been issued; the + interleaving (and lack thereof) of responses has been observed as + expected. + +Tests show a correctly working program, except for the problems below, +which come all from the previous implementation. No newly introduced +bugs are known to exist. Known bugs diff -Pur sshell/shconfig rsshell/shconfig --- sshell/shconfig 2026-02-16 11:47:25.985732173 -0500 +++ rsshell/shconfig 2026-02-16 11:40:28.665610685 -0500 @@ -1,5 +1,7 @@ -# Part of the solution for Assignment 1 (CS 464/564) +# Part of the solution for Assignment 2 (CS 464/564) # by Stefan Bruda. VSIZE 10 HSIZE 30 +RHOST turing.ubishops.ca +RPORT 80 diff -Pur sshell/sshell.cc rsshell/sshell.cc --- sshell/sshell.cc 2026-02-16 11:38:09.902040335 -0500 +++ rsshell/sshell.cc 2026-02-16 11:48:38.755709462 -0500 @@ -1,5 +1,5 @@ /* - * Part of the solution for Assignment 1 (CS 464/564), + * Part of the solution for Assignment 2 (CS 464/564), * by Stefan Bruda. */ @@ -14,7 +14,7 @@ #include #include "tokenize.h" -#include "tcp-utils.h" // for `readline' only +#include "tcp-utils.h" /* * Global configuration variables. @@ -152,15 +152,161 @@ delete [] line; } +/* + * Establishes a connection to `host':`port'. Returns the socket + * descriptor, -1 on failure. Minor variation on the typical code + * from triv_client. + */ +int connect_it(const char* host, const unsigned short port) { + int sd; + // interrupted system calls will be reissued + while ((sd = connectbyportint(host, port)) < 0 && errno == EINTR) + /* NOP*/ ; + if (sd == err_host) { + fprintf(stderr, "Cannot find host %s.\n", host); + return -1; + } + if (sd < 0) { + perror("connectbyport"); + return -1; + } + // we now have a valid, connected socket + printf("Connected to %s on port %d.\n", host, port); + return sd; +} + +/* + * Communicates with the server through socket `sd' by sending to the + * server all the strings in `request' separated by blanks and + * followed by a newline. Prints then the answer received from the + * server. + * + * If the socket is negative (meaning that no connection to the server + * is in effect) then it connects to `host':`port' first. If + * `keepalive' is non-null, then the socket is closed upon termination + * and `sd' becomes negative. If the server closes connection then + * the socket is closed and `sd' becomes negative. + * + * Returns the (new) value of `sd'. + * + * Minor variation on the typical code from triv_client. + */ +int do_communicate(int sd, char* request, + const char* host, const unsigned short port, const int keepalive) { + #ifdef DEBUG + fprintf(stderr, "%s: do_communicate received descriptor %d.\n", __FILE__, sd); + #endif + + // connect unless already connected + if (sd < 0) { + sd = connect_it(host, port); + } + if (sd < 0) { + // failed connection attempt + // error already reported by connect_it + #ifdef DEBUG + fprintf(stderr, "%s: do_communicate returns descriptor %d.\n", __FILE__, sd); + #endif + return sd; + } + + // Receive and print whatever the server sends prior to client requests. + // Many modern servers expect this; Postfix in particular will otherwise throw a 554 error. + const int ALEN = 256; + char ans[ALEN]; + int n; + while ((n = recv_nonblock(sd, ans, ALEN-1, 500)) != recv_nodata) { + if (n == 0) { + close(sd); + printf("Connection closed by %s.\n", host); + #ifdef DEBUG + fprintf(stderr, "%s: do_communicate returns descriptor %d.\n", __FILE__, -1); + #endif + return -1; + } + if (n < 0) { + // problem with receiving, socket probably unusable + perror("recv_nonblock"); + shutdown(sd, SHUT_WR); + close(sd); + #ifdef DEBUG + fprintf(stderr, "%s: do_communicate returns descriptor %d.\n", __FILE__, -1); + #endif + return -1; + } + ans[n] = '\0'; + printf("%s", ans); + fflush(stdout); + } + + // have socket, will deliver + send(sd, request, strlen(request), 0); + send(sd, "\n", 1, 0); + // should probably check for sending errors, but they are so + // unlikely, and we will probably detect the problems when we + // attempt to receive anyway + + // receive and print answer + while ((n = recv_nonblock(sd, ans, ALEN-1, 500)) != recv_nodata) { + if (n == 0) { + close(sd); + printf("Connection closed by %s.\n", host); + #ifdef DEBUG + fprintf(stderr, "%s: do_communicate returns descriptor %d.\n", __FILE__, -1); + #endif + return -1; + } + if (n < 0) { + // problem with receiving, socket probably unusable + perror("recv_nonblock"); + shutdown(sd, SHUT_WR); + close(sd); + #ifdef DEBUG + fprintf(stderr, "%s: do_communicate returns descriptor %d.\n", __FILE__, -1); + #endif + return -1; + } + ans[n] = '\0'; + printf("%s", ans); + fflush(stdout); + } + + // close socket if applicable + if (!keepalive) { + shutdown(sd, SHUT_WR); + close(sd); + #ifdef DEBUG + fprintf(stderr, "%s: do_communicate returns descriptor %d.\n", __FILE__, sd); + #endif + return -1; + } + + #ifdef DEBUG + fprintf(stderr, "%s: do_communicate returns descriptor %d.\n", __FILE__, sd); + #endif + return sd; +} + int main (int argc, char** argv, char** envp) { + unsigned short port = 0; // port for remote connection + char host[129]; // remote host + host[128] = '\0'; // make sure we have a string + int keepalive = 0; // keep connections alive? (default no) + int rsocket = -1; // client socket (not connected = negative value) + int bgpipe[2]; // pipe that allows bacground commands to announce + // the closure of rsocket. + int remote_err = 0; // true whenever remote configuration + // variables are not good. size_t hsize = 0, vsize = 0; // terminal dimensions, read from // the config file char command[129]; // buffer for commands - command[128] = '\0'; + char cmd[129]; // input buffer copy + cmd[128] = command[128] = '\0'; + char* com_tok[129]; // buffer for the tokenized commands size_t num_tok; // number of tokens - printf("Simple shell v1.0.\n"); + printf("Simple shell v2.0.\n"); // Config: int confd = open(config, O_RDONLY); @@ -187,8 +333,9 @@ } } - // read terminal size - while (hsize == 0 || vsize == 0) { + // read terminal size and remote information + int host_set = 0; + while (hsize == 0 || vsize == 0 || port == 0 || host_set == 0) { int n = readline(confd, command, 128); if (n == recv_nodata) break; @@ -205,6 +352,13 @@ vsize = atol(com_tok[1]); else if (strcmp(com_tok[0], "HSIZE") == 0 && atol(com_tok[1]) > 0) hsize = atol(com_tok[1]); + // remote host and port + else if (strcmp(com_tok[0], "RPORT") == 0 && atoi(com_tok[1]) > 0) + port = (unsigned short)atoi(com_tok[1]); + else if (strcmp(com_tok[0], "RHOST") == 0) { + strncpy(host,com_tok[1],128); + host_set = 1; + } // lines that do not make sense are hereby ignored } close(confd); @@ -230,27 +384,92 @@ config); } + if (port <= 0) { + fprintf(stderr, "%s: remote port not specified or not valid.\n", config); + remote_err = 1; + } + printf("Terminal set to %ux%u.\n", (unsigned int)hsize, (unsigned int)vsize); + // We attempt to resolve the host to see whether we can connect. + struct hostent *hinfo; + hinfo = gethostbyname(host); + if (hinfo == NULL) { + if (strlen(host) == 0) { + strcpy(host, "(null)"); + } + fprintf(stderr, "%s: host name \"%s\" does not resolve.\n", config, host); + remote_err = 1; + } + + if (remote_err) + fprintf(stderr, "%s: remote commands are disabled.\n", config); + + if (! remote_err) { + if (keepalive) + printf("keepalive is on.\n"); + else + printf("keepalive is off.\n"); + printf("Peer is %s:%d.\n", host, port); + } + + if (pipe(bgpipe) < 0) { + perror("comm pipe"); + return 1; + } + // install the typical signal handler for zombie cleanup // (we will inhibit it later when we need a different behaviour, // see run_it) signal(SIGCHLD, zombie_reaper); + // Ignore SIGPIPE + signal(SIGPIPE, block_zombie_reaper); + // Command loop: while(1) { + // Re-initialize rsocket from the pipe as appropriate: + struct pollfd pipepoll; + pipepoll.fd = bgpipe[0]; + pipepoll.events = POLLIN; + + int polled; + while ((polled = poll(&pipepoll,1,100)) != 0 || + (polled == -1 && errno == EINTR) ) { + if (polled == -1 && errno != EINTR) { + perror("Pipe poll"); + // Arguably we do not want to kill the program since + // failing to read from the pipe is an error condition + // with a limited scope, meaning that most of the + // program probably functions correctly still. + } + if (polled == -1 && errno == EINTR) + continue; + char rsocket_str[128]; + rsocket_str[127] = '\0'; + readline(bgpipe[0], rsocket_str, 126); + rsocket = atoi(rsocket_str); + #ifdef DEBUG + fprintf(stderr, "%s: remote socket from pipe: %d\n", __FILE__, rsocket); + #endif + } + // Read input: printf("%s",prompt); fflush(stdin); - if (fgets(command, 128, stdin) == 0) { + if (fgets(cmd, 128, stdin) == 0) { // EOF, will quit printf("\nBye\n"); return 0; } // fgets includes the newline in the buffer, get rid of it - if(strlen(command) > 0 && command[strlen(command) - 1] == '\n') - command[strlen(command) - 1] = '\0'; + if(strlen(cmd) > 0 && cmd[strlen(cmd) - 1] == '\n') + cmd[strlen(cmd) - 1] = '\0'; + + // meed to make a copy of the command line since the we alter it during tokenization + // (no need for bound checks since command[] and cmd[] have the same size) + strcpy(command, cmd); // Tokenize input: char** real_com = com_tok; // may need to skip the first @@ -259,28 +478,117 @@ num_tok = str_tokenize(command, real_com, strlen(command)); real_com[num_tok] = 0; // null termination for execve + int remote = 1; // remote command unless first token is a bang + if (strcmp(com_tok[0], "!") == 0) { // bang? + #ifdef DEBUG // bang! + fprintf(stderr, "%s: local command\n", __FILE__); + #endif + remote = 0; + // discard the first token now that we know what is means... + real_com = com_tok + 1; + } + int bg = 0; - if (strcmp(com_tok[0], "&") == 0) { // background command coming + if (strcmp(real_com[0], "&") == 0) { // background command coming #ifdef DEBUG fprintf(stderr, "%s: background command\n", __FILE__); #endif bg = 1; // discard the first token now that we know that it // specifies a background process... - real_com = com_tok + 1; + real_com = real_com + 1; } // ASSERT: num_tok > 0 // Process input: - if (strlen(real_com[0]) == 0) // no command, user just pressed return - continue; + // Empty lines are fine now, possibly part of the application protocol (such as HTTP) + //if (strlen(real_com[0]) == 0) // no command, user just pressed return + // continue; + + else if (remote) { // remote commands are the simplest, we + // just send them to the server + if (remote_err) { + fprintf(stderr, "%s: host/port not initialized, remote commands are disabled.\n", __FILE__); + } + else { + char* request = cmd; + // eat up prefix + while (request[0] == '&' || request[0] == ' ') { + request++; + } + + if (bg) { // background + // connect unless already connected + if (rsocket < 0) { + rsocket = connect_it(host, port); + if (rsocket < 0) { + // failed connection attempt + // error already reported by connect_it + continue; + } + } + + int bgp = fork(); + if (bgp == 0) { // child handles execution + int rsocket1 = do_communicate(rsocket, request, host, port, keepalive); + printf("remote & %s done.\n", real_com[0]); + #ifdef DEBUG + fprintf(stderr, "%s: rsocket before: %d, after: %d\n", __FILE__, rsocket, rsocket1); + #endif + if (rsocket != rsocket1) { // socket change! + char rsocket_str[128]; + rsocket_str[127] = '\0'; + snprintf(rsocket_str, 127, "%d\n", rsocket1); + write(bgpipe[1], rsocket_str, strlen(rsocket_str)); + #ifdef DEBUG + fprintf(stderr, "%s: remote socket to pipe: %s\n", __FILE__, rsocket_str); + #endif + } + exit(0); + } + else { // parent goes ahead and accept next command + continue; + } + } + else // foreground + rsocket = do_communicate(rsocket, request, host, port, keepalive); + } + } else if (strcmp(real_com[0], "exit") == 0) { + // one more thing to take care of: closing the socket + if (rsocket > 0) { + printf("Closing connection to %s:%d.\n", host, port); + shutdown(rsocket, SHUT_WR); + close(rsocket); + } printf("Bye\n"); return 0; } + #ifdef DEBUG + else if (strcmp(real_com[0], "env") == 0) { + printf("rsocket=%d\n", rsocket); + } + #endif + + else if (strcmp(real_com[0], "keepalive") == 0) { + keepalive = 1; + printf("keepalive is on.\n"); + } + + else if (strcmp(real_com[0], "close") == 0) { + keepalive = 0; + printf("keepalive is off.\n"); + if (rsocket > 0) { + printf("Closing connection to %s:%d.\n", host, port); + shutdown(rsocket, SHUT_WR); + close(rsocket); + rsocket = -1; + } + } + else if (strcmp(real_com[0], "more") == 0) { // note: more never goes into background (any prefixing // `&' is ignored)