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