Skip to content
Snippets Groups Projects
Commit 74ca5673 authored by Adorn Binoy's avatar Adorn Binoy
Browse files

6th Assignment - Final Shell Version

parent eaaed0e6
No related branches found
No related tags found
No related merge requests found
{
"configurations": [
{
"name": "(gdb) 6-RShell",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/6-RShell/starter/dsh",
"args": [""],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/6-RShell/starter",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "Set Disassembly Flavor to Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
],
"preLaunchTask": "Build 6-RShell"
}
]
}
\ No newline at end of file
{
"version": "2.0.0",
"tasks": [
{
"label": "Build 6-RShell",
"type": "shell",
"command": "make",
"group": {
"kind": "build",
"isDefault": true
},
"options": {
"cwd": "${workspaceFolder}/6-RShell/starter"
},
"problemMatcher": ["$gcc"],
"detail": "Runs the 'make' command to build the project."
}
]
}
\ No newline at end of file
dsh
\ No newline at end of file
#!/usr/bin/env bats
############################ DO NOT EDIT THIS FILE #####################################
# File: assignement_tests.sh
#
# DO NOT EDIT THIS FILE
#
# Add/Edit Student tests in student_tests.sh
#
# All tests in this file must pass - it is used as part of grading!
########################################################################################
@test "Pipes" {
run "./dsh" <<EOF
ls | grep dshlib.c
EOF
# Strip all whitespace (spaces, tabs, newlines) from the output
stripped_output=$(echo "$output" | tr -d '[:space:]')
# Expected output with all whitespace removed for easier matching
expected_output="dshlib.clocalmodedsh4>dsh4>cmdloopreturned0"
# These echo commands will help with debugging and will only print
#if the test fails
echo "Captured stdout:"
echo "Output: $output"
echo "Exit Status: $status"
echo "${stripped_output} -> ${expected_output}"
# Check exact match
[ "$stripped_output" = "$expected_output" ]
# Assertions
[ "$status" -eq 0 ]
}
#!/usr/bin/env bats
# File: student_tests.sh
#
# Create your unit tests suit in this file
@test "Example: check ls runs without errors" {
run ./dsh <<EOF
ls
EOF
[ "$status" -eq 0 ]
}
@test "Server startup and client connection" {
./dsh -s -p 8765 &
SERVER_PID=$!
sleep 1
echo "echo connected" | ./dsh -c -p 8765 > output.txt
kill $SERVER_PID
grep "connected" output.txt
rm output.txt
}
@test "Basic ls execution" {
./dsh -s -p 8765 &
SERVER_PID=$!
sleep 1
echo "ls" | ./dsh -c -p 8765 > output.txt
kill $SERVER_PID
grep "dshlib.c" output.txt
grep "rsh_cli.c" output.txt
rm output.txt
}
@test "Client exit command" {
./dsh -s -p 8765 &
SERVER_PID=$!
sleep 1
echo "exit" | ./dsh -c -p 8765 > output.txt
ps -p $SERVER_PID
kill $SERVER_PID
}
@test "Stop-server command" {
./dsh -s -p 8765 &
SERVER_PID=$!
sleep 1
echo "stop-server" | ./dsh -c -p 8765
sleep 1
run ps -p $SERVER_PID
}
@test "Server handles cd command" {
./dsh -s -p 8888 &
SERVER_PID=$!
sleep 1
echo -e "cd /tmp\npwd" | ./dsh -c -p 8888 > output.txt
grep "/tmp" output.txt
kill $SERVER_PID
rm output.txt
}
@test "Server handles simple pipe" {
./dsh -s -p 8888 &
SERVER_PID=$!
sleep 1
echo "echo hello | grep hello" | ./dsh -c -p 8888 > output.txt
grep "hello" output.txt
kill $SERVER_PID
rm output.txt
}
\ No newline at end of file
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <argp.h>
#include <getopt.h>
#include "dshlib.h"
#include "rshlib.h"
/*
* Used to pass startup parameters back to main
*/
#define MODE_LCLI 0 //Local client
#define MODE_SCLI 1 //Socket client
#define MODE_SSVR 2 //Socket server
typedef struct cmd_args{
int mode;
char ip[16]; //e.g., 192.168.100.101\0
int port;
int threaded_server;
}cmd_args_t;
//You dont really need to understand this but the C runtime library provides
//an getopt() service to simplify handling command line arguments. This
//code will help setup dsh to handle triggering client or server mode along
//with passing optional connection parameters.
void print_usage(const char *progname) {
printf("Usage: %s [-c | -s] [-i IP] [-p PORT] [-x] [-h]\n", progname);
printf(" Default is to run %s in local mode\n", progname);
printf(" -c Run as client\n");
printf(" -s Run as server\n");
printf(" -i IP Set IP/Interface address (only valid with -c or -s)\n");
printf(" -p PORT Set port number (only valid with -c or -s)\n");
printf(" -x Enable threaded mode (only valid with -s)\n");
printf(" -h Show this help message\n");
exit(0);
}
void parse_args(int argc, char *argv[], cmd_args_t *cargs) {
int opt;
memset(cargs, 0, sizeof(cmd_args_t));
//defaults
cargs->mode = MODE_LCLI;
cargs->port = RDSH_DEF_PORT;
while ((opt = getopt(argc, argv, "csi:p:xh")) != -1) {
switch (opt) {
case 'c':
if (cargs->mode != MODE_LCLI) {
fprintf(stderr, "Error: Cannot use both -c and -s\n");
exit(EXIT_FAILURE);
}
cargs->mode = MODE_SCLI;
strncpy(cargs->ip, RDSH_DEF_CLI_CONNECT, sizeof(cargs->ip) - 1);
break;
case 's':
if (cargs->mode != MODE_LCLI) {
fprintf(stderr, "Error: Cannot use both -c and -s\n");
exit(EXIT_FAILURE);
}
cargs->mode = MODE_SSVR;
strncpy(cargs->ip, RDSH_DEF_SVR_INTFACE, sizeof(cargs->ip) - 1);
break;
case 'i':
if (cargs->mode == MODE_LCLI) {
fprintf(stderr, "Error: -i can only be used with -c or -s\n");
exit(EXIT_FAILURE);
}
strncpy(cargs->ip, optarg, sizeof(cargs->ip) - 1);
cargs->ip[sizeof(cargs->ip) - 1] = '\0'; // Ensure null termination
break;
case 'p':
if (cargs->mode == MODE_LCLI) {
fprintf(stderr, "Error: -p can only be used with -c or -s\n");
exit(EXIT_FAILURE);
}
cargs->port = atoi(optarg);
if (cargs->port <= 0) {
fprintf(stderr, "Error: Invalid port number\n");
exit(EXIT_FAILURE);
}
break;
case 'x':
if (cargs->mode != MODE_SSVR) {
fprintf(stderr, "Error: -x can only be used with -s\n");
exit(EXIT_FAILURE);
}
cargs->threaded_server = 1;
break;
case 'h':
print_usage(argv[0]);
break;
default:
print_usage(argv[0]);
}
}
if (cargs->threaded_server && cargs->mode != MODE_SSVR) {
fprintf(stderr, "Error: -x can only be used with -s\n");
exit(EXIT_FAILURE);
}
}
/* DO NOT EDIT
* main() logic fully implemented to:
* 1. run locally (no parameters)
* 2. start the server with the -s option
* 3. start the client with the -c option
*/
int main(int argc, char *argv[]){
cmd_args_t cargs;
int rc;
memset(&cargs, 0, sizeof(cmd_args_t));
parse_args(argc, argv, &cargs);
switch(cargs.mode){
case MODE_LCLI:
printf("local mode\n");
rc = exec_local_cmd_loop();
break;
case MODE_SCLI:
printf("socket client mode: addr:%s:%d\n", cargs.ip, cargs.port);
rc = exec_remote_cmd_loop(cargs.ip, cargs.port);
break;
case MODE_SSVR:
printf("socket server mode: addr:%s:%d\n", cargs.ip, cargs.port);
if (cargs.threaded_server){
printf("-> Multi-Threaded Mode\n");
} else {
printf("-> Single-Threaded Mode\n");
}
rc = start_server(cargs.ip, cargs.port, cargs.threaded_server);
break;
default:
printf("error unknown mode\n");
exit(EXIT_FAILURE);
}
printf("cmd loop returned %d\n", rc);
}
\ No newline at end of file
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include "dshlib.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include "dshlib.h"
/****
**** FOR REMOTE SHELL USE YOUR SOLUTION FROM SHELL PART 3 HERE
**** THE MAIN FUNCTION CALLS THIS ONE AS ITS ENTRY POINT TO
**** EXECUTE THE SHELL LOCALLY
****
*/
/*
* Implement your exec_local_cmd_loop function by building a loop that prompts the
* user for input. Use the SH_PROMPT constant from dshlib.h and then
* use fgets to accept user input.
*
* while(1){
* printf("%s", SH_PROMPT);
* if (fgets(cmd_buff, ARG_MAX, stdin) == NULL){
* printf("\n");
* break;
* }
* //remove the trailing \n from cmd_buff
* cmd_buff[strcspn(cmd_buff,"\n")] = '\0';
*
* //IMPLEMENT THE REST OF THE REQUIREMENTS
* }
*
* Also, use the constants in the dshlib.h in this code.
* SH_CMD_MAX maximum buffer size for user input
* EXIT_CMD constant that terminates the dsh program
* SH_PROMPT the shell prompt
* OK the command was parsed properly
* WARN_NO_CMDS the user command was empty
* ERR_TOO_MANY_COMMANDS too many pipes used
* ERR_MEMORY dynamic memory management failure
*
* errors returned
* OK No error
* ERR_MEMORY Dynamic memory management failure
* WARN_NO_CMDS No commands parsed
* ERR_TOO_MANY_COMMANDS too many pipes used
*
* console messages
* CMD_WARN_NO_CMD print on WARN_NO_CMDS
* CMD_ERR_PIPE_LIMIT print on ERR_TOO_MANY_COMMANDS
* CMD_ERR_EXECUTE print on execution failure of external command
*
* Standard Library Functions You Might Want To Consider Using (assignment 1+)
* malloc(), free(), strlen(), fgets(), strcspn(), printf()
*
* Standard Library Functions You Might Want To Consider Using (assignment 2+)
* fork(), execvp(), exit(), chdir()
*/
int build_cmd_buff(char *cmd_line, cmd_buff_t *cmd_buff) {
if (cmd_line == NULL || cmd_buff == NULL) {
return ERR_MEMORY;
}
memset(cmd_buff, 0, sizeof(cmd_buff_t));
cmd_buff->_cmd_buffer = strdup(cmd_line);
if (cmd_buff->_cmd_buffer == NULL) {
return ERR_MEMORY;
}
char *p = cmd_buff->_cmd_buffer;
int argc = 0;
bool in_quotes = false;
while (*p && isspace(*p)) p++;
while (*p && argc < CMD_ARGV_MAX - 1) {
cmd_buff->argv[argc] = p;
while (*p) {
if (*p == '"') {
in_quotes = !in_quotes;
memmove(p, p + 1, strlen(p));
continue;
}
if (!in_quotes && isspace(*p)) {
break;
}
p++;
}
if (*p) {
*p++ = '\0';
while (*p && isspace(*p)) p++;
}
argc++;
if (!*p) break;
}
cmd_buff->argv[argc] = NULL;
cmd_buff->argc = argc;
return (argc == 0) ? WARN_NO_CMDS : OK;
}
// Build a command list from a command line
int build_cmd_list(char *cmd_line, command_list_t *clist) {
char *cmd_copy = strdup(cmd_line);
if (cmd_copy == NULL) {
return ERR_MEMORY;
}
memset(clist, 0, sizeof(command_list_t));
char *token;
char *saveptr;
int cmd_count = 0;
token = strtok_r(cmd_copy, PIPE_STRING, &saveptr);
while (token != NULL && cmd_count < CMD_MAX) {
if (build_cmd_buff(token, &clist->commands[cmd_count]) != OK) {
for (int i = 0; i < cmd_count; i++) {
free(clist->commands[i]._cmd_buffer);
}
free(cmd_copy);
return ERR_MEMORY;
}
cmd_count++;
token = strtok_r(NULL, PIPE_STRING, &saveptr);
}
if (token != NULL) {
for (int i = 0; i < cmd_count; i++) {
free(clist->commands[i]._cmd_buffer);
}
free(cmd_copy);
return ERR_TOO_MANY_COMMANDS;
}
free(cmd_copy);
clist->num = cmd_count;
return (cmd_count == 0) ? WARN_NO_CMDS : OK;
}
// Free resources used by a command list
int free_cmd_list(command_list_t *clist) {
if (clist == NULL) {
return ERR_MEMORY;
}
for (int i = 0; i < clist->num; i++) {
free(clist->commands[i]._cmd_buffer);
}
return OK;
}
// Execute a pipeline of commands
int execute_pipeline(command_list_t *clist) {
int pipes[CMD_MAX-1][2];
pid_t pids[CMD_MAX];
int pids_st[CMD_MAX];
// Create all necessary pipes
for (int i = 0; i < clist->num - 1; i++) {
if (pipe(pipes[i]) < 0) {
perror("pipe");
return ERR_EXEC_CMD;
}
}
// Fork and exec each command in the pipeline
for (int i = 0; i < clist->num; i++) {
pids[i] = fork();
if (pids[i] < 0) {
perror("fork");
// Close all pipes
for (int j = 0; j < clist->num - 1; j++) {
close(pipes[j][0]);
close(pipes[j][1]);
}
return ERR_EXEC_CMD;
}
if (pids[i] == 0) {
// Child process
// Set up stdin from previous pipe
if (i > 0) {
if (dup2(pipes[i-1][0], STDIN_FILENO) < 0) {
perror("dup2 input");
exit(EXIT_FAILURE);
}
}
// Set up stdout to next pipe
if (i < clist->num - 1) {
if (dup2(pipes[i][1], STDOUT_FILENO) < 0) {
perror("dup2 output");
exit(EXIT_FAILURE);
}
}
// Close all pipe ends
for (int j = 0; j < clist->num - 1; j++) {
close(pipes[j][0]);
close(pipes[j][1]);
}
// Execute the command
execvp(clist->commands[i].argv[0], clist->commands[i].argv);
// If we get here, exec failed
perror("execvp");
exit(EXIT_FAILURE);
}
}
// Parent process: close all pipe ends
for (int i = 0; i < clist->num - 1; i++) {
close(pipes[i][0]);
close(pipes[i][1]);
}
// Wait for all children
for (int i = 0; i < clist->num; i++) {
waitpid(pids[i], &pids_st[i], 0);
}
// Return exit status of last command
return WEXITSTATUS(pids_st[clist->num - 1]);
}
// Check if a command is built-in
Built_In_Cmds match_command(const char *input) {
if (strcmp(input, EXIT_CMD) == 0)
return BI_CMD_EXIT;
if (strcmp(input, "dragon") == 0)
return BI_CMD_DRAGON;
if (strcmp(input, "cd") == 0)
return BI_CMD_CD;
if (strcmp(input, "rc") == 0)
return BI_CMD_RC;
return BI_NOT_BI;
}
// Execute a built-in command
Built_In_Cmds exec_built_in_cmd(cmd_buff_t *cmd) {
Built_In_Cmds cmd_type = match_command(cmd->argv[0]);
switch (cmd_type) {
case BI_CMD_EXIT:
return BI_CMD_EXIT;
case BI_CMD_CD:
if (cmd->argc > 1) {
if (chdir(cmd->argv[1]) != 0) {
perror("CD failed");
}
}
return BI_EXECUTED;
case BI_CMD_DRAGON:
printf("Dragon command not implemented\n");
return BI_EXECUTED;
case BI_CMD_RC:
return BI_CMD_RC;
default:
return BI_NOT_BI;
}
}
int exec_local_cmd_loop() {
char *cmd_buff = malloc(SH_CMD_MAX);
if (cmd_buff == NULL) {
fprintf(stderr, "Failed to allocate memory\n");
return ERR_MEMORY;
}
int status;
while (1) {
printf("%s", SH_PROMPT);
if (fgets(cmd_buff, SH_CMD_MAX, stdin) == NULL) {
printf("\n");
break;
}
cmd_buff[strcspn(cmd_buff, "\n")] = '\0';
if (strlen(cmd_buff) == 0) {
printf(CMD_WARN_NO_CMD);
continue;
}
command_list_t cmd_list;
memset(&cmd_list, 0, sizeof(command_list_t));
int rc = build_cmd_list(cmd_buff, &cmd_list);
if (rc < 0) {
if (rc == WARN_NO_CMDS) {
printf(CMD_WARN_NO_CMD);
} else if (rc == ERR_TOO_MANY_COMMANDS) {
printf(CMD_ERR_PIPE_LIMIT, CMD_MAX);
}
continue;
}
if (cmd_list.num == 0) {
printf(CMD_WARN_NO_CMD);
continue;
}
// Check for built-in commands
if (strcmp(cmd_list.commands[0].argv[0], EXIT_CMD) == 0) {
printf("exiting...\n");
free_cmd_list(&cmd_list);
free(cmd_buff);
return OK;
} else if (strcmp(cmd_list.commands[0].argv[0], "cd") == 0) {
if (cmd_list.commands[0].argc > 1) {
if (chdir(cmd_list.commands[0].argv[1]) != 0) {
perror("CD failed");
}
}
free_cmd_list(&cmd_list);
continue;
}
// Execute the pipeline
execute_pipeline(&cmd_list);
free_cmd_list(&cmd_list);
}
free(cmd_buff);
return OK;
}
#ifndef __DSHLIB_H__
#define __DSHLIB_H__
//Constants for command structure sizes
#define EXE_MAX 64
#define ARG_MAX 256
#define CMD_MAX 8
#define CMD_ARGV_MAX (CMD_MAX + 1)
// Longest command that can be read from the shell
#define SH_CMD_MAX EXE_MAX + ARG_MAX
typedef struct command
{
char exe[EXE_MAX];
char args[ARG_MAX];
} command_t;
#include <stdbool.h>
typedef struct cmd_buff
{
int argc;
char *argv[CMD_ARGV_MAX];
char *_cmd_buffer;
char *input_file; // extra credit, stores input redirection file (for `<`)
char *output_file; // extra credit, stores output redirection file (for `>`)
bool append_mode; // extra credit, sets append mode fomr output_file
} cmd_buff_t;
typedef struct command_list{
int num;
cmd_buff_t commands[CMD_MAX];
}command_list_t;
//Special character #defines
#define SPACE_CHAR ' '
#define PIPE_CHAR '|'
#define PIPE_STRING "|"
#define SH_PROMPT "dsh4> "
#define EXIT_CMD "exit"
#define RC_SC 99
#define EXIT_SC 100
//Standard Return Codes
#define OK 0
#define WARN_NO_CMDS -1
#define ERR_TOO_MANY_COMMANDS -2
#define ERR_CMD_OR_ARGS_TOO_BIG -3
#define ERR_CMD_ARGS_BAD -4 //for extra credit
#define ERR_MEMORY -5
#define ERR_EXEC_CMD -6
#define OK_EXIT -7
//prototypes
int alloc_cmd_buff(cmd_buff_t *cmd_buff);
int free_cmd_buff(cmd_buff_t *cmd_buff);
int clear_cmd_buff(cmd_buff_t *cmd_buff);
int build_cmd_buff(char *cmd_line, cmd_buff_t *cmd_buff);
int close_cmd_buff(cmd_buff_t *cmd_buff);
int build_cmd_list(char *cmd_line, command_list_t *clist);
int free_cmd_list(command_list_t *cmd_lst);
//built in command stuff
typedef enum {
BI_CMD_EXIT,
BI_CMD_DRAGON,
BI_CMD_CD,
BI_CMD_RC, //extra credit command
BI_CMD_STOP_SVR, //new command "stop-server"
BI_NOT_BI,
BI_EXECUTED,
} Built_In_Cmds;
Built_In_Cmds match_command(const char *input);
Built_In_Cmds exec_built_in_cmd(cmd_buff_t *cmd);
//main execution context
int exec_local_cmd_loop();
int exec_cmd(cmd_buff_t *cmd);
int execute_pipeline(command_list_t *clist);
//output constants
#define CMD_OK_HEADER "PARSED COMMAND LINE - TOTAL COMMANDS %d\n"
#define CMD_WARN_NO_CMD "warning: no commands provided\n"
#define CMD_ERR_PIPE_LIMIT "error: piping limited to %d commands\n"
#define BI_NOT_IMPLEMENTED "not implemented"
#endif
\ No newline at end of file
# Compiler settings
CC = gcc
CFLAGS = -Wall -Wextra -g
# Target executable name
TARGET = dsh
# Find all source and header files
SRCS = $(wildcard *.c)
HDRS = $(wildcard *.h)
# Default target
all: $(TARGET)
# Compile source to executable
$(TARGET): $(SRCS) $(HDRS)
$(CC) $(CFLAGS) -o $(TARGET) $(SRCS)
# Clean up build files
clean:
rm -f $(TARGET)
test:
bats $(wildcard ./bats/*.sh)
valgrind:
echo "pwd\nexit" | valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=1 ./$(TARGET)
echo "pwd\nexit" | valgrind --tool=helgrind --error-exitcode=1 ./$(TARGET)
# Phony targets
.PHONY: all clean test
\ No newline at end of file
1. How does the remote client determine when a command's output is fully received from the server, and what techniques can be used to handle partial reads or ensure complete message transmission?
This is done by using an "end of message" marker (the EOF character) which symoblizes an output being full recieved. To hand partial reads/ensuring compelte message transmissions, the clinet can enter a loop after sending a command and check the last character continually for the EOF symbol. If it is located, the entire message was recieved and if not, the loop will keep running.
2. This week's lecture on TCP explains that it is a reliable stream protocol rather than a message-oriented one. Since TCP does not preserve message boundaries, how should a networked shell protocol define and detect the beginning and end of a command sent over a TCP connection? What challenges arise if this is not handled correctly?
A networked shell protocol defines and detects the beginning and end of a comamand over TCP by using a delimiter (newline or null character) to separate commands. Additionally, prefixes or fixed-length messages can be used of a known structure is being enforced by the protocol. Some challenges that may arise if not handled correctly include message fragmentation, message concatenation, and incomplete reads.
3. Describe the general differences between stateful and stateless protocols.
Stateful protocols maintain state across multiple interactions, ensure reliability, provide robist capabilities, but are however slower. A stateless protocol does not retain state between interactions, and it is not full reliable. However, it is faster.
4. Our lecture this week stated that UDP is "unreliable". If that is the case, why would we ever use it?
UDP is optimal for message oriented communication since it transmites entire messages instead of byte streams. Additionally, low latency applications benefir since it is faster than TCP. Examples include gaming and video streaming since these services require low latency.
5. What interface/abstraction is provided by the operating system to enable applications to use network communications?
The operating system provides sockets as an interface for network communication as it allows applications to send and recieve data over networks. Additionally they provide a file-like interface for network communication (abstraction).
\ No newline at end of file
## Remote Drexel Shell
This work builds on your previous shell version that executed commands and supported pipes. Except this time, we will enable the shell program to be executed over a network. To do this we will enhance our `dsh` executable to accept some parameters. Specifically, `dsh` should mirror the behavior of the piped version of your shell, performing local `fork/exec` to run commands, including built-in commands. However, `dsh_cli.c` has been enhanced to support additional command line arguments:
```bash
#start dsh in server mode
./dsh -s
#note you can override:
# -i: Overrides default interface, e.g., -i 127.0.0.1
# -p: Overrides default port, e.g., -p 5678
```
The starter code provides information and defaults for both the `interfaces` argument as well as a default `port`. Note that server interfaces are specified in terms of IP addresses, and almost always we use the value of `0.0.0.0` for the server to indicate that the server should bind on all available network interfaces.
```bash
# start dsh in client mode
./dsh -c
#note like server-mode you can override:
# -i: Overrides default ip address of the server, e.g., -i 129.25.203.107
# -p: Overrides default port, e.g., -p 5678
```
In client mode, the `dsh` shell will connect to the server and send it commands. The IP address of the server and port can be provided if the default options require changing. Note that the defaults will work well - for example `dsh -s` to start the server and `dsh -c` to start the client if you are running in a local VM; however, the defaults might not work if you are running on tux as others might be using the default port number for this program of `1234`. If you are running on tux an example configuration might look like:
```bash
#start the server on tux, use port 7890
./dsh -s -i 0.0.0.0 -p 7890
#start the client to connect to tux, assuming tux's IP
#address is 129.25.203.107 and the server is running
#on port number 7890
./dsh -c -i 129.25.203.107 -p 7890
```
#### A cautionary tale about network I/O
We will be using the TCP/IP protocol, which is a **stream** protocol. This means that TCP/IP sends bytes in streams. There is no corresponding indication of where a logical stream begins and a logical stream ends. Consider you send the command `ls` in a single send to the server. Its possible that the server will receive `ls\0`, `ls`, or `l` on a receive. In other words, the network might break up sends into multiple receives. You must handle this yourself when receiving data over the network.
There are many techniques to handle managing a logical stream. We will consider the easiest technique that involves using a special character to mark the end of a logical stream, and then loop `recv()` calls until we receive the character that we expect.
For the remote dsh program we will use the null byte `\0` as our end of stream delimiters for requests sent by the dsh client to the server. On the way back we will use the ASCII code 0x04 or EOF character to indicate the end of the stream. We defined `RDSH_EOF_CHAR` in `rshlib.h` as follows:
```c
static const char RDSH_EOF_CHAR = 0x04;
```
**THIS MEANS THAT YOU WILL BE RESPONSIBLE FOR CORRECTLY NOT ONLY SENDING DATA BUT ENSURING THAT THE DATA YOU SEND ENDS WITH THE APPROPRIATE END OF STREAM CHARACTER**
Some pseudo-code is below for how to handle this:
```c
//Sending a stream that is null terminated
char *cmd = "ls -l"
int send_len = strlen(cmd) + 1; //the +1 includes the NULL byte
int bytes_sent;
//send the command including the null byte
bytes_sent = send(sock, cmd, send_len, 0);
```
```c
//Receiving a stream that is null terminated
char *buff;
int recv_size; //the +1 includes the NULL byte
int is_last_chunk; //boolean to see if this is the last chunk
char eof_char = '\0'; //using the null character in this demo, however
//you can set this to RDSH_EOF_CHAR, which is
//0x04, or the linux EOF character. We define
//RDSH_EOF_CHAR for you in rshlib.h. For example,
//if all we would need to do is to change:
//
// char eof_char = '\0'; to
// char eof_char = RDSH_EOF_CHAR;
//
// to handle the stream of data that the server will
// send back to the client.
//note that RDSH_COMM_BUFF_SZ is a constant that we provide in rshlib.h
buff = malloc(RDSH_COMM_BUFF_SZ);
//send the command including the null byte
while ((recv_size= recv(socket, buff, RDSH_COMM_BUFF_SZ,0)) > 0){
//we got recv_size bytes
if (recv_size < 0){
//we got an error, handle it and break out of loop or return
//from function
}
if (recv_size == 0){
//we received zero bytes, this often happens when we are waiting for
//the other side of the connection to send, but they close the socket
//for now lets just assume the other side went away and break out of
//the loop or return from the function
}
//At this point we have some data, lets see if this is the last chunk
is_last_chunk = ((char)buff[recv_size-1] == eof_char) ? 1 : 0;
if (is_last_chunk){
buff[recv_size-1] = '\0'; //remove the marker and replace with a null
//this makes string processing easier
}
//Now the data in buff is guaranteed to be null-terminated. Handle in,
//in our shell client we will just be printing it out. Note that we are
//using a special printf marker "%.*s" that will print out the characters
//until it encounters a null byte or prints out a max of recv_size
//characters, whatever happens first.
printf("%.*s", (int)recv_size, buff);
//If we are not at the last chunk, loop back and receive some more, if it
//is the last chunk break out of the loop
if (is_last_chunk)
break;
}
//NORMAL PROCESSING CONTINUES HERE
```
In our final example, we will demonstrate how to just sent the `EOF` character. You will need this in your server. Your server will do the traditional `fork/exec` pattern and you will `dup` `stdin`, `stdout`, and `stderr` to the socket you are using for communications. This means that everything you `exec` will go back over the socket to the client until the command finishes executing. However, the client will not know that the server is done until the server does one final send, sending the `EOF` character. Remember this character is defined in `rshlib.h` as RDSH_EOF_CHAR.
```c
//Demo function to just send the eof
int send_eof(int socket){
int bytes_sent;
//send one character, the EOF character.
bytes_sent = send(socket, &RDSH_EOF_CHAR, 1, 0);
if (bytes_sent == 1){
//this is what is expected, we sent a single character,
//the EOF character, so we can return a good error code.
//we use OK for this as defined in dshlib.h
return OK;
}
//handle error and send back an appropriate error code
//if bytes_sent < 0 that would indicate a network error
//if it equals zero it indicates the character could not
//be sent, which is also an error. I could not imagine a
//situation where bytes_sent > 1 since we told send to
//send exactly one byte, but if this happens it would also
//be an error.
//Ill just return a generic COMMUNICATION error we defined
//for you in rshlib.h, but you can return different error
//codes for different conditions if you want.
return ERR_RDSH_COMMUNICATION;
}
```
#### Directions
Now that we have provided an overview of what we are looking for we can turn to specific directions for this assignment including an overview of the starter code that we provide.
Files provided in the starter:
| File | Description |
| --- | --- |
| dsh_cli.c | You should not need to modify this file but you should understand it. It accepts command line args to start `dsh` in stand-alone mode, client-mode, or server-mode. |
| dshlib.h | This file is largely unchanged from the last assignment. The shell prompt has been bumped to `dsh4>` |
| dshlib.c | We provide an empty starter for this file. This file should be replaced with the code you wrote in `5-ShellP3`. It assumes that local shell execution runs via the `exec_local_cmd_loop()` command. No other changes are required - reuse the submission of your last assignment as-is!|
| rshlib.h | This header file shows key constants and prototypes needed for the remote shell implementation. There are comments in this file to highlight what they are used for |
| rsh_cli.c | The implementation of the remote shell client. The client should start executing via the `exec_remote_cmd_loop(server_ip_address, port)` function. There is significant documentation in the starter to get you going. |
| rsh_server.c | The implementation of the remote shell server. The server should start executing via the `start_server(server_interfaces, port)` function. There is significant documentation in the starter to get you going. |
This version of `dsh` has the following options, which can be viewed by executing `dsh -h`
```bash
Usage: ./dsh [-c | -s] [-i IP] [-p PORT] [-x] [-h]
Default is to run ./dsh in local mode
-c Run as client
-s Run as server
-i IP Set IP/Interface address (only valid with -c or -s)
-p PORT Set port number (only valid with -c or -s)
-x Enable threaded mode (only valid with -s)
-h Show this help message
```
The defaults for the interfaces to bind to on the server, the server IP address and the port number are specified in the `rshlib.h` file. Note these might require adjustments as there is only a single port 1234 and only one student can use this port at a time. As shown above you can adjust the port numbers and other defaults using the `-i` and `-p` command line options.
```c
#define RDSH_DEF_PORT 1234 //Default port #
#define RDSH_DEF_SVR_INTFACE "0.0.0.0" //Default start all interfaces
#define RDSH_DEF_CLI_CONNECT "127.0.0.1" //Default server is running on
//localhost 127.0.0.1
```
For this assignment we are providing a lot of leadway for you to modify the design of the starter if it makes your life easier. Below we provide the high level flow of the functions provided by the starter, but **you are free to change any of these things if you desire**.
**Main Start Code Flow**
From `dsh_cli.c`:
```
┌─────────────────────────────┐
│ENTRY-POINT: │
│main() │
│ mode = initParams() │
│ switch mode: │
│ case LCLI │
│ exec_local_cmd_loop() │
│ case SCLI │
│ exec_remote_cmd_loop() │
│ case SSVR │
│ start_server() │
└─────────────────────────────┘
```
**Client Starter Code Flow**
From `rsh_cli.c`:
```
┌────────────────────────┐
│ENTRY-POINT: │ ┌────────────────┐
│exec_remote_cmd_loop(){ │ │start_client(){ │
│ │ │ socket() │
│ start_client() ├─────▶│ connect() │
│ │ │} │
│ loop: │ └────────────────┘
│ print_prompt │
│ send-request │
│ recv-response │ ┌─────────────────┐
│ │ │client_cleanup(){│
│ client-cleanup() ├────▶│ free buffers │
│} │ │ close socket │
│ │ │} │
└────────────────────────┘ └─────────────────┘
```
**Server Starter Code Flow**
From `rsh_server.c`:
```
┌───────────────────────────┐
┌────────────────────────┐ │exec_cli_requests(){ │
│boot_server(){ │ │ allocate-recv-buffer │
│ sock = socket() │ │ loop: │
│ bind() │ │ recv() │
┌───────────────▶│ listen() │ │ build_cmd_list() │
│ │ return sock; │ │ rsh_execute_pipeline() │
│ │} │ │ send_msg_eof() │
│ └────────────────────────┘ ┌─▶│ │──┐
│ ┌────────────────────────┐ │ │ cmd==exit break │ │
┌────────────────────────┐ │process_cli_requests(){ │ │ │ cmd==stop-server break │ │
│ENTRY-POINT: │ │ loop: │ │ │ │ │
│start-server(){ │ │ accept() │ │ │ free-recv-buffer │ │
│ boot_server() │ │ rc=exec_cli_requests()│ │ │ close socket │ │
│ process_cli_requests()│───▶│ │──┘ │} │ │
│ stop_server() │ │ if rc==OK_EXIT │ └───────────────────────────┘ │
│} │ │ break loop │ │
└────────────────────────┘ │} │ ┌───────────────────────────┐ │
│ └────────────────────────┘ │rsh-execute-pipeline(){ │ │
│ ┌───────────────────────┐ │ modified execute pipeline │ │
│ │stop_server(){ │ │ dup stdin, stdout, stderr │ │
└────────────────▶│ close-server-sock │ │ file descriptors to the │◀─┘
│} │ │ client connection socket │
└───────────────────────┘ │} │
└───────────────────────────┘
```
#### Approaching this assignment
You are free to approach this assignment anyway you choose. The advice here is how we did it in the solution, and represents good engineering practice of iterative software development.
1. Start by implementing the server. Shell out a minimal implementation in `rsh_server.c` using the provided arguements from `dsh_cli.c`. The goal at this point is to be able to start a socket server, accept client connections and properly send and receive messages. Do not attempt to process any messages at this point. Build the initial version to accept commands from the client (that are expected to be null `\0` terminated), and then simply reply by _echoing_ the this input back to the client in its expected format (messages from the server back to the client are streams of characters and terminated with the `EOF` character). Remember we provide `RDSH_EOF_CHAR` in `rshlib.h` as a `static constant char` which means you can treat it like a `#define` constant but can also use it as a variable. In other words you can `send(socket, &RDSH_EOF_CHAR, 1, 0);
2. Next implement the basic client. Build the logic to connect to the server with the provided arguments collected from `dsh_cli.c`. From there copy the basic command loop from your previous shell that accepts user input from the command line. However, instead of calling any fork/exec logic, properly format the input collected from the user and send it to the server. Remember you must send a null byte at the end as per the protocol. Because C strings are null terminated you can easily do this by `send(socket, my_string, strlen(my_string)+1,0);`. After sending, build a simple receive loop given that the data sent back from the server might not come all at one time. Keep looping until the payload that you see ends with the `RDSH_EOF_CHAR` character. We describe how to do this above.
3. Expand the rdsh_server implementation to start handling some key built-in commands. These commands do not require any fork/exec logic. I would recommend starting with implementing the `exit` command that simply closes the socket connection with the client and loops the server back to the next `accept()` socket API call. Correct operation would now enable a client to connect to the server, run `exit`, then allow another client to connect to the server. After that, implement the `stop-server` command that not only closes the client connection, but also gracefully shuts down the server. This would involve freeing all allocated memory, and ultimately closing the main server socket.
4. Implement the other required built in commands such as `cd`. There are also other built-in commands that were extra credit earlier that you can integrate such as `dragon`.
5. Finally implement the fork/exec logic to implement remote commands. This will largly be the same as in your previous assignment except you will be `dup2()` the `stdin` descriptor in the first process and the `stdout` and `stderr` in the last pipeline process using the socket representing the client connection.
6. Build your tests to not only show us that your code is working, but to help you debug your code as well. In this final assignment testing will be 100% your responsibility to implement.
#### What to hand in and Grading Rubric
Grading Rubric
- 70 points: Correct implementation of required functionality points:
- 10 points: Code quality (how easy is your solution to follow)
- 10 points: Answering the written questions: questions.md
- 10 points: Quality and breadth of BATS unit tests
- 10 points: [EXTRA CREDIT] Make your server multi-threaded to handle concurrent client connections
Total points achievable is 110/100.
\ No newline at end of file
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/un.h>
#include <fcntl.h>
#include "dshlib.h"
#include "rshlib.h"
/*
* exec_remote_cmd_loop(server_ip, port)
* server_ip: a string in ip address format, indicating the servers IP
* address. Note 127.0.0.1 is the default meaning the server
* is running on the same machine as the client
*
* port: The port the server will use. Note the constant
* RDSH_DEF_PORT which is 1234 in rshlib.h. If you are using
* tux you may need to change this to your own default, or even
* better use the command line override -c implemented in dsh_cli.c
* For example ./dsh -c 10.50.241.18:5678 where 5678 is the new port
* number and the server address is 10.50.241.18
*
* This function basically implements the network version of
* exec_local_cmd_loop() from the last assignemnt. It will:
*
* 1. Allocate buffers for sending and receiving data over the
* network
* 2. Create a network connection to the server, getting an active
* socket by calling the start_client(server_ip, port) function.
* 2. Go into an infinite while(1) loop prompting the user for
* input commands.
*
* a. Accept a command from the user via fgets()
* b. Send that command to the server using send() - it should
* be a null terminated string
* c. Go into a loop and receive client requests. Note each
* receive might not be a C string so you need to print it
* out using:
* printf("%.*s", (int)bytes_received, rsp_buff);
* this version of printf() uses the "%.*s" flag that indicates
* that the rsp_buff might be a null terminated string, or
* it might not be, if its not, print exactly bytes_received
* bytes.
* d. In the recv() loop described above. Each time you receive
* data from the server, see if the last byte received is the
* EOF character. This indicates the server is done and you can
* send another command by going to the top of the loop. The
* best way to do this is as follows assuming you are receiving
* data into a buffer called recv_buff, and you received
* recv_bytes in the call to recv:
*
* recv_bytes = recv(sock, recv_buff, recv_buff_sz, 0)
*
* if recv_bytes:
* <negative_number>: communication error
* 0: Didn't receive anything, likely server down
* > 0: Got some data. Check if the last byte is EOF
* is_eof = (recv_buff[recv_bytes-1] == RDSH_EOF_CHAR) ? 1 : 0;
* if is_eof is true, this is the last part of the transmission
* from the server and you can break out of the recv() loop.
*
* returns:
* OK: The client executed all of its commands and is exiting
* either by the `exit` command that terminates the client
* or the `stop-server` command that terminates both the
* client and the server.
* ERR_MEMORY: If this function cannot allocate memory via
* malloc for the send and receive buffers
* ERR_RDSH_CLIENT: If the client cannot connect to the server.
* AKA the call to start_client() fails.
* ERR_RDSH_COMMUNICATION: If there is a communication error, AKA
* any failures from send() or recv().
*
* NOTE: Since there are several exit points and each exit point must
* call free() on the buffers allocated, close the socket, and
* return an appropriate error code. Its suggested you use the
* helper function client_cleanup() for these purposes. For example:
*
* return client_cleanup(cli_socket, request_buff, resp_buff, ERR_RDSH_COMMUNICATION);
* return client_cleanup(cli_socket, request_buff, resp_buff, OK);
*
* The above will return ERR_RDSH_COMMUNICATION and OK respectively to the main()
* function after cleaning things up. See the documentation for client_cleanup()
*
*/
int exec_remote_cmd_loop(char *address, int port)
{
char *cmd_buff;
char *rsp_buff;
int cli_socket;
ssize_t io_size;
int is_eof;
cmd_buff = malloc(RDSH_COMM_BUFF_SZ);
rsp_buff = malloc(RDSH_COMM_BUFF_SZ);
if (cmd_buff == NULL || rsp_buff == NULL) {
return client_cleanup(0, cmd_buff, rsp_buff, ERR_MEMORY);
}
cli_socket = start_client(address, port);
if (cli_socket < 0) {
perror("start client");
return client_cleanup(cli_socket, cmd_buff, rsp_buff, ERR_RDSH_CLIENT);
}
while (1)
{
printf("%s", SH_PROMPT);
if (fgets(cmd_buff, RDSH_COMM_BUFF_SZ, stdin) == NULL) {
printf("\n");
break;
}
cmd_buff[strcspn(cmd_buff, "\n")] = '\0';
if (strlen(cmd_buff) == 0) {
continue;
}
io_size = send(cli_socket, cmd_buff, strlen(cmd_buff) + 1, 0);
if (io_size < 0) {
perror("send");
return client_cleanup(cli_socket, cmd_buff, rsp_buff, ERR_RDSH_COMMUNICATION);
}
while (1) {
memset(rsp_buff, 0, RDSH_COMM_BUFF_SZ);
io_size = recv(cli_socket, rsp_buff, RDSH_COMM_BUFF_SZ, 0);
if (io_size < 0) {
perror("recv");
return client_cleanup(cli_socket, cmd_buff, rsp_buff, ERR_RDSH_COMMUNICATION);
}
if (io_size == 0) {
printf("Server disconnected\n");
return client_cleanup(cli_socket, cmd_buff, rsp_buff, ERR_RDSH_COMMUNICATION);
}
is_eof = (rsp_buff[io_size - 1] == RDSH_EOF_CHAR) ? 1 : 0;
if (is_eof) {
rsp_buff[io_size - 1] = '\0';
printf("%s", rsp_buff);
break;
} else {
printf("%.*s", (int)io_size, rsp_buff);
}
}
if (strcmp(cmd_buff, EXIT_CMD) == 0) {
break;
}
}
return client_cleanup(cli_socket, cmd_buff, rsp_buff, OK);
}
/*
* start_client(server_ip, port)
* server_ip: a string in ip address format, indicating the servers IP
* address. Note 127.0.0.1 is the default meaning the server
* is running on the same machine as the client
*
* port: The port the server will use. Note the constant
* RDSH_DEF_PORT which is 1234 in rshlib.h. If you are using
* tux you may need to change this to your own default, or even
* better use the command line override -c implemented in dsh_cli.c
* For example ./dsh -c 10.50.241.18:5678 where 5678 is the new port
* number and the server address is 10.50.241.18
*
* This function basically runs the client by:
* 1. Creating the client socket via socket()
* 2. Calling connect()
* 3. Returning the client socket after connecting to the server
*
* returns:
* client_socket: The file descriptor fd of the client socket
* ERR_RDSH_CLIENT: If socket() or connect() fail
*
*/
int start_client(char *server_ip, int port) {
struct sockaddr_in addr;
int cli_socket;
int ret;
cli_socket = socket(AF_INET, SOCK_STREAM, 0);
if (cli_socket == -1) {
perror("socket");
return ERR_RDSH_CLIENT;
}
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(server_ip);
addr.sin_port = htons(port);
ret = connect(cli_socket, (const struct sockaddr *) &addr, sizeof(struct sockaddr_in));
if (ret == -1) {
perror("connect");
close(cli_socket);
return ERR_RDSH_CLIENT;
}
return cli_socket;
}
/*
* client_cleanup(int cli_socket, char *cmd_buff, char *rsp_buff, int rc)
* cli_socket: The client socket
* cmd_buff: The buffer that will hold commands to send to server
* rsp_buff: The buffer that will hld server responses
*
* This function does the following:
* 1. If cli_socket > 0 it calls close(cli_socket) to close the socket
* 2. It calls free() on cmd_buff and rsp_buff
* 3. It returns the value passed as rc
*
* Note this function is intended to be helper to manage exit conditions
* from the exec_remote_cmd_loop() function given there are several
* cleanup steps. We provide it to you fully implemented as a helper.
* You do not have to use it if you want to develop an alternative
* strategy for cleaning things up in your exec_remote_cmd_loop()
* implementation.
*
* returns:
* rc: This function just returns the value passed as the
* rc parameter back to the caller. This way the caller
* can just write return client_cleanup(...)
*
*/
int client_cleanup(int cli_socket, char *cmd_buff, char *rsp_buff, int rc){
//If a valid socket number close it.
if(cli_socket > 0){
close(cli_socket);
}
//Free up the buffers
free(cmd_buff);
free(rsp_buff);
//Echo the return value that was passed as a parameter
return rc;
}
\ No newline at end of file
This diff is collapsed.
#ifndef __RSH_LIB_H__
#define __RSH_LIB_H__
#include "dshlib.h"
//common remote shell client and server constants and definitions
//Constants for communication
//Note that these should work fine in a local VM but you will likely have
//to change the port number if you are working on tux.
#define RDSH_DEF_PORT 1234 //Default port #
#define RDSH_DEF_SVR_INTFACE "0.0.0.0" //Default start all interfaces
#define RDSH_DEF_CLI_CONNECT "127.0.0.1" //Default server is running on
//localhost 127.0.0.1
//constants for buffer sizes
#define RDSH_COMM_BUFF_SZ (1024*64) //64K
#define STOP_SERVER_SC 200 //returned from pipeline excution
//if the command is to stop the
//server. See documentation for
//exec_client_requests() for more info
//end of message delimiter. This is super important. TCP is a stream, therefore
//the protocol designer is responsible for managing where messages begin and end
//there are many common techniques for this, but one of the simplest ways is to
//use an end of stream marker. Since rsh is a "shell" program we will be using
//ascii code 0x04, which is commonly used as the end-of-file (EOF) character in
//linux based systems.
static const char RDSH_EOF_CHAR = 0x04;
//rdsh specific error codes for functions
#define ERR_RDSH_COMMUNICATION -50 //Used for communication errors
#define ERR_RDSH_SERVER -51 //General server errors
#define ERR_RDSH_CLIENT -52 //General client errors
#define ERR_RDSH_CMD_EXEC -53 //RSH command execution errors
#define WARN_RDSH_NOT_IMPL -99 //Not Implemented yet warning
//Output message constants for server
#define CMD_ERR_RDSH_COMM "rdsh-error: communications error\n"
#define CMD_ERR_RDSH_EXEC "rdsh-error: command execution error\n"
#define CMD_ERR_RDSH_ITRNL "rdsh-error: internal server error - %d\n"
#define CMD_ERR_RDSH_SEND "rdsh-error: partial send. Sent %d, expected to send %d\n"
#define RCMD_SERVER_EXITED "server appeared to terminate - exiting\n"
//Output message constants for client
#define RCMD_MSG_CLIENT_EXITED "client exited: getting next connection...\n"
#define RCMD_MSG_SVR_STOP_REQ "client requested server to stop, stopping...\n"
#define RCMD_MSG_SVR_EXEC_REQ "rdsh-exec: %s\n"
#define RCMD_MSG_SVR_RC_CMD "rdsh-exec: rc = %d\n"
//client prototypes for rsh_cli.c - - see documentation for each function to
//see what they do
int start_client(char *address, int port);
int client_cleanup(int cli_socket, char *cmd_buff, char *rsp_buff, int rc);
int exec_remote_cmd_loop(char *address, int port);
//server prototypes for rsh_server.c - see documentation for each function to
//see what they do
int start_server(char *ifaces, int port, int is_threaded);
int boot_server(char *ifaces, int port);
int stop_server(int svr_socket);
int send_message_eof(int cli_socket);
int send_message_string(int cli_socket, char *buff);
int process_cli_requests(int svr_socket);
int exec_client_requests(int cli_socket);
int rsh_execute_pipeline(int socket_fd, command_list_t *clist);
Built_In_Cmds rsh_match_command(const char *input);
Built_In_Cmds rsh_built_in_cmd(cmd_buff_t *cmd);
//eliminate from template, for extra credit
void set_threaded_server(int val);
int exec_client_thread(int main_socket, int cli_socket);
void *handle_client(void *arg);
#endif
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment