Show a splash screen during startup.

master
Pacman Ghost 3 years ago
parent 4fd57b5e75
commit 903f2f29ff
  1. 48
      freeze.py
  2. BIN
      loader/assets/loading.gif
  3. 107
      loader/freeze.py
  4. 206
      loader/main.py

@ -19,11 +19,6 @@ BASE_DIR = os.path.split( os.path.abspath(__file__) )[ 0 ]
MAIN_SCRIPT = "vasl_templates/main.py"
APP_ICON = os.path.join( BASE_DIR, "vasl_templates/webapp/static/images/app.ico" )
TARGET_NAMES = {
"win32": "vasl-templates.exe",
}
DEFAULT_TARGET_NAME = "vasl-templates"
# ---------------------------------------------------------------------
def get_git_info():
@ -54,19 +49,26 @@ def get_git_info():
return { "last_commit_id": last_commit_id, "branch_name": branch_name }
def make_target_name( fname ):
"""Generate a target filename."""
return fname+".exe" if sys.platform == "win32" else fname
# ---------------------------------------------------------------------
# parse the command-line options
output_fname = None
no_loader = False
work_dir = None
cleanup = True
opts,args = getopt.getopt( sys.argv[1:], "o:w:", ["output=","work=","noclean"] )
for opt,val in opts:
opts,args = getopt.getopt( sys.argv[1:], "o:w:", ["output=","no-loader","work=","no-clean"] )
for opt, val in opts:
if opt in ["-o","--output"]:
output_fname = val.strip()
elif opt in ["--no-loader"]:
no_loader = True
elif opt in ["-w","--work"]:
work_dir = val.strip()
elif opt in ["--noclean"]:
elif opt in ["--no-clean"]:
cleanup = False
else:
raise RuntimeError( "Unknown argument: {}".format( opt ) )
@ -75,6 +77,7 @@ if not output_fname:
# figure out where to locate our work directories
if work_dir:
work_dir = os.path.abspath( work_dir )
build_dir = os.path.join( work_dir, "build" )
if os.path.isdir( build_dir ):
shutil.rmtree( build_dir )
@ -98,19 +101,23 @@ if not output_fmt:
# configure pyinstaller
# NOTE: Using UPX gave ~25% saving on Windows, but failed to run because of corrupt DLL's :-/
# NOTE: Setting --specpath breaks the build - it's being used as the project root...? (!)
target_name = TARGET_NAMES.get( sys.platform, DEFAULT_TARGET_NAME )
target_name = make_target_name( "vasl-templates" )
args = [
"--distpath", dist_dir,
"--workpath", build_dir,
"--specpath", build_dir,
"--onefile",
"--name", target_name,
]
args.extend( [ "--add-data", "vassal-shim/release/vassal-shim.jar" + os.pathsep + "vasl_templates/webapp" ] )
args.extend( [ "--add-data",
os.path.join( BASE_DIR, "vassal-shim/release/vassal-shim.jar" + os.pathsep + "vasl_templates/webapp" )
] )
# NOTE: We also need to include the config/ and data/ subdirectories, but we would like to
# make them available to the user, so we include them ourself in the final release archive.
def map_dir( src, dest ): #pylint: disable=missing-docstring
args.extend( [ "--add-data", src + os.pathsep + dest ] )
args.extend( [ "--add-data",
os.path.join( BASE_DIR, src + os.pathsep + dest )
] )
map_dir( "vasl_templates/ui", "vasl_templates/ui" )
map_dir( "vasl_templates/resources", "vasl_templates/resources" )
map_dir( "vasl_templates/webapp/static", "vasl_templates/webapp/static" )
@ -168,6 +175,22 @@ dname = os.path.join( dist_dir, "config" )
with open( os.path.join(dname,"build-info.json"), "w" ) as fp:
json.dump( build_info, fp )
# freeze the loader
if no_loader:
print( "Not including the loader." )
else:
print( "--- BEGIN FREEZE LOADER ---" )
shutil.move(
os.path.join( dist_dir, target_name ),
os.path.join( dist_dir, make_target_name("vasl-templates-main") )
)
from loader.freeze import freeze_loader #pylint: disable=no-name-in-module
freeze_loader(
os.path.join( dist_dir, target_name ),
build_dir, # nb: a "loader" sub-directory will be created and used
False # nb: we will clean up, or not, everything ourself
)
# create the release archive
os.chdir( dist_dir )
print()
@ -179,7 +202,6 @@ print( "- Done: {0:.1f} MB".format( float(file_size) / 1024 / 1024 ) )
# clean up
if cleanup:
os.chdir( BASE_DIR ) # so we can delete the build directory :-/
os.unlink( target_name + ".spec" )
shutil.rmtree( build_dir )
shutil.rmtree( dist_dir )

Binary file not shown.

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…
Cancel
Save