Store config and data files in the standard locations.

master
Pacman Ghost 2 years ago
parent fbcf4e9184
commit 1294d0e3d2
  1. 1
      requirements.txt
  2. 14
      vasl_templates/main.py
  3. 60
      vasl_templates/webapp/__init__.py
  4. 4
      vasl_templates/webapp/config/logging.yaml.example
  5. 8
      vasl_templates/webapp/downloads.py
  6. 1
      vasl_templates/webapp/globvars.py
  7. 2
      vasl_templates/webapp/scenarios.py
  8. 4
      vasl_templates/webapp/templates/configure-vo-notes-image-cache.html
  9. 153
      vasl_templates/webapp/user_profile.py
  10. 3
      vasl_templates/webapp/vo_notes.py
  11. 6
      vasl_templates/webapp/webdriver.py

@ -5,4 +5,5 @@ pyyaml==6.0
pillow==9.1.1
selenium==4.2.0
waitress==2.1.2
appdirs==1.4.4
click==8.1.3

@ -22,6 +22,10 @@ from PyQt5.QtCore import Qt, QSettings, QDir
import PyQt5.QtCore
import click
# notify everyone that we're being run as the desktop application
os.environ[ "IS_DESKTOP_APP" ] = "1"
os.environ[ "QDIR_HOME_PATH" ] = QDir.homePath()
from vasl_templates.webapp.utils import SimpleError, is_windows
# NOTE: We're supposed to do the following to support HiDPI, but it causes the main window
@ -95,7 +99,7 @@ def _do_main( template_pack, default_scenario, remote_debugging, debug ): #pylin
# NOTE: We do these imports here (instead of at the top of the file) so that we can catch errors.
from vasl_templates.webapp import app as webapp
from vasl_templates.webapp import load_debug_config
from vasl_templates.webapp import globvars, load_debug_config
from vasl_templates.webapp import main as webapp_main, snippets as webapp_snippets
# configure the default template pack
@ -121,12 +125,12 @@ def _do_main( template_pack, default_scenario, remote_debugging, debug ): #pylin
os.environ["QTWEBENGINE_REMOTE_DEBUGGING"] = remote_debugging
# load the application settings
app_settings_fname = "vasl-templates.ini" if sys.platform == "win32" else ".vasl-templates.conf"
if not os.path.isfile( app_settings_fname ) :
app_settings_fname = os.path.join( QDir.homePath(), app_settings_fname )
# FUDGE! Declaring app_settings as global here doesn't work on Windows (?!), we have to do this weird import :-/
import vasl_templates.main #pylint: disable=import-self
vasl_templates.main.app_settings = QSettings( app_settings_fname, QSettings.IniFormat )
vasl_templates.main.app_settings = QSettings(
globvars.user_profile.desktop_settings_fname,
QSettings.IniFormat
)
# install the debug config file
if debug:

@ -5,7 +5,6 @@ import os
import signal
import threading
import time
import tempfile
import configparser
import logging
import logging.config
@ -17,7 +16,6 @@ import yaml
from vasl_templates.webapp.config.constants import BASE_DIR
shutdown_event = threading.Event()
_LOCK_FNAME = os.path.join( tempfile.gettempdir(), "vasl-templates.lock" )
# ---------------------------------------------------------------------
@ -92,9 +90,7 @@ def _init_webapp():
elif dname:
webapp_vo_notes._vo_notes_image_cache_dname = dname #pylint: disable=protected-access
else:
webapp_vo_notes._vo_notes_image_cache_dname = os.path.join( #pylint: disable=protected-access
tempfile.gettempdir(), "vasl-templates", "vo-notes-image-cache"
)
webapp_vo_notes._vo_notes_image_cache_dname = globvars.user_profile.vo_notes_image_cache_dname #pylint: disable=protected-access
# load integration data from asl-rulebook2
from vasl_templates.webapp.vo_notes import load_asl_rulebook2_vo_note_targets #pylint: disable=cyclic-import
@ -159,13 +155,13 @@ def _on_sigint( signum, stack ): #pylint: disable=unused-argument
shutdown_event.set()
# call any registered cleanup handlers
from vasl_templates.webapp import globvars #pylint: disable=cyclic-import
for handler in globvars.cleanup_handlers:
handler()
lock_fname = globvars.user_profile.flask_lock_fname
if _is_flask_child_process():
# notify the parent process that we're done
os.unlink( _LOCK_FNAME )
os.unlink( lock_fname )
else:
# we are the Flask parent process (so we wait for the child process to finish) or Flask reloading
# is disabled (and the wait below will end immediately, because the lock file was never created).
@ -176,7 +172,7 @@ def _on_sigint( signum, stack ): #pylint: disable=unused-argument
# NOTE: os.path.isfile() and .exists() both return True even after the log file has gone!?!?
# Is somebody caching something somewhere? :-/
try:
with open( _LOCK_FNAME, "rb" ):
with open( lock_fname, "rb" ):
pass
except FileNotFoundError:
break
@ -187,10 +183,6 @@ def _on_sigint( signum, stack ): #pylint: disable=unused-argument
# initialize Flask
app = Flask( __name__ )
if _is_flask_child_process():
# we are the Flask child process - create a lock file
with open( _LOCK_FNAME, "wb" ):
pass
# set config defaults
# NOTE: These are defined here since they are used by both the back- and front-ends.
@ -201,19 +193,30 @@ app.config[ "ASA_GET_SCENARIO_URL" ] = "https://aslscenarioarchive.com/rest/scen
app.config[ "ASA_MAX_VASL_SETUP_SIZE" ] = 200 # nb: KB
app.config[ "ASA_MAX_SCREENSHOT_SIZE" ] = 200 # nb: KB
# initialize logging
_config_dir = os.path.join( BASE_DIR, "config" )
_fname = os.path.join( _config_dir, "logging.yaml" )
if os.path.isfile( _fname ):
with open( _fname, "r", encoding="utf-8" ) as fp:
try:
logging.config.dictConfig( yaml.safe_load( fp ) )
except Exception as _ex: #pylint: disable=broad-except
logging.error( "Can't load the logging config: %s", _ex )
else:
# stop Flask from logging every request :-/
logging.getLogger( "werkzeug" ).setLevel( logging.WARNING )
# load the application configuration
config_dir = os.path.join( BASE_DIR, "config" )
_fname = os.path.join( config_dir, "app.cfg" )
_fname = os.path.join( _config_dir, "app.cfg" )
_load_config( _fname, "System" )
# load any site configuration
_fname = os.path.join( config_dir, "site.cfg" )
_fname = os.path.join( _config_dir, "site.cfg" )
_load_config( _fname, "Site Config" )
# load any debug configuration
_fname = os.path.join( config_dir, "debug.cfg" )
if os.path.isfile( _fname ) :
load_debug_config( _fname )
_fname = os.path.join( _config_dir, "debug.cfg" )
load_debug_config( _fname )
# load any config from environment variables (e.g. set in the Docker container)
# NOTE: We could add these settings to the container's site.cfg, so that they are always defined, and things
@ -230,17 +233,16 @@ _set_config_from_env( "USER_FILES_DIR" )
# NOTE: The Docker container also sets DEFAULT_TEMPLATE_PACK, but we read it directly from
# the environment variable, since it is not something that is stored in app.config.
# initialize logging
_fname = os.path.join( config_dir, "logging.yaml" )
if os.path.isfile( _fname ):
with open( _fname, "r", encoding="utf-8" ) as fp:
try:
logging.config.dictConfig( yaml.safe_load( fp ) )
except Exception as _ex: #pylint: disable=broad-except
logging.error( "Can't load the logging config: %s", _ex )
else:
# stop Flask from logging every request :-/
logging.getLogger( "werkzeug" ).setLevel( logging.WARNING )
# initialize the user profile
from vasl_templates.webapp.user_profile import UserProfile #pylint: disable=cyclic-import
from vasl_templates.webapp import globvars #pylint: disable=cyclic-import
globvars.user_profile = UserProfile( app.config )
# check if we are the Flask child process
if _is_flask_child_process():
# yup - create a lock file
with open( globvars.user_profile.flask_lock_fname, "wb" ):
pass
# load the application
import vasl_templates.webapp.main #pylint: disable=cyclic-import

@ -63,3 +63,7 @@ loggers:
level: "WARNING"
handlers: [ "console", "file" ]
propagate: 0
user_profile:
level: "WARNING"
handlers: [ "console", "file" ]
propagate: 0

@ -11,10 +11,9 @@ import urllib.error
import gzip
import time
import datetime
import tempfile
import logging
from vasl_templates.webapp import app
from vasl_templates.webapp import app, globvars
from vasl_templates.webapp.utils import parse_int
_registry = set()
@ -27,12 +26,11 @@ _etags = {}
class DownloadedFile:
"""Manage a downloaded file."""
def __init__( self, key, ttl, fname, url, on_data, extra_args=None ):
def __init__( self, key, ttl, url, on_data, extra_args=None ):
# initialize
self.key = key
self.ttl = ttl
self.fname = fname
self.url = url
self.on_data = on_data
self.error_msg = None
@ -50,7 +48,7 @@ class DownloadedFile:
_registry.add( self )
# check if we have a cached copy of the file
self.cache_fname = os.path.join( tempfile.gettempdir(), "vasl-templates."+fname )
self.cache_fname = globvars.user_profile.downloaded_files[ self.key ]
if os.path.isfile( self.cache_fname ):
# yup - load it
_logger.info( "Using cached %s file: %s", key, self.cache_fname )

@ -3,6 +3,7 @@
from vasl_templates.webapp import app
from vasl_templates.webapp.config.constants import APP_NAME, APP_VERSION
user_profile = None
template_pack = None
vasl_mod = None
vo_listings = None

@ -48,7 +48,6 @@ def _tidyup_strings( scenario ):
scenario[key] = val.strip()
_asa_scenarios = DownloadedFile( "ASA", 6, # nb: TTL = #hours
"asl-scenario-archive.json",
"https://vasl-templates.org/services/asl-scenario-archive/scenario-index.json",
_build_asa_scenario_index,
extra_args = { "index": None }
@ -93,7 +92,6 @@ def _make_roar_matching_key( val ):
return re.sub( "[^a-z0-9]", "", val.lower() )
_roar_scenarios = DownloadedFile( "ROAR", 6, # nb: TTL = #hours
"roar-scenario-index.json",
"https://vasl-templates.org/services/roar/scenario-index.json",
_build_roar_scenario_index,
extra_args = { "index": None }

@ -1,7 +1,7 @@
If you have set up your vehicle/ordnance notes using HTML, and have configured <em>"Show Chapter H vehicle/ordnance notes as images"</em> in the Settings, you will find that scenarios can be slow to open in VASSAL, since converting each note to an image takes time.
<p> This process can be optimized by configuring a directory to cache the generated images, and this page will pre-load the cache with all the available vehicle/ordnance notes.
<p> These images will be cached, but you can speed things up even more by pre-loading the cache with all the available vehicle/ordnance notes.
{%if NO_CACHE_DIR %}
<p> Configure <tt>VO_NOTES_IMAGE_CACHE_DIR</tt> in your <tt>site.cfg</tt>, restart the server, then reload this page.
<p> Your cache is currently disabled (<tt>VO_NOTES_IMAGE_CACHE_DIR</tt> in your <tt>site.cfg</tt>), so enable it, restart the server, then reload this page.
{%endif%}

@ -0,0 +1,153 @@
""" Manage information about the current user. """
import sys
import os
import shutil
import tempfile
import logging
import appdirs
_APP_NAME = "vasl-templates"
_APP_AUTHOR = "pacman-ghost"
_logger = logging.getLogger( "user_profile" )
# ---------------------------------------------------------------------
class UserProfile:
"""Manage information about the current user."""
def __init__( self, app_config ):
# initialize
is_desktop_app = os.environ.get( "IS_DESKTOP_APP" ) is not None
is_container = app_config.get( "IS_CONTAINER" ) is not None
# configure the location of the user's config files
if is_desktop_app:
self.config_dname = "/tmp" if is_container \
else self._check_dir( appdirs.user_config_dir( _APP_NAME, _APP_AUTHOR, roaming=True ) )
self.desktop_settings_fname = os.path.join( self.config_dname,
"settings.ini" if sys.platform == "win32" else "settings.conf"
)
# configure the location of the user's local data files
self.local_data_dname = "/tmp" if is_container \
else self._check_dir( appdirs.user_data_dir( _APP_NAME, _APP_AUTHOR, roaming=False ) )
self.flask_lock_fname = os.path.join( self.local_data_dname, "flask.lock" )
# configure the location of the log files
self.logs_dname = "/tmp" if is_container \
else self._check_dir( appdirs.user_log_dir( _APP_NAME, _APP_AUTHOR ) )
self.webdriver_log_fname = app_config.get( "WEBDRIVER_LOG",
os.path.join( self.logs_dname, "webdriver.log" )
)
# configure the location of the cached data files
self.cache_dname = "/tmp" if is_container \
else self._check_dir( appdirs.user_cache_dir( _APP_NAME, _APP_AUTHOR ) )
self.downloaded_files = {
"ASA": os.path.join( self.cache_dname, "asl-scenario-archive.json" ),
"ROAR": os.path.join( self.cache_dname, "roar-scenario-index.json" ),
}
self.vo_notes_image_cache_dname = self._check_dir( os.path.join( self.cache_dname, "vo-notes-image-cache" ) )
# log our settings
_logger.info( "UserProfile:" )
if is_desktop_app:
_logger.info( "- config = %s", self.config_dname )
_logger.debug( " - settings = %s", self.desktop_settings_fname )
_logger.info( "- local data = %s", self.local_data_dname )
_logger.debug( " - Flask lock file = %s", self.flask_lock_fname )
_logger.info( "- logs = %s", self.logs_dname )
_logger.debug( " - webdriver = %s", self.webdriver_log_fname )
_logger.info( "- cache = %s", self.cache_dname )
for key, val in self.downloaded_files.items():
_logger.debug( " - Downloaded file (%s) = %s", key, val )
_logger.debug( " - V/O note image cache = %s", self.vo_notes_image_cache_dname )
# fixup any legacy files
if not is_container:
self._fixup_legacy()
def _fixup_legacy( self ):
"""Fixup any legacy files and directories.
NOTE: Config and data files were moved to the standard locations in v1.10.beta3; this function
looks for them in the legacy locations and moves them to their new location.
"""
def move_file( caption, src_fname, dest_fname ):
if not os.path.isfile( src_fname ):
_logger.debug( "Legacy %s file not found: %s", caption, src_fname )
return
_logger.info( "Moving legacy %s file:\n- from: %s\n- to: %s",
caption, src_fname, dest_fname
)
try:
shutil.move( src_fname, dest_fname )
except Exception as ex: #pylint: disable=broad-except
# NOTE: It would be nice to report this as a startup error, but this happens
# so early in the startup process, nothing has been initialized yet :-/
logging.error( "Can't move legacy %s file: %s\n- %s", caption, src_fname, ex )
# NOTE: We try to keep going.
def remove_dir( caption, dname ):
if not os.path.isdir( dname ):
_logger.debug( "Legacy %s directory not found: %s", caption, dname )
return
_logger.info( "Deleting legacy %s directory: %s", caption, dname )
try:
shutil.rmtree( dname )
except Exception as ex: #pylint: disable=broad-except
logging.error( "Can't delete legacy %s directory: %s\n- %s", caption, dname, ex )
# NOTE: We try to keep going.
# fixup the desktop settings file
qdir_home_path = os.environ.get( "QDIR_HOME_PATH" )
if qdir_home_path:
fname = os.path.join( qdir_home_path,
"vasl-templates.ini" if sys.platform == "win32" else ".vasl-templates.conf"
)
move_file( "desktop settings", fname, self.desktop_settings_fname )
# fixup the ASA/ROAR downloaded files
# NOTE: We don't *need* to do this (since the files will just be downloaded again), and they're
# in the temp directory (so we don't *need* to remove them), but it'd be nice to have scenario search
# working straight away after startup.
move_file( "ASA scenarios",
os.path.join( tempfile.gettempdir(), "vasl-templates.asl-scenario-archive.json" ),
self.downloaded_files[ "ASA" ]
)
move_file( "ROAR scenarios",
os.path.join( tempfile.gettempdir(), "vasl-templates.roar-scenario-index.json" ),
self.downloaded_files[ "ROAR" ]
)
# NOTE: The Flask lock file and webdriver log file are temp files, so we don't move them.
# NOTE: The V/O notes image cache can either be:
# - disabled
# - manually configured (via VO_NOTES_IMAGE_CACHE_DIR)
# - default ($TEMP-DIR/vasl-templates/vo-notes-image-cache/)
# In the first 2 cases, we don't need to do anything, in the last case, we delete our temp directory.
# Note that while we could try to move it, it may well not be on the same file system (which makes it
# a non-trivial operation), and there have been CSS changes in the v1.10 release cycle, which normally
# won't cause an image to be re-generated, so it's not a bad idea to force this to happen.
remove_dir( "app temp",
os.path.join( tempfile.gettempdir(), "vasl-templates" )
)
@staticmethod
def _check_dir( dname ):
"""Check that a directory exists."""
try:
if not os.path.isdir( dname ):
os.makedirs( dname )
except Exception as ex: #pylint: disable=broad-except
logging.error( "Can't create UserProfile directory: %s\n- %s", dname, ex )
raise
if dname[-1] != os.sep:
dname += os.sep
return dname

@ -377,8 +377,7 @@ def _make_vo_note_cached_image_fname( vo_type, nat, key ):
@app.route( "/load-vo-notes-image-cache" )
def load_vo_notes_image_cache():
"""Show the helper page to preload the v/o notes image cache."""
dname = app.config.get( "VO_NOTES_IMAGE_CACHE_DIR" )
if not dname:
if not _vo_notes_image_cache_dname:
return render_template( "configure-vo-notes-image-cache.html",
NO_CACHE_DIR = True
)

@ -1,9 +1,7 @@
""" Wrapper for a Selenium webdriver. """
import os
import threading
import io
import tempfile
import atexit
import logging
@ -60,9 +58,7 @@ class WebDriver:
# create the webdriver
_logger.debug( "- Launching webdriver process: %s", webdriver_path )
log_fname = app.config.get( "WEBDRIVER_LOG",
os.path.join( tempfile.gettempdir(), "webdriver.log" )
)
log_fname = globvars.user_profile.webdriver_log_fname
if "chromedriver" in webdriver_path:
options = webdriver.ChromeOptions()
options.headless = True

Loading…
Cancel
Save