0

I'm working on a company that deploy their system on local computer and servers.

The task given to me is to create a python application which communicates with a plc and display gathered data using Reactjs/Electron.js or save the data on MS SQL Server.

My idea was to create gui using tkinter and convert it to an app by creating a <filename>.spec file and pyinstaller that look like XAMPP where I could restart and start the api.

If I use tkinter where should I start? I manage to start the backend when the gui opened but I have no clue to stop or restart the backend. I also don't have much idea on threads but I think it would help me create what I want.

I was also searching if I could use XAMPP as a host to run the api but no luck.

If you guys have a better way to run the api on localhost without the cmd would really help

j_4321
  • 15,431
  • 3
  • 34
  • 61
Nellartsa
  • 29
  • 1
  • 7
  • 1
    I don't think that running the server on another `thread` would suffice, I would suggest using another process if possible since servers are pretty resource consuming (at least I imagine so) so making it run in the same thread as tkinter may very much slow things down (this is doable if flask is pickleable, EDIT: actually that may not be needed), what I could imagine is running a `subprocess.Popen` process which will launch the server and then stream output to a text widget in tkinter, haven't yet thought about stopping but that shouldn't be too hard either (will try to write an answer) – Matiiss Jul 27 '21 at 07:35
  • I'll try to study `subprocess`. Thank you for the answer – Nellartsa Jul 28 '21 at 02:06

1 Answers1

0

Ok, so this took a while but I figured out how to not use subprocess (mainly because there was another question regarding not using python so the only way would be to convert flask app to .exe and then use subprocess but I found out how to do this using just python), the main issue (simple fix but has to be resolved when using subprocess.Popen too) is that flask restarts the server which starts another process so you have to use use_reloader=False in .run() method. Explanation in comments:

app.py

# import what's necessary
from flask import Flask, render_template_string, url_for
from flask import request


app = Flask(__name__)


# sample route
@app.route('/')
def home():
    return render_template_string('<a href="{{ url_for("about") }}">To About Page</a>'
                                  '<h1>Home Page</h1>')


# sample route
@app.route('/about')
def about():
    return render_template_string('<a href="{{ url_for("home") }}">To Home Page</a>'
                                  '<h1>About Page</h1>')


# important route that will do the stopping part since that was a requirement in the question
# link to this in the answer at the bottom
@app.route('/kill_server', methods=['GET'])
def kill_server():
    func = request.environ.get('werkzeug.server.shutdown')
    if func is None:
        raise RuntimeError('Not running with the Werkzeug Server. Could not shut down server.')
    func()
    return 'Server shutting down...'


# to prevent this from running when importing
if __name__ == '__main__':
    app.run(debug=True)

run.py

# import all that is necessary
from tkinter import Tk, Text, Button, Frame
from for_so_dir.app import app as server
from threading import Thread
from queue import Queue, Empty
import requests
import logging.handlers


# simple dictionary to avoid using `global`, just a preference of mine
info = {'server_is_running': False}


# the function that starts server (multiprocessing is not possible with flask as far as I know)
# basically checks if the server is not running already and if it is not then starts a thread
# with the server (which is the same as `app` in the app.py) and sets that server is running
def start_server():
    if info['server_is_running']:
        return
    Thread(target=server.run, kwargs={'debug': True, 'use_reloader': False}, daemon=True).start()
    info['server_is_running'] = True


# function from stopping server, again in the answer at the bottom, but basically
# this sends a request to the server and that request executes a function
# that stops the server
def stop_server():
    if not info['server_is_running']:
        return
    requests.get('http://127.0.0.1:5000/kill_server')


# function for showing the logs in the Text widget, it simply tries to get data from
# the queue (if there is nothing it simply loops again) and then inserts that data into
# the text widget
def update_text_log():
    try:
        data = queue.get(block=False)
    except Empty:
        pass
    else:
        log.config(state='normal')
        log.insert('end', data.msg + '\n')
        log.config(state='disabled')
    finally:
        root.after(100, update_text_log)


# this `if statement` is not that necessary in the current code but it might as well
# stay here
if __name__ == '__main__':
    # initialise the Queue
    queue = Queue()
    # now the main part, to get the info from flask you need to use `logging`
    # since that is what it uses for all the messages, and use the `QueueHandler`
    # to put the messages in the queue and then update them using the above
    # function
    logging.basicConfig(handlers=(logging.handlers.QueueHandler(queue), ))
    
    # simple `tkinter` stuff
    root = Tk()
    # this part can be removed or made toggleable but it allows to easier see how
    # this works in action
    root.attributes('-topmost', True)

    log = Text(root, state='disabled')
    log.pack(expand=True, fill='both')

    update_text_log()

    button_frame = Frame(root)
    button_frame.pack(fill='x')
    Button(button_frame, text='Start Server', command=start_server).pack(expand=True, fill='x', side='left')
    Button(button_frame, text='Stop Server', command=stop_server).pack(expand=True, fill='x', side='right')

    root.mainloop()

Sources:

The only issue is about displaying the starting message in console, there is a way to remove it by adding import os and then os.environ['WERKZEUG_RUN_MAIN'] = 'true' to the app.py file but there is a slight issue then that stopping the server using the button will do this: Process finished with exit code 15 (at least on Windows) so you will have to find a way to workaround this since I am not able yet to do so

Matiiss
  • 5,970
  • 2
  • 12
  • 29
  • And I just noticed that you also asked the other question, that I have mentioned in my answer, if you still need the option with subprocess (that requires python to be installed) I can try writing that one too, otherwise you could convert the app to exe and then run it using subprocess – Matiiss Aug 06 '21 at 06:48