diff --git a/4-ShellP2/fork-exec-1.png b/4-ShellP2/fork-exec-1.png new file mode 100644 index 0000000000000000000000000000000000000000..56186a921c27a47641e54efbb72397c544f93a32 Binary files /dev/null and b/4-ShellP2/fork-exec-1.png differ diff --git a/4-ShellP2/fork-exec-2.png b/4-ShellP2/fork-exec-2.png new file mode 100644 index 0000000000000000000000000000000000000000..30e5f4ff67c5baaaa99bf7fee75e4f2a7b802bde Binary files /dev/null and b/4-ShellP2/fork-exec-2.png differ diff --git a/4-ShellP2/questions.md b/4-ShellP2/questions.md new file mode 100644 index 0000000000000000000000000000000000000000..323f51da1a37566b4947bee46df7dc26cf8b42ea --- /dev/null +++ b/4-ShellP2/questions.md @@ -0,0 +1,70 @@ +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**: We use fork() before execvp() to maintain the shell process, by forking first I make a child process to execute the command while the parent, which is the shell stays the same. As far as execvp() failing, without fork() it crash the whole shell. + + +2. What happens if the fork() system call fails? How does your implementation handle this scenario? + + + > **Answer**: If fork() fails, it means it's a system-level error. My implementation checks if pid < 0 and if true it has an error message but the parent shell doesn't crash. The function returns ERR_EXEC_CMD but the shell keeps going so users can input other commands. + + +3. How does execvp() find the command to execute? What system environment variable plays a role in this process? + + + > **Answer**: execvp() finds the command to execute by searching through the directories listed by using PATH. When a user enters a command, such as 'ls', execvp() does not require the full path. Instead, it checks each directory in PATH one by one to see if the command exist + + +4. What is the purpose of calling wait() in the parent process after forking? What would happen if we didn’t call it? + + + > **Answer**: The purpose of calling wait() in the parent process after forking is to make sure that the parent waits for the child process to complete before continuing execution. Without wait(), the child process could become a zombie proces + + +5. In the referenced demo code we used WEXITSTATUS(). What information does this provide, and why is it important? + + + > **Answer**: WEXITSTATUS() extracts the exit status of a child process that has terminated normally. This status is the value passed to exit() in the child process or returned by the main function. It is important b/c it allows the parent process to determine whether the child worked successfully or enncountered an error, allowing proper error handling and decision-making based on the child's execution outcome. + + +6. Describe how your implementation of build_cmd_buff() handles quoted arguments. Why is this necessary? + + + > **Answer**: My implementation of build_cmd_buff() handles quoted arguments by tracking whether the parser is inside a quoted section using a boolean flag. When inside quotes, spaces are preserved so thaat multi-word arguments remain intact, preventing unintended splitting. This make sure commands avoid execution errors + + +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**: In this assignment, I modified my parsing logic to better handle multiple commands separated by pipes and properly manage quoted arguments. I introduced a command_list_t structure to store multiple parsed commands, making sure that each command and its arguments were correctly identified. One unexpected challenge was making sure that spaces within quoted arguments were preserved while still correctly splitting unquoted arguments + + +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**: Signals in a Linux system are asynchronous interprocess communication (IPC) used to notify processes of events, such as termination requests, segmentation faults, or user-defined interrupts. Unlike other IPC mechanisms like pipes, message queues, or shared memory, signals are lightweight and do not involve data transfer; they send a numeeric code to a process, which can then handle it with a predefined or custom signal handler. This makes signals useful for stopping execution (SIGSTOP), or responding to external triggers like timers (SIGALRM) + + +- Find and describe three commonly used signals (e.g., SIGKILL, SIGTERM, SIGINT). What are their typical use cases? + + + > **Answer**: SIGKILL (Signal 9) – This signal forcefully terminates a process and cannot be ignored, blocked, or handled by the process. It is used when a process becomes unresponsive or needs to be terminated immediately, such as when using the kill -9 <pid> command + + +SIGTERM (Signal 15) – This signal requests a process to terminate gracefully, allowing it to clean up resources before exiting. It is the default signal sent by the kill command and is often used to shut down services or applications safely + + +SIGINT (Signal 2) – This signal is sent when a user interrupts a process from the terminal, typically by pressing Ctrl + C. It allows programs to handle interruptions properly, such as stopping execution while keeping data or prompting the user for confirmation before exiting + + +- What happens when a process receives SIGSTOP? Can it be caught or ignored like SIGINT? Why or why not? + + + > **Answer**: When a process receives SIGSTOP, it is immediately paused (suspended) by the operating system and stops executing until it is resumed with SIGCONT. Unlike SIGINT, which can be caught and handled by the process, SIGSTOP cannot be caught, ignored, or handled in any way. This is b/c it is designed to be an unconditional stop signal that makes sure the process stops execution + + + diff --git a/4-ShellP2/readme.md b/4-ShellP2/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..a0a38162c5704952d2bb1be57e4872f648c42858 --- /dev/null +++ b/4-ShellP2/readme.md @@ -0,0 +1,192 @@ +# 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. + + + + + +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. + + diff --git a/4-ShellP2/screenshot-code-working-2.png b/4-ShellP2/screenshot-code-working-2.png new file mode 100644 index 0000000000000000000000000000000000000000..ebbbc6fb77d9ee1fa42aed524b533927de6b2cb5 Binary files /dev/null and b/4-ShellP2/screenshot-code-working-2.png differ diff --git a/4-ShellP2/screenshot-code-working.png b/4-ShellP2/screenshot-code-working.png new file mode 100644 index 0000000000000000000000000000000000000000..6838a0e78832dd938aaea609f948533c702a5e74 Binary files /dev/null and b/4-ShellP2/screenshot-code-working.png differ diff --git a/4-ShellP2/starter/.debug/launch.json b/4-ShellP2/starter/.debug/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..aedc1efa4984aaf3265f70a3581d4956d569cf26 --- /dev/null +++ b/4-ShellP2/starter/.debug/launch.json @@ -0,0 +1,29 @@ +{ + "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 diff --git a/4-ShellP2/starter/.debug/tasks.json b/4-ShellP2/starter/.debug/tasks.json new file mode 100644 index 0000000000000000000000000000000000000000..71013e37fe8ec4999fb301dae2b55b1fc9b72543 --- /dev/null +++ b/4-ShellP2/starter/.debug/tasks.json @@ -0,0 +1,20 @@ +{ + "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 diff --git a/4-ShellP2/starter/.gitignore b/4-ShellP2/starter/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..eb47a8e82ad127f89b6fdbbf0f531cb8865b7bf8 --- /dev/null +++ b/4-ShellP2/starter/.gitignore @@ -0,0 +1 @@ +dsh \ No newline at end of file diff --git a/4-ShellP2/starter/bats/assignment_tests.sh b/4-ShellP2/starter/bats/assignment_tests.sh new file mode 100755 index 0000000000000000000000000000000000000000..31e3394972fead272aa25b74f5f755208de6ca5e --- /dev/null +++ b/4-ShellP2/starter/bats/assignment_tests.sh @@ -0,0 +1,118 @@ +#!/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 diff --git a/4-ShellP2/starter/bats/student_tests.sh b/4-ShellP2/starter/bats/student_tests.sh new file mode 100755 index 0000000000000000000000000000000000000000..e74d71489d08b65a8bb3416a4f5445a0e43b2139 --- /dev/null +++ b/4-ShellP2/starter/bats/student_tests.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bats + +# File: student_tests.sh +# +# Create your unit tests suit in this file + +@test "Change directory" { + run ./dsh <<EOF +cd /tmp +pwd +exit +EOF + + echo "Output: $output" + [[ "$output" == *"/tmp"* ]] +} + +@test "Change directory - no args (should not change)" { + run ./dsh <<EOF +pwd +cd +pwd +exit +EOF + + #get only the first and last pwd outputs + first_pwd=$(echo "$output" | grep -Eo '/.*' | head -n 1) + second_pwd=$(echo "$output" | grep -Eo '/.*' | tail -n 1) + + echo "First PWD: $first_pwd" + echo "Second PWD: $second_pwd" + + [[ "$first_pwd" == "$second_pwd" ]] +} + +@test "External command execution - ls" { + run ./dsh <<EOF +ls +exit +EOF + + #expect ls to return something + echo "Output: $output" + [[ -n "$output" ]] +} + +@test "External command execution - echo" { + run ./dsh <<EOF +echo "Hello, World!" +exit +EOF + + [[ "$output" == *"Hello, World!"* ]] +} + +@test "Invalid command handling" { + run ./dsh <<EOF +nonexistentcommand +exit +EOF + + #expect an error message that matches the expected output + [[ "$output" == *"Command not found in PATH"* ]] +} + + +@test "Exit command" { + run ./dsh <<EOF +exit +EOF + + [[ "$status" -eq 0 ]] +} + +@test "Extra Credit - Invalid command handling (ENOENT)" { + run ./dsh <<EOF +notacommand +rc +exit +EOF + + #should see "Command not found" error followed by 2 + echo "ACTUAL OUTPUT: $output" + [[ "$output" == *"Command not found in PATH"* ]] + [[ "$output" == *"2"* ]] +} + +@test "Extra Credit - Permission denied command (EACCES)" { + touch ./no_exec + chmod -rwx ./no_exec + + run ./dsh <<EOF +./no_exec +rc +exit +EOF + + echo "ACTUAL OUTPUT: $output" + #match error message with filename + [[ "$output" == *"Permission denied: ./no_exec"* ]] + #exit code 13 + [[ "$output" == *"13"* ]] + + rm -f ./no_exec +} + + +@test "Extra Credit - RC command returns last exit code" { + run ./dsh <<EOF +notacommand +rc +exit +EOF + + echo "Debug: output of rc test" + echo "$output" + + #make sure rc outputs 2 after a failed command + [[ "$output" == *$'2\n'* ]] +} \ No newline at end of file diff --git a/4-ShellP2/starter/dragon.c b/4-ShellP2/starter/dragon.c new file mode 100644 index 0000000000000000000000000000000000000000..c4c1165188dca1961968cc28dca56655e95b8c09 --- /dev/null +++ b/4-ShellP2/starter/dragon.c @@ -0,0 +1,62 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +//same as in your main.c from 3-part1 +const char *compressed_dragon = + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg" + "ICAgICAgICAgICAgICAgQCUlJSUgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAg" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAl" + "JSUlJSUgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAg" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAlICUlJSUlJSUgICAgICAgICAg" + "IEAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg" + "ICAgICAgICAgICAgICAgICAgICAgICAgJSUlJSUlJSUlJSAgICAgICAgJSUlJSUlJSAgICAgICAg" + "ICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICUlJSUlJSUgICUlJSVAICAgICAg" + "ICAgJSUlJSUlJSUlJSUlQCAgICAlJSUlJSUgIEAlJSUlICAgICAgICAKICAgICAgICAgICAgICAg" + "ICAgICAgICAgICAgICAgICAgICUlJSUlJSUlJSUlJSUlJSUlJSUlJSUgICAgICAlJSUlJSUlJSUl" + "JSUlJSUlJSUlJSUlJSUlICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg" + "ICUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlICAgJSUlJSUlJSUlJSUlICUlJSUlJSUlJSUlJSUl" + "JSAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAlJSUlJSUlJSUlJSUl" + "JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlICAgICAgICAgICAgICAK" + "ICAgICAgICAgICAgICAgICAgICAgICAgICAgICUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUl" + "JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlQCUlJSUlJUAgICAgICAgICAgICAgIAogICAgICAl" + "JSUlJSUlJUAgICAgICAgICAgICUlJSUlJSUlJSUlJSUlJSUgICAgICAgICUlJSUlJSUlJSUlJSUl" + "JSUlJSUlJSUlJSUlICAgICAgJSUgICAgICAgICAgICAgICAgCiAgICAlJSUlJSUlJSUlJSUlICAg" + "ICAgICAgJSVAJSUlJSUlJSUlJSUlICAgICAgICAgICAlJSUlJSUlJSUlJSAlJSUlJSUlJSUlJSUg" + "ICAgICBAJSAgICAgICAgICAgICAgICAKICAlJSUlJSUlJSUlICAgJSUlICAgICAgICAlJSUlJSUl" + "JSUlJSUlJSAgICAgICAgICAgICUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSAgICAgICAgICAgICAg" + "ICAgICAgICAgIAogJSUlJSUlJSUlICAgICAgICUgICAgICAgICAlJSUlJSUlJSUlJSUlICAgICAg" + "ICAgICAgICUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlICAgICAgICAgICAgICAgICAgICAgICAg" + "CiUlJSUlJSUlJUAgICAgICAgICAgICAgICAgJSAlJSUlJSUlJSUlJSUlICAgICAgICAgICAgQCUl" + "JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUgICAgICAgICAgICAgICAgICAgICAKJSUlJSUlJSVAICAg" + "ICAgICAgICAgICAgICAlJUAlJSUlJSUlJSUlJSUgICAgICAgICAgICBAJSUlJSUlJSUlJSUlJSUl" + "JSUlJSUlJSUlJSUlJSAgICAgICAgICAgICAgICAgIAolJSUlJSUlJSUlICAgICAgICAgICAgICAg" + "ICAgJSUlJSUlJSUlJSUlJSUlJSUgICAgICAgICUlJSUlJSUlJSUlJSUgICAgICAlJSUlJSUlJSUl" + "JSUlJSUlJSUgJSUlJSUlJSUlCiUlJSUlJSUlJSUgICAgICAgICAgICAgICAgICAlJSUlJSUlJSUl" + "JSUlJSUgICAgICAgICAgJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUgICAgICAl" + "JSUlICAKJSUlJSUlJSUlQCAgICAgICAgICAgICAgICAgICBAJSUlJSUlJSUlJSUlJSUgICAgICAg" + "ICAlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlICAlJSUgCiAgICUlJSUl" + "JSUlJSUlJSAgQCAgICAgICAgICAgJSUlJSUlJSUlJSUlJSUlJSUlICAgICAgICAlJSUlJSUlJSUl" + "JSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlICAlJSUgCiAgICAlJSUlJSUlJSUlJSUlJSUl" + "JSUlJSAlJSUlJSUlJSUlJSUlJSUlJSUlICAgICAgICAlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUl" + "JSUlJSUlJSUlJSUlJSUlJSUlICAKICAgICUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUl" + "JSUlJSUlJSAgICAgICAgICAgICAgJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUgICAgICAg" + "ICAgICAgICAKICAgICAlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlICAgICAg" + "ICAgICAgICAgICAgICAlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlICAgICAgICAgICAgICAKICAg" + "ICAgICAlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSUlJSAgICAgICAgICAgICAgICAgICAg" + "ICAgICAgICAlJSUlJSUlJSUlJSUlJSUlJSUlICAgICAgICAK"; + +void decode_dragon() { + FILE *pipe = popen("base64 -d", "w"); + if (!pipe) { + printf("Error: Could not open decoding pipe\n"); + return; + } + fwrite(compressed_dragon, 1, strlen(compressed_dragon), pipe); + pclose(pipe); +} + +//function called from dsh to print the dragon +void print_dragon() { + decode_dragon(); +} \ No newline at end of file diff --git a/4-ShellP2/starter/dragon.h b/4-ShellP2/starter/dragon.h new file mode 100644 index 0000000000000000000000000000000000000000..978c1fa2745351bee4b9cacb308d36f61589d769 --- /dev/null +++ b/4-ShellP2/starter/dragon.h @@ -0,0 +1,6 @@ +#ifndef DRAGON_H +#define DRAGON_H + +void print_dragon(); + +#endif \ No newline at end of file diff --git a/4-ShellP2/starter/dsh_cli.c b/4-ShellP2/starter/dsh_cli.c new file mode 100644 index 0000000000000000000000000000000000000000..9262cf457e6b235b19999efa04153e1de0962255 --- /dev/null +++ b/4-ShellP2/starter/dsh_cli.c @@ -0,0 +1,13 @@ +#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 diff --git a/4-ShellP2/starter/dshlib.c b/4-ShellP2/starter/dshlib.c new file mode 100644 index 0000000000000000000000000000000000000000..337019332186c92a06abdf504b98c3223dced9be --- /dev/null +++ b/4-ShellP2/starter/dshlib.c @@ -0,0 +1,182 @@ +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <ctype.h> +#include <stdbool.h> +#include <unistd.h> +#include <errno.h> +#include <sys/wait.h> +#include "dshlib.h" +#include "dragon.h" + +int last_return_code = 0; + +int alloc_cmd_buff(cmd_buff_t *cmd_buff) { + cmd_buff->_cmd_buffer = malloc(SH_CMD_MAX); + if (!cmd_buff->_cmd_buffer) return ERR_MEMORY; + memset(cmd_buff->_cmd_buffer, 0, SH_CMD_MAX); + cmd_buff->argc = 0; + return OK; +} + +int free_cmd_buff(cmd_buff_t *cmd_buff) { + if (cmd_buff->_cmd_buffer) free(cmd_buff->_cmd_buffer); + cmd_buff->_cmd_buffer = NULL; + cmd_buff->argc = 0; + return OK; +} + +int clear_cmd_buff(cmd_buff_t *cmd_buff) { + if (cmd_buff->_cmd_buffer) memset(cmd_buff->_cmd_buffer, 0, SH_CMD_MAX); + cmd_buff->argc = 0; + return OK; +} + +int build_cmd_buff(char *cmd_line, cmd_buff_t *cmd_buff) { + if (!cmd_line || !*cmd_line) return WARN_NO_CMDS; + + clear_cmd_buff(cmd_buff); + char *dst = cmd_buff->_cmd_buffer; + bool in_quote = false; + + //trim leading spaces + while (*cmd_line == ' ') cmd_line++; + + //parse command line + for (char *src = cmd_line; *src; src++) { + if (*src == '"') { + in_quote = !in_quote; + continue; + } + + if (!in_quote && *src == ' ') { + //handle multiple spaces + if (dst == cmd_buff->_cmd_buffer || *(dst-1) == '\0') continue; + *dst++ = '\0'; + } else { + *dst++ = *src; + } + } + *dst = '\0'; + + if (dst > cmd_buff->_cmd_buffer && *(dst-1) == '\0') dst--; + + //populate argv + char *ptr = cmd_buff->_cmd_buffer; + while (*ptr && cmd_buff->argc < CMD_ARGV_MAX-1) { + cmd_buff->argv[cmd_buff->argc++] = ptr; + ptr += strlen(ptr) + 1; + } + cmd_buff->argv[cmd_buff->argc] = NULL; + + return OK; +} + +Built_In_Cmds exec_built_in_cmd(cmd_buff_t *cmd) { + if (cmd->argc == 0) return BI_NOT_BI; + + if (strcmp(cmd->argv[0], "cd") == 0) { + if (cmd->argc == 1) return BI_EXECUTED; + + if (chdir(cmd->argv[1])) { + fprintf(stderr, "cd: %s\n", strerror(errno)); + last_return_code = 1; + } + return BI_EXECUTED; + } + + if (strcmp(cmd->argv[0], "rc") == 0) { + printf("%d\n", last_return_code); + return BI_EXECUTED; + } + + if (strcmp(cmd->argv[0], "dragon") == 0) { + print_dragon(); + return BI_EXECUTED; + } + + if (strcmp(cmd->argv[0], EXIT_CMD) == 0) { + exit(0); + } + + return BI_NOT_BI; +} + +int exec_cmd(cmd_buff_t *cmd) { + pid_t pid = fork(); + if (pid < 0) { + perror("fork failed"); + return ERR_EXEC_CMD; + } + + if (pid == 0) { //child process + execvp(cmd->argv[0], cmd->argv); + + //deals with execvp errors + switch (errno) { + case ENOENT: + fprintf(stderr, "Command not found in PATH\n"); + break; + case EACCES: + fprintf(stderr, "Permission denied: %s\n", cmd->argv[0]); + break; + default: + fprintf(stderr, "Execution failed: %s\n", strerror(errno)); + } + exit(errno); + } + + //parent process + int status; + waitpid(pid, &status, 0); + + if (WIFEXITED(status)) { + last_return_code = WEXITSTATUS(status); + } else { + last_return_code = 1; + } + return last_return_code; +} + +int exec_local_cmd_loop() { + char input[SH_CMD_MAX]; + cmd_buff_t cmd; + + if (alloc_cmd_buff(&cmd) != OK) { + fprintf(stderr, "Failed to allocate command buffer\n"); + return ERR_MEMORY; + } + + while (1) { + printf("%s", SH_PROMPT); + if (!fgets(input, SH_CMD_MAX, stdin)) { + printf("\n"); + break; + } + input[strcspn(input, "\n")] = '\0'; + + //skip empty input + if (strlen(input) == 0) { + printf(CMD_WARN_NO_CMD); + continue; + } + + clear_cmd_buff(&cmd); + int rc = build_cmd_buff(input, &cmd); + if (rc != OK) { + printf(CMD_WARN_NO_CMD); + continue; + } + + if (exec_built_in_cmd(&cmd) != BI_NOT_BI) { + continue; + } + + if (cmd.argc > 0) { + exec_cmd(&cmd); + } + } + + free_cmd_buff(&cmd); + return OK; +} \ No newline at end of file diff --git a/4-ShellP2/starter/dshlib.h b/4-ShellP2/starter/dshlib.h new file mode 100644 index 0000000000000000000000000000000000000000..06b0f7bd859d09288543752a652837a4562e5f5a --- /dev/null +++ b/4-ShellP2/starter/dshlib.h @@ -0,0 +1,94 @@ +#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 + +// Structure for storing parsed command-line input +typedef struct cmd_buff { + int argc; + char *argv[CMD_ARGV_MAX]; + char *_cmd_buffer; +} cmd_buff_t; + +// Structure for storing a single command and its arguments +typedef struct { + char *exe; + char *args; +} command_t; + +// Structure for handling multiple commands +typedef struct { + int num; + command_t commands[CMD_MAX]; +} command_list_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 definitions +#define SPACE_CHAR ' ' +#define PIPE_CHAR '|' +#define PIPE_STRING "|" + +// Shell prompt and exit command +#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 + +// Function prototypes for memory and command buffer handling +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); + +// Function prototype for command list parsing +int build_cmd_list(char *cmd_line, command_list_t *clist); + +// Built-in command handling +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); +// Function prototype for executing piped commands +int exec_piped_commands(command_list_t *clist); + + +// Output message 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 // __DSHLIB_H__ diff --git a/4-ShellP2/starter/makefile b/4-ShellP2/starter/makefile new file mode 100644 index 0000000000000000000000000000000000000000..b14b0723e23ea2036f0220406b40a93e6a0cd283 --- /dev/null +++ b/4-ShellP2/starter/makefile @@ -0,0 +1,31 @@ +# 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 diff --git a/4-ShellP2/starter/shell_roadmap.md b/4-ShellP2/starter/shell_roadmap.md new file mode 100644 index 0000000000000000000000000000000000000000..9af14341f8cd435e39cfa8c1bc6666428be203dd --- /dev/null +++ b/4-ShellP2/starter/shell_roadmap.md @@ -0,0 +1,13 @@ +## 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 diff --git a/SysProg-Class b/SysProg-Class index 6352ca409e67e2ee1df7f35cda529bcd2b59fdc4..9caef6417a1a97de52e8b021c7890c8ce58b75ad 160000 --- a/SysProg-Class +++ b/SysProg-Class @@ -1 +1 @@ -Subproject commit 6352ca409e67e2ee1df7f35cda529bcd2b59fdc4 +Subproject commit 9caef6417a1a97de52e8b021c7890c8ce58b75ad