diff --git a/run-container.sh b/run-container.sh index 251542b..6aaaea8 100755 --- a/run-container.sh +++ b/run-container.sh @@ -143,9 +143,10 @@ if [ -n "$VASSAL" ]; then echo "Can't find the VASSAL directory: $VASSAL" exit 1 fi - target=/data/vassal/ - VASSAL_VOLUME="--volume `readlink -f "$VASSAL"`:$target" - VASSAL_ENV="--env VASSAL_DIR=$target" + mpoint=/data/vassal/ + target=$( readlink -f "$VASSAL" ) + VASSAL_VOLUME="--volume $target:$mpoint" + VASSAL_ENV="--env VASSAL_DIR=$mpoint --env VASSAL_DIR_TARGET=$target" fi # check if a VASL module file has been specified @@ -154,9 +155,10 @@ if [ -n "$VASL_MOD" ]; then echo "Can't find the VASL .vmod file: $VASL_MOD" exit 1 fi - target=/data/vasl.vmod - VASL_MOD_VOLUME="--volume `readlink -f "$VASL_MOD"`:$target" - VASL_MOD_ENV="--env VASL_MOD=$target" + mpoint=/data/vasl.vmod + target=$( readlink -f "$VASL_MOD" ) + VASL_MOD_VOLUME="--volume $target:$mpoint" + VASL_MOD_ENV="--env VASL_MOD=$mpoint --env VASL_MOD_TARGET" fi # check if a VASL extensions directory has been specified @@ -165,9 +167,10 @@ if [ -n "$VASL_EXTNS" ]; then echo "Can't find the VASL extensions directory: $VASL_EXTNS" exit 1 fi - target=/data/vasl-extensions/ - VASL_EXTNS_VOLUME="--volume `readlink -f "$VASL_EXTNS"`:$target" - VASL_EXTNS_ENV="--env VASL_EXTNS_DIR=$target" + mpoint=/data/vasl-extensions/ + target=$( readlink -f "$VASL_EXTNS" ) + VASL_EXTNS_VOLUME="--volume $target:$mpoint" + VASL_EXTNS_ENV="--env VASL_EXTNS_DIR=$mpoint --env VASL_EXTNS_DIR_TARGET=$target" fi # check if a VASL boards directory has been specified @@ -176,9 +179,10 @@ if [ -n "$VASL_BOARDS" ]; then echo "Can't find the VASL boards directory: $VASL_BOARDS" exit 1 fi - target=/data/boards/ - VASL_BOARDS_VOLUME="--volume `readlink -f "$VASL_BOARDS"`:$target" - VASL_BOARDS_ENV="--env BOARDS_DIR=$target" + mpoint=/data/boards/ + target=$( readlink -f "$VASL_BOARDS" ) + VASL_BOARDS_VOLUME="--volume $target:$mpoint" + VASL_BOARDS_ENV="--env BOARDS_DIR=$mpoint --env BOARDS_DIR_TARGET=$target" fi # check if a Chapter H notes directory has been specified @@ -187,9 +191,10 @@ if [ -n "$CHAPTER_H_NOTES" ]; then echo "Can't find the Chapter H notes directory: $CHAPTER_H_NOTES" exit 1 fi - target=/data/chapter-h-notes/ - CHAPTER_H_NOTES_VOLUME="--volume `readlink -f "$CHAPTER_H_NOTES"`:$target" - CHAPTER_H_NOTES_ENV="--env CHAPTER_H_NOTES_DIR=$target" + mpoint=/data/chapter-h-notes/ + target=$( readlink -f "$CHAPTER_H_NOTES" ) + CHAPTER_H_NOTES_VOLUME="--volume $target:$mpoint" + CHAPTER_H_NOTES_ENV="--env CHAPTER_H_NOTES_DIR=$mpoint --env CHAPTER_H_NOTES_DIR_TARGET=$target" fi # check if a user files directory has been specified @@ -198,9 +203,10 @@ if [ -n "$USER_FILES" ]; then echo "Can't find the user files directory: $USER_FILES" exit 1 fi - target=/data/user-files/ - USER_FILES_VOLUME="--volume `readlink -f "$USER_FILES"`:$target" - USER_FILES_ENV="--env USER_FILES_DIR=$target" + mpoint=/data/user-files/ + target=$( readlink -f "$USER_FILES" ) + USER_FILES_VOLUME="--volume $target:$mpoint" + USER_FILES_ENV="--env USER_FILES_DIR=$mpoint --env USER_FILES_DIR_TARGET=$target" fi # check if a template pack has been specified @@ -210,9 +216,10 @@ if [ -n "$TEMPLATE_PACK" ]; then echo "Can't find the template pack: $TEMPLATE_PACK" exit 1 fi - target=/data/template-pack - TEMPLATE_PACK_VOLUME="--volume `readlink -f "$TEMPLATE_PACK"`:$target" - TEMPLATE_PACK_ENV="--env DEFAULT_TEMPLATE_PACK=$target" + mpoint=/data/template-pack + target=$( readlink -f "$TEMPLATE_PACK" ) + TEMPLATE_PACK_VOLUME="--volume $target:$mpoint" + TEMPLATE_PACK_ENV="--env DEFAULT_TEMPLATE_PACK=$mpoint --env DEFAULT_TEMPLATE_PACK_TARGET" fi # check if testing has been enabled @@ -239,6 +246,9 @@ echo Launching the \"$IMAGE_TAG\" image as \"$CONTAINER_NAME\"... docker run \ --name $CONTAINER_NAME \ --publish $PORT:5010 \ + --env DOCKER_IMAGE_NAME="vasl-templates:$IMAGE_TAG" \ + --env DOCKER_IMAGE_TIMESTAMP="$(date --utc +"%Y-%m-%d %H:%M:%S %:z")" \ + --env DOCKER_CONTAINER_NAME="$CONTAINER_NAME" \ $CONTROL_TESTS_PORT_RUN \ $VASSAL_VOLUME $VASL_MOD_VOLUME $VASL_EXTNS_VOLUME $VASL_BOARDS_VOLUME $CHAPTER_H_NOTES_VOLUME $TEMPLATE_PACK_VOLUME $USER_FILES_VOLUME \ $VASSAL_ENV $VASL_MOD_ENV $VASL_EXTNS_ENV $VASL_BOARDS_ENV $CHAPTER_H_NOTES_ENV $TEMPLATE_PACK_ENV $USER_FILES_ENV \ diff --git a/vasl_templates/webapp/main.py b/vasl_templates/webapp/main.py index 15deefe..03da3a1 100644 --- a/vasl_templates/webapp/main.py +++ b/vasl_templates/webapp/main.py @@ -5,11 +5,14 @@ import threading import concurrent import json import uuid +from datetime import datetime, timedelta +import re import logging from flask import request, render_template, jsonify, send_file, redirect, url_for, abort from vasl_templates.webapp import app, shutdown_event +from vasl_templates.webapp.vassal import VassalShim from vasl_templates.webapp.utils import MsgStore, parse_int import vasl_templates.webapp.config.constants from vasl_templates.webapp.config.constants import BASE_DIR, DATA_DIR @@ -39,7 +42,6 @@ def get_startup_msgs(): if _check_versions: _check_versions = False # check the VASSAL version - from vasl_templates.webapp.vassal import VassalShim try: VassalShim.check_vassal_version( startup_msg_store ) except Exception as ex: #pylint: disable=broad-except @@ -95,7 +97,7 @@ def get_app_config(): } if isinstance( vals["THEATERS"], str ): vals["THEATERS"] = vals["THEATERS"].split() - for key in ["APP_NAME","APP_VERSION","APP_DESCRIPTION","APP_HOME_URL"]: + for key in [ "APP_NAME", "APP_VERSION", "APP_DESCRIPTION", "APP_HOME_URL" ]: vals[ key ] = getattr( vasl_templates.webapp.config.constants, key ) # include the ASL Scenario Archive config @@ -120,7 +122,6 @@ def get_app_config(): vals[ "ALTERNATE_WEBAPP_BASE_URL" ] = alt_webapp_base_url # include information about VASSAL and VASL - from vasl_templates.webapp.vassal import VassalShim try: vals[ "VASSAL_VERSION" ] = VassalShim.get_version() except Exception as ex: #pylint: disable=broad-except @@ -157,6 +158,83 @@ def get_app_config(): # --------------------------------------------------------------------- +@app.route( "/program-info" ) +def get_program_info(): + """Get the program info.""" + + # NOTE: We can't convert to local time, since the time zone inside a Docker container + # may not be the same as on the host (or where the client is). It's possible to make it so, + # but messy, so to keep things simple, we get the client to pass in the timezone offset. + tz_offset = parse_int( request.args.get( "tz_offset", 0 ) ) + def to_localtime( tstamp ): + """Convert a timestamp to local time.""" + return tstamp + timedelta( minutes=tz_offset ) + + # set the basic details + params = { + "APP_VERSION": vasl_templates.webapp.config.constants.APP_VERSION, + "VASSAL_VERSION": VassalShim.get_version() + } + if globvars.vasl_mod: + params[ "VASL_VERSION" ] = globvars.vasl_mod.vasl_version + for key in [ "VASSAL_DIR", "VASL_MOD", "VASL_EXTNS_DIR", "BOARDS_DIR", + "JAVA_PATH", "WEBDRIVER_PATH", "CHAPTER_H_NOTES_DIR", "USER_FILES_DIR" ]: + params[ key ] = app.config.get( key ) + + def parse_timestamp( val ): + """Parse a timestamp.""" + if not val: + return None + # FUDGE! Adjust the timezone offset from "HH:MM" to "HHMM". + val = re.sub( r"(\d{2}):(\d{2})$", r"\1\2", val ) + try: + val = datetime.strptime( val, "%Y-%m-%d %H:%M:%S %z" ) + except ValueError: + return None + return to_localtime( val ) + + def replace_mountpoint( key ): + """Replace a mount point with its corresponding target (on the host).""" + params[ key ] = os.environ.get( "{}_TARGET".format( key ) ) + + # check if we are running inside a Docker container + if app.config.get( "IS_CONTAINER" ): + # yup - return related information + params[ "DOCKER_IMAGE_NAME" ] = os.environ.get( "DOCKER_IMAGE_NAME" ) + params[ "DOCKER_IMAGE_TIMESTAMP" ] = datetime.strftime( + parse_timestamp( os.environ.get( "DOCKER_IMAGE_TIMESTAMP" ) ), + "%H:%M %d %b %Y" + ) + params[ "DOCKER_CONTAINER_NAME" ] = os.environ.get( "DOCKER_CONTAINER_NAME" ) + with open( "/proc/self/cgroup", "r" ) as fp: + buf = fp.read() + mo = re.search( r"^\d+:name=.+/docker/([0-9a-f]{12})", buf, re.MULTILINE ) + params[ "DOCKER_CONTAINER_ID" ] = mo.group(1) if mo else "???" + # replace Docker mount points with their targets on the host + for key in [ "VASSAL_DIR", "VASL_MOD", "VASL_EXTNS_DIR", "BOARDS_DIR", + "CHAPTER_H_NOTES_DIR", "USER_FILES_DIR" ]: + replace_mountpoint( key ) + + # check the scenario index downloads + def check_df( df ): #pylint: disable=missing-docstring + with df: + if not os.path.isfile( df.cache_fname ): + return + mtime = datetime.utcfromtimestamp( os.path.getmtime( df.cache_fname ) ) + key = "LAST_{}_SCENARIO_INDEX_DOWNLOAD_TIME".format( df.key ) + params[ key ] = datetime.strftime(to_localtime(mtime), "%H:%M (%d %b %Y)" ) + generated_at = parse_timestamp( getattr( df, "generated_at", None ) ) + if generated_at: + key = "LAST_{}_SCENARIO_INDEX_GENERATED_AT".format( df.key ) + params[ key ] = datetime.strftime( generated_at, "%H:%M %d %b %Y" ) + from vasl_templates.webapp.scenarios import _asa_scenarios, _roar_scenarios + check_df( _asa_scenarios ) + check_df( _roar_scenarios ) + + return render_template( "program-info-content.html", **params ) + +# --------------------------------------------------------------------- + @app.route( "/help" ) def show_help(): """Show the help page.""" diff --git a/vasl_templates/webapp/scenarios.py b/vasl_templates/webapp/scenarios.py index 2a97ad1..233f4ef 100644 --- a/vasl_templates/webapp/scenarios.py +++ b/vasl_templates/webapp/scenarios.py @@ -34,6 +34,7 @@ def _build_asa_scenario_index( df, new_data, logger ): } # install the results df.index = index + df.generated_at = new_data.get( "_generatedAt_" ) if logger: logger.debug( "Loaded the ASL Secenario Archive index: #scenarios=%d", len(df.index) ) logger.debug( "- Generated at: %s", new_data.get( "_generatedAt_", "n/a" ) ) @@ -60,6 +61,7 @@ def _build_roar_scenario_index( df, new_data, logger ): _update_roar_matching_index( id_matching, scenario.get("scenario_id"), roar_id ) # install the results df.index, df.title_matching, df.id_matching = index, title_matching, id_matching + df.generated_at = new_data.get( "_generatedAt_" ) if logger: logger.debug( "Loaded the ROAR scenario index: #scenarios=%d", len(df.index) ) logger.debug( "- Generated at: %s", new_data.get( "_generatedAt_", "n/a" ) ) diff --git a/vasl_templates/webapp/static/css/program-info.css b/vasl_templates/webapp/static/css/program-info.css new file mode 100644 index 0000000..e845556 --- /dev/null +++ b/vasl_templates/webapp/static/css/program-info.css @@ -0,0 +1,12 @@ +.ui-dialog.program-info .ui-dialog-titlebar { background: #80d0ff ; } + +#program-info table { margin-bottom: 0.5em ; } +#program-info td { text-align: bottom ; } +#program-info td.key { width: 8.5em ; font-weight: bold ; white-space: nowrap ; } +#program-info td.val { border: none ; } +#program-info td ul { margin-top: 0 ; } + +#program-info .na { font-style: italic ; color: #444 ; } +#program-info .path { font-family: monospace ; font-size: 90% ; } +#program-info .extra { padding-left: 0.75em ; } +#program-info .info { font-size: 80% ; font-style: italic ; color: #666 ; } diff --git a/vasl_templates/webapp/static/images/menu/info.png b/vasl_templates/webapp/static/images/menu/info.png new file mode 100644 index 0000000..6e88ea6 Binary files /dev/null and b/vasl_templates/webapp/static/images/menu/info.png differ diff --git a/vasl_templates/webapp/static/main.js b/vasl_templates/webapp/static/main.js index 49dde1f..93721ad 100644 --- a/vasl_templates/webapp/static/main.js +++ b/vasl_templates/webapp/static/main.js @@ -52,7 +52,7 @@ $(document).ready( function () { // initialize the menu var $menu = $("#menu input") ; var imagesDir = gImagesBaseUrl + "/menu" ; - $menu.popmenu( { + var menuItems = { new_scenario: { label: "New scenario", icon: imagesDir+"/new.png", action: on_new_scenario }, load_scenario: { label: "Load scenario", icon: imagesDir+"/open.png", action: on_load_scenario }, save_scenario: { label: "Save scenario", icon: imagesDir+"/save.png", action: on_save_scenario }, @@ -66,8 +66,12 @@ $(document).ready( function () { user_settings( null, null ) ; } }, separator3: { type: "separator" }, + program_info: { label: "Program info", icon: imagesDir+"/info.png", action: show_program_info }, show_help: { label: "Help", icon: imagesDir+"/help.png", action: show_help }, - } ) ; + } ; + if ( getUrlParam( "pyqt" ) ) + delete menuItems.program_info ; + $menu.popmenu( menuItems ) ; // nb: we only show the popmenu on left click (not the normal right-click) $menu.off( "contextmenu" ) ; $menu.click( function() { @@ -910,6 +914,40 @@ function handle_escape( evt ) // -------------------------------------------------------------------- +function show_program_info() +{ + // show the PROGRAM INFO dialog + $( "#program-info" ).dialog( { + title: gAppConfig.APP_NAME + " (" + gAppConfig.APP_VERSION + ")", + dialogClass: "program-info", + modal: true, + width: $(window).width() * 0.8, + minWidth: 750, + height: $(window).height() * 0.8, + minHeight: 400, + open: function() { + var $dlg = $(this) ; + $dlg.find( ".content" ).hide() ; + $dlg.find( ".loader" ).show() ; + var url = gGetProgramInfoUrl + "?tz_offset=" + -new Date().getTimezoneOffset() ; + $.get( url, function( resp ) { + $dlg.find( ".loader" ).hide() ; + $dlg.find( ".content" ).html( resp ).fadeIn( 250 ) ; + } ).fail( function( xhr, status, errorMsg ) { + showErrorMsg( "Can't get the program info:
" + escapeHTML(errorMsg) + "
" ) ; + $dlg.dialog( "close" ) ; + } ) ; + }, + buttons: { + OK: function() { + $(this).dialog( "close" ) ; + } + }, + } ) ; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + function show_help() { // check if we need to load the HELP tab diff --git a/vasl_templates/webapp/templates/index.html b/vasl_templates/webapp/templates/index.html index 2bd25fc..306e115 100644 --- a/vasl_templates/webapp/templates/index.html +++ b/vasl_templates/webapp/templates/index.html @@ -34,6 +34,7 @@ + @@ -90,6 +91,7 @@ {%include "lfa-upload.html"%} {%include "user-settings-dialog.html"%} +{%include "program-info-dialog.html"%} {%include "ask-dialog.html"%} {%include "please-wait.html"%} @@ -140,6 +142,7 @@ gUpdateVsavUrl = "{{url_for('update_vsav')}}" ; gPrepareAsaUploadUrl = "{{url_for('prepare_asa_upload')}}" ; gOnSuccessfulAsaUploadUrl = "{{url_for('on_successful_asa_upload',scenario_id='ID')}}" ; gMakeSnippetImageUrl = "{{url_for('make_snippet_image')}}" ; +gGetProgramInfoUrl = "{{url_for('get_program_info')}}" ; gHelpUrl = "{{url_for('show_help')}}" ; diff --git a/vasl_templates/webapp/templates/program-info-content.html b/vasl_templates/webapp/templates/program-info-content.html new file mode 100644 index 0000000..5584965 --- /dev/null +++ b/vasl_templates/webapp/templates/program-info-content.html @@ -0,0 +1,49 @@ +{%if DOCKER_CONTAINER_ID%} + +
Docker container: + {{DOCKER_CONTAINER_NAME}} ({{DOCKER_CONTAINER_ID}}) +
Docker image: + {{DOCKER_IMAGE_NAME}} (built {{DOCKER_IMAGE_TIMESTAMP}}) +
+{%endif%} + + +
VASSAL: + {%if VASSAL_VERSION%} {{VASSAL_VERSION}} {%else%} n/a {%endif%} + {%if VASSAL_DIR%} {{VASSAL_DIR}} {%endif%} +
VASL: + {%if VASL_VERSION%} {{VASL_VERSION}} {%else%} n/a {%endif%} + {%if VASL_MOD%} {{VASL_MOD}} {%endif%} +
VASL extensions: + {%if VASL_EXTNS_DIR%} {{VASL_EXTNS_DIR}} {%else%} - {%endif%} +
VASL boards: + {%if BOARDS_DIR%} {{BOARDS_DIR}} {%else%} - {%endif%} +
+ + +
Java: + {%if JAVA_PATH%} {{JAVA_PATH}} {%else%} - {%endif%} +
Web driver: + {%if WEBDRIVER_PATH%} {{WEBDRIVER_PATH}} {%else%} - {%endif%} +
+ + +
Chapter H: + {%if CHAPTER_H_NOTES_DIR%} {{CHAPTER_H_NOTES_DIR}} {%else%} - {%endif%} +
User files: + {%if USER_FILES_DIR%} {{USER_FILES_DIR}} {%else%} - {%endif%} +
+ + +
Scenario downloads: +
  • ASA:
+
{%if LAST_ASA_SCENARIO_INDEX_DOWNLOAD_TIME%} + {{LAST_ASA_SCENARIO_INDEX_DOWNLOAD_TIME}} + {%if LAST_ASA_SCENARIO_INDEX_GENERATED_AT%} (generated {{LAST_ASA_SCENARIO_INDEX_GENERATED_AT}}) {%endif%} + {%else%} - {%endif%} +
  • ROAR:
+
{%if LAST_ROAR_SCENARIO_INDEX_DOWNLOAD_TIME%} + {{LAST_ROAR_SCENARIO_INDEX_DOWNLOAD_TIME}} + {%if LAST_ROAR_SCENARIO_INDEX_GENERATED_AT%} (generated {{LAST_ROAR_SCENARIO_INDEX_GENERATED_AT}}) {%endif%} + {%else%} - {%endif%} +
diff --git a/vasl_templates/webapp/templates/program-info-dialog.html b/vasl_templates/webapp/templates/program-info-dialog.html new file mode 100644 index 0000000..a8b43f5 --- /dev/null +++ b/vasl_templates/webapp/templates/program-info-dialog.html @@ -0,0 +1,9 @@ +