How to Make a Process Monitor in Python

Abdou Rockikz · 09 sep 2019

Abdou Rockikz · 7 min read · Updated oct 2019 · General Python Topics

Monitoring operating system processes enables us to monitor and display process activity in the real time. In this tutorial, you will learn how to retrieve information on running processes in the operating system using Python, and build a task manager around it !

Now you're may be thinking about creating something like this:

Windows Task Manager

Well, not exactly, we gonna make a command line version of this, the final script output will be this:

Python Process Monitor

However, if you are a GUI programmer, you can make this a lot better with your own design and compete with Windows Task Manager !

Alright, now let's get into building this. First, let's install the dependencies:

pip3 install psutil pandas

Open up a new python file and import the necessary modules:

import psutil
from datetime import datetime
import pandas as pd

We'll use psutil as it is a cross-platform library for retrieving running processes information in Python.

The reason that we need pandas here is that after retrieving processes information, we gonna need to sort by columns and printing in a tabular way.

Now, we need a way to retrieve all processes in a for loop. Luckily for us, there is the function psutil.process_iter() which returns a generator yielding a process instance for all running processes in the operating system.

We gonna store all the processes in a list of dictionaries:

# the list the contain all process dictionaries
processes = []

Let's iterate over the generator:

for process in psutil.process_iter():
    # get all process info in one shot
    with process.oneshot():
        # get the process id
        pid = process.pid

process.oneshot() will help us retrieve process information efficiently ( faster way ), let's get the process name:

        # get the name of the file executed
        name = process.name()

Retrieving the time when the process was created in timestamp, as a result, we'll convert to a proper datetime object:

        # get the time the process was spawned
        create_time = datetime.fromtimestamp(process.create_time())

Let's get the process CPU usage as well as the number of cores that can execute this process:

        try:
            # get the number of CPU cores that can execute this process
            cores = len(process.cpu_affinity())
        except psutil.AccessDenied:
            cores = 0
        # get the CPU usage percentage
        cpu_usage = process.cpu_percent()

The reason I wrapped process.cpu_affinity() in a try/except block is that sometimes it will raise a psutil.AccessDenied for system processes.

Getting the status of the process, whether it is running, sleeping, etc:

        # get the status of the process (running, idle, etc.)
        status = process.status()

Process priority:

        try:
            # get the process priority (a lower value means a more prioritized process)
            nice = int(process.nice())
        except psutil.AccessDenied:
            nice = 0

Memory usage:

        try:
            # get the memory usage of this process in bytes
            memory_usage = process.memory_full_info().uss
        except psutil.AccessDenied:
            memory_usage = 0

Total written and read bytes by this process:

        # total process read and written bytes
        io_counters = process.io_counters()
        read_bytes = io_counters.read_bytes
        write_bytes = io_counters.write_bytes

Total threads spawned:

        # get the number of total threads spawned by this process
        n_threads = process.num_threads()

Finally, the user that spawned that process:

        # get the username of user spawned the process
        try:
            username = process.username()
        except psutil.AccessDenied:
            username = "N/A"

Let's add all this information to our list:

    processes.append({
        'pid': pid, 'name': name, 'create_time': create_time,
        'cores': cores, 'cpu_usage': cpu_usage, 'status': status, 'nice': nice,
        'memory_usage': memory_usage, 'read_bytes': read_bytes, 'write_bytes': write_bytes,
        'n_threads': n_threads, 'username': username,
    })

As mentioned earlier, we gonna convert processes list into a pandas.DataFrame:

# convert to pandas dataframe
df = pd.DataFrame(processes)

Setting the index of the each row as the process id:

# set the process id as index of a process
df.set_index('pid', inplace=True)

Some command line argument parsing:

if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="Process Viewer & Monitor")
    parser.add_argument("-c", "--columns", help="""Columns to show,
                                                available are name,create_time,cores,cpu_usage,status,nice,memory_usage,read_bytes,write_bytes,n_threads,username.
                                                Default is name,cpu_usage,memory_usage,read_bytes,write_bytes,status,create_time,nice,n_threads,cores.""",
                        default="name,cpu_usage,memory_usage,read_bytes,write_bytes,status,create_time,nice,n_threads,cores")
    parser.add_argument("-s", "--sort-by", dest="sort_by", help="Column to sort by, default is memory_usage .", default="memory_usage")
    parser.add_argument("--descending", action="store_true", help="Whether to sort in descending order.")
    parser.add_argument("-n", help="Number of processes to show, will show all if 0 is specified, default is 25 .", default=25)

    # parse arguments
    args = parser.parse_args()
    columns = args.columns
    sort_by = args.sort_by
    descending = args.descending
    n = int(args.n)

Let's sort the rows by sort_by column that is passed in the command line arguments:

    # sort rows by the column passed as argument
    df.sort_values(sort_by, inplace=True, ascending=not descending)

We need a good way to print bytes, not just bunch of big numbers, let's do a function like this:

def get_size(bytes):
    """
    Returns size of bytes in a nice format
    """
    for unit in ['', 'K', 'M', 'G', 'T', 'P']:
        if bytes < 1024:
            return f"{bytes:.2f}{unit}B"
        bytes /= 1024

Let's apply this on our bytes columns (memory_usage, write_bytes and read_bytes):

    # pretty printing bytes
    df['memory_usage'] = df['memory_usage'].apply(get_size)
    df['write_bytes'] = df['write_bytes'].apply(get_size)
    df['read_bytes'] = df['read_bytes'].apply(get_size)

Also, let's apply a nice formatting for create_time:

    # convert to proper date format
    df['create_time'] = df['create_time'].apply(datetime.strftime, args=("%Y-%m-%d %H:%M:%S",))

Let's define what columns we'll show based on what's passed in the command line arguments:

    # reorder and define used columns
    df = df[columns.split(",")]

Finally, let's print all of this:

    if n == 0:
        print(df.to_string())
    elif n > 0:
        print(df.head(n).to_string())

I'm using head() method here which prints the first n rows.

Here is a sample output from my linux box:

root@rockikz:~/pythonscripts# python3 process_monitor.py --columns name,cpu_usage,memory_usage,status -n 20 --sort-by memory_usage --descending
                name  cpu_usage memory_usage    status
pid
1312          mysqld        0.0     144.63MB  sleeping
915      gnome-shell        0.0      81.00MB  sleeping
3214         python3        0.0      58.12MB   running
1660   rtorrent main        0.0      35.84MB  sleeping
2466   rtorrent main        0.0      24.02MB  sleeping
3186             php        0.0      19.58MB  sleeping
737             Xorg        0.0      15.52MB  sleeping
1452         apache2        0.0      12.18MB  sleeping
872      teamviewerd        0.0      11.53MB  sleeping
974        gsd-color        0.0       8.65MB  sleeping
553   NetworkManager        0.0       7.71MB  sleeping
1045          colord        0.0       7.16MB  sleeping
982     gsd-keyboard        0.0       6.23MB  sleeping
969    gsd-clipboard        0.0       6.09MB  sleeping
548     ModemManager        0.0       5.68MB  sleeping
986   gsd-media-keys        0.0       4.94MB  sleeping
1001       gsd-power        0.0       4.72MB  sleeping
962    gsd-xsettings        0.0       4.59MB  sleeping
1023       gsd-wacom        0.0       4.40MB  sleeping
961      packagekitd        0.0       4.31MB  sleeping

And there we go ! We are done with this, as discussed above, you can make a GUI version of this, with buttons to kill, suspend, and resume the process as there are already available functions for that ( process.kill(), process.suspend() and process.resume() ). There are also another information you can retrieve that are not discussed here, type help(psutil.Process) for all available fields and methods!

Also, you can use psutil to retrieve general system and hardware information !

Check the full code here.

Happy Coding ♥

View Full Code
Sharing is caring!


Read Also





Comment panel

   
Comment system is still in Beta, if you find any bug, please consider contacting us here.