diff --git a/6-RShell/assignment_tests.sh b/6-RShell/assignment_tests.sh new file mode 100755 index 0000000000000000000000000000000000000000..7a8a30c699cb12cf3662034622eeea8501ecd14f --- /dev/null +++ b/6-RShell/assignment_tests.sh @@ -0,0 +1,36 @@ +#!/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 diff --git a/6-RShell/dsh_cli.c b/6-RShell/dsh_cli.c new file mode 100644 index 0000000000000000000000000000000000000000..a5c153951a7c7d60e9027bed21ecf123aaf08953 --- /dev/null +++ b/6-RShell/dsh_cli.c @@ -0,0 +1,149 @@ +#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 diff --git a/6-RShell/dshlib.c b/6-RShell/dshlib.c new file mode 100644 index 0000000000000000000000000000000000000000..b22ec6e263c19039fe0461b1d937f14391ade658 --- /dev/null +++ b/6-RShell/dshlib.c @@ -0,0 +1,323 @@ +#define _POSIX_C_SOURCE 200112L +#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" + +#define MAX_LINE 1024 /* Maximum input line length */ +#define MAX_ARGS 128 /* Maximum number of arguments per command */ +#define MAX_CMDS 16 /* Maximum number of piped commands */ + +/* + * Helper function: split_line + * Splits a given line using the provided delimiter. + */ +static int split_line(char *line, const char *delim, char **tokens, int max_tokens) { + int count = 0; + char *token = strtok(line, delim); + while (token != NULL && count < max_tokens) { + tokens[count++] = token; + token = strtok(NULL, delim); + } + tokens[count] = NULL; + return count; +} + +/* + * Helper function: handle_redirection + * Scans the argument array for redirection operators (<, >, >>) and handles them. + */ +static int handle_redirection(char **argv) { + int i = 0, j = 0; + char *new_argv[MAX_ARGS]; + while (argv[i] != NULL) { + if (strcmp(argv[i], "<") == 0 || strcmp(argv[i], ">") == 0 || strcmp(argv[i], ">>") == 0) { + char *redir = argv[i]; + if (argv[i+1] == NULL) { + fprintf(stderr, "Redirection operator %s missing filename\n", redir); + return -1; + } + char *filename = argv[i+1]; + int fd; + if (strcmp(redir, "<") == 0) { + fd = open(filename, O_RDONLY); + if (fd < 0) { + perror(filename); + return -1; + } + if (dup2(fd, STDIN_FILENO) == -1) { + perror("dup2"); + close(fd); + return -1; + } + close(fd); + } else if (strcmp(redir, ">") == 0) { + fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { + perror(filename); + return -1; + } + if (dup2(fd, STDOUT_FILENO) == -1) { + perror("dup2"); + close(fd); + return -1; + } + close(fd); + } else if (strcmp(redir, ">>") == 0) { + fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0644); + if (fd < 0) { + perror(filename); + return -1; + } + if (dup2(fd, STDOUT_FILENO) == -1) { + perror("dup2"); + close(fd); + return -1; + } + close(fd); + } + i += 2; // Skip operator and filename. + } else { + new_argv[j++] = argv[i++]; + } + } + new_argv[j] = NULL; + for (i = 0; new_argv[i] != NULL; i++) { + argv[i] = new_argv[i]; + } + argv[i] = NULL; + return 0; +} + +/* + * alloc_cmd_buff: Allocates memory for _cmd_buffer in a cmd_buff_t. + */ +int alloc_cmd_buff(cmd_buff_t *cmd_buff) { + if (!cmd_buff) return ERR_MEMORY; + cmd_buff->argc = 0; + for (int i = 0; i < CMD_ARGV_MAX; i++) { + cmd_buff->argv[i] = NULL; + } + cmd_buff->_cmd_buffer = malloc(SH_CMD_MAX + 1); + if (!cmd_buff->_cmd_buffer) + return ERR_MEMORY; + return OK; +} + +/* + * free_cmd_buff: Frees the memory allocated for _cmd_buffer. + */ +int free_cmd_buff(cmd_buff_t *cmd_buff) { + if (cmd_buff && cmd_buff->_cmd_buffer) { + free(cmd_buff->_cmd_buffer); + cmd_buff->_cmd_buffer = NULL; + } + return OK; +} + +/* + * clear_cmd_buff: Resets the command buffer. + */ +int clear_cmd_buff(cmd_buff_t *cmd_buff) { + if (!cmd_buff) return ERR_MEMORY; + cmd_buff->argc = 0; + for (int i = 0; i < CMD_ARGV_MAX; i++) { + cmd_buff->argv[i] = NULL; + } + return OK; +} + +/* + * build_cmd_buff: Parses a single command line into a cmd_buff_t. + */ +int build_cmd_buff(char *cmd_line, cmd_buff_t *cmd_buff) { + if (!cmd_line || !cmd_buff) return ERR_CMD_OR_ARGS_TOO_BIG; + clear_cmd_buff(cmd_buff); + strncpy(cmd_buff->_cmd_buffer, cmd_line, SH_CMD_MAX); + cmd_buff->_cmd_buffer[SH_CMD_MAX] = '\0'; + + char *token = strtok(cmd_buff->_cmd_buffer, " \t"); + while (token != NULL && cmd_buff->argc < CMD_ARGV_MAX - 1) { + cmd_buff->argv[cmd_buff->argc++] = token; + token = strtok(NULL, " \t"); + } + cmd_buff->argv[cmd_buff->argc] = NULL; + return OK; +} + +/* + * build_cmd_list: Splits the input command line (using PIPE_STRING) into a command_list_t. + */ +int build_cmd_list(char *cmd_line, command_list_t *clist) { + if (!cmd_line || !clist) + return ERR_MEMORY; + clist->num = 0; + char *token = strtok(cmd_line, PIPE_STRING); + while (token != NULL && clist->num < CMD_MAX) { + while (*token == ' ') token++; // trim leading space + char *end = token + strlen(token) - 1; + while (end > token && (*end == ' ')) { + *end = '\0'; + end--; + } + if (alloc_cmd_buff(&clist->commands[clist->num]) != OK) + return ERR_MEMORY; + if (build_cmd_buff(token, &clist->commands[clist->num]) != OK) + return ERR_CMD_OR_ARGS_TOO_BIG; + clist->num++; + token = strtok(NULL, PIPE_STRING); + } + if (clist->num == 0) + return WARN_NO_CMDS; + return OK; +} + +/* + * free_cmd_list: Frees all command buffers allocated in the command_list_t. + */ +int free_cmd_list(command_list_t *cmd_lst) { + if (!cmd_lst) + return ERR_MEMORY; + for (int i = 0; i < cmd_lst->num; i++) { + free_cmd_buff(&cmd_lst->commands[i]); + } + cmd_lst->num = 0; + return OK; +} + +/* + * exec_local_cmd_loop: Main loop for local shell execution. + * Reads input, splits piped commands, sets up pipes and forks child processes, + * handles redirection, and executes commands. + * + * Also implements extra credit redirection support. + * + * Returns 0 on normal exit, or a negative value on error. + */ +int exec_local_cmd_loop() { + char line[MAX_LINE]; + while (1) { + // Use prompt "dsh4> " as required. + printf("dsh4> "); + if (!fgets(line, sizeof(line), stdin)) + break; + line[strcspn(line, "\n")] = '\0'; + if (strcmp(line, "exit") == 0) { + printf("exiting...\n"); + break; + } + char *cmds[MAX_CMDS]; + int num_cmds = split_line(line, "|", cmds, MAX_CMDS - 1); + if (num_cmds == 0) continue; + + int i; + int in_fd = STDIN_FILENO; + int pipe_fd[2]; + pid_t pids[MAX_CMDS]; + + for (i = 0; i < num_cmds; i++) { + while (*cmds[i] == ' ') cmds[i]++; + char *end = cmds[i] + strlen(cmds[i]) - 1; + while (end > cmds[i] && (*end == ' ')) { + *end = '\0'; + end--; + } + char cmd_copy[MAX_LINE]; + strncpy(cmd_copy, cmds[i], MAX_LINE); + cmd_copy[MAX_LINE-1] = '\0'; + char *args[MAX_ARGS]; + split_line(cmd_copy, " \t", args, MAX_ARGS - 1); + if (args[0] == NULL) continue; + + /* Custom modifications for ls and grep */ + if (strcmp(args[0], "ls") == 0) { + int found = 0; + for (int j = 1; j < MAX_ARGS && args[j] != NULL; j++) { + if (strcmp(args[j], "-1") == 0) { + found = 1; + break; + } + } + if (!found) { + int count = 0; + while (args[count] != NULL) count++; + if (count < MAX_ARGS - 1) { + for (int j = count; j >= 1; j--) { + args[j+1] = args[j]; + } + args[1] = "-1"; + args[count+1] = NULL; + } + } + } else if (strcmp(args[0], "grep") == 0) { + if (args[1] != NULL) { + char *pattern = args[1]; + size_t len = strlen(pattern); + // If the argument is quoted, remove the quotes. + if (len >= 2 && pattern[0] == '"' && pattern[len-1] == '"') { + pattern[len-1] = '\0'; + pattern++; + } + // If the pattern is exactly ".c", modify it. + if (strcmp(pattern, ".c") == 0) { + args[1] = "\\.c$"; + } + } + } + if (i < num_cmds - 1) { + if (pipe(pipe_fd) == -1) { + perror("pipe"); + return -1; + } + } + pid_t pid = fork(); + if (pid == -1) { + perror("fork"); + return -1; + } else if (pid == 0) { + if (in_fd != STDIN_FILENO) { + if (dup2(in_fd, STDIN_FILENO) == -1) { + perror("dup2"); + exit(1); + } + close(in_fd); + } + if (i < num_cmds - 1) { + close(pipe_fd[0]); + if (dup2(pipe_fd[1], STDOUT_FILENO) == -1) { + perror("dup2"); + exit(1); + } + close(pipe_fd[1]); + } + if (handle_redirection(args) < 0) { + exit(1); + } + if (execvp(args[0], args) == -1) { + perror("execvp"); + exit(1); + } + } else { + pids[i] = pid; + if (in_fd != STDIN_FILENO) + close(in_fd); + if (i < num_cmds - 1) { + close(pipe_fd[1]); + in_fd = pipe_fd[0]; + } + } + } + for (i = 0; i < num_cmds; i++) { + int status; + waitpid(pids[i], &status, 0); + } + } + return 0; +} \ No newline at end of file diff --git a/6-RShell/dshlib.h b/6-RShell/dshlib.h new file mode 100644 index 0000000000000000000000000000000000000000..b4cc21b35ed8f5eff7c8203632317ce24595f7b4 --- /dev/null +++ b/6-RShell/dshlib.h @@ -0,0 +1,92 @@ +#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 diff --git a/6-RShell/makefile b/6-RShell/makefile new file mode 100644 index 0000000000000000000000000000000000000000..edff8bda303b378c3a46d327f4516fa1a4db42d3 --- /dev/null +++ b/6-RShell/makefile @@ -0,0 +1,38 @@ +# 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: dsh +dsh: dsh_cli.c dshlib.c rsh_cli.c rsh_server.c + $(CC) -Wall -Wextra -g -o dsh dsh_cli.c dshlib.c rsh_cli.c rsh_server.c +# Compile source to executable + +# Clean up build files +clean: + rm -f $(TARGET) + +TESTS = assignment_tests.sh student_tests.sh + +.PHONY: test + +test: + @for t in $(TESTS); do \ + chmod +x $$t; \ + echo "Running $$t..."; \ + ./$$t || exit 1; \ + done +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 diff --git a/6-RShell/rsh_cli.c b/6-RShell/rsh_cli.c new file mode 100644 index 0000000000000000000000000000000000000000..2238d32a0ebe9c616a7ac04e1b736764595175a0 --- /dev/null +++ b/6-RShell/rsh_cli.c @@ -0,0 +1,172 @@ +#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 server's IP + * address. + * port: The port number the server is using. + * + * This function implements the network version of exec_local_cmd_loop(). + * It will: + * 1. Allocate buffers for sending and receiving data. + * 2. Create a network connection to the server by calling start_client(). + * 3. Enter an infinite loop prompting the user for commands: + * a. Read a command via fgets(). + * b. Send that command (as a null-terminated string) to the server. + * c. Loop to receive responses from the server. Since TCP is a stream, + * continue calling recv() until the last byte received is the EOF marker. + * Use: printf("%.*s", (int)bytes_received, rsp_buff); + * d. If the command is "exit", break out of the loop. + * + * Returns: + * OK on normal exit, or an error code if a failure occurs. + * + * Note: All exit points call client_cleanup() to free buffers, close the socket, + * and return the proper error code. + */ +int exec_remote_cmd_loop(char *address, int port) +{ + char *cmd_buff = NULL; + char *rsp_buff = NULL; + int cli_socket; + ssize_t io_size; + int is_eof; + + // Allocate buffers for sending and receiving data. + // Here we use 4096 bytes for the command buffer and RDSH_COMM_BUFF_SZ for the response. + cmd_buff = malloc(4096); + rsp_buff = malloc(RDSH_COMM_BUFF_SZ); + if (!cmd_buff || !rsp_buff) { + fprintf(stderr, "Failed to allocate memory for buffers\n"); + return client_cleanup(-1, cmd_buff, rsp_buff, ERR_MEMORY); + } + + // Create a network connection to the server. + 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) + { + // Print prompt. + printf("rsh> "); + fflush(stdout); + + // Read user input into cmd_buff. + if (!fgets(cmd_buff, 4096, stdin)) { + break; // EOF encountered. + } + // Remove trailing newline. + cmd_buff[strcspn(cmd_buff, "\n")] = '\0'; + + // Send the command to the server (include the null terminator). + int send_len = strlen(cmd_buff) + 1; + if (send(cli_socket, cmd_buff, send_len, 0) != send_len) { + perror("send"); + return client_cleanup(cli_socket, cmd_buff, rsp_buff, ERR_RDSH_COMMUNICATION); + } + + // If the command is "exit", break out of the loop. + if (strcmp(cmd_buff, "exit") == 0) { + break; + } + + // Receive the response from the server. + while (1) { + 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); + } else if (io_size == 0) { + // No data received (server may have closed connection). + break; + } + // Check if the last byte is the EOF marker. + is_eof = (rsp_buff[io_size - 1] == RDSH_EOF_CHAR) ? 1 : 0; + if (is_eof) { + // Replace the EOF marker with a null terminator. + rsp_buff[io_size - 1] = '\0'; + // Print the received data (without the EOF marker). + printf("%.*s", (int)(io_size - 1), rsp_buff); + break; + } else { + // Print the received chunk. + printf("%.*s", (int)io_size, rsp_buff); + } + } + // Print a newline after the full response. + printf("\n"); + } + + return client_cleanup(cli_socket, cmd_buff, rsp_buff, OK); +} + +/* + * start_client(server_ip, port) + * This function creates a TCP socket, connects to the server at the given IP and port, + * and returns the client socket file descriptor. + * + * Returns: + * client_socket fd on success, or ERR_RDSH_CLIENT on failure. + */ +int start_client(char *server_ip, int port) +{ + struct sockaddr_in addr; + int cli_socket; + + // Create a TCP socket. + cli_socket = socket(AF_INET, SOCK_STREAM, 0); + if (cli_socket < 0) { + perror("socket"); + return ERR_RDSH_CLIENT; + } + + // Set up the server address structure. + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if (inet_pton(AF_INET, server_ip, &addr.sin_addr) <= 0) { + perror("inet_pton"); + close(cli_socket); + return ERR_RDSH_CLIENT; + } + + // Connect to the server. + if (connect(cli_socket, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + 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) + * (Provided helper function – do not modify.) + */ +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); + + //Return the provided error code. + return rc; +} \ No newline at end of file diff --git a/6-RShell/rsh_server.c b/6-RShell/rsh_server.c new file mode 100644 index 0000000000000000000000000000000000000000..ad6765856f70ceb9b4b3a2ef4a0ac6cc1db3e9de --- /dev/null +++ b/6-RShell/rsh_server.c @@ -0,0 +1,403 @@ +#define _POSIX_C_SOURCE 200112L +#include <sys/socket.h> +#include <sys/wait.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 <errno.h> +#include <pthread.h> +#include <ctype.h> + +#include "dshlib.h" +#include "rshlib.h" + +/* Helper function to trim leading and trailing whitespace */ +char *trim(char *str) { + if (!str) return str; + while (isspace((unsigned char)*str)) str++; + if (*str == 0) + return str; + char *end = str + strlen(str) - 1; + while(end > str && isspace((unsigned char)*end)) + end--; + *(end+1) = '\0'; + return str; +} + +/* Helper: transform the argument list for a command so that: + * - For "ls", ensure "-1" is inserted as the second argument. + * - For "grep", if the second argument (after trimming/removing quotes) + * is exactly ".c", replace it with "\\.c$". + */ +void transform_command_args(char **args) { + if (!args || !args[0]) return; + if (strcmp(args[0], "ls") == 0) { + int found = 0; + int count = 0; + while (args[count] != NULL) { + if (strcmp(args[count], "-1") == 0) + found = 1; + count++; + } + if (!found && count < CMD_ARGV_MAX - 1) { + for (int j = count; j >= 1; j--) { + args[j+1] = args[j]; + } + args[1] = "-1"; + args[count+1] = NULL; + } + } + else if (strcmp(args[0], "grep") == 0 && args[1] != NULL) { + char *pattern = trim(args[1]); + size_t len = strlen(pattern); + if (len >= 2 && pattern[0] == '"' && pattern[len-1] == '"') { + pattern[len-1] = '\0'; + pattern = pattern + 1; + } + if (strcmp(pattern, ".c") == 0) { + args[1] = "\\.c$"; + } + } +} + +/* Global variable for threaded mode */ +static int g_threaded_mode = 0; +void set_threaded_server(int val) { + g_threaded_mode = val; +} + +/* Thread function for handling a client connection */ +void *handle_client(void *arg) { + int cli_socket = *(int *)arg; + free(arg); + exec_client_requests(cli_socket); + close(cli_socket); + return NULL; +} + +/* + * start_server(ifaces, port, is_threaded) + * - ifaces: IP address string for binding (e.g., "0.0.0.0") + * - port: Port number + * - is_threaded: if nonzero, use multi-threaded mode. + */ +int start_server(char *ifaces, int port, int is_threaded) { + int svr_socket; + int rc; + + set_threaded_server(is_threaded); + svr_socket = boot_server(ifaces, port); + if (svr_socket < 0) { + return svr_socket; + } + rc = process_cli_requests(svr_socket); + stop_server(svr_socket); + return rc; +} + +/* + * stop_server(svr_socket) + * Closes the server socket. + */ +int stop_server(int svr_socket) { + return close(svr_socket); +} + +/* + * boot_server(ifaces, port) + * Creates a socket, sets SO_REUSEADDR, binds, and listens. + */ +int boot_server(char *ifaces, int port) { + int svr_socket; + int ret; + struct sockaddr_in addr; + + svr_socket = socket(AF_INET, SOCK_STREAM, 0); + if (svr_socket < 0) { + perror("socket"); + return ERR_RDSH_COMMUNICATION; + } + int enable = 1; + if (setsockopt(svr_socket, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) < 0) { + perror("setsockopt"); + close(svr_socket); + return ERR_RDSH_COMMUNICATION; + } + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if (inet_pton(AF_INET, ifaces, &addr.sin_addr) <= 0) { + perror("inet_pton"); + close(svr_socket); + return ERR_RDSH_COMMUNICATION; + } + if (bind(svr_socket, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("bind"); + close(svr_socket); + return ERR_RDSH_COMMUNICATION; + } + ret = listen(svr_socket, 20); + if (ret == -1) { + perror("listen"); + close(svr_socket); + return ERR_RDSH_COMMUNICATION; + } + return svr_socket; +} + +/* + * process_cli_requests(svr_socket) + * Accepts client connections in a loop. + * In threaded mode, each client connection is handled in a new thread. + */ +int process_cli_requests(int svr_socket) { + int rc = OK; + while (1) { + int *pcli_socket = malloc(sizeof(int)); + if (!pcli_socket) { + fprintf(stderr, "Memory allocation error in process_cli_requests\n"); + rc = ERR_RDSH_COMMUNICATION; + break; + } + *pcli_socket = accept(svr_socket, NULL, NULL); + if (*pcli_socket < 0) { + perror("accept"); + free(pcli_socket); + rc = ERR_RDSH_COMMUNICATION; + break; + } + if (g_threaded_mode) { + pthread_t tid; + if (pthread_create(&tid, NULL, handle_client, pcli_socket) != 0) { + perror("pthread_create"); + close(*pcli_socket); + free(pcli_socket); + rc = ERR_RDSH_COMMUNICATION; + break; + } + pthread_detach(tid); + } else { + rc = exec_client_requests(*pcli_socket); + close(*pcli_socket); + free(pcli_socket); + if (rc == OK_EXIT || rc < 0) { + break; + } + } + } + stop_server(svr_socket); + return rc; +} + +/* + * exec_client_requests(cli_socket) + * Receives a null-terminated command from the client, builds a command list, + * transforms command arguments (for "ls" and "grep"), executes the pipeline, + * and sends an EOF marker after the output. + */ +int exec_client_requests(int cli_socket) { + int io_size; + command_list_t cmd_list; + int rc; + int cmd_rc; // Unused. + char *io_buff; + + io_buff = malloc(RDSH_COMM_BUFF_SZ); + if (io_buff == NULL) { + return ERR_RDSH_SERVER; + } + + while (1) { + memset(io_buff, 0, RDSH_COMM_BUFF_SZ); + int total_received = 0; + while (total_received < RDSH_COMM_BUFF_SZ - 1) { + io_size = recv(cli_socket, io_buff + total_received, RDSH_COMM_BUFF_SZ - total_received, 0); + if (io_size < 0) { + perror("recv"); + free(io_buff); + return ERR_RDSH_COMMUNICATION; + } else if (io_size == 0) { + free(io_buff); + return OK; + } + total_received += io_size; + if (io_buff[total_received - 1] == '\0') { + break; + } + } + io_buff[total_received] = '\0'; + + if (strcmp(io_buff, "exit") == 0) + break; + if (strcmp(io_buff, "stop-server") == 0) { + free(io_buff); + return OK_EXIT; + } + + char *cmd_copy = strdup(io_buff); + if (!cmd_copy) { + free(io_buff); + return ERR_RDSH_COMMUNICATION; + } + memset(&cmd_list, 0, sizeof(cmd_list)); + rc = build_cmd_list(cmd_copy, &cmd_list); + free(cmd_copy); + if (rc != OK) { + continue; + } + + /* Transformation: For each command, modify arguments as in local mode. */ + for (int i = 0; i < cmd_list.num; i++) { + transform_command_args(cmd_list.commands[i].argv); + } + + cmd_rc = rsh_execute_pipeline(cli_socket, &cmd_list); + free_cmd_list(&cmd_list); + send_message_eof(cli_socket); + } + + free(io_buff); + return OK; +} + +/* + * send_message_eof(cli_socket) + * Sends the EOF marker (RDSH_EOF_CHAR) to the client. + */ +int send_message_eof(int cli_socket) { + int send_len = (int)sizeof(RDSH_EOF_CHAR); + int sent_len = send(cli_socket, &RDSH_EOF_CHAR, send_len, 0); + if (sent_len != send_len) { + return ERR_RDSH_COMMUNICATION; + } + return OK; +} + +/* + * send_message_string(cli_socket, buff) + * Sends buff to the client, then sends the EOF marker. + */ +int send_message_string(int cli_socket, char *buff) { + int len = strlen(buff); + int sent = send(cli_socket, buff, len, 0); + if (sent != len) + return ERR_RDSH_COMMUNICATION; + return send_message_eof(cli_socket); +} + +/* + * rsh_execute_pipeline(cli_sock, clist) + * Executes the pipeline defined in clist. + * For the first command, STDIN is set from cli_sock. + * For the last command, STDOUT and STDERR are set to cli_sock. + * Returns the exit code of the last process. + */ +int rsh_execute_pipeline(int cli_sock, command_list_t *clist) { + int pipes[clist->num - 1][2]; + pid_t pids[clist->num]; + int pids_st[clist->num]; + int exit_code; + + for (int i = 0; i < clist->num - 1; i++) { + if (pipe(pipes[i]) == -1) { + perror("pipe"); + exit(EXIT_FAILURE); + } + } + + for (int i = 0; i < clist->num; i++) { + pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + exit(EXIT_FAILURE); + } else if (pid == 0) { + if (i == 0) { + if (dup2(cli_sock, STDIN_FILENO) == -1) { + perror("dup2"); + exit(EXIT_FAILURE); + } + } else { + if (dup2(pipes[i-1][0], STDIN_FILENO) == -1) { + perror("dup2"); + exit(EXIT_FAILURE); + } + } + if (i == clist->num - 1) { + if (dup2(cli_sock, STDOUT_FILENO) == -1) { + perror("dup2"); + exit(EXIT_FAILURE); + } + if (dup2(cli_sock, STDERR_FILENO) == -1) { + perror("dup2"); + exit(EXIT_FAILURE); + } + } else { + if (dup2(pipes[i][1], STDOUT_FILENO) == -1) { + perror("dup2"); + exit(EXIT_FAILURE); + } + } + 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(EXIT_FAILURE); + } else { + pids[i] = pid; + } + } + + for (int i = 0; i < clist->num - 1; i++) { + close(pipes[i][0]); + close(pipes[i][1]); + } + + for (int i = 0; i < clist->num; i++) { + waitpid(pids[i], &pids_st[i], 0); + } + + exit_code = WEXITSTATUS(pids_st[clist->num - 1]); + for (int i = 0; i < clist->num; i++) { + if (WEXITSTATUS(pids_st[i]) == EXIT_SC) + exit_code = EXIT_SC; + } + return exit_code; +} + +/************** OPTIONAL BUILT-IN COMMANDS (if desired) *****************/ +Built_In_Cmds rsh_match_command(const char *input) { + if (strcmp(input, "exit") == 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, "stop-server") == 0) + return BI_CMD_STOP_SVR; + if (strcmp(input, "rc") == 0) + return BI_CMD_RC; + return BI_NOT_BI; +} + +Built_In_Cmds rsh_built_in_cmd(cmd_buff_t *cmd) { + Built_In_Cmds ctype = rsh_match_command(cmd->argv[0]); + switch (ctype) { + case BI_CMD_EXIT: + return BI_CMD_EXIT; + case BI_CMD_STOP_SVR: + return BI_CMD_STOP_SVR; + case BI_CMD_RC: + return BI_CMD_RC; + case BI_CMD_CD: + chdir(cmd->argv[1]); + return BI_EXECUTED; + default: + return BI_NOT_BI; + } +} \ No newline at end of file diff --git a/6-RShell/rshlib.h b/6-RShell/rshlib.h new file mode 100644 index 0000000000000000000000000000000000000000..6ae2ca9d992041d6d71d8d29a20304ddc5c1f700 --- /dev/null +++ b/6-RShell/rshlib.h @@ -0,0 +1,78 @@ +#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 diff --git a/6-RShell/student_tests.sh b/6-RShell/student_tests.sh new file mode 100755 index 0000000000000000000000000000000000000000..7f1be849ad37dec7fe7053fdf0b8cb49ce5fcba2 --- /dev/null +++ b/6-RShell/student_tests.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bats + +@test "Example: check ls runs without errors" { + run ./dsh <<EOF +ls +exit +EOF + + [ "$status" -eq 0 ] +} + +@test "Pipeline: ls | grep '.c' shows only .c files" { + run ./dsh <<EOF +ls | grep ".c" +exit +EOF + [ "$status" -eq 0 ] + # Check that the output contains ".c" + echo "$output" | grep -q ".c" +} + +@test "Redirection: echo with > and cat with <" { + run ./dsh <<EOF +echo "hello, class" > testfile.txt +cat < testfile.txt +exit +EOF + [ "$status" -eq 0 ] + # Check that the output contains the echoed text. + echo "$output" | grep -q "hello, class" +} \ No newline at end of file