Skip to content
Snippets Groups Projects
Commit ef8da33d authored by Joey Le's avatar Joey Le
Browse files

Added starter files for assignment 4

parent aa4d9c3d
No related branches found
No related tags found
No related merge requests found
Showing
with 628 additions and 0 deletions
Assignment-04/fork-exec-1.png

209 KiB

Assignment-04/fork-exec-2.png

166 KiB

1. Can you think of why we use `fork/execvp` instead of just calling `execvp` directly? What value do you think the `fork` provides?
> **Answer**: _start here_
2. What happens if the fork() system call fails? How does your implementation handle this scenario?
> **Answer**: _start here_
3. How does execvp() find the command to execute? What system environment variable plays a role in this process?
> **Answer**: _start here_
4. What is the purpose of calling wait() in the parent process after forking? What would happen if we didn’t call it?
> **Answer**: _start here_
5. In the referenced demo code we used WEXITSTATUS(). What information does this provide, and why is it important?
> **Answer**: _start here_
6. Describe how your implementation of build_cmd_buff() handles quoted arguments. Why is this necessary?
> **Answer**: _start here_
7. What changes did you make to your parsing logic compared to the previous assignment? Were there any unexpected challenges in refactoring your old code?
> **Answer**: _start here_
8. For this quesiton, you need to do some research on Linux signals. You can use [this google search](https://www.google.com/search?q=Linux+signals+overview+site%3Aman7.org+OR+site%3Alinux.die.net+OR+site%3Atldp.org&oq=Linux+signals+overview+site%3Aman7.org+OR+site%3Alinux.die.net+OR+site%3Atldp.org&gs_lcrp=EgZjaHJvbWUyBggAEEUYOdIBBzc2MGowajeoAgCwAgA&sourceid=chrome&ie=UTF-8) to get started.
- What is the purpose of signals in a Linux system, and how do they differ from other forms of interprocess communication (IPC)?
> **Answer**: _start here_
- Find and describe three commonly used signals (e.g., SIGKILL, SIGTERM, SIGINT). What are their typical use cases?
> **Answer**: _start here_
- What happens when a process receives SIGSTOP? Can it be caught or ignored like SIGINT? Why or why not?
> **Answer**: _start here_
# Assignment: Custom Shell Part 2 - Fork/Exec
This week we will build on our `dsh` Drexel Shell by adding an implementation for the builtin command `cd`, and a `fork/exec` implementation to run "external commands".
This content builds on the prior assignment; if you need a refresher on what a shell is or the difference between built-in and external commands, please review the readme from that assignment.
# Reuse Prior Work!
The `dsh` assignments are meant to be additive. Much of the parsing logic from the last assignement can be re-used in this assignement. The structures are a little different so you might have to refactor some of your code, but that's a great practical lesson in software engineering; the highest quality results come from frequent iteration.
The next section highlights the differences (both conceptually and in file structure) from the prior assignement.
# Differences from Part 1 Assignment
- We've restructured the code slightly to move all implementation details into the lib file (`dshlib.c`) out of `dsh_cli.c`. **You do not need to write any code in `dsh_cli.c`!**
- This week we'll implement a fork/exec pattern to execute external commands; these commands should execute and behave just as they would if you ran them from your default shell; last week we only printed command lines that we parsed
- If you did the `dragon` extra credit, we moved the implementation into `dragon.c`
- We will NOT implement pipe splitting or multiple commands in one command line input; last week we implemented parsing the CLI by pipe just to print commands, but actually implementing pipes to execute commands is beyond the scope of this week's assignement - we will get to it but not this week!
- This week we work with a single `cmd_buff` type at a time; we will not use the `command_list_t` from last week
- This an example of some refactoring from last week's code, you can adapt your parsing logic but omit the pipe logic until we get to that in a future assignement
# Fork / Exec
Let's introduce two new system calls: fork() and exec(). These calls are fundamental to process creation and execution in all Unix-like operating systems.
When a process calls fork(), the operating system creates a new child process that is an exact copy of the parent, inheriting its memory, file descriptors, and execution state. The child process receives a return value of 0 from fork(), while the parent receives the child's process ID. After forking, the child process often replaces its memory image with a new executable using one of the exec() family of functions (e.g., execl(), execv(), execvp()).
Unlike fork(), exec() does not create a new process but instead replaces the calling process’s address space with a new program, preserving file descriptors unless explicitly changed. This mechanism allows Unix shells to execute new programs by first forking a child process and then using exec() to run the desired binary while the parent process waits for the child to complete using wait().
Recall the fork/exec pattern from lecture slides and demo - we are implementing this two-step process using system calls.
![fork-exec](fork-exec-1.png)
![fork-exec](fork-exec-2.png)
Remember that the fork/exec pattern requires you to use conditional branching logic to implement the child path and the parent path in the code. We did a basic demo of this in class using this demo code https://github.com/drexel-systems/SysProg-Class/blob/main/demos/process-thread/2-fork-exec/fork-exec.c. In the demo we used `execv()`, which requires an absolute path to the binary. In this assignement you should use `execvp()`; `execvp()` will search the `PATH` variable locations for binaries. As with the demo, you can use `WEXITSTATUS` to extract the status code from the child process.
# Assignment Details
### Step 1 - Review [./starter/dshlib.h](./starter/dshlib.h)
The file [./starter/dshlib.h](./starter/dshlib.h) contains some useful definitions and types. Review the available resources in this file before you start coding - these are intended to make your work easier and more robust!
### Step 2 - Implement `cd` in [./starter/dshlib.c](./starter/dshlib.c)
Building on your code from last week, implement the `cd` command.
- when called with no arguments, `cd` does nothing (this is different than Linux shell behavior; shells implement `cd` as `cd ~1` or `cd $HOME`; we'll do that in a future assignement)
- when called with one argument, `chdir()` the current dsh process into the directory provided by argument
### Step 3 - Re-implement Your Main Loop and Parsing Code in exec_local_cmd_loop() [./starter/dshlib.c](./starter/dshlib.c)
Implement `exec_local_cmd_loop()` by refactoring your code from last week to use 1 `cmd_buff` type in the main loop instead of using a command list.
On each line-of-input parsing, you should populate `cmd_buff` using these rules:
- trim ALL leading and trailing spaces
- eliminate duplicate spaces UNLESS they are in a quoted string
- account for quoted strings in input; treat a quoted string with spaces as a single argument
- for example, given ` echo " hello, world" ` you would parse this as: `["echo", " hello, world"]`; note that spaces inside the double quotes were preserved
`cmd_buff` is provided to get you started. You don't have to use this struct, but it is all that's required to parse a line of input into a `cmd_buff`.
```c
typedef struct cmd_buff
{
int argc;
char *argv[CMD_ARGV_MAX];
char *_cmd_buffer;
} cmd_buff_t;
```
### Step 4 - Implement fork/exec pattern in [./starter/dshlib.c](./starter/dshlib.c)
Implement fork/exec of external commands using `execvp()`. This is a pretty straight-forward task; once the command and it's arguments are parsed, you can pass them straight to `execvp()`.
Don't forget to implement a wait of the return code, and extraction of the return code. We're not doing anything with the return code yet, unless you are doing extra credit.
### Step 5 - Create BATS Tests
So far we've provided pre-built a `test.sh` file with assigments. These files use the [bash-based BATS unit test framework](https://bats-core.readthedocs.io/en/stable/tutorial.html#your-first-test).
Going forward, assignements will have a bats folder structure like this:
- your-workspace-folder/
- bats/assignement_tests.sh
- bats/student_tests.sh
**bats/assignment_tests.sh**
- DO NOT EDIT THIS FILE
- assignment_tests.sh contains tests that must pass to meet the requirements of the assignment
- it is run as part of `make test`; remember to run this to verify your code
**bats/student_tests.sh**
- this file must contain YOUR test suite to help verify your code
- for some assignments you will be graded on creation of the tests, and it is your responsibility to make sure the tests provide adequate coverage
- this file is also run with `make test`
**About BATS**
Key points of BATS testing -
- file header is `#!/usr/bin/env bats` such that you can execute tests by simply running `./test_file.sh`
- incorrect `\r\n` can cause execution to fail - easiest way to avoid is use the [drexel-cci](https://marketplace.visualstudio.com/items?itemName=bdlilley.drexel-cci) extension to download assignement code; if you do not use this, make sure you do not copy any windows line endings into the file during a copy/paste
- assertions are in square braces
- example: check output `[ "$stripped_output" = "$expected_output" ]`
- example: check return code `$status` variable: `[ "$status" -eq 0 ]`
Please review the BATS link above if you have questions on syntax or usage. You can also look at test files we provided with assignment for more examples. **You will be graded on the quality of breadth of your unit test suite.**
What this means to you - follow these guidelines when writing tests:
- cover every type of functionallity; for example, you need to cover built-in command and external commands
- test for all use cases / edge cases - for example, for the built-in `cd` command you might want to verify that:
- when called without arguments, the working dir doesn't change (you could verify with `pwd`)
- when called with one argument, it changes directory to the given argument (again, you can verify with `pwd`)
- be thorough - try to cover all the possible ways a user might break you program!
- write tests first; this is called "Test Driven Development" - to learn more, check out [Martin Fowler on TDD](https://martinfowler.com/bliki/TestDrivenDevelopment.html)
### Step 6 - Answer Questions
Answer the questions located in [./questions.md](./questions.md).
### Sample Run with Sample Output
The below shows a sample run executing multiple commands and the expected program output:
```bash
./dsh
dsh2> uname -a
Linux ubuntu 6.12.10-orbstack-00297-gf8f6e015b993 #42 SMP Sun Jan 19 03:00:07 UTC 2025 aarch64 aarch64 aarch64 GNU/Linux
dsh2> uname
Linux
dsh2> echo "hello, world"
hello, world
dsh2> pwd
/home/ben/SysProg-Class-Solutions/assignments/4-ShellP2/solution
dsh2> ls
dir1 dragon.c dragon.txt dsh dsh_cli.c dshlib.c dshlib.h fancy_code_do_not_use makefile shell_roadmap.md test.sh wip
dsh2> cd dir1
dsh2> pwd
/home/ben/SysProg-Class-Solutions/assignments/4-ShellP2/solution/dir1
dsh2>
```
### Extra Credit: +10
This week we're being naive about return codes from external commands; if there is any kind of failure, we just print the `CMD_ERR_EXECUTE` message.
Implement return code handling for extra credit. Hint - check out `man execvp` and review the `errno` and return value information.
Errno and value definitions are in `#include <errno.h>`.
Tips:
- in the child process, `errno` will contain the error value if there was an error; so return this from your child process
- the `WEXITSTATUS` macro will extract `errno`
Requirements:
- Check for all file-related status codes from `errno.h` that you might expect when trying to invoke a binary from $PATH; for example - `ENOENT` is file not found, `EACCES` is permission denied
- Print a suitable message for each error you detect
- Implement a "rc" builtin command that prints the return code of the last operation; for example, if the child process returns `-1`, `rc` should output `-1`
- **Don't forget to add unit tests in** `./bats/student_tests.sh`!
Example run:
```bash
./dsh
dsh2> not_exists
Command not found in PATH
dsh2> rc
2
dsh2>
```
This extra credit is a precursor to implementing variables; shells set the variable `$?` to the return code of the last executed command. A full variable implementation is beyond the scope of this assignement, so we opted to create the `rc` builtin to mimic the behavior of the `$?` variable in other shells.
#### Grading Rubric
This assignment will be weighted 50 points.
- 25 points: Correct implementation of required functionality
- 5 points: Code quality (how easy is your solution to follow)
- 15 points: Answering the written questions: [questions.md](./questions.md)
- 15 points: Quality and breadth of BATS unit tests
- 10 points: [EXTRA CREDIT] handle return codes for execvp
Total points achievable is 70/60.
{
"configurations": [
{
"name": "(gdb) 4-ShellP2",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/4-ShellP2/starter/dsh",
"args": [""],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/4-ShellP2/starter",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "Set Disassembly Flavor to Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
],
"preLaunchTask": "Build 4-ShellP2"
}
]
}
\ No newline at end of file
{
"version": "2.0.0",
"tasks": [
{
"label": "Build 4-ShellP2",
"type": "shell",
"command": "make",
"group": {
"kind": "build",
"isDefault": true
},
"options": {
"cwd": "${workspaceFolder}/4-ShellP2/starter"
},
"problemMatcher": ["$gcc"],
"detail": "Runs the 'make' command to build the project."
}
]
}
\ No newline at end of file
#!/usr/bin/env bats
############################ DO NOT EDIT THIS FILE #####################################
# File: assignement_tests.sh
#
# DO NOT EDIT THIS FILE
#
# Add/Edit Student tests in student_tests.sh
#
# All tests in this file must pass - it is used as part of grading!
########################################################################################
@test "Change directory" {
current=$(pwd)
cd /tmp
mkdir -p dsh-test
run "${current}/dsh" <<EOF
cd dsh-test
pwd
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="/tmp/dsh-testdsh2>dsh2>dsh2>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 ]
}
@test "Change directory - no args" {
current=$(pwd)
cd /tmp
mkdir -p dsh-test
run "${current}/dsh" <<EOF
cd
pwd
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="/tmpdsh2>dsh2>dsh2>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 ]
}
@test "Which which ... which?" {
run "./dsh" <<EOF
which which
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="/usr/bin/whichdsh2>dsh2>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" ]
}
@test "It handles quoted spaces" {
run "./dsh" <<EOF
echo " hello world "
EOF
# Strip all whitespace (spaces, tabs, newlines) from the output
stripped_output=$(echo "$output" | tr -d '\t\n\r\f\v')
# Expected output with all whitespace removed for easier matching
expected_output=" hello world dsh2> dsh2> cmd loop returned 0"
# 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" ]
}
\ 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 ]
}
\ No newline at end of file
#include <stdio.h>
// EXTRA CREDIT - print the drexel dragon from the readme.md
extern void print_dragon(){
// TODO implement
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "dshlib.h"
/* DO NOT EDIT
* main() logic moved to exec_local_cmd_loop() in dshlib.c
*/
int main(){
int rc = exec_local_cmd_loop();
printf("cmd loop returned %d\n", rc);
}
\ No newline at end of file
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include "dshlib.h"
/*
* 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 exec_local_cmd_loop()
{
char *cmd_buff;
int rc = 0;
cmd_buff_t cmd;
// TODO IMPLEMENT MAIN LOOP
// TODO IMPLEMENT parsing input to cmd_buff_t *cmd_buff
// TODO IMPLEMENT if built-in command, execute builtin logic for exit, cd (extra credit: dragon)
// the cd command should chdir to the provided directory; if no directory is provided, do nothing
// TODO IMPLEMENT if not built-in command, fork/exec as an external command
// for example, if the user input is "ls -l", you would fork/exec the command "ls" with the arg "-l"
return OK;
}
#ifndef __DSHLIB_H__
#define __DSHLIB_H__
//Constants for command structure sizes
#define EXE_MAX 64
#define ARG_MAX 256
#define CMD_MAX 8
#define CMD_ARGV_MAX (CMD_MAX + 1)
// Longest command that can be read from the shell
#define SH_CMD_MAX EXE_MAX + ARG_MAX
typedef struct cmd_buff
{
int argc;
char *argv[CMD_ARGV_MAX];
char *_cmd_buffer;
} cmd_buff_t;
/* WIP - Move to next assignment
#define N_ARG_MAX 15 //MAX number of args for a command
typedef struct command{
char exe [EXE_MAX];
char args[ARG_MAX];
int argc;
char *argv[N_ARG_MAX + 1]; //last argv[LAST] must be \0
}command_t;
*/
//Special character #defines
#define SPACE_CHAR ' '
#define PIPE_CHAR '|'
#define PIPE_STRING "|"
#define SH_PROMPT "dsh2> "
#define EXIT_CMD "exit"
//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);
//built in command stuff
typedef enum {
BI_CMD_EXIT,
BI_CMD_DRAGON,
BI_CMD_CD,
BI_NOT_BI,
BI_EXECUTED,
BI_RC,
} 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);
//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"
#endif
\ No newline at end of file
dsh
\ 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
## Proposed Shell Roadmap
Week 5: Shell CLI Parser Due
Week 6: Execute Basic Commands
Week 7: Add support for pipes (extra-credit for file redirection)
Week 8: Basic socket assignment
Week 9: Remote Shell Extension
Week 11: Add support for something like `nsenter` to enter a namespace
\ 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