Skip to content
Snippets Groups Projects
Select Git revision
  • fa6bbd6012fa879b63957e0fe856117b4a0c12d1
  • main default
2 results

rsh_server.c

Blame
  • Bao Mai's avatar
    bgm47 authored
    fa6bbd60
    History
    rsh_server.c 27.01 KiB
    
    #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>
    
    //INCLUDES for extra credit
    #include <signal.h>
    //#include <pthread.h>
    //-------------------------
    
    #include "dshlib.h"
    #include "rshlib.h"
    
    int server_socket = -1;
    void handle_signal(int sig) {
        printf("Received signal %d, shutting down server...\n", sig);
        if (server_socket != -1) {
            printf("Closing server socket %d\n", server_socket);
            close(server_socket);
        }
        printf("Server shutdown complete\n");
        fflush(stdout);
        exit(0);  // Exit cleanly
    }
    
    void initialize_server(int port) {    
        // Setup signal handling
        signal(SIGINT, handle_signal);   // Handle Ctrl+C
        signal(SIGTERM, handle_signal);  // Handle kill command
        
        printf("Signal handlers registered for server\n");
        fflush(stdout);
    }
    /*
     * start_server(ifaces, port, is_threaded)
     *      ifaces:  a string in ip address format, indicating the interface
     *              where the server will bind.  In almost all cases it will
     *              be the default "0.0.0.0" which binds to all interfaces.
     *              note the constant RDSH_DEF_SVR_INTFACE in rshlib.h
     * 
     *      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 -s implemented in dsh_cli.c
     *              For example ./dsh -s 0.0.0.0:5678 where 5678 is the new port  
     * 
     *      is_threded:  Used for extra credit to indicate the server should implement
     *                   per thread connections for clients  
     * 
     *      This function basically runs the server by: 
     *          1. Booting up the server
     *          2. Processing client requests until the client requests the
     *             server to stop by running the `stop-server` command
     *          3. Stopping the server. 
     * 
     *      This function is fully implemented for you and should not require
     *      any changes for basic functionality.  
     * 
     *      IF YOU IMPLEMENT THE MULTI-THREADED SERVER FOR EXTRA CREDIT YOU NEED
     *      TO DO SOMETHING WITH THE is_threaded ARGUMENT HOWEVER.  
     */
    int start_server(char *ifaces, int port, int is_threaded) {
        initialize_server(port);
        
        printf("START_SERVER PID: %d\n", getpid());
        fflush(stdout);
    
        server_socket = boot_server(ifaces, port);
        if (server_socket < 0) {
            printf("Server boot failed with error: %d\n", server_socket);
            return server_socket;
        }
    
        printf("Server successfully booted on socket %d, PID: %d\n", server_socket, getpid());
        fflush(stdout);
        
        int rc = process_cli_requests(server_socket);
    
        printf("Server shutting down, rc: %d\n", rc);
        stop_server(server_socket);
        
        server_socket = -1;
        return rc;
    }
    
    
    /*
     * stop_server(svr_socket)
     *      svr_socket: The socket that was created in the boot_server()
     *                  function. 
     * 
     *      This function simply returns the value of close() when closing
     *      the socket.  
     */
    int stop_server(int svr_socket){
        return close(svr_socket);
    }
    
    /*
     * boot_server(ifaces, port)
     *      ifaces & port:  see start_server for description.  They are passed
     *                      as is to this function.   
     * 
     *      This function "boots" the rsh server.  It is responsible for all
     *      socket operations prior to accepting client connections.  Specifically: 
     * 
     *      1. Create the server socket using the socket() function. 
     *      2. Calling bind to "bind" the server to the interface and port
     *      3. Calling listen to get the server ready to listen for connections.
     * 
     *      after creating the socket and prior to calling bind you might want to 
     *      include the following code:
     * 
     *      int enable=1;
     *      setsockopt(svr_socket, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
     * 
     *      when doing development you often run into issues where you hold onto
     *      the port and then need to wait for linux to detect this issue and free
     *      the port up.  The code above tells linux to force allowing this process
     *      to use the specified port making your life a lot easier.
     * 
     *  Returns:
     * 
     *      server_socket:  Sockets are just file descriptors, if this function is
     *                      successful, it returns the server socket descriptor, 
     *                      which is just an integer.
     * 
     *      ERR_RDSH_COMMUNICATION:  This error code is returned if the socket(),
     *                               bind(), or listen() call fails. 
     * 
     */
    int boot_server(char *ifaces, int port){
        int svr_socket;
        int ret;
        
        struct sockaddr_in addr;
    
        printf("Booting server on %s:%d\n", ifaces, port);
    
        svr_socket = socket(AF_INET, SOCK_STREAM, 0);
        if (svr_socket < 0) {
            perror("socket creation failed");
            return ERR_RDSH_COMMUNICATION;
        }
        printf("Socket created: %d\n", svr_socket);
        int enable = 1;
        if (setsockopt(svr_socket, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) < 0) {
            perror("Failed to set socket options");
            close(svr_socket);
            return ERR_RDSH_COMMUNICATION;
        }
        memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        if (strcmp(ifaces, "0.0.0.0") == 0) {
            addr.sin_addr.s_addr = INADDR_ANY;
            printf("Binding to all available interfaces\n");
        } else if (inet_pton(AF_INET, ifaces, &addr.sin_addr) <= 0) {
            perror("Invalid IP address format");
            close(svr_socket);
            return ERR_RDSH_COMMUNICATION;
        }
        printf("Attempting to bind on %s:%d\n", ifaces, port);
        if (bind(svr_socket, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
            perror("Binding failed");
            close(svr_socket);
            return ERR_RDSH_COMMUNICATION;
        }
        printf("Binding successful, preparing to listen...\n");
    
        if (listen(svr_socket, 20) < 0) {
            perror("Failed to start listening");
            close(svr_socket);
            return ERR_RDSH_COMMUNICATION;
        }
        printf("Server started successfully, listening on port %d\n", port);
        return svr_socket;
    
    }
    
    /*
     * process_cli_requests(svr_socket)
     *      svr_socket:  The server socket that was obtained from boot_server()
     *   
     *  This function handles managing client connections.  It does this using
     *  the following logic
     * 
     *      1.  Starts a while(1) loop:
     *  
     *          a. Calls accept() to wait for a client connection. Recall that 
     *             the accept() function returns another socket specifically
     *             bound to a client connection. 
     *          b. Calls exec_client_requests() to handle executing commands
     *             sent by the client. It will use the socket returned from
     *             accept().
     *          c. Loops back to the top (step 2) to accept connecting another
     *             client.  
     * 
     *          note that the exec_client_requests() return code should be
     *          negative if the client requested the server to stop by sending
     *          the `stop-server` command.  If this is the case step 2b breaks
     *          out of the while(1) loop. 
     * 
     *      2.  After we exit the loop, we need to cleanup.  Dont forget to 
     *          free the buffer you allocated in step #1.  Then call stop_server()
     *          to close the server socket. 
     * 
     *  Returns:
     * 
     *      OK_EXIT:  When the client sends the `stop-server` command this function
     *                should return OK_EXIT. 
     * 
     *      ERR_RDSH_COMMUNICATION:  This error code terminates the loop and is
     *                returned from this function in the case of the accept() 
     *                function failing. 
     * 
     *      OTHERS:   See exec_client_requests() for return codes.  Note that positive
     *                values will keep the loop running to accept additional client
     *                connections, and negative values terminate the server. 
     * 
     */
    int process_cli_requests(int svr_socket){
        int cli_socket;
        int rc = OK;    
        struct sockaddr_in client_addr;
        socklen_t client_addr_len = sizeof(client_addr);
    
        while(1){
            cli_socket = accept(svr_socket, (struct sockaddr *)&client_addr, &client_addr_len);
            if (cli_socket < 0) {
                perror("accept failed");
                return ERR_RDSH_COMMUNICATION;
            }
            char client_ip[INET_ADDRSTRLEN];
            inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
            printf("Client connected from %s:%d\n", client_ip, ntohs(client_addr.sin_port));
            rc = exec_client_requests(cli_socket);
            close(cli_socket);
            
            if (rc == EXIT_SC || rc < 0) {
                printf("Exit code %d received, terminating server process\n", rc);
                close(svr_socket);
                if (rc == EXIT_SC) {
                    exit(0);
                }
                break;
            }
        }
    
        return rc;
    }
    
    /*
     * exec_client_requests(cli_socket)
     *      cli_socket:  The server-side socket that is connected to the client
     *   
     *  This function handles accepting remote client commands. The function will
     *  loop and continue to accept and execute client commands.  There are 2 ways
     *  that this ongoing loop accepting client commands ends:
     * 
     *      1.  When the client executes the `exit` command, this function returns
     *          to process_cli_requests() so that we can accept another client
     *          connection. 
     *      2.  When the client executes the `stop-server` command this function
     *          returns to process_cli_requests() with a return code of OK_EXIT
     *          indicating that the server should stop. 
     * 
     *  Note that this function largely follows the implementation of the
     *  exec_local_cmd_loop() function that you implemented in the last 
     *  shell program deliverable. The main difference is that the command will
     *  arrive over the recv() socket call rather than reading a string from the
     *  keyboard. 
     * 
     *  This function also must send the EOF character after a command is
     *  successfully executed to let the client know that the output from the
     *  command it sent is finished.  Use the send_message_eof() to accomplish 
     *  this. 
     * 
     *  Of final note, this function must allocate a buffer for storage to 
     *  store the data received by the client. For example:
     *     io_buff = malloc(RDSH_COMM_BUFF_SZ);
     *  And since it is allocating storage, it must also properly clean it up
     *  prior to exiting.
     * 
     *  Returns:
     * 
     *      OK:       The client sent the `exit` command.  Get ready to connect
     *                another client. 
     *      OK_EXIT:  The client sent `stop-server` command to terminate the server
     * 
     *      ERR_RDSH_COMMUNICATION:  A catch all for any socket() related send
     *                or receive errors. 
     */
    int exec_client_requests(int cli_socket) {
        int io_size;
        command_list_t cmd_list;
        int rc;
        int cmd_rc;
        int last_rc;
        char *io_buff;
        char *command_buffer = NULL;
        size_t command_buffer_size = 0;
        int is_last_chunk = 0;
        int command_complete = 0;
        int result = OK;
    
        io_buff = malloc(RDSH_COMM_BUFF_SZ);
        if (io_buff == NULL) {
            return ERR_RDSH_SERVER;
        }
    
        while (1) {
            command_complete = 0;
            free(command_buffer);
            command_buffer = NULL;
            command_buffer_size = 0;
    
            while (!command_complete) {
                memset(io_buff, 0, RDSH_COMM_BUFF_SZ);
                io_size = recv(cli_socket, io_buff, RDSH_COMM_BUFF_SZ - 1, 0);
    
                if (io_size <= 0) {
                    if (io_size == 0) printf("Client disconnected\n");
                    else perror("recv failed");
                    free(io_buff);
                    free(command_buffer);
                    return OK;
                }
    
                for (int i = 0; i < io_size; i++) {
                    if (io_buff[i] == '\0') {
                        is_last_chunk = 1;
                        io_size = i;
                        command_complete = 1;
                        break;
                    }
                }
    
                size_t new_size = command_buffer_size + io_size;
                char *new_buffer = realloc(command_buffer, new_size + 1);
    
                if (!new_buffer) {
                    perror("memory reallocation failed");
                    free(io_buff);
                    free(command_buffer);
                    return ERR_MEMORY;
                }
    
                command_buffer = new_buffer;
                memcpy(command_buffer + command_buffer_size, io_buff, io_size);
                command_buffer_size = new_size;
                command_buffer[command_buffer_size] = '\0';
    
                if (command_complete) break;
            }
    
            rc = build_cmd_list(command_buffer, &cmd_list);
    
            if (rc < 0) {
                char err_msg[RDSH_COMM_BUFF_SZ];
    
                if (rc == WARN_NO_CMDS) {
                    snprintf(err_msg, sizeof(err_msg), CMD_WARN_NO_CMD);
                } else if (rc == ERR_TOO_MANY_COMMANDS) {
                    snprintf(err_msg, sizeof(err_msg), CMD_ERR_PIPE_LIMIT, CMD_MAX);
                } else {
                    snprintf(err_msg, sizeof(err_msg), "Error parsing command: %d\n", rc);
                }
    
                send_message_string(cli_socket, err_msg);
                continue;
            }
    
            if (cmd_list.num > 0) {
                Built_In_Cmds cmd_type = rsh_match_command(cmd_list.commands[0].argv[0]);
    
                switch (cmd_type) {
                    case BI_CMD_EXIT:
                        send_message_string(cli_socket, "Goodbye!\n");
                        free_cmd_list(&cmd_list);
                        free(io_buff);
                        free(command_buffer);
                        return OK;
    
                    case BI_CMD_STOP_SVR:
                        send_message_string(cli_socket, "Stopping server...\n");
                        free_cmd_list(&cmd_list);
                        free(io_buff);
                        free(command_buffer);
                        return EXIT_SC;
    
                    case BI_CMD_CD:
                        if (cmd_list.commands[0].argc < 2) {
                            send_message_string(cli_socket, "cd: missing argument\n");
                        } else {
                            if (chdir(cmd_list.commands[0].argv[1]) != 0) {
                                char error_msg[RDSH_COMM_BUFF_SZ];
                                snprintf(error_msg, sizeof(error_msg), "cd: %s: %s\n", 
                                        cmd_list.commands[0].argv[1], strerror(errno));
                                send_message_string(cli_socket, error_msg);
                            } else {
                                send_message_eof(cli_socket);
                            }
                        }
                        free_cmd_list(&cmd_list);
                        continue;
    
                    case BI_CMD_DRAGON:
                    case BI_CMD_RC:
                        send_message_string(cli_socket, BI_NOT_IMPLEMENTED);
                        free_cmd_list(&cmd_list);
                        continue;
    
                    default:
                        break;
                }
            }
    
            cmd_rc = rsh_execute_pipeline(cli_socket, &cmd_list);
            send_message_eof(cli_socket);
            free_cmd_list(&cmd_list);
        }
    
        
        free(io_buff);
        if (command_buffer != NULL) {
            free(command_buffer);
        }
        
        return result;
    }
    
    /*
     * send_message_eof(cli_socket)
     *      cli_socket:  The server-side socket that is connected to the client
    
     *  Sends the EOF character to the client to indicate that the server is
     *  finished executing the command that it sent. 
     * 
     *  Returns:
     * 
     *      OK:  The EOF character was sent successfully. 
     * 
     *      ERR_RDSH_COMMUNICATION:  The send() socket call returned an error or if
     *           we were unable to send the EOF character. 
     */
    int send_message_eof(int cli_socket){
        int send_len = (int)sizeof(RDSH_EOF_CHAR);
        int sent_len;
        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, char *buff)
     *      cli_socket:  The server-side socket that is connected to the client
     *      buff:        A C string (aka null terminated) of a message we want
     *                   to send to the client. 
     *   
     *  Sends a message to the client.  Note this command executes both a send()
     *  to send the message and a send_message_eof() to send the EOF character to
     *  the client to indicate command execution terminated. 
     * 
     *  Returns:
     * 
     *      OK:  The message in buff followed by the EOF character was 
     *           sent successfully. 
     * 
     *      ERR_RDSH_COMMUNICATION:  The send() socket call returned an error or if
     *           we were unable to send the message followed by the EOF character. 
     */
    int send_message_string(int cli_socket, char *buff) {
        int bytes_sent = 0;
    
        if (buff && *buff) {
            bytes_sent = send(cli_socket, buff, strlen(buff), 0);
            if (bytes_sent < 0) return ERR_RDSH_COMMUNICATION;
        }
    
        return send_message_eof(cli_socket);
    }
    
    
    /*
     * rsh_execute_pipeline(int cli_sock, command_list_t *clist)
     *      cli_sock:    The server-side socket that is connected to the client
     *      clist:       The command_list_t structure that we implemented in
     *                   the last shell. 
     *   
     *  This function executes the command pipeline.  It should basically be a
     *  replica of the execute_pipeline() function from the last deliverable. 
     *  The only thing different is that you will be using the cli_sock as the
     *  main file descriptor on the first executable in the pipeline for STDIN,
     *  and the cli_sock for the file descriptor for STDOUT, and STDERR for the
     *  last executable in the pipeline.  See picture below:  
     * 
     *      
     *┌───────────┐                                                    ┌───────────┐
     *│ cli_sock  │                                                    │ cli_sock  │
     *└─────┬─────┘                                                    └────▲──▲───┘
     *      │   ┌──────────────┐     ┌──────────────┐     ┌──────────────┐  │  │    
     *      │   │   Process 1  │     │   Process 2  │     │   Process N  │  │  │    
     *      │   │              │     │              │     │              │  │  │    
     *      └───▶stdin   stdout├─┬──▶│stdin   stdout├─┬──▶│stdin   stdout├──┘  │    
     *          │              │ │   │              │ │   │              │     │    
     *          │        stderr├─┘   │        stderr├─┘   │        stderr├─────┘    
     *          └──────────────┘     └──────────────┘     └──────────────┘   
     *                                                      WEXITSTATUS()
     *                                                      of this last
     *                                                      process to get
     *                                                      the return code
     *                                                      for this function       
     * 
     *  Returns:
     * 
     *      EXIT_CODE:  This function returns the exit code of the last command
     *                  executed in the pipeline.  If only one command is executed
     *                  that value is returned.  Remember, use the WEXITSTATUS()
     *                  macro that we discussed during our fork/exec lecture to
     *                  get this value. 
     */
    int rsh_execute_pipeline(int cli_sock, command_list_t *clist) {
        int pipes[clist->num - 1][2];  // Array of pipes
        pid_t pids[clist->num];
        int pids_st[clist->num];         // Array to store process status
        int exit_code;
    
        // Create all necessary pipes
        for (int i = 0; i < clist->num - 1; i++) {
            if (pipe(pipes[i]) == -1) {
                perror("pipe");
                return ESPIPE;
            }
        }
    
        // Create processes for each command
        for (int i = 0; i < clist->num; i++) {
            pids[i] = fork();
            if (pids[i] < 0) {
                perror("fork failed");
                return ERR_MEMORY;
            }
            if (pids[i] == 0) { 
                if (i == 0) { 
                    if (clist->num > 1) close(pipes[0][0]);
    
                    dup2(cli_sock, STDIN_FILENO);
                    dup2((clist->num > 1) ? pipes[0][1] : cli_sock, STDOUT_FILENO);
                    dup2(cli_sock, STDERR_FILENO);
    
                    if (clist->commands[i].input_file) {
                        int fd = open(clist->commands[i].input_file, O_RDONLY);
                        if (fd < 0) {
                            perror(clist->commands[i].input_file);
                            exit(EXIT_FAILURE);
                        }
                        dup2(fd, STDIN_FILENO);
                        close(fd);
                    }
    
                    for (int j = 1; j < clist->num - 1; j++) {
                        close(pipes[j][0]);
                        close(pipes[j][1]);
                    }
                    if (clist->num > 1) close(pipes[0][1]);
                } 
                
                else if (i == clist->num - 1) { 
                    close(pipes[i - 1][1]);
                    dup2(pipes[i - 1][0], STDIN_FILENO);
                    dup2(cli_sock, STDOUT_FILENO);
                    dup2(cli_sock, STDERR_FILENO);
    
                    if (clist->commands[i].output_file) {
                        int flags = O_WRONLY | O_CREAT | (clist->commands[i].append_mode ? O_APPEND : O_TRUNC);
                        int fd = open(clist->commands[i].output_file, flags, 0644);
                        if (fd < 0) {
                            perror(clist->commands[i].output_file);
                            exit(EXIT_FAILURE);
                        }
                        dup2(fd, STDOUT_FILENO);
                        close(fd);
                    }
    
                    for (int j = 0; j < clist->num - 2; j++) {
                        close(pipes[j][0]);
                        close(pipes[j][1]);
                    }
                    close(pipes[i - 1][0]);
                } 
                
                else { 
                    close(pipes[i][0]);
                    close(pipes[i - 1][1]);
                    dup2(pipes[i - 1][0], STDIN_FILENO);
                    dup2(pipes[i][1], STDOUT_FILENO);
                    dup2(cli_sock, STDERR_FILENO);
    
                    for (int j = 0; j < clist->num - 1; j++) {
                        if (j != i && j != i - 1) {
                            close(pipes[j][0]);
                            close(pipes[j][1]);
                        }
                    }
                    close(pipes[i - 1][0]);
                    close(pipes[i][1]);
                }
    
                execvp(clist->commands[i].argv[0], clist->commands[i].argv);
                fprintf(stderr, "%s: command not found\n", clist->commands[i].argv[0]);
                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);
        }
    
        //by default get exit code of last process
        //use this as the return value
        exit_code = WEXITSTATUS(pids_st[clist->num - 1]);
        for (int i = 0; i < clist->num; i++) {
            //if any commands in the pipeline are EXIT_SC
            //return that to enable the caller to react
            if (WEXITSTATUS(pids_st[i]) == EXIT_SC)
                exit_code = EXIT_SC;
        }
        return exit_code;
    }
    
    /**************   OPTIONAL STUFF  ***************/
    /****
     **** NOTE THAT THE FUNCTIONS BELOW ALIGN TO HOW WE CRAFTED THE SOLUTION
     **** TO SEE IF A COMMAND WAS BUILT IN OR NOT.  YOU CAN USE A DIFFERENT
     **** STRATEGY IF YOU WANT.  IF YOU CHOOSE TO DO SO PLEASE REMOVE THESE
     **** FUNCTIONS AND THE PROTOTYPES FROM rshlib.h
     **** 
     */
    
    /*
     * rsh_match_command(const char *input)
     *      cli_socket:  The string command for a built-in command, e.g., dragon,
     *                   cd, exit-server
     *   
     *  This optional function accepts a command string as input and returns
     *  one of the enumerated values from the BuiltInCmds enum as output. For
     *  example:
     * 
     *      Input             Output
     *      exit              BI_CMD_EXIT
     *      dragon            BI_CMD_DRAGON
     * 
     *  This function is entirely optional to implement if you want to handle
     *  processing built-in commands differently in your implementation. 
     * 
     *  Returns:
     * 
     *      BI_CMD_*:   If the command is built-in returns one of the enumeration
     *                  options, for example "cd" returns BI_CMD_CD
     * 
     *      BI_NOT_BI:  If the command is not "built-in" the BI_NOT_BI value is
     *                  returned. 
     */
    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;
    }
    
    /*
     * rsh_built_in_cmd(cmd_buff_t *cmd)
     *      cmd:  The cmd_buff_t of the command, remember, this is the 
     *            parsed version fo the command
     *   
     *  This optional function accepts a parsed cmd and then checks to see if
     *  the cmd is built in or not.  It calls rsh_match_command to see if the 
     *  cmd is built in or not.  Note that rsh_match_command returns BI_NOT_BI
     *  if the command is not built in. If the command is built in this function
     *  uses a switch statement to handle execution if appropriate.   
     * 
     *  Again, using this function is entirely optional if you are using a different
     *  strategy to handle built-in commands.  
     * 
     *  Returns:
     * 
     *      BI_NOT_BI:   Indicates that the cmd provided as input is not built
     *                   in so it should be sent to your fork/exec logic
     *      BI_EXECUTED: Indicates that this function handled the direct execution
     *                   of the command and there is nothing else to do, consider
     *                   it executed.  For example the cmd of "cd" gets the value of
     *                   BI_CMD_CD from rsh_match_command().  It then makes the libc
     *                   call to chdir(cmd->argv[1]); and finally returns BI_EXECUTED
     *      BI_CMD_*     Indicates that a built-in command was matched and the caller
     *                   is responsible for executing it.  For example if this function
     *                   returns BI_CMD_STOP_SVR the caller of this function is
     *                   responsible for stopping the server.  If BI_CMD_EXIT is returned
     *                   the caller is responsible for closing the client connection.
     * 
     *   AGAIN - THIS IS TOTALLY OPTIONAL IF YOU HAVE OR WANT TO HANDLE BUILT-IN
     *   COMMANDS DIFFERENTLY. 
     */
    Built_In_Cmds rsh_built_in_cmd(cmd_buff_t *cmd)
    {
        Built_In_Cmds ctype = BI_NOT_BI;
        ctype = rsh_match_command(cmd->argv[0]);
    
        switch (ctype)
        {
        // case BI_CMD_DRAGON:
        //     print_dragon();
        //     return BI_EXECUTED;
        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;
        }
    }