Search code examples
pythonpython-3.xsubprocess

Terminate a subprocess in a python app using a button from the GUI of a program


So I am trying to do a program in python using customtkinter and I have to start a command using a button from the GUI I have created and once it is started i would like to terminate it using the stop button also from GUI. I have divided my my work into 2 different files app.py where I keep the design and a method.py where I keep the methods that are run when the buttons are pressed. The command i ran it is a terminal command similar to the ping command that I have done here below. The files below if someone wants to try or get a better view on my code

In app.py:

import customtkinter as ctk
import methods as mtd

# System Defaults
ctk.set_appearance_mode('Dark')
ctk.set_default_color_theme('dark-blue')
app = ctk.CTk()
app.title('APP')
my_font = ctk.CTkFont(family="Sans Serif", size=15)

# Set up the grid layout
app.grid_columnconfigure((0, 1, 2), weight=1)
app.grid_rowconfigure(1, weight=1)
app.grid_rowconfigure(0, weight=15)


ipAdd = ctk.CTkEntry(app, font=my_font, placeholder_text='Enter IP address...')
ipAdd.grid(row=1, column=0, sticky='we', padx=(5, 10), pady=(5, 5))

# Set Up Display
display = ctk.CTkTextbox(app)
display.grid(row=0, column=0, columnspan=3, sticky='nswe', padx=5, pady=(5, 0))

# Set up the buttons
startBtn = ctk.CTkButton(app, font=my_font, text='START', fg_color='#006600', border_width=2, corner_radius=50,
                         hover_color='#009900', border_color='#006600', command=lambda: mtd.ping_host(display, ipAdd))
startBtn.grid(row=1, column=1, padx=(0, 5))
stopBtn = ctk.CTkButton(app, font=my_font, text='STOP', fg_color='#FF0000', border_width=2, corner_radius=50,
                        hover_color='#CC0000', border_color='#FF0000', command=lambda: mtd.stop_ping(display))
stopBtn.grid(row=1, column=2, padx=(5, 5))

app.mainloop()

In method.py I have:

import customtkinter as ctk
import subprocess

def clear_display(display: ctk.CTkTextbox, ipadd: ctk.CTkEntry):
    display.delete(0.0, 'end')
    ipadd.delete(0, 'end')


def ping_host(display: ctk.CTkTextbox, ipadd: ctk.CTkEntry):
    host = ipadd.get()
    display.delete(1.0, ctk.END)  # Clear previous results
    try:
        # Open a process with the ping command
        process = subprocess.Popen(['ping', '-n', '5', host], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

        # Read and display the output line by line
        while True:
            line = process.stdout.readline()
            if not line:
                break  # Break the loop when there's no more output
            display.insert(ctk.END, f"{line}")
            display.update_idletasks()  # Force update of the display

        # Wait for the process to complete
        process.wait()

        # Display any remaining output after the process completes
        remaining_output = process.stdout.read()
        if remaining_output:
            display.insert(ctk.END, f"remain out: {remaining_output}\n")

        # Display any errors
        errors = process.stderr.read()
        if errors:
            display.insert(ctk.END, f"Error: {errors}\n")
    except Exception as e:
        # Handle any exceptions that may occur
        display.insert(ctk.END, f"Exception: {str(e)}\n")

I need the stop_ping() method to stop the ping once it has started. Tried with ChatGPT but no luck it wasn't giving any valuable info. Appreciate any help!


Solution

  • I would actually recommend that you put this into a class so that it's a little cleaner to keep track of these variables, but here is a minimally invasive solution. It relies on threading, and should meet your needs as described.

    The core of it is, the ping_host holds the execution thread till it finishes, so the stop button can't do anything till it finishes. By putting the ping_host call in a separate threading.Thread, we give the stop button an opportunity to act.

    I created a class to hold the process generated in your methods module, and slightly modified that module to store the process handle.

    And lastly the stop button function call was rerouted to a function which uses python's Popen.kill function, details here.

    app.py

    import customtkinter as ctk
    import methods as mtd
    import threading
    
    class ProcHandleHolder:
        def __init__(self):
            self.procHandle = None
        @property
        def procHandle(self):
            return self._procHandle
        @procHandle.setter
        def procHandle(self, invalue):
            self._procHandle = invalue
        def set(self, procHandle):
            self.procHandle = procHandle
        def reset(self):
            self.procHandle = None
    
    def stop_proc():
        if phh.procHandle:
            phh.procHandle.kill()
            phh.reset()
            print('proc killed')
        else:
            print('proc not running?')
    def start_proc():
        t = threading.Thread(target=mtd.ping_host, args=(display, ipAdd, phh))
        t.start()
    
    phh = ProcHandleHolder()
    
    # System Defaults
    ctk.set_appearance_mode('Dark')
    ctk.set_default_color_theme('dark-blue')
    app = ctk.CTk()
    app.title('APP')
    my_font = ctk.CTkFont(family="Sans Serif", size=15)
    
    # Set up the grid layout
    app.grid_columnconfigure((0, 1, 2), weight=1)
    app.grid_rowconfigure(1, weight=1)
    app.grid_rowconfigure(0, weight=15)
    
    
    ipAdd = ctk.CTkEntry(app, font=my_font, placeholder_text='Enter IP address...')
    ipAdd.grid(row=1, column=0, sticky='we', padx=(5, 10), pady=(5, 5))
    
    # Set Up Display
    display = ctk.CTkTextbox(app)
    display.grid(row=0, column=0, columnspan=3, sticky='nswe', padx=5, pady=(5, 0))
    
    # Set up the buttons
    startBtn = ctk.CTkButton(app, font=my_font, text='START', fg_color='#006600', 
            border_width=2, corner_radius=50,
            hover_color='#009900', border_color='#006600', 
            command=lambda: start_proc())
    startBtn.grid(row=1, column=1, padx=(0, 5))
    stopBtn = ctk.CTkButton(app, font=my_font, text='STOP', fg_color='#FF0000', 
            border_width=2, corner_radius=50,
            hover_color='#CC0000', border_color='#FF0000', 
            command=lambda: stop_proc())
    stopBtn.grid(row=1, column=2, padx=(5, 5))
    
    app.mainloop()
    

    methods.py

    import customtkinter as ctk
    import subprocess
    
    def clear_display(display: ctk.CTkTextbox, ipadd: ctk.CTkEntry):
        display.delete(0.0, 'end')
        ipadd.delete(0, 'end')
    
    
    def ping_host(display: ctk.CTkTextbox, ipadd: ctk.CTkEntry, phh: object):
        host = ipadd.get()
        display.delete(1.0, ctk.END)  # Clear previous results
        try:
            # Open a process with the ping command
            process = subprocess.Popen(['ping', '-n', '5', host], 
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            phh.set(process)
            # Read and display the output line by line
            while True:
                line = process.stdout.readline()
                if not line:
                    break  # Break the loop when there's no more output
                display.insert(ctk.END, f"{line}")
                display.update_idletasks()  # Force update of the display
    
            # Wait for the process to complete
            process.wait()
    
            # Display any remaining output after the process completes
            remaining_output = process.stdout.read()
            if remaining_output:
                display.insert(ctk.END, f"remain out: {remaining_output}\n")
    
            # Display any errors
            errors = process.stderr.read()
            if errors:
                display.insert(ctk.END, f"Error: {errors}\n")
        except Exception as e:
            # Handle any exceptions that may occur
            display.insert(ctk.END, f"Exception: {str(e)}\n")
    

    Hope this helps! Feel free to comment with any questions.