1

I am trying to create a tkinter GUI that will essentially function as an SSH connection to a remote linux computer. The GUI needs to be functional on a windows operating system. The GUI has a few buttons that allows the user to start and stop programs on the linux computer but I am having issues printing a continuous output from this remote program to tkinter. I essentially want to have a embedded window, in a tkinter GUI, that shows the command line output from the remote desktop.

In the code below I successfully call my GUI class and build the tkinter GUI. Using paramiko I can successfully establish an SSH connection but the issue is when I press "Start Script". Start script successfully starts the program over an SSH connection, but I can not print a continuous output to the GUI (to test this the main_script.py is a basic program that just prints a increasing number every 3 seconds). I believe the issue is that I stall the tkinter mainloop but I do not know how to address this issue. I can use the tkinter.after command to update the window after a delay but because of how I read stdout my script is still stalled. Is there a better way to read stdout? Do I need to call a subprocess or use multithreading (if so how do I do this when I would like to establish the SSH connection when the main_window opens). Or am I missing a much simpler way to essentially have a remote desktop command line output with a few GUI buttons? Thanks for any help.

import paramiko
import tkinter as tk


class main_window():
    def __init__(self, root2, ip, user, pword):
        self.root2 = root2

        # Connection Label
        conn_label = tk.Label(root2, text=str('CONNECTED TO: ' + user + '    ' + 'ON: ' + ip))
        conn_label.grid(row=0, columnspan=6, sticky=tk.W)

        # Frequency Entery
        freq_label = tk.Label(root2, text='Tag Frequency:')
        freq_entry = tk.Entry(root2)
        self.freq_enter_button = tk.Button(root2, text='Enter')
        freq_label.grid(row=1, column=1, sticky=tk.E, padx=(5, 0))
        freq_entry.grid(row=1, column=2, sticky=tk.W)
        self.freq_enter_button.grid(row=1, column=3, padx=5)

        # Scroll Bar and Command Line Ouput
        self.scrollbar = tk.Scrollbar(root2)
        self.scrollbar.grid(row=3, column=5, rowspan=1, sticky=tk.N+tk.S)

        self.mylist = tk.Listbox(root2, yscrollcommand=self.scrollbar.set)
        self.mylist.grid(row=3, column=0, columnspan=5, sticky=tk.W+tk.E+tk.N+tk.S)

        # SSH Connection
        self.ssh = paramiko.SSHClient()
        self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self.ssh.connect(ip, port=22, username=user, password=pword)
        # stdin, stdout, stderr = ssh.exec_command('ls')
        # for line in stdout:
        #     mylist.insert(tk.END, str('... ' + line.strip('\n')))
        # ssh.close()

        # Start Script Button
        self.script_btn = tk.Button(root2, text="Start Script", command=self.toggle)
        self.script_btn.grid(row=2, column=1, columnspan=3, sticky=tk.W+tk.E, padx=(10, 8), pady=2)

        # Customize Grid Expansion
        root2.columnconfigure(0, weight=1)
        root2.columnconfigure(4, weight=1)
        root2.rowconfigure(3, weight=1)

        # Visual Touches
        root2.title("Terminal Connection")
        root2.wm_iconbitmap('logo_blue.ico')

    def toggle(self):
        def_color = self.freq_enter_button.cget('bg')
        if self.script_btn.config('text')[-1] == 'Start Script':
            stdin, self.stdout, stderr = self.ssh.exec_command('python main_script.py', get_pty=True)
            self.mylist.insert(tk.END, str('Starting Script\n'))
            self.script_btn.config(text='End Script', bg='red')
            self.update_window()
        else:
            stdin, self.stdout, stderr = self.ssh.exec_command(chr(3))
            self.mylist.insert(tk.END, str('Ending Script\n'))
            self.script_btn.config(text='Start Script', bg=def_color)

    def update_window(self):
        for line in iter(lambda: self.stdout.readline(), ""):
            self.mylist.insert(tk.END, str(line))
            self.scrollbar.config(command=self.mylist.yview)
        # self.root2.after(1000, self.update_window)


def main():
    root = tk.Tk()
    IP = '*********'
    user = '*******'
    pswd = '*******'
    runGUI = main_window(root, IP, user, pswd)
    root.title("Connect to Drone")
    root.wm_iconbitmap('logo_blue.ico')
    root.mainloop()


if __name__ == "__main__":
    main()
gvega
  • 11
  • 4
  • You are likely blocking the mainloop with your call to the other computer. You may want to look into using threading here. That way you can run your script in parallel with tkinter without blocking the mainloop. – Mike - SMT Nov 07 '18 at 19:11
  • Thanks. That was my hypothesis I just wasn't quite sure how to address it. Is threading or multiprocessing better? I would ideally like to create the paramiko class when main_window opens (i.e establish the connection immediately) and then send this information to the new process/thread but got a "TypeError: can't pickle thread.lock objects" when I tried. – gvega Nov 07 '18 at 19:18
  • I would have a hard time building an example for you as I have not had to do something remote like this before but I am willing to bet if you build function that uses threading you will be able to manage both the tkinter window being open and the connection without issues once you understand how to share information between threads. Take a look here for some resources on this. [How to share data between threads in this threaded TCPServer?](https://stackoverflow.com/questions/16044452/how-to-share-data-between-threads-in-this-threaded-tcpserver) – Mike - SMT Nov 07 '18 at 19:20
  • Thanks for the resources. I will take a look at threading my code. – gvega Nov 07 '18 at 20:10

1 Answers1

0

There is actually a very simple solution to this, although it's hard to find a working example!

You need to use ThreadPoolExecutor:

from concurrent import futures
from time import sleep

thread_pool_executor = futures.ThreadPoolExecutor(max_workers=1)

=== the following is part of your class ===

def toggle(self):
    thread_pool_executor.submit(self.blocking_code)

def blocking_code(self):
    sin, sout, serr = self.ssh.exec_command('xxxxxxxx')
    while not sout.channel.exit_status_ready():
        n = len(sout.channel.in_buffer)
        if n:
            buf = sout.read(n).decode('utf-8')
            ##### (output buf to gui)
        elif self.my_input:
            ##### (gui assigns any string to self.my_input)
            sin.write(self.my_input.encode('utf-8'))
            self.my_input = ''
        else:
            sleep(.1)
Domo
  • 1
  • 1