parent
4fd57b5e75
commit
903f2f29ff
After Width: | Height: | Size: 3.4 KiB |
@ -0,0 +1,107 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
""" Freeze the vasl-templates loader program. |
||||||
|
|
||||||
|
This script is called by the main freeze script. |
||||||
|
""" |
||||||
|
|
||||||
|
import sys |
||||||
|
import os |
||||||
|
import shutil |
||||||
|
import tempfile |
||||||
|
import getopt |
||||||
|
|
||||||
|
from PyInstaller.__main__ import run as run_pyinstaller |
||||||
|
from PIL import Image |
||||||
|
|
||||||
|
APP_ICON = os.path.join( |
||||||
|
os.path.abspath( os.path.dirname( __file__ ) ), |
||||||
|
"../vasl_templates/webapp/static/images/app.ico" |
||||||
|
) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def main( args ): |
||||||
|
"""Main processing.""" |
||||||
|
|
||||||
|
# parse the command-line options |
||||||
|
output_fname = "./loader" |
||||||
|
work_dir = os.path.join( tempfile.gettempdir(), "freeze-loader" ) |
||||||
|
cleanup = True |
||||||
|
opts,args = getopt.getopt( args, "o:w:", ["output=","work=","no-clean"] ) |
||||||
|
for opt, val in opts: |
||||||
|
if opt in ["-o","--output"]: |
||||||
|
output_fname = val.strip() |
||||||
|
elif opt in ["-w","--work"]: |
||||||
|
work_dir = val.strip() |
||||||
|
elif opt in ["--no-clean"]: |
||||||
|
cleanup = False |
||||||
|
else: |
||||||
|
raise RuntimeError( "Unknown argument: {}".format( opt ) ) |
||||||
|
|
||||||
|
# freeze the loader program |
||||||
|
freeze_loader( output_fname, work_dir, cleanup ) |
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||||
|
|
||||||
|
def freeze_loader( output_fname, work_dir, cleanup ): |
||||||
|
"""Freeze the loader program.""" |
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as dist_dir: |
||||||
|
|
||||||
|
# initialize |
||||||
|
base_dir = os.path.abspath( os.path.dirname( __file__ ) ) |
||||||
|
assets_dir = os.path.join( base_dir, "assets" ) |
||||||
|
|
||||||
|
# convert the app icon to an image |
||||||
|
if not os.path.isdir( work_dir ): |
||||||
|
os.makedirs( work_dir ) |
||||||
|
app_icon_fname = os.path.join( work_dir, "app-icon.png" ) |
||||||
|
_convert_app_icon( app_icon_fname ) |
||||||
|
|
||||||
|
# initialize |
||||||
|
app_name = "loader" |
||||||
|
args = [ |
||||||
|
"--distpath", dist_dir, |
||||||
|
"--workpath", work_dir, |
||||||
|
"--specpath", work_dir, |
||||||
|
"--onefile", |
||||||
|
"--name", app_name, |
||||||
|
] |
||||||
|
args.extend( [ |
||||||
|
"--add-data", app_icon_fname + os.pathsep + "assets/", |
||||||
|
"--add-data", os.path.join(assets_dir,"loading.gif") + os.pathsep + "assets/" |
||||||
|
] ) |
||||||
|
if sys.platform == "win32": |
||||||
|
args.append( "--noconsole" ) |
||||||
|
args.extend( [ "--icon", APP_ICON ] ) |
||||||
|
args.append( os.path.join( base_dir, "main.py" ) ) |
||||||
|
|
||||||
|
# freeze the program |
||||||
|
run_pyinstaller( args ) |
||||||
|
|
||||||
|
# save the generated artifact |
||||||
|
fname = app_name+".exe" if sys.platform == "win32" else app_name |
||||||
|
shutil.move( |
||||||
|
os.path.join( dist_dir, fname ), |
||||||
|
output_fname |
||||||
|
) |
||||||
|
|
||||||
|
# clean up |
||||||
|
if cleanup: |
||||||
|
shutil.rmtree( work_dir ) |
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||||
|
|
||||||
|
def _convert_app_icon( save_fname ): |
||||||
|
"""Convert the app icon to an image.""" |
||||||
|
# NOTE: Tkinter's PhotoImage doesn't handle .ico files, so we convert the app icon |
||||||
|
# to an image, then insert it into the PyInstaller-generated executable (so that |
||||||
|
# we don't have to bundle Pillow into the release). |
||||||
|
img = Image.open( APP_ICON ) |
||||||
|
img = img.convert( "RGBA" ).resize( (64, 64) ) |
||||||
|
img.save( save_fname, "png" ) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
main( sys.argv[1:] ) |
@ -0,0 +1,206 @@ |
|||||||
|
""" Load the main vasl-templates program. |
||||||
|
|
||||||
|
vasl-templates can be slow to start (especially on Windows), since it has to unpack the PyInstaller-generated EXE, |
||||||
|
then startup Qt. We want to show a splash screen while all this happening, but we can't just put it in vasl-templates, |
||||||
|
since it would only happen *after* all the slow stuff has finished :-/ So, we have this stub program that shows |
||||||
|
a splash screen, launches the main vasl-templates program, and waits for it to finish starting up. |
||||||
|
""" |
||||||
|
|
||||||
|
import sys |
||||||
|
import os |
||||||
|
import subprocess |
||||||
|
import threading |
||||||
|
import itertools |
||||||
|
import urllib.request |
||||||
|
from urllib.error import URLError |
||||||
|
import time |
||||||
|
import configparser |
||||||
|
|
||||||
|
# NOTE: It's important that this program start up quickly (otherwise it becomes pointless), |
||||||
|
# so we use tkinter, instead of PyQt (and also avoid bundling a 2nd copy of PyQt :-/). |
||||||
|
import tkinter |
||||||
|
import tkinter.messagebox |
||||||
|
|
||||||
|
if getattr( sys, "frozen", False ): |
||||||
|
BASE_DIR = sys._MEIPASS #pylint: disable=no-member,protected-access |
||||||
|
else: |
||||||
|
BASE_DIR = os.path.abspath( os.path.dirname( __file__ ) ) |
||||||
|
|
||||||
|
STARTUP_TIMEOUT = 60 # how to long to wait for vasl-templates to start (seconds) |
||||||
|
|
||||||
|
main_window = None |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def main( args ): |
||||||
|
"""Load the main vasl-templates program.""" |
||||||
|
|
||||||
|
# initialize Tkinter |
||||||
|
global main_window |
||||||
|
main_window = tkinter.Tk() |
||||||
|
main_window.option_add( "*Dialog.msg.font", "Helvetica 12" ) |
||||||
|
|
||||||
|
# load the app icon |
||||||
|
# NOTE: This image file doesn't exist in source control, but is created dynamically from |
||||||
|
# the main app icon by the freeze script, and inserted into the PyInstaller-generated executable. |
||||||
|
# We do things this way so that we don't have to bundle Pillow into the release. |
||||||
|
app_icon = tkinter.PhotoImage( |
||||||
|
file = make_asset_path( "app-icon.png" ) |
||||||
|
) |
||||||
|
|
||||||
|
# locate the main vasl-templates executable |
||||||
|
fname = os.path.join( os.path.dirname( sys.executable ), "vasl-templates-main" ) |
||||||
|
if sys.platform == "win32": |
||||||
|
fname += ".exe" |
||||||
|
if not os.path.isfile( fname ): |
||||||
|
show_error_msg( "Can't find the main vasl-templates program.", withdraw=True ) |
||||||
|
return -1 |
||||||
|
|
||||||
|
# launch the main vasl-templates program |
||||||
|
try: |
||||||
|
proc = subprocess.Popen( itertools.chain( [fname], args ) ) |
||||||
|
except Exception as ex: #pylint: disable=broad-except |
||||||
|
show_error_msg( "Can't start vasl-templates:\n\n{}".format( ex ), withdraw=True ) |
||||||
|
return -2 |
||||||
|
|
||||||
|
# get the webapp port number |
||||||
|
port = 5010 |
||||||
|
fname = os.path.join( os.path.dirname( fname ), "config/app.cfg" ) |
||||||
|
if os.path.isfile( fname ): |
||||||
|
config_parser = configparser.ConfigParser() |
||||||
|
config_parser.optionxform = str # preserve case for the keys :-/ |
||||||
|
config_parser.read( fname ) |
||||||
|
args = dict( config_parser.items( "System" ) ) |
||||||
|
port = args.get( "FLASK_PORT_NO", port ) |
||||||
|
|
||||||
|
# create the splash window |
||||||
|
create_window( app_icon ) |
||||||
|
|
||||||
|
# start a background thread to check on the main vasl-templates process |
||||||
|
threading.Thread( |
||||||
|
target = check_startup, |
||||||
|
args = ( proc, port ) |
||||||
|
).start() |
||||||
|
|
||||||
|
# run the main loop |
||||||
|
main_window.mainloop() |
||||||
|
|
||||||
|
return 0 |
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||||
|
|
||||||
|
def create_window( app_icon ): |
||||||
|
"""Create the splash window.""" |
||||||
|
|
||||||
|
# create the splash window |
||||||
|
main_window.geometry( "290x75" ) |
||||||
|
main_window.title( "vasl-templates loader" ) |
||||||
|
main_window.overrideredirect( 1 ) # nb: "-type splash" doesn't work on Windows :-/ |
||||||
|
main_window.eval( "tk::PlaceWindow . center" ) |
||||||
|
main_window.wm_attributes( "-topmost", 1 ) |
||||||
|
main_window.tk.call( "wm", "iconphoto", main_window._w, app_icon ) #pylint: disable=protected-access |
||||||
|
main_window.protocol( "WM_DELETE_WINDOW", lambda: None ) |
||||||
|
|
||||||
|
# add the app icon |
||||||
|
label = tkinter.Label( main_window, image=app_icon ) |
||||||
|
label.grid( row=0, column=0, rowspan=2, padx=5, pady=5 ) |
||||||
|
|
||||||
|
# add the caption |
||||||
|
label = tkinter.Label( main_window, text="Loading vasl-templates...", font=("Helvetica",12) ) |
||||||
|
label.grid( row=0, column=1, padx=5, pady=(5,0) ) |
||||||
|
|
||||||
|
# add the "loading" image (we have to animate it ourself :-/) |
||||||
|
anim_label = tkinter.Label( main_window ) |
||||||
|
anim_label.grid( row=1, column=1, sticky=tkinter.N, padx=0, pady=0 ) |
||||||
|
fname = make_asset_path( "loading.gif" ) |
||||||
|
nframes = 13 |
||||||
|
frames = [ |
||||||
|
tkinter.PhotoImage( file=fname, format="gif -index {}".format( i ) ) |
||||||
|
for i in range(nframes) |
||||||
|
] |
||||||
|
frame_index = 0 |
||||||
|
def next_frame(): |
||||||
|
nonlocal frame_index |
||||||
|
frame = frames[ frame_index ] |
||||||
|
frame_index = ( frame_index + 1 ) % nframes |
||||||
|
anim_label.configure( image=frame ) |
||||||
|
main_window.after( 75, next_frame ) |
||||||
|
main_window.after( 0, next_frame ) |
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||||
|
|
||||||
|
def check_startup( proc, port ): |
||||||
|
"""Check the startup of the main vasl-templates process.""" |
||||||
|
|
||||||
|
def do_check(): |
||||||
|
|
||||||
|
# check if we've waited for too long |
||||||
|
if time.time() - start_time > STARTUP_TIMEOUT: |
||||||
|
# yup - give up |
||||||
|
raise RuntimeError( "Couldn't start vasl-templates." ) |
||||||
|
|
||||||
|
# check if the main vasl-templates process has gone away |
||||||
|
if proc.poll() is not None: |
||||||
|
raise RuntimeError( "The vasl-templates program ended unexpectedly." ) |
||||||
|
|
||||||
|
# check if the webapp is responding |
||||||
|
url = "http://localhost:{}/ping".format( port ) |
||||||
|
try: |
||||||
|
_ = urllib.request.urlopen( url ).read() |
||||||
|
except URLError: |
||||||
|
# no response - the webapp is probably still starting up |
||||||
|
return False |
||||||
|
except Exception as ex: #pylint: disable=broad-except |
||||||
|
raise RuntimeError( "Couldn't communicate with vasl-templates:\n\n{}".format( ex ) ) from ex |
||||||
|
|
||||||
|
# the main vasl-templates program has started up and is responsive - our job is done! |
||||||
|
if sys.platform == "win32": |
||||||
|
# FUDGE! There is a short amount of time between the webapp server starting and |
||||||
|
# the main window appearing. We delay here for a bit, to try to synchronize |
||||||
|
# our window fading out with the main vasl-templates window appearing. |
||||||
|
time.sleep( 1 ) |
||||||
|
return True |
||||||
|
|
||||||
|
def on_done( msg ): |
||||||
|
if msg: |
||||||
|
show_error_msg( msg, withdraw=True ) |
||||||
|
fade_out( main_window, main_window.quit ) |
||||||
|
|
||||||
|
# run the main loop |
||||||
|
start_time = time.time() |
||||||
|
while True: |
||||||
|
try: |
||||||
|
if do_check(): |
||||||
|
on_done( None ) |
||||||
|
break |
||||||
|
except Exception as ex: #pylint: disable=broad-except |
||||||
|
on_done( str(ex) ) |
||||||
|
return |
||||||
|
time.sleep( 0.25 ) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
def fade_out( target, on_done ): |
||||||
|
"""Fade out the target window.""" |
||||||
|
alpha = target.attributes( "-alpha" ) |
||||||
|
if alpha > 0: |
||||||
|
alpha -= 0.1 |
||||||
|
target.attributes( "-alpha", alpha ) |
||||||
|
target.after( 50, lambda: fade_out( target, on_done ) ) |
||||||
|
else: |
||||||
|
on_done() |
||||||
|
|
||||||
|
def make_asset_path( fname ): |
||||||
|
"""Generate the path to an asset file.""" |
||||||
|
return os.path.join( BASE_DIR, "assets", fname ) |
||||||
|
|
||||||
|
def show_error_msg( error_msg, withdraw=False ): |
||||||
|
"""Show an error dialog.""" |
||||||
|
if withdraw: |
||||||
|
main_window.withdraw() |
||||||
|
tkinter.messagebox.showinfo( "vasl-templates loader error", error_msg ) |
||||||
|
|
||||||
|
# --------------------------------------------------------------------- |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
sys.exit( main( sys.argv[1:] ) ) |
Loading…
Reference in new issue