diff --git a/.gitignore b/.gitignore
index 946d8aa5..e53da6d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,7 +16,6 @@ build/
.DS_Store
# project specific
-data/**
metadata/**
incomplete/**
reject/**
@@ -26,3 +25,8 @@ node_modules/
/playwright-report/
/blob-report/
/playwright/.cache/
+
+# ignore all contents of data/ EXCEPT data/base_executables & contents of base_executables
+data/**
+!data/base_executables
+!data/base_executables/**
\ No newline at end of file
diff --git a/app/config.py b/app/config.py
index 308d158e..aca7b3fb 100644
--- a/app/config.py
+++ b/app/config.py
@@ -29,8 +29,8 @@
if not os.path.isdir(task_badupload_dir): os.makedirs(task_badupload_dir)
dl_dir = os.path.join(data_dir, 'download')
if not os.path.isdir(dl_dir): os.makedirs(dl_dir)
-exe_dir = os.path.join(data_dir, 'exe')
-if not os.path.isdir(exe_dir): os.makedirs(exe_dir)
+base_exe_dir = os.path.join(data_dir, 'base_executables')
+if not os.path.isdir(base_exe_dir): os.makedirs(base_exe_dir)
survey_dir = os.path.join(data_dir, 'survey')
if not os.path.isdir(survey_dir): os.makedirs(survey_dir)
survey_incomplete_dir = os.path.join(survey_dir, 'incomplete')
@@ -65,7 +65,8 @@
s_reject=survey_reject_dir,
s_complete=survey_complete_dir,
download=dl_dir,
- exe=dl_dir,
+ base_exe=os.path.join(base_exe_dir, 'SUPREME.exe'),
+ base_app=os.path.join(base_exe_dir, 'SUPREME.app'),
disallowed_agents=json.loads(cfg['FLASK']['DISALLOWED_AGENTS']),
allowed_agents=json.loads(cfg['FLASK']['ALLOWED_AGENTS']),
blocks=json.loads(cfg['SUPREME']['BLOCKS']),
diff --git a/app/taskstart.py b/app/taskstart.py
index b7a4befa..3a8639a3 100644
--- a/app/taskstart.py
+++ b/app/taskstart.py
@@ -12,7 +12,7 @@
from .io import write_metadata, initialize_taskdata
from .config import CFG
from .routing import routing
-from .utils import make_download
+from .utils import edit_exe_worker_id, edit_app_worker_id
from hashlib import blake2b
@@ -29,8 +29,8 @@ def taskstart():
supreme_subid = current_app.config['SUPREME_serializer'].dumps(h_workerId)
win_dlpath = os.path.join(CFG['download'], 'win_' + str(session['subId']) + '.exe')
mac_dlpath = os.path.join(CFG['download'], 'mac_' + str(session['subId']) + '.app')
- make_download(supreme_subid, win_dlpath, 'windows')
- make_download(supreme_subid, mac_dlpath, 'mac')
+ edit_exe_worker_id(exe_file_path=CFG['base_exe'], new_worker_id=supreme_subid, output_file_path=win_dlpath)
+ edit_app_worker_id(app_path=CFG['base_app'], new_worker_id=supreme_subid, output_app_path=mac_dlpath)
session['dlready'] = True
write_metadata(session, ['dlready'], 'a')
initialize_taskdata(session)
diff --git a/app/utils.py b/app/utils.py
index b7d44fbf..27c36723 100644
--- a/app/utils.py
+++ b/app/utils.py
@@ -1,22 +1,26 @@
import random
import string
import copy
+import logging
+import os
+import plistlib
+import shutil
+from typing import Optional
+from pathlib import Path
+from pefile import PE, DIRECTORY_ENTRY
+
def gen_code(N):
"""Generate random completion code."""
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=N))
-def make_download(sid, dlpath, platform):
- pass
-
-
def pseudorandomize(inblocks, nreps, shuffle_blocks=True, nested_output=False):
"""
pseudorandomize the input blocks so that each task occurs once before
any task is repeated and no tasks occur back to back. Will trigger an
infinite loop if the number of blocks it too small to generate enough unique
- orders to satisfy the shuffle blocks condition for the number of repititions
+ orders to satisfy the shuffle blocks condition for the number of repetitions
requested.
Parameters
@@ -24,9 +28,9 @@ def pseudorandomize(inblocks, nreps, shuffle_blocks=True, nested_output=False):
inblocks : list of strings or list of list of strings
list of tasks to randomize
nreps : int
- number of repititions of each task
+ number of repetitions of each task
shuffle blocks : bool
- Should task order be shuffeled everytime they're repeated
+ Should task order be shuffled every time they're repeated
nested_output : bool
Should the output be nested (list of lists)
@@ -54,3 +58,132 @@ def pseudorandomize(inblocks, nreps, shuffle_blocks=True, nested_output=False):
for task in task_block:
flat_blocks.append(task)
return flat_blocks
+
+
+logging.basicConfig(level=logging.INFO,
+ format='%(asctime)s - %(levelname)s - %(message)s')
+
+
+def edit_app_worker_id(app_path: str, new_worker_id: str, output_app_path: Optional[str] = None) -> None:
+ """
+ Modifies the 'WorkerID' field in the Info.plist of a macOS .app bundle.
+
+ Args:
+ app_path (str): Path to the original .app bundle whose 'WorkerID' will be modified.
+ new_worker_id (str): New 'WorkerID' value to replace the existing one.
+ output_app_path (Optional[str]): Path to save the modified .app bundle. If not provided,
+ the original bundle will be overwritten.
+
+ Raises:
+ FileNotFoundError: If the specified .app bundle or Info.plist file does not exist.
+ ValueError: If the Info.plist file cannot be loaded or parsed.
+ """
+
+ # Construct the path to the Info.plist file inside the original .app bundle
+ plist_path = Path(app_path) / 'Contents' / 'Info.plist'
+
+ # Ensure the .app bundle and Info.plist exist
+ if not plist_path.exists():
+ raise FileNotFoundError(f"The file {plist_path} does not exist.")
+
+ try:
+ # If an output path is specified, copy the original .app bundle to the new location
+ if output_app_path:
+ output_path = Path(output_app_path)
+ if output_path.exists():
+ raise FileExistsError(
+ f"The output path {output_app_path} already exists.")
+ shutil.copytree(app_path, output_app_path)
+ # Update plist path to the new location
+ plist_path = output_path / 'Contents' / 'Info.plist'
+ else:
+ logging.info(
+ "No output path specified. The original .app bundle will be modified.")
+
+ # Load the plist file
+ with plist_path.open('rb') as plist_file:
+ plist_data = plistlib.load(plist_file)
+
+ # Log current WorkerID, if present
+ current_worker_id = plist_data.get('WorkerID', None)
+ if current_worker_id:
+ logging.info(f"Current WorkerID: {current_worker_id}")
+
+ # Update the WorkerID
+ plist_data['WorkerID'] = new_worker_id
+
+ # Write the updated plist back
+ with plist_path.open('wb') as plist_file:
+ plistlib.dump(plist_data, plist_file)
+
+ logging.info(f"Successfully updated WorkerID to {new_worker_id}")
+
+ except Exception as e:
+ raise ValueError(f"Failed to modify the plist file: {e}")
+
+
+def edit_exe_worker_id(exe_file_path: str, new_worker_id: str, output_file_path: Optional[str] = None) -> None:
+ """
+ Modifies the 'WorkerID' field in the version information of an executable.
+
+ Args:
+ exe_file_path (str): Path to the executable whose 'WorkerID' will be modified.
+ new_worker_id (str): New 'WorkerID' value to replace the existing one.
+ **Must** be less than or equal to the length of the current 'WorkerID' for a successful update.
+ output_file_path (str): **Optional**. Path where the modified executable will be saved.
+ If not provided, the original executable will be overwritten.
+
+ Raises:
+ FileNotFoundError: If the specified executable does not exist.
+ ValueError: If the PE file cannot be loaded or parsed.
+ """
+
+ if not os.path.exists(exe_file_path):
+ raise FileNotFoundError(f"The file {exe_file_path} does not exist.")
+
+ output_file_path = Path(output_file_path or exe_file_path)
+
+ try:
+ pe: PE = PE(exe_file_path, fast_load=True)
+ pe.parse_data_directories(
+ directories=[DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE']])
+ except Exception as e:
+ raise ValueError(f"Failed to load or parse the PE file: {e}")
+
+ # Access the WorkerID and update it if found
+ if hasattr(pe, 'FileInfo'):
+ for file_info in pe.FileInfo:
+ for info in file_info:
+ if info.name == 'StringFileInfo':
+ version_info_dict: dict = info.StringTable[0].entries
+ worker_id_bytes: bytes = version_info_dict.get(
+ b'WorkerID', None)
+
+ if worker_id_bytes:
+ logging.info(
+ f"Found WorkerID: {worker_id_bytes.decode('utf-8')}")
+
+ # Calculate the original size in bytes and the new value size
+ original_size: int = len(worker_id_bytes)
+ new_worker_id_bytes: bytes = new_worker_id.encode(
+ 'utf-8')
+ new_size: int = len(new_worker_id_bytes)
+
+ if new_size <= original_size:
+ # Create the new value padded with null bytes up to the original size
+ padded_new_worker_id: bytes = new_worker_id_bytes + \
+ b'\x00' * (original_size - new_size)
+
+ # Update the value with the padded version
+ version_info_dict[b"WorkerID"] = padded_new_worker_id
+
+ # Write the updated attribute to the output file
+ pe.write(output_file_path)
+ logging.info(
+ f"Successfully updated WorkerID to {new_worker_id}")
+ else:
+ logging.error(
+ f"Error: New value '{new_worker_id}' is larger than the existing space of {original_size} bytes.")
+ return
+
+ logging.error(f"Error: WorkerID not found in {exe_file_path}")
\ No newline at end of file
diff --git a/data/base_executables/SUPREME.app/Contents/Info.plist b/data/base_executables/SUPREME.app/Contents/Info.plist
new file mode 100644
index 00000000..9963fdeb
--- /dev/null
+++ b/data/base_executables/SUPREME.app/Contents/Info.plist
@@ -0,0 +1,33 @@
+
+
+
+
+ CFBundleDisplayName
+ SUPREME
+ CFBundleExecutable
+ SUPREME
+ CFBundleIconFile
+ icon-windowed.icns
+ CFBundleIdentifier
+ uva.compmem.supreme
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ SUPREME
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0.0
+ CFBundleVersion
+ 1.0.0
+ LSArchitecturePriority
+
+ x86_64
+ arm64
+
+ NSHighResolutionCapable
+ True
+ WorkerID
+ "------------------------".---------------------------
+
+
diff --git a/data/base_executables/SUPREME.app/Contents/MacOS/SUPREME b/data/base_executables/SUPREME.app/Contents/MacOS/SUPREME
new file mode 100755
index 00000000..5320f7f2
Binary files /dev/null and b/data/base_executables/SUPREME.app/Contents/MacOS/SUPREME differ
diff --git a/data/base_executables/SUPREME.app/Contents/Resources/icon-windowed.icns b/data/base_executables/SUPREME.app/Contents/Resources/icon-windowed.icns
new file mode 100644
index 00000000..954a9a05
Binary files /dev/null and b/data/base_executables/SUPREME.app/Contents/Resources/icon-windowed.icns differ
diff --git a/data/base_executables/SUPREME.app/Contents/_CodeSignature/CodeResources b/data/base_executables/SUPREME.app/Contents/_CodeSignature/CodeResources
new file mode 100644
index 00000000..ae69c027
--- /dev/null
+++ b/data/base_executables/SUPREME.app/Contents/_CodeSignature/CodeResources
@@ -0,0 +1,128 @@
+
+
+
+
+ files
+
+ Resources/icon-windowed.icns
+
+ eEHOuYpZLB0vKGVIWGZOh5rH8+o=
+
+
+ files2
+
+ Resources/icon-windowed.icns
+
+ hash2
+
+ uQo7VuWRab4Phv4EEGmfQsyqFqDIXZgO8OtgaAMvCzY=
+
+
+
+ rules
+
+ ^Resources/
+
+ ^Resources/.*\.lproj/
+
+ optional
+
+ weight
+ 1000
+
+ ^Resources/.*\.lproj/locversion.plist$
+
+ omit
+
+ weight
+ 1100
+
+ ^Resources/Base\.lproj/
+
+ weight
+ 1010
+
+ ^version.plist$
+
+
+ rules2
+
+ .*\.dSYM($|/)
+
+ weight
+ 11
+
+ ^(.*/)?\.DS_Store$
+
+ omit
+
+ weight
+ 2000
+
+ ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/
+
+ nested
+
+ weight
+ 10
+
+ ^.*
+
+ ^Info\.plist$
+
+ omit
+
+ weight
+ 20
+
+ ^PkgInfo$
+
+ omit
+
+ weight
+ 20
+
+ ^Resources/
+
+ weight
+ 20
+
+ ^Resources/.*\.lproj/
+
+ optional
+
+ weight
+ 1000
+
+ ^Resources/.*\.lproj/locversion.plist$
+
+ omit
+
+ weight
+ 1100
+
+ ^Resources/Base\.lproj/
+
+ weight
+ 1010
+
+ ^[^/]+$
+
+ nested
+
+ weight
+ 10
+
+ ^embedded\.provisionprofile$
+
+ weight
+ 20
+
+ ^version\.plist$
+
+ weight
+ 20
+
+
+
+
diff --git a/data/base_executables/SUPREME.exe b/data/base_executables/SUPREME.exe
new file mode 100644
index 00000000..b4f4eb28
Binary files /dev/null and b/data/base_executables/SUPREME.exe differ
diff --git a/requirements.txt b/requirements.txt
index 796dd39f..498460bd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
numpy
+pefile
flask>=2.0
gunicorn
pytest