Skip to content
Snippets Groups Projects
Commit fa6bbd60 authored by bgm47's avatar bgm47 :boy_tone1:
Browse files

Update A6

parent 09dcc55c
No related branches found
No related tags found
No related merge requests found
#!/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 ]
}
\ No newline at end of file
#!/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
# Assertions
[ "$status" -eq 0 ]
}
@test "Basic command execution" {
run ./dsh <<EOF
echo "hello world"
exit
EOF
echo "Output: $output"
[[ "$output" == *"hello world"* ]]
[ "$status" -eq 0 ]
}
@test "Simple pipe" {
run ./dsh <<EOF
echo "hello world" | grep "hello"
exit
EOF
echo "Output: $output"
[[ "$output" == *"hello world"* ]]
[ "$status" -eq 0 ]
}
@test "Multiple pipes" {
run ./dsh <<EOF
echo -e "line1\nline2\nline3" | grep "line" | wc -l
exit
EOF
echo "Output: $output"
[[ "$output" == *"3"* ]]
[ "$status" -eq 0 ]
}
@test "Built-in cd command" {
run ./dsh <<EOF
cd /tmp
pwd
exit
EOF
echo "Output: $output"
[[ "$output" == *"/tmp"* ]]
[ "$status" -eq 0 ]
}
@test "Command not found" {
run ./dsh <<EOF
nonexistentcommand12345
exit
EOF
echo "Output: $output"
[[ "$output" == *"Command not found"* ]]
[ "$status" -eq 0 ] # Shell should continue after command not found
}
@test "Server startup and shutdown" {
# Find an available port
TEST_PORT=$(shuf -i 20000-30000 -n 1)
# Start server in background with the available port
./dsh -s -p $TEST_PORT > server_output.tmp 2>&1 &
SERVER_PID=$!
# Give server time to start
sleep 3
# Verify server is running
if ! ps -p $SERVER_PID > /dev/null; then
echo "ERROR: Server failed to start"
cat server_output.tmp
return 1
fi
echo "Server started with PID $SERVER_PID"
# Try graceful shutdown first
echo "Attempting graceful shutdown with SIGTERM"
kill $SERVER_PID
sleep 2
# If still running, force kill
if ps -p $SERVER_PID > /dev/null; then
echo "Server didn't respond to SIGTERM, using SIGKILL"
kill -9 $SERVER_PID
sleep 2
fi
# Verify server is stopped
if ps -p $SERVER_PID > /dev/null; then
echo "ERROR: Server failed to shut down even with SIGKILL"
ps -f $SERVER_PID
return 1
else
echo "Server successfully shut down"
fi
# Clean up
rm -f server_output.tmp
}
@test "Client tests with server running" {
# Start server in background
TEST_PORT=25123
./dsh -s -p $TEST_PORT &
SERVER_PID=$!
sleep 2 # Give server time to start
# Run client tests
echo "exit" | ./dsh -c "localhost:$TEST_PORT"
# Verify client connected and exited successfully
[ "$?" -eq 0 ]
# Clean up server
kill $SERVER_PID
sleep 1
}
@test "Client connection to server" {
# Start server with a fixed port (easier to debug)
TEST_PORT=25884
# Kill any process using the port (just to be safe)
pkill -f "dsh -s -p $TEST_PORT" || true
sleep 1
# Start server in background with explicit port
./dsh -s -p $TEST_PORT > server_output.log 2>&1 &
SERVER_PID=$!
# Give server time to start
sleep 3
# Debug: Check if server is running and on which port
ps -f -p $SERVER_PID
netstat -tuln | grep $TEST_PORT || echo "No process listening on port $TEST_PORT"
# Now for the client - try different formats to connect
# Format 1: Try with space between -c and address
echo "echo 'Testing connection'" | ./dsh -c 127.0.0.1:$TEST_PORT > client_output.log 2>&1
cat client_output.log
# Format 2: Try with address and port separately
echo "echo 'Testing connection'" | ./dsh -c 127.0.0.1 -p $TEST_PORT > client_output2.log 2>&1
cat client_output2.log
# Format 3: Try with full address:port in quotes
echo "echo 'Testing connection'" | ./dsh -c "127.0.0.1:$TEST_PORT" > client_output3.log 2>&1
cat client_output3.log
# Grab one that worked, if any
if grep -q "Testing connection" client_output.log; then
output=$(cat client_output.log)
elif grep -q "Testing connection" client_output2.log; then
output=$(cat client_output2.log)
elif grep -q "Testing connection" client_output3.log; then
output=$(cat client_output3.log)
else
output="None of the client connection attempts succeeded"
fi
# Output for debugging
echo "Final output: $output"
# Clean up
kill $SERVER_PID || true
sleep 2
rm -f server_output.log client_output*.log
# Check results (output contains success message)
[[ "$output" == *"Testing connection"* ]]
}
@test "Remote: Simple command" {
# Start server in background with a unique port
TEST_PORT=12346
./dsh -s -p $TEST_PORT &
SERVER_PID=$!
# Give server time to start
sleep 1
# Run client command
run ./dsh -c -p $TEST_PORT <<EOF
echo "hello from remote"
exit
EOF
# Stop the server
kill $SERVER_PID
echo "Output: $output"
[[ "$output" == *"hello from remote"* ]]
[ "$status" -eq 0 ]
}
@test "Remote: Pipes" {
# Start server in background with a unique port
TEST_PORT=12347
./dsh -s -p $TEST_PORT &
SERVER_PID=$!
# Give server time to start
sleep 1
# Run client command with pipes
run ./dsh -c -p $TEST_PORT <<EOF
ls | grep dshlib.c
exit
EOF
# Stop the server
kill $SERVER_PID
echo "Output: $output"
[[ "$output" == *"dshlib.c"* ]]
[ "$status" -eq 0 ]
}
@test "Remote: Built-in command cd" {
# Start server in background with a unique port
TEST_PORT=12348
./dsh -s -p $TEST_PORT &
SERVER_PID=$!
# Give server time to start
sleep 1
# Run cd command and then check pwd
run ./dsh -c -p $TEST_PORT <<EOF
cd /tmp
pwd
exit
EOF
# Stop the server
kill $SERVER_PID
echo "Output: $output"
[[ "$output" == *"/tmp"* ]]
[ "$status" -eq 0 ]
}
@test "Remote: stop-server command" {
# Start server in background with a unique port
TEST_PORT=12349
./dsh -s -p $TEST_PORT &
SERVER_PID=$!
# Give server time to start
sleep 2
# Verify server is running before sending command
ps -p $SERVER_PID > /dev/null || {
echo "Server failed to start properly"
return 1
}
# Run stop-server command
echo "stop-server" | ./dsh -c localhost -p $TEST_PORT
# Give more time for server to shut down
sleep 3
# Check if server process still exists
if ps -p $SERVER_PID > /dev/null; then
echo "ERROR: Server process $SERVER_PID still running!"
kill -9 $SERVER_PID # Force kill it to clean up
return 1
else
echo "Server successfully terminated"
fi
}
# Network error handling tests
@test "Connection to non-existent server" {
# Try to connect to a port where no server is running
OUTPUT=$(./dsh -c localhost -p 65432 2>&1 || true)
STATUS_CODE=$?
echo "Output: $OUTPUT"
echo "Status code: $STATUS_CODE"
# Check for connection failure message
[[ "$OUTPUT" == *"connection failed"* ]]
}
@test "Shell starts and accepts input" {
run "./dsh" <<EOF
echo "Hello, dsh!"
EOF
[ "$status" -eq 0 ]
[[ "$output" =~ "Hello, dsh!" ]]
}
@test "exit command" {
run "./dsh" <<EOF
exit
EOF
[ "$status" -eq 0 ]
}
@test "Pipe with multiple commands" {
run ./dsh <<EOF
printf "line1\nline2\nline3\n" | grep line | wc -l
EOF
echo "Command output: $output"
[ "$status" -eq 0 ]
[[ "$output" == *"3"* ]]
}
@test "Multiple pipes in command" {
run ./dsh <<EOF
echo "one two three four" | tr ' ' '\n' | sort | grep o
EOF
[ "$status" -eq 0 ]
[[ "$output" == *"four"* ]]
[[ "$output" == *"one"* ]]
[[ "$output" != *"three"* ]]
}
@test "Command with pipe followed by regular command" {
run ./dsh <<EOF
echo hello | grep hello
echo another command
EOF
[ "$status" -eq 0 ]
[[ "$output" == *"hello"* ]]
[[ "$output" == *"another command"* ]]
}
\ No newline at end of file
#include <stdio.h>
#include "dshlib.h"
// EXTRA CREDIT - print the drexel dragon from the readme.md
extern void print_dragon(){
printf("%s\n", DRAGON_ASCII);
}
File added
#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 <errno.h>
#include "dshlib.h"
/*
* 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 last_rc = 0;
int execute_pipeline(command_list_t *clist) {
if (!clist->num) return OK;
if (clist->num == 1) {
char *cmd = clist->commands[0].argv[0];
if (clist->commands[0].argc > 0) {
if (!strcmp(cmd, EXIT_CMD)) return OK_EXIT;
if (!strcmp(cmd, "cd")) {
handle_cd(clist->commands[0].argc > 1 ? clist->commands[0].argv[1] : NULL);
return OK;
}
if (!strcmp(cmd, "rc")) {
printf("%d\n", last_rc);
return OK;
}
}
}
pid_t *child_pids = malloc(clist->num * sizeof(pid_t));
if (!child_pids) {
fprintf(stderr, "Memory allocation failed\n");
return ERR_MEMORY;
}
int pipes[CMD_MAX - 1][2];
for (int i = 0; i < clist->num - 1; i++) {
if (pipe(pipes[i]) == -1) {
perror("pipe");
while (i--) close(pipes[i][0]), close(pipes[i][1]);
free(child_pids);
return ERR_EXEC_CMD;
}
}
for (int i = 0; i < clist->num; i++) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
for (int j = 0; j < clist->num - 1; j++) close(pipes[j][0]), close(pipes[j][1]);
free(child_pids);
return ERR_EXEC_CMD;
}
if (!pid) {
if (i > 0) dup2(pipes[i - 1][0], STDIN_FILENO);
if (i < clist->num - 1) dup2(pipes[i][1], STDOUT_FILENO);
for (int j = 0; j < clist->num - 1; j++) close(pipes[j][0]), close(pipes[j][1]);
execvp(clist->commands[i].argv[0], clist->commands[i].argv);
perror("execvp");
exit(errno);
}
child_pids[i] = pid;
}
for (int i = 0; i < clist->num - 1; i++) close(pipes[i][0]), close(pipes[i][1]);
for (int i = 0, status; i < clist->num; i++) {
waitpid(child_pids[i], &status, 0);
if (i == clist->num - 1) last_rc = WEXITSTATUS(status);
}
free(child_pids);
return OK;
}
int build_cmd_list(char *cmd_line, command_list_t *clist) {
int ret = OK;
char *cmd_copy = strdup(cmd_line);
if (!cmd_copy) {
fprintf(stderr, "Memory allocation failed\n");
return ERR_MEMORY;
}
clist->num = 0;
char *token = NULL, *context = NULL;
token = strtok_r(cmd_copy, PIPE_STRING, &context);
while (token != NULL) {
if (clist->num >= CMD_MAX) {
ret = ERR_TOO_MANY_COMMANDS;
fprintf(stderr, CMD_ERR_PIPE_LIMIT, CMD_MAX);
break;
}
if (alloc_cmd_buff(&clist->commands[clist->num]) != OK) {
ret = ERR_MEMORY;
break;
}
/* Copy token into the command buffer */
strncpy(clist->commands[clist->num]._cmd_buffer, token, SH_CMD_MAX);
clist->commands[clist->num]._cmd_buffer[SH_CMD_MAX] = '\0';
if (build_cmd_buff(clist->commands[clist->num]._cmd_buffer,
&clist->commands[clist->num]) != OK) {
free_cmd_buff(&clist->commands[clist->num]);
ret = ERR_CMD_ARGS_BAD;
break;
}
clist->num++;
token = strtok_r(NULL, PIPE_STRING, &context);
}
free(cmd_copy);
if (ret != OK) {
for (int i = 0; i < clist->num; i++) {
free_cmd_buff(&clist->commands[i]);
}
return ret;
}
if (clist->num == 0) {
fprintf(stderr, CMD_WARN_NO_CMD);
return WARN_NO_CMDS;
}
return OK;
}
int free_cmd_list(command_list_t *clist) {
if (clist == NULL) {
return ERR_MEMORY;
}
for (int i = 0; i < clist->num; i++) {
free_cmd_buff(&clist->commands[i]);
}
clist->num = 0;
return OK;
}
int exec_local_cmd_loop() {
cmd_buff_t cmd;
command_list_t cmd_list;
int rc;
if ((rc = alloc_cmd_buff(&cmd)) != OK) {
fprintf(stderr, "Memory allocation failed\n");
return ERR_MEMORY;
}
for (;;) {
printf("%s", SH_PROMPT);
if (!fgets(cmd._cmd_buffer, SH_CMD_MAX, stdin)) {
putchar('\n');
break;
}
cmd._cmd_buffer[strcspn(cmd._cmd_buffer, "\n")] = '\0';
char *trimmed = cmd._cmd_buffer;
while (*trimmed && isspace((unsigned char)*trimmed)) trimmed++;
if (!*trimmed) {
puts(CMD_WARN_NO_CMD);
continue;
}
if (!strcmp(trimmed, EXIT_CMD)) break;
if (!strcmp(trimmed, "dragon")) {
print_dragon();
continue;
}
if ((rc = build_cmd_list(trimmed, &cmd_list)) != OK) {
if (rc != WARN_NO_CMDS) fprintf(stderr, "Error parsing command: %d\n", rc);
continue;
}
rc = execute_pipeline(&cmd_list);
free_cmd_list(&cmd_list);
if (rc == OK_EXIT) break;
}
free_cmd_buff(&cmd);
return OK;
}
int handle_cd(char *path) {
if (path == NULL)
return 0;
if (chdir(path) != 0) {
perror("cd");
return -1;
}
return 0;
}
int alloc_cmd_buff(cmd_buff_t *cmd) {
if (cmd == NULL) return ERR_MEMORY;
cmd->_cmd_buffer = malloc(SH_CMD_MAX + 1); // +1 for the null terminator
if (cmd->_cmd_buffer == NULL) return ERR_MEMORY;
return OK;
}
int free_cmd_buff(cmd_buff_t *cmd) {
if (cmd == NULL) return ERR_MEMORY;
free(cmd->_cmd_buffer);
cmd->_cmd_buffer = NULL;
return OK;
}
int clear_cmd_buff(cmd_buff_t *cmd) {
if (cmd == NULL) return ERR_MEMORY;
cmd->argc = 0;
for (int i = 0; i < CMD_ARGV_MAX; i++) {
cmd->argv[i] = NULL;
}
return OK;
}
int build_cmd_buff(char *cmd_line, cmd_buff_t *cmd) {
if (cmd_line == NULL || cmd == NULL)
return ERR_MEMORY;
clear_cmd_buff(cmd);
int tokenCount = 0;
char *ptr = cmd_line;
while (*ptr) {
while (*ptr && isspace((unsigned char)*ptr))
ptr++;
if (!*ptr)
break;
char *token = ptr;
if (*ptr == '"' || *ptr == '\'') {
char quote = *ptr;
ptr++;
token = ptr;
while (*ptr && *ptr != quote)
ptr++;
if (*ptr == quote) {
*ptr = '\0';
ptr++;
}
} else {
while (*ptr && !isspace((unsigned char)*ptr))
ptr++;
if (*ptr) {
*ptr = '\0';
ptr++;
}
}
cmd->argv[tokenCount++] = token;
if (tokenCount >= CMD_ARGV_MAX - 1)
break;
}
cmd->argv[tokenCount] = NULL;
cmd->argc = tokenCount;
return tokenCount == 0 ? WARN_NO_CMDS : OK;
}
#ifndef __DSHLIB_H__
#define __DSHLIB_H__
//Constants for command structure sizes
#define DRAGON_ASCII "\
@%%%% \n\
%%%%%% \n\
%%%%%% \n\
% %%%%%%% @ \n\
%%%%%%%%%% %%%%%%% \n\
%%%%%%% %%%%@ %%%%%%%%%%%%@ %%%%%% @%%%% \n\
%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%% \n\
%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%% %%%%%%%%%%%%%%% \n\
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%% %%% \n\
%%%%%%%%%%%%%%%%%%%%%%%%%%%%@ @%%%%%%%%%%%%%%%%%% %% \n\
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%% \n\
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \n\
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%@ \n\
%%%%%%%%@ %%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%% %% \n\
%%%%%%%%%%%%% %%@%%%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%%% @% \n\
%%%%%%%%%% %%% %%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%% \n\
%%%%%%%%% % %%%%%%%%%%%%% %%%%%%%%%%%%@%%%%%%%%%%% \n\
%%%%%%%%%@ % %%%%%%%%%%%%% @%%%%%%%%%%%%%%%%%%%%%%%%% \n\
%%%%%%%%@ %%@%%%%%%%%%%%% @%%%%%%%%%%%%%%%%%%%%%%%%%%%% \n\
%%%%%%%@ %%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \n\
%%%%%%%%%% %%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% \n\
%%%%%%%%%@ @%%%%%%%%%%%%%% %%%%%%%%%%%%@ %%%% %%%%%%%%%%%%%%%%% %%%%%%%%\n\
%%%%%%%%%% %%%%%%%%%%%%%%%%% %%%%%%%%%%%%% %%%%%%%%%%%%%%%%%% %%%%%%%%%\n\
%%%%%%%%%@%%@ %%%%%%%%%%%%%%%%@ %%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%% %%\n\
%%%%%%%%%% % %%%%%%%%%%%%%%@ %%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%% %%\n\
%%%%%%%%%%%% @ %%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% \n\
%%%%%%%%%%%%% %% % %@ %%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% \n\
%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%% @%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%% \n\
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% \n\
@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%% \n\
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%% %%%%%%% \n\
%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%% @%%%%%%%%% \n\
%%%%%%%%%%%%%%%%%%%% @%@% @%%%%%%%%%%%%%%%%%% %%% \n\
%%%%%%%%%%%%%%% %%%%%%%%%% %%%%%%%%%%%%%%% % \n\
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%% \n\
%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% %%% %%%%%%%%%% %%%@ \n\
%%%%%%%%%%%%%%%%%%% %%%%%% %% %%%%%%%%%%%%%@ \n\
%%%%%%%@ \n"
#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);
int handle_cd(char *path);
void print_dragon();
//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?
The remote client determines when command output is complete by detecting the EOF character (RDSH_EOF_CHAR) sent by the server. Since TCP may split messages into multiple packets, the client must use a loop to continuously receive data until the EOF is found, buffer partial reads while appending new data, and check each received chunk for the EOF marker. The command is only considered complete once the EOF is received, ensuring reliable message boundaries even if TCP fragments the data across multiple network packets.
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 should use null termination ('\0') for commands delivered from the client to the server, a specific EOF character for server responses back to the client, and message length headers as needed. Failure to manage these aspects properly can result in a variety of issues, including commands being merged or split incorrectly, the client hanging while waiting for additional data, buffer overflows due to incomplete message boundaries, and difficulties synchronising requests with their corresponding responses.
3. Describe the general differences between stateful and stateless protocols.
Stateful and stateless protocols vary in the way session information is handled between client and server requests. Stateful protocols retain session information across multiple requests such that the server maintains the client's state, and each request depends on past ones. This approach, used in protocols like FTP and TCP, offers continuity at the expense of additional resources consumed to maintain session information. On the other hand, stateless protocols do not keep any session data, treating each request individually and sending all the data required for processing. This simplifies the design of servers, improves scalability, and reduces memory usage, as in HTTP and UDP. Stateful protocols are optimally used where there is ongoing communication required, such as in online gaming or streaming, but stateless protocols are appropriate for large-scale web services and APIs.
4. Our lecture this week stated that UDP is "unreliable". If that is the case, why would we ever use it?
UDP is great when speed is greater than reliability. It is commonly used in video streaming and online gaming, where losing a few frames is better than latency, and DNS lookups, where cheap and effective retries are fast. In addition, some light network protocols handle reliability themselves, and therefore UDP is a suitable choice. It is also suitable for IoT devices that do not require much overhead. Since UDP forsakes handshaking and acknowledgment, it is significantly faster than TCP where there is no such need for total reliability.
5. What interface/abstraction is provided by the operating system to enable applications to use network communications?
The OS provides network sockets as an abstraction level to enable applications to utilize network communications. A socket is an abstraction level that enables applications to communicate data over a network with protocol aids like TCP and UDP. The OS takes care of the low-level networking details, such as connection establishment, data transfer, and errors, and allows applications to access sockets by means of system calls or APIs. Some of the popular socket interfaces are the Berkeley Sockets API (ported in Unix/Linux and Windows) and Winsock (proprietary of Windows). These interfaces enable programs to communicate on networks without needing to directly handle low-level network operations.
\ 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;
// TODO set up cmd and response buffs
cmd_buff = (char *)malloc(RDSH_COMM_BUFF_SZ);
rsp_buff = (char *)malloc(RDSH_COMM_BUFF_SZ);
if (cmd_buff == NULL || rsp_buff == NULL) {
perror("memory allocation failed");
return client_cleanup(-1, 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(SH_PROMPT);
fflush(stdout);
if (fgets(cmd_buff, RDSH_COMM_BUFF_SZ, stdin) == NULL) {
break;
}
cmd_buff[strcspn(cmd_buff, "\n")] = '\0';
if (*cmd_buff == '\0') {
continue;
}
io_size = send(cli_socket, cmd_buff, strlen(cmd_buff) + 1, 0);
if (io_size != (ssize_t)(strlen(cmd_buff) + 1)) {
perror("send failed");
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 - 1, 0);
if (io_size < 0) {
perror("recv failed");
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, OK);
}
if (rsp_buff[io_size - 1] == RDSH_EOF_CHAR) {
printf("%.*s", (int)io_size - 1, rsp_buff);
break;
}
printf("%.*s", (int)io_size, rsp_buff);
}
if (strcmp(cmd_buff, EXIT_CMD) == 0 || strcmp(cmd_buff, "stop-server") == 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(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(server_ip);
addr.sin_port = htons(port);
ret = connect(cli_socket, (struct sockaddr *)&addr, sizeof(addr));
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