Linux System Health & Utility Monitor
Build a Python CLI to monitor health, processes, and disk usage in WSL.
Introduction
30 Second Summary
When a computer feels slow or storage starts disappearing, the cause can be hard to pin down from symptoms alone. The useful clues are scattered across memory, storage, how long the system has been running, and active programs.
In this project, you will build a menu-driven program in Python inside Ubuntu on Windows Subsystem for Linux (WSL). Its labelled reports reveal system health, running processes, and folder sizes from one repeating terminal menu.
What You'll Build
During your Internal Evaluation 3 demonstration, you'll launch one terminal menu, choose a numbered feature, and watch a clearly labelled system report appear in seconds.
By the end of this project, you'll have:
- A System Health view that displays human-readable RAM usage, root filesystem usage, uptime, and load averages.
- A Process Viewer that shows process IDs with their command names in a readable list of up to 10 rows.
- A Disk Analyzer that measures a Linux folder you choose. Invalid paths produce clear feedback instead of a crash.
- Secret Mission: Add a case-insensitive process-name filter that searches the process list while keeping the 10-row limit.
Are there any prerequisites?
Ubuntu on WSL must already launch. You also need Python 3 and GNU nano installed, with comfortable Python experience.
Before We Start
Before the hands-on work begins, let's lock in your goal: a Python menu application for Internal Evaluation 3 that uses Linux commands for system resource monitoring, process inspection, plus folder disk analysis.
Set Up and Verify the Project
For Internal Evaluation 3, your monitor depends on Python plus command-line utilities inside Ubuntu on Windows Subsystem for Linux.
A short setup check confirms GNU nano plus every required command before you build features. This keeps environment problems from being mistaken for code problems later.
In this step, get ready to:
- Create the linux-system-monitor project directory with a starter system_monitor.py file.
- Verify Python 3, GNU nano, and the five required Linux utilities.
- Run the starter monitor and see its title in the Ubuntu terminal.
Create the project folder and starter file
The Ubuntu terminal gives your monitor access to the Linux commands it needs. Your project folder keeps the Python file in one predictable location.
- Press the Windows key to open Windows search.
- Type Ubuntu in the search field.
- Select Ubuntu from the search results.
You'll see an Ubuntu terminal prompt ready for Linux commands.
- Create and enter the linux-system-monitor project folder by running these commands:
mkdir linux-system-monitor
cd linux-system-monitor
What do these commands do?
- The mkdir command creates the linux-system-monitor folder.
- The cd command moves your terminal into that folder.
Your terminal prompt should now include linux-system-monitor. This confirms that later files and commands stay inside the project folder.
- Open the starter file with nano by running this command:
nano system_monitor.py
What does this command do?
The nano command opens system_monitor.py for editing. Nano creates the file when it does not already exist.
- Add the starter title by typing this code into system_monitor.py:
print("Linux System Health & Utility Monitor")
What does this code do?
The print() call writes the project title to the terminal. This gives you a direct check that Python can find and run your file.
- Save system_monitor.py by pressing Ctrl+O.
- Press Enter to confirm the filename.
- Exit nano by pressing Ctrl+X.
You are back at the terminal prompt inside linux-system-monitor. Your first project artifact is now saved and ready to run.
✔️ Awesome, I've got everything!
Great. Make sure system_monitor.py is saved before you verify the installed tools.
ⓧ I'd like to double check the full code
Here is the complete starter version of system_monitor.py. Make sure your file matches exactly:
print("Linux System Health & Utility Monitor")
What should the starter contain?
The starter file contains one print() call. Its text matches the title you will look for during the final check.
Having trouble saving the file?
If nano is still open, repeat the save sequence with Ctrl+O. Press Enter before using Ctrl+X.
Check that the filename at the top of nano is system_monitor.py.
help me save and exit my nano file.
Verify the installed tools
Each check prints evidence that one project dependency works. Running them now gives you a known-good baseline before the monitor starts calling these utilities from Python.
- Verify Python 3, GNU nano, and the required Linux utilities by running these commands:
python3 -V
nano -V
free -h
df -h /
uptime
ps -e -o pid,comm
du -sh .
What should you see?
- The python3 check prints your installed Python 3 version. The nano check prints your installed editor version.
- The free check prints physical memory plus swap usage in human-readable units.
- The df check prints storage information for the filesystem containing /.
- The uptime check prints the running time plus load averages.
- The ps check prints running process IDs plus command names.
- The du check prints one human-readable total for the current project folder.
These readings describe the Ubuntu environment running inside WSL. Every command should return its own version or system information without stopping the sequence.
Did one of the checks fail?
Confirm that you launched Ubuntu on WSL. These utilities need the Linux environment used by this project.
Check which command failed before moving on. A missing utility now would cause the corresponding monitor feature to fail later.
help me troubleshoot the failed setup check.
Run the starter monitor
The final check connects the project directory, the saved file, and the Python interpreter. A successful run proves your setup is ready for the menu features.
Before you run the script, ask yourself what single line you expect the terminal to print.
- Run the starter monitor from the linux-system-monitor folder with this command:
python3 system_monitor.py
What does this command do?
The python3 command runs the saved system_monitor.py script. Python executes its print() call and sends the title to the terminal.
You'll see Linux System Health & Utility Monitor in the Ubuntu terminal. That's your setup complete: the starter monitor runs from the project folder.
Don't see the project title?
Check that your terminal prompt includes linux-system-monitor. A different location prevents Python from finding the project file.
Compare system_monitor.py with the full-code tab. Pay close attention to the quotation marks plus parentheses around the title.
help me fix my starter monitor.
Your Ubuntu environment and starter file are ready. Next, you'll turn this single title into a repeating menu with a live system health report.
Build the Menu and System Health Check
Your Ubuntu environment in Windows Subsystem for Linux can run every command this monitor needs. The starter file proves system_monitor.py runs with Python.
The assessment needs visible resource readings from the Linux environment. In this step, option 1 will display the first complete health report.
In this step, get ready to:
- Build reusable helpers for command execution.
- Create the labelled System Health report.
- Connect the report to a repeating menu.
Replace the starter with command helpers
Linux commands produce the raw measurements. The subprocess module lets your program run those commands and collect their output.
- Return to system_monitor.py in GNU nano by running this command:
nano system_monitor.py
- Replace the existing print("Linux System Health & Utility Monitor") line with this foundation code:
"""Linux System Health & Utility Monitor for Ubuntu on WSL."""
import subprocess
TITLE = "Linux System Health & Utility Monitor"
def print_header(title):
print("\n" + "=" * 60)
print(title)
print("=" * 60)
def run_command(command):
result = subprocess.run(
command,
capture_output=True,
universal_newlines=True,
)
return result.stdout.strip()
def pause():
input("\nPress Enter to return to the menu...")
Why use subprocess?
- The TITLE constant keeps the main heading in one place.
- The print_header() function gives every screen the same visible structure.
- The subprocess.run() call runs each required command through Python's standard library.
- The capture_output=True setting captures the command output for your program to display.
- The universal_newlines=True setting returns captured output as text.
- The pause() function keeps each report visible until you are ready to return.
Add the System Health report
A useful health snapshot needs separate labels for each measurement. The show_health() function groups each label with the argument list for its command.
- Place this function directly below pause() in system_monitor.py:
def show_health():
print_header("System Health")
checks = (
("RAM Usage", ["free", "-h"]),
("Root Filesystem Usage", ["df", "-h", "/"]),
("Uptime and Load Averages", ["uptime"]),
)
for label, command in checks:
print("\n--- {} ---".format(label))
print(run_command(command))
pause()
How does the health report work?
- The checks tuple pairs each report label with one command argument list.
- The free command supplies human-readable memory usage.
- The df command reports usage for the root filesystem.
- The uptime command supplies uptime information plus load averages.
- The loop sends each argument list to run_command() before printing the captured result.
Build the repeating menu
The menu gives the learner one place to choose a feature. A while loop displays it again after the health report closes.
- Place this code directly below show_health() in system_monitor.py:
def show_menu():
print_header(TITLE)
print("1. System Health")
print("4. Exit")
def main():
while True:
show_menu()
choice = input("Choose an option [1-4]: ").strip()
if choice == "1":
show_health()
elif choice == "4":
print("Goodbye.")
break
else:
print("Invalid choice. Enter a number from 1 to 4.")
if __name__ == "__main__":
main()
How does the menu keep running?
- The show_menu() function prints the project title plus the available choices.
- The while True loop keeps the monitor available after a feature finishes.
- Choice 1 opens show_health().
- Choice 4 prints the closing message before ending the loop.
- The final condition starts main() when you run this file as a script.
- Press Ctrl+O to open nano's save prompt.
- Press Enter to confirm system_monitor.py.
- Press Ctrl+X to return to the Ubuntu terminal.
Before you run the monitor, what do you expect option 1 to collect before the menu comes back?
- Start the monitor by running this command:
python3 system_monitor.py
You will see Linux System Health & Utility Monitor above choices 1 and 4.
- Type 1 at the choice prompt.
- Press Enter.
You will see System Health followed by labelled sections for RAM usage, root filesystem usage, and uptime with load averages.
- Press Enter at the pause prompt.
You will see the main menu return. This confirms the loop stays active after the report finishes.
- Type 4 at the choice prompt.
- Press Enter.
You will see Goodbye. before the program closes. That is the first promised monitoring feature working from the menu.
Monitor stops before the menu?
- Check that show_health() appears above show_menu().
- Check that the lines inside each function use consistent indentation.
- Compare every closing parenthesis with the matching opening parenthesis.
help me fix my system_monitor.py startup error.
✔️ Awesome, I've got everything!
Your saved monitor displays the menu, runs the System Health report, and returns to the menu.
ⓧ I'd like to double check the full code
Compare your complete system_monitor.py file with this version:
"""Linux System Health & Utility Monitor for Ubuntu on WSL."""
import subprocess
TITLE = "Linux System Health & Utility Monitor"
def print_header(title):
print("\n" + "=" * 60)
print(title)
print("=" * 60)
def run_command(command):
result = subprocess.run(
command,
capture_output=True,
universal_newlines=True,
)
return result.stdout.strip()
def pause():
input("\nPress Enter to return to the menu...")
def show_health():
print_header("System Health")
checks = (
("RAM Usage", ["free", "-h"]),
("Root Filesystem Usage", ["df", "-h", "/"]),
("Uptime and Load Averages", ["uptime"]),
)
for label, command in checks:
print("\n--- {} ---".format(label))
print(run_command(command))
pause()
def show_menu():
print_header(TITLE)
print("1. System Health")
print("4. Exit")
def main():
while True:
show_menu()
choice = input("Choose an option [1-4]: ").strip()
if choice == "1":
show_health()
elif choice == "4":
print("Goodbye.")
break
else:
print("Invalid choice. Enter a number from 1 to 4.")
if __name__ == "__main__":
main()
Your monitor now turns Linux command output into a readable health report. Next, you will add a process viewer that keeps the terminal output focused.
Add the Process Viewer
Your Python monitor already turns memory, filesystem, and uptime data into a labelled health report.
That report cannot identify which Linux processes are running. You will add a Process Viewer that shows each process ID beside its command name without flooding the terminal.
In this step, get ready to:
- Inspect the process table with ps.
- Build show_processes() with a 10-row limit.
- Connect menu option 2 to the Process Viewer.
Inspect the process table
The ps utility can select every running process. Its output format can show only the columns your monitor needs.
- Inspect the current process table by running this command:
ps -e -o pid,comm
What does this command do?
- The -e option selects every running process.
- The -o option controls which output columns appear.
- The pid,comm value requests the process ID beside the command name.
You will see a PID and COMMAND header above the running processes. The full table can fill much of the terminal.
Build the limited process view
A fixed row limit keeps screenshots readable while preserving the process table header. The viewer captures the complete output before selecting the first 10 process rows.
- Open the existing system_monitor.py file in nano by running this command:
nano system_monitor.py
What does this command do?
This opens your existing monitor file in GNU nano. You will extend the same program that already displays System Health.
- Find the TITLE constant near the top of system_monitor.py.
- Place the process row limit directly below it by adding this line:
PROCESS_LIMIT = 10
Why set a process limit?
The PROCESS_LIMIT constant gives the viewer one place to control its maximum number of process rows. Its value of 10 keeps the output short enough for terminal screenshots.
The new function uses your existing run_command() helper to capture the process table as text. It splits that text into rows before applying the limit.
- Add the following function directly above show_menu():
def show_processes():
print_header("Process Viewer")
output = run_command(["ps", "-e", "-o", "pid,comm"])
lines = output.splitlines()
visible_lines = lines[: PROCESS_LIMIT + 1]
print("\n".join(visible_lines))
if len(lines) > PROCESS_LIMIT + 1:
print("\nShowing {} of {} processes.".format(
PROCESS_LIMIT, len(lines) - 1
))
pause()
What does this code do?
- The argument list passed to run_command() keeps each part of the process command separate.
- The splitlines() method turns the captured output into separate rows.
- The slice keeps the header plus the number of rows stored in PROCESS_LIMIT.
- The final condition reports the full process count when more rows are available.
- Save the changes using nano's save sequence from Step 1.
- Exit nano using the exit sequence from Step 1.
Before you test the function, how many process rows do you expect beneath the header?
- Run the new function directly by entering this command:
python3 -c "import system_monitor; system_monitor.show_processes()"
How does this test work?
The -c option executes the supplied Python statements. The command imports your file before calling show_processes() directly.
You will see the Process Viewer header followed by up to 10 process rows. A summary appears when additional processes exist.
- Press Enter when the function asks to return to the menu.
Process function not running?
Check that PROCESS_LIMIT = 10 sits below TITLE. A misspelled constant prevents the row slice from working.
Confirm that show_processes() starts at the left edge of the file. Accidental indentation can place it inside the previous function.
help me debug my show_processes function
Connect the viewer to the menu
The function now works when called directly. The repeating menu needs a visible option plus a matching branch before learners can reach it normally.
- Return to system_monitor.py in nano by running this command:
nano system_monitor.py
Why return to the file?
Your first edit built the Process Viewer itself. This edit exposes that function through the repeating menu.
- Replace the existing show_menu() function with this version:
def show_menu():
print_header(TITLE)
print("1. System Health")
print("2. Process Viewer")
print("4. Exit")
What changes in the menu?
The menu now displays 2. Process Viewer between the existing health and exit options. This gives the new function a visible entry point.
- Replace the existing main() function with this version:
def main():
while True:
show_menu()
choice = input("Choose an option [1-4]: ").strip()
if choice == "1":
show_health()
elif choice == "2":
show_processes()
elif choice == "4":
print("Goodbye.")
break
else:
print("Invalid choice. Enter a number from 1 to 4.")
How does option 2 work?
The new elif branch checks whether the learner selected 2. A match calls show_processes() before the loop displays the menu again.
- Save the changes using nano's save sequence from Step 1.
- Exit nano using the exit sequence from Step 1.
Before you launch the monitor, do you expect option 2 to show the complete process table or a limited list?
- Launch the updated monitor by running this command:
python3 system_monitor.py
What does this command do?
This starts the repeating menu from system_monitor.py. The menu now includes the Process Viewer option.
- Enter 2 at the menu prompt.
You will see a PID and COMMAND header followed by up to 10 running processes. You will also see a summary when more process rows exist.
That is the second monitor feature working: your process table is readable without flooding the terminal.
- Record one process ID from the displayed rows: one displayed process ID.
- Press Enter to return to the menu.
- Enter 4 to exit the monitor.
Before you inspect the direct output, do you expect the recorded process to appear with the same ID?
- Display the complete process table again by running this command:
ps -e -o pid,comm
Why compare the outputs?
The direct command shows the source data captured by run_command(). Your Python function presents a shorter view of the same process table.
- Locate one displayed process ID in the direct output.
A matching row confirms that the Process Viewer preserves the process ID and command name from ps.
Recorded process no longer listed?
Process tables change as programs start or stop. Return to the viewer to record another running process when the original one has ended.
help me compare the process viewer with ps output
✔️ Awesome, I've got everything!
Great. Make sure system_monitor.py is saved with the Process Viewer connected to option 2.
ⓧ I'd like to double check the full code
Compare your complete system_monitor.py file with this version:
"""Linux System Health & Utility Monitor for Ubuntu on WSL."""
import subprocess
TITLE = "Linux System Health & Utility Monitor"
PROCESS_LIMIT = 10
def print_header(title):
print("\n" + "=" * 60)
print(title)
print("=" * 60)
def run_command(command):
result = subprocess.run(
command,
capture_output=True,
universal_newlines=True,
)
return result.stdout.strip()
def pause():
input("\nPress Enter to return to the menu...")
def show_health():
print_header("System Health")
checks = (
("RAM Usage", ["free", "-h"]),
("Root Filesystem Usage", ["df", "-h", "/"]),
("Uptime and Load Averages", ["uptime"]),
)
for label, command in checks:
print("\n--- {} ---".format(label))
print(run_command(command))
pause()
def show_processes():
print_header("Process Viewer")
output = run_command(["ps", "-e", "-o", "pid,comm"])
lines = output.splitlines()
visible_lines = lines[: PROCESS_LIMIT + 1]
print("\n".join(visible_lines))
if len(lines) > PROCESS_LIMIT + 1:
print("\nShowing {} of {} processes.".format(
PROCESS_LIMIT, len(lines) - 1
))
pause()
def show_menu():
print_header(TITLE)
print("1. System Health")
print("2. Process Viewer")
print("4. Exit")
def main():
while True:
show_menu()
choice = input("Choose an option [1-4]: ").strip()
if choice == "1":
show_health()
elif choice == "2":
show_processes()
elif choice == "4":
print("Goodbye.")
break
else:
print("Invalid choice. Enter a number from 1 to 4.")
if __name__ == "__main__":
main()
Your monitor can now reveal which programs are running without burying the result in a long process table. Next, you will add a disk analyzer that measures the current folder.
Add a Current-Folder Disk Analyzer
Your monitor now turns a long process table into a focused ten-row view. That gives your report a clear picture of what is running inside Ubuntu on Windows Subsystem for Linux.
Disk usage answers a different system-monitoring question. This step uses du to summarize the folder that contains your monitor.
In this step, get ready to:
- Add an analyze_folder() function that reports current-folder usage.
- Connect option 3 to the Disk Analyzer.
- Test the disk-usage output and evaluate its usefulness.
Add the current-folder analyzer
The du command summarizes disk usage. Its -s argument requests one total. The -h argument makes that total human-readable.
- Return to system_monitor.py in GNU nano by running this command:
nano system_monitor.py
What does this command do?
This opens your existing system_monitor.py file in GNU nano. Your terminal remains inside the linux-system-monitor directory.
- Find the show_menu() function near the bottom of system_monitor.py.
- Place your cursor on the blank line immediately above def show_menu():.
- Add the new analyzer function using this code:
def analyze_folder():
print_header("Disk Analyzer")
output = run_command(["du", "-s", "-h", "."])
print("\nCurrent-folder usage:")
print(output)
pause()
What does this code do?
- The print_header() call gives the feature a clear terminal heading.
- The argument list passes du directly to run_command() without a shell pipeline.
- The final . selects the current linux-system-monitor directory.
- The pause() call keeps the result visible until you are ready to return.
- Save system_monitor.py by pressing Ctrl+O.
- Confirm the filename by pressing Enter.
- Return to the Ubuntu terminal by pressing Ctrl+X.
Before you run the monitor, ask yourself whether the existing menu will still launch cleanly.
- Check that the file still runs by using this command:
python3 system_monitor.py
What does this check prove?
Python loads the complete file before starting main(). A working menu confirms the new function has valid syntax.
- Enter 2 to check the existing Process Viewer.
- Press Enter to return to the menu.
- Enter 4 to close the monitor.
Good. Your limited process list still works after the new analyzer function was added.
Seeing a syntax error?
Check that def analyze_folder(): starts at the far-left edge. Make sure each line inside the function has the same four-space indentation.
Compare the brackets and quotation marks in the run_command() line with the code above.
help me fix the syntax in analyze_folder()
Connect option 3 to the menu
The analyzer function now exists in your file. The menu needs a visible label and a matching branch before anyone can run it.
- Return to system_monitor.py by running this command:
nano system_monitor.py
Why return to the file?
The next two edits expose analyze_folder() through the repeating menu. They keep the existing health and process options intact.
- Find the existing show_menu() function.
- Replace that function with this complete version:
def show_menu():
print_header(TITLE)
print("1. System Health")
print("2. Process Viewer")
print("3. Disk Analyzer")
print("4. Exit")
What changed in the menu?
The new third line advertises the Disk Analyzer beside the existing features. The numbering now matches the prompt range shown by main().
- Find the elif choice == "2": branch inside main().
- Insert this new branch immediately before elif choice == "4"::
elif choice == "3":
analyze_folder()
How does option 3 work?
The branch compares the learner's choice with 3. A match calls analyze_folder() before the loop displays the menu again.
- Save system_monitor.py by pressing Ctrl+O.
- Confirm the filename by pressing Enter.
- Return to the Ubuntu terminal by pressing Ctrl+X.
✔️ Awesome, I've got everything!
That is the third menu option wired into your monitor. Your health report and limited process viewer remain available.
ⓧ I'd like to double check the full code
The complete system_monitor.py file should match this version:
"""Linux System Health & Utility Monitor for Ubuntu on WSL."""
import subprocess
TITLE = "Linux System Health & Utility Monitor"
PROCESS_LIMIT = 10
def print_header(title):
print("\n" + "=" * 60)
print(title)
print("=" * 60)
def run_command(command):
result = subprocess.run(
command,
capture_output=True,
universal_newlines=True,
)
return result.stdout.strip()
def pause():
input("\nPress Enter to return to the menu...")
def show_health():
print_header("System Health")
checks = (
("RAM Usage", ["free", "-h"]),
("Root Filesystem Usage", ["df", "-h", "/"]),
("Uptime and Load Averages", ["uptime"]),
)
for label, command in checks:
print("\n--- {} ---".format(label))
print(run_command(command))
pause()
def show_processes():
print_header("Process Viewer")
output = run_command(["ps", "-e", "-o", "pid,comm"])
lines = output.splitlines()
visible_lines = lines[: PROCESS_LIMIT + 1]
print("\n".join(visible_lines))
if len(lines) > PROCESS_LIMIT + 1:
print("\nShowing {} of {} processes.".format(
PROCESS_LIMIT, len(lines) - 1
))
pause()
def analyze_folder():
print_header("Disk Analyzer")
output = run_command(["du", "-s", "-h", "."])
print("\nCurrent-folder usage:")
print(output)
pause()
def show_menu():
print_header(TITLE)
print("1. System Health")
print("2. Process Viewer")
print("3. Disk Analyzer")
print("4. Exit")
def main():
while True:
show_menu()
choice = input("Choose an option [1-4]: ").strip()
if choice == "1":
show_health()
elif choice == "2":
show_processes()
elif choice == "3":
analyze_folder()
elif choice == "4":
print("Goodbye.")
break
else:
print("Invalid choice. Enter a number from 1 to 4.")
if __name__ == "__main__":
main()
What should the full file contain?
This cumulative version contains the existing health and process features. It adds analyze_folder() plus the two menu connections required for option 3.
Before you run the monitor, ask yourself which directory option 3 will measure.
- Launch the updated monitor by running this command:
python3 system_monitor.py
What does this run test?
This starts the repeating menu with all three monitoring features. Selecting option 3 now calls the analyzer branch you added.
- Enter 3 at the menu prompt.
You'll see the Disk Analyzer heading followed by Current-folder usage: and one human-readable total for ..
- Press Enter to return to the menu.
- Enter 4 to close the monitor.
Does option 3 fall through?
Check that the option 3 branch sits inside the same if chain as options 1 and 2.
Make sure the branch calls analyze_folder() with matching spelling and parentheses.
help me connect menu option 3 to analyze_folder()
Before the final check, ask yourself whether the direct command will report the same current-folder total as your monitor.
- Compare the analyzer with the direct Linux result by running:
du -sh .
What does this comparison prove?
The direct command summarizes the same current directory as analyze_folder(). Matching totals prove the menu feature passes the intended arguments to du.
The total reflects the contents of linux-system-monitor. It follows the folder as files are added or removed.
Identify the analyzer's boundary
When you selected option 3, the monitor measured . without asking which folder you wanted to inspect. That fixed behavior is the intended shortfall in this version.
A useful disk analyzer needs a folder path from the user. It also needs to reject missing paths and regular files without crashing.
- Use the checkpoint below to capture the successful output and record the limitation for your viva.
Your third menu option now reports the current folder's disk usage. Next up, you'll let the user choose a folder and handle invalid paths safely.
Accept Folder Paths and Handle Errors
Your monitor inside Windows Subsystem for Linux can already measure the current directory. The fixed . path is the limitation you identified in the last step.
A useful disk analyzer needs to accept a selected folder. It also needs to handle invalid paths plus command failures without crashing.
In this step, get ready to:
- Accept a user-selected directory in the Disk Analyzer with path validation.
- Handle Linux command failures without crashing.
- Test every menu path for your report.
Accept and validate folder paths
The os.path functions can expand a home-directory shortcut. They can also confirm that the selected path is an existing directory.
- Use GNU nano in Ubuntu to edit the existing system_monitor.py file.
- Find the current import line:
import subprocess
Why Add Another Import?
The file currently imports only subprocess. Path expansion plus validation need the os module.
- Insert the new import above the existing line so the imports look like this:
import os
import subprocess
What Do These Imports Provide?
The os module provides the path helpers used by the new analyzer. The existing subprocess module continues to run Linux commands.
- Find the current analyze_folder() function:
def analyze_folder():
print_header("Disk Analyzer")
output = run_command(["du", "-s", "-h", "."])
print("\nCurrent-folder usage:")
print(output)
pause()
Where Is the Limitation?
This version always gives du the fixed . path. The learner using the monitor has no way to select another folder.
- Replace the entire current analyze_folder() function with this version:
def analyze_folder():
print_header("Disk Analyzer")
raw_path = input("Enter a folder path [default: .]: ").strip()
path = os.path.expanduser(raw_path or ".")
if not os.path.exists(path):
print("Error: that path does not exist.")
pause()
return
if not os.path.isdir(path):
print("Error: the path is not a directory.")
pause()
return
output = run_command(["du", "-s", "-h", path])
print("\nFolder usage for '{}':".format(path))
print(output)
pause()
What Does This Code Do?
- The raw_path variable stores the text entered at the prompt.
- The os.path.expanduser() call expands an initial ~ to the home directory.
- The os.path.exists() check rejects a missing path before the command runs.
- The os.path.isdir() check rejects a regular file before passing the path to du.
- Press Ctrl+O to save system_monitor.py.
- Press Enter to confirm the existing file name.
- Press Ctrl+X to return to the Ubuntu terminal.
Before you test the new prompt, do you expect ~ to reach your Ubuntu home directory?
- Launch the updated monitor by running:
python3 system_monitor.py
What Does This Command Do?
This starts system_monitor.py with Python 3. The repeating menu lets you test the new analyzer without restarting after each result.
- Select option 3.
- Enter ~ at the folder prompt.
You should see a Folder usage for label followed by one human-readable total for your Ubuntu home directory.
Does the Analyzer Reject Your Home Directory?
Check that the import section contains import os. A missing import prevents the path helpers from running.
Check that path receives the result from os.path.expanduser() before either validation check runs.
Help me debug the folder-path validation.
Protect Linux command execution
A valid folder can still trigger a command problem. The command runner needs to convert missing utilities, non-zero exits, plus timeouts into readable results.
- Press Enter to return to the menu.
- Select option 4 to close the monitor.
- Use GNU nano in Ubuntu to edit system_monitor.py again.
- Find the current run_command() function:
def run_command(command):
result = subprocess.run(
command,
capture_output=True,
universal_newlines=True,
)
return result.stdout.strip()
What Is Missing Here?
This version captures normal output. It does not convert a failed process into a useful message for the menu.
- Replace the entire current run_command() function with this defensive version:
def run_command(command, timeout=60):
try:
result = subprocess.run(
command,
capture_output=True,
universal_newlines=True,
check=True,
timeout=timeout,
)
return result.stdout.strip()
except FileNotFoundError:
return "Error: '{}' is not installed.".format(command[0])
except subprocess.CalledProcessError as error:
detail = error.stderr.strip() or "command returned an error"
return "Error: {}".format(detail)
except subprocess.TimeoutExpired:
return "Error: command exceeded {} seconds.".format(timeout)
How Does the Command Runner Stay Safe?
- The command remains an argument list. Python handles each argument without a shell pipeline.
- The check=True setting turns a non-zero exit into a handled exception.
- The timeout parameter prevents a command from running indefinitely.
- Each exception handler returns a readable error string to the active feature.
- Find the current show_processes() function:
def show_processes():
print_header("Process Viewer")
output = run_command(["ps", "-e", "-o", "pid,comm"])
lines = output.splitlines()
visible_lines = lines[: PROCESS_LIMIT + 1]
print("\n".join(visible_lines))
if len(lines) > PROCESS_LIMIT + 1:
print("\nShowing {} of {} processes.".format(
PROCESS_LIMIT, len(lines) - 1
))
pause()
Why Update the Process Viewer?
The process viewer currently treats every returned string as process data. A command error needs to be displayed before the code splits the output into rows.
- Replace the current show_processes() function with this version:
def show_processes():
print_header("Process Viewer")
output = run_command(["ps", "-e", "-o", "pid,comm"])
if output.startswith("Error:"):
print(output)
pause()
return
lines = output.splitlines()
visible_lines = lines[: PROCESS_LIMIT + 1]
print("\n".join(visible_lines))
if len(lines) > PROCESS_LIMIT + 1:
print("\nShowing {} of {} processes.".format(
PROCESS_LIMIT, len(lines) - 1
))
pause()
How Does the Error Guard Work?
The guard prints any error returned by run_command(). The early return prevents that message from being formatted as a process table.
- Press Ctrl+O to save system_monitor.py.
- Press Enter to confirm the file name.
- Press Ctrl+X to return to the terminal.
- Start the monitor by running:
python3 system_monitor.py
What Are You Checking?
This run confirms that successful command output still passes through the defensive runner. The Process Viewer should remain readable after the new error guard.
- Select option 2.
You should see the PID plus COMMAND header followed by up to ten process rows.
Does the Process Viewer Stop Early?
Check that the error guard sits after the output assignment. It must sit before output.splitlines().
Check that normal process output does not start with Error:. Only returned command failures should take the early exit.
Help me debug the defensive command runner.
Verify the finished monitor
The final defensive layer handles a manual interruption. This keeps Ctrl+C from ending the application with a traceback.
- Press Enter to return to the menu.
- Select option 4 to close the monitor.
- Use GNU nano in Ubuntu to edit system_monitor.py again.
- Find the final lines of the file:
if __name__ == "__main__":
main()
What Happens During an Interruption?
This version calls main() directly. A keyboard interruption can therefore escape to the terminal as an unhandled exception.
- Replace those final lines with this guarded entry point:
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\nMonitor closed with Ctrl+C.")
How Does the Final Guard Help?
The try block starts the normal menu loop. The KeyboardInterrupt handler prints a short closing message when you press Ctrl+C.
- Press Ctrl+O to save system_monitor.py.
- Press Enter to confirm the file name.
- Press Ctrl+X to return to the terminal.
✔️ Awesome, I've got everything!
Your finished monitor is saved. The menu is ready for its complete test run.
ⓧ I'd like to double check the full code
Compare your saved system_monitor.py with the complete file below:
"""Linux System Health & Utility Monitor for Ubuntu on WSL."""
import os
import subprocess
TITLE = "Linux System Health & Utility Monitor"
PROCESS_LIMIT = 10
def print_header(title):
print("\n" + "=" * 60)
print(title)
print("=" * 60)
def run_command(command, timeout=60):
try:
result = subprocess.run(
command,
capture_output=True,
universal_newlines=True,
check=True,
timeout=timeout,
)
return result.stdout.strip()
except FileNotFoundError:
return "Error: '{}' is not installed.".format(command[0])
except subprocess.CalledProcessError as error:
detail = error.stderr.strip() or "command returned an error"
return "Error: {}".format(detail)
except subprocess.TimeoutExpired:
return "Error: command exceeded {} seconds.".format(timeout)
def pause():
input("\nPress Enter to return to the menu...")
def show_health():
print_header("System Health")
checks = (
("RAM Usage", ["free", "-h"]),
("Root Filesystem Usage", ["df", "-h", "/"]),
("Uptime and Load Averages", ["uptime"]),
)
for label, command in checks:
print("\n--- {} ---".format(label))
print(run_command(command))
pause()
def show_processes():
print_header("Process Viewer")
output = run_command(["ps", "-e", "-o", "pid,comm"])
if output.startswith("Error:"):
print(output)
pause()
return
lines = output.splitlines()
visible_lines = lines[: PROCESS_LIMIT + 1]
print("\n".join(visible_lines))
if len(lines) > PROCESS_LIMIT + 1:
print("\nShowing {} of {} processes.".format(
PROCESS_LIMIT, len(lines) - 1
))
pause()
def analyze_folder():
print_header("Disk Analyzer")
raw_path = input("Enter a folder path [default: .]: ").strip()
path = os.path.expanduser(raw_path or ".")
if not os.path.exists(path):
print("Error: that path does not exist.")
pause()
return
if not os.path.isdir(path):
print("Error: the path is not a directory.")
pause()
return
output = run_command(["du", "-s", "-h", path])
print("\nFolder usage for '{}':".format(path))
print(output)
pause()
def show_menu():
print_header(TITLE)
print("1. System Health")
print("2. Process Viewer")
print("3. Disk Analyzer")
print("4. Exit")
def main():
while True:
show_menu()
choice = input("Choose an option [1-4]: ").strip()
if choice == "1":
show_health()
elif choice == "2":
show_processes()
elif choice == "3":
analyze_folder()
elif choice == "4":
print("Goodbye.")
break
else:
print("Invalid choice. Enter a number from 1 to 4.")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\nMonitor closed with Ctrl+C.")
How to Use This Reference
Check each function plus the final entry point against your file. Save any corrections before starting the verification run.
Before you run the finished monitor, which inputs do you expect the Disk Analyzer to accept?
- Launch the finished monitor by running:
python3 system_monitor.py
What Does This Final Run Prove?
This run checks all three features through the same menu loop. It also checks every defensive path added in this step.
- Capture a screenshot of the main menu before selecting an option.
- Select option 1.
You should see labelled sections for RAM usage, root filesystem usage, plus uptime and load averages.
- Capture a screenshot of the System Health output.
- Press Enter to return to the menu.
- Select option 2.
You should see the PID plus COMMAND header followed by up to ten running processes.
- Capture a screenshot of the Process Viewer output.
- Press Enter to return to the menu.
- Select option 3.
- Enter ~ at the folder prompt.
You should see the expanded home-directory path followed by one human-readable size.
- Capture a screenshot of the successful Disk Analyzer output.
- Press Enter to return to the menu.
- Select option 3 again.
- Enter missing-folder at the folder prompt.
You should see Error: that path does not exist. instead of a crash.
- Press Enter to return to the menu.
- Select option 3 again.
- Enter system_monitor.py at the folder prompt.
You should see Error: the path is not a directory. because the selected path is a regular file.
- Press Enter to return to the menu.
- Enter 9 at the menu prompt.
You should see Invalid choice. Enter a number from 1 to 4. before the menu returns.
- Select option 4.
You should see Goodbye. before the program closes.
Before the final interruption test, do you expect Ctrl+C to show a traceback or a controlled closing message?
- Start the monitor again by running:
python3 system_monitor.py
Why Run It Again?
The previous run verified the menu exit. This separate run tests the keyboard-interruption handler.
- Press Ctrl+C while the menu is waiting for a choice.
You should see Monitor closed with Ctrl+C. with no traceback. Your monitor now handles valid choices, invalid choices, path mistakes, command failures, plus keyboard interruptions.
Does the Final Test Show a Traceback?
Check that the try block wraps the call to main(). The except KeyboardInterrupt line must align with that try block.
Check that the final guard remains inside the if __name__ == "__main__": section.
Help me fix the final interruption handler.
That completes the assessed application. You now have clear outputs for your report plus concrete examples to explain during the viva.
Secret mission
Filter Processes by Name
Turn the Process Viewer into a practical search tool. Add an optional process-name filter that keeps the table header, limits matching rows, and handles empty results clearly.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your project available, pause your terminal session, or delete the project files. This project runs locally inside Ubuntu on Windows Subsystem for Linux, so it has no ongoing costs.
Resources you used:
- The local linux-system-monitor directory stores system_monitor.py.
Keep everything running
No action needed. Choose this if you want to keep demonstrating the monitor or improving its features.
- Leave the linux-system-monitor directory in its current WSL location.
- Keep system_monitor.py available for your Internal Evaluation 3 screenshots or viva demonstration.
- Remember that the monitor uses no background service after you exit its menu.
Pause - I'll come back to this later
End any active monitor session to free the terminal. Your project files stay available for the next time you return.
- Press Enter if the monitor is waiting at a feature result.
- Enter 4 when the main menu returns.
You'll see Goodbye. before the Ubuntu shell prompt returns.
- Close the Ubuntu terminal window with its window close control.
Your monitor is now paused. The linux-system-monitor directory remains ready for your next session.
Delete - I don't want to use this again
Remove the local project directory when you no longer need the monitor. This leaves your existing Ubuntu, Python 3, and GNU nano installations untouched.
Delete the project folder
Deleting the folder is permanent. The command below targets only linux-system-monitor after you move to its parent directory.
- Press Enter if the monitor is waiting at a feature result.
- Enter 4 at the main menu if the monitor is still running.
- Move out of linux-system-monitor and remove it by running these commands:
cd ..
rm -rf linux-system-monitor
What does this cleanup do?
The first line moves the shell to the directory above linux-system-monitor.
The second line removes the project directory plus system_monitor.py.
Cleanup complete: your monitor directory and its local file are now removed from WSL.
Nice Work!
Nice Work!
You did it! Your finished Python monitor now runs inside Ubuntu on Windows Subsystem for Linux. It gives you one menu for system health, process inspection, and folder analysis.
You've learned how to:
- Create a repeating menu-driven application with reusable functions. Use subprocess.run() with argument lists for safe command execution.
- Build a system health report with free, df, and uptime. Present up to 10 process IDs and command names from ps through a readable process viewer.
- Create a validated disk analyzer that accepts user-selected folders for du. Add defensive input handling for invalid choices, missing paths, regular files, command failures, timeouts, and Ctrl+C.
- Secret Mission: Added a case-insensitive process-name filter that preserves the PID COMMAND header. The filtered view keeps the 10-row limit and reports when no process matches.
Ready to quiz yourself?