Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ conf/
log_merging_config.json
dockerComposeMPI.yml
*.csv
statistics_*.json
reversim-conf

# Debugpy logs, WinMerge Backups etc.
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ instance/statistics/**

# Don't track generated statistics
*.csv
statistics_*.json
log_merging_config.json
investigateLogs.py

Expand Down
12 changes: 10 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,13 @@
"type": "debugpy",
"request": "launch",
"module": "app.statistics3.statistics3",
"args": [
],
"env": {
"REVERSIM_INSTANCE": "${input:instancePath}"
},
"args": [
"--beginning",
"${input:date}"
],
"justMyCode": true,
"console": "integratedTerminal",
},
Expand Down Expand Up @@ -145,5 +147,11 @@
"type": "promptString",
"default": "instance"
},
{
"id": "date",
"description": "A date in ISO 8601 format at which the logs shall start",
"type": "promptString",
"default": ""
},
]
}
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@

# Labels as per:
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
MAINTAINER Max Planck Institute for Security and Privacy

Check warning on line 18 in Dockerfile

View workflow job for this annotation

GitHub Actions / build-and-push-image

The MAINTAINER instruction is deprecated, use a label instead to define an image author

MaintainerDeprecated: Maintainer instruction is deprecated in favor of using label More info: https://docs.docker.com/go/dockerfile/rule/maintainer-deprecated/
LABEL org.opencontainers.image.authors="Max Planck Institute for Security and Privacy"
# NOTE Also change the version in config.py
LABEL org.opencontainers.image.version="2.1.3"
LABEL org.opencontainers.image.version="2.1.4"
LABEL org.opencontainers.image.licenses="AGPL-3.0-only"
LABEL org.opencontainers.image.description="Ready to deploy Docker container to use ReverSim for research. ReverSim is an open-source environment for the browser, originally developed at the Max Planck Institute for Security and Privacy (MPI-SP) to study human aspects in hardware reverse engineering."
LABEL org.opencontainers.image.source="https://github.com/emsec/ReverSim"
Expand Down
2 changes: 1 addition & 1 deletion app/gameConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

# CONFIG Current Log File Version.
# NOTE Also change this in the Dockerfile
LOGFILE_VERSION = "2.1.3" # Major.Milestone.Subversion
LOGFILE_VERSION = "2.1.4" # Major.Milestone.Subversion

PSEUDONYM_LENGTH = 32
LEVEL_ENCODING = 'UTF-8' # was Windows-1252
Expand Down
4 changes: 3 additions & 1 deletion app/model/Participant.py
Original file line number Diff line number Diff line change
Expand Up @@ -730,11 +730,13 @@ def selectDrawingTool(self, timeStamp: int, tool: Any):

self.logger.writeToLog(EventType.Click, e, timeStamp)

LEVEL_INTRO_DRAWING = (LevelType.LEVEL, 'elementIntroduction/simple_circuit.txt')

event = SelectDrawToolEvent(
clientTime=timeStamp, serverTime=now(),
pseudonym=self.pseudonym,
phase=self.getPhaseName(),
level=self.getLevelContext(),
level=self.getLevelContext() if self.getPhase().hasLevels() else LEVEL_INTRO_DRAWING,
object=tool
)
event.commit()
Expand Down
12 changes: 10 additions & 2 deletions app/screenshotGenerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,16 @@ def screenshotLevel(page: Page, levelName: str):
# print(f'Skipping: "{outputPath}"')
# return

quotedLevelName = urllib.parse.quote_plus(currentLevel)
page.goto(f'{base_url}/game?group=viewer&lang=en&ui={pseudonym}&level={quotedLevelName}')
query_string = urllib.parse.urlencode({
'group': 'viewer',
'lang': 'en',
'showSwitchID': '',
'showClues': '',
'ui': pseudonym,
'level': currentLevel,
})

page.goto(f'{base_url}/game?' + query_string)
page.wait_for_timeout(1000)

downloadCanvasImage(page, outputName=outputPath)
Expand Down
59 changes: 39 additions & 20 deletions app/statistics3/LogEventValidator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import timedelta
import logging

from sqlalchemy.orm import Session
Expand Down Expand Up @@ -58,7 +59,8 @@ def handle_event(self, event: LogEvent, session: Session, statsParticipant: Stat
assert isinstance(event, ChronoEvent)
self.event_chrono(statsParticipant, player, event)
case StartSessionEvent.__tablename__:
pass
assert isinstance(event, StartSessionEvent)
self.event_start_session(statsParticipant, event)
case SkillAssessmentEvent.__tablename__:
pass
case QualiEvent.__tablename__:
Expand Down Expand Up @@ -204,6 +206,33 @@ def event_chrono(self,
raise LogValidationError(f'Unknown timer type "{event.timerType}"')


def event_start_session(self,
statsParticipant: StatsParticipant,
event: StartSessionEvent
):
assert event.timeClient is not None
assert event.phase is not None

# No need to set the page reload flag, if this is the first launch
if not statsParticipant.game_started:
statsParticipant.game_started = True
statsParticipant.start_time = event.timeClient
return

# Otherwise at this point this must be a page reload
statsParticipant.activePhase.reloaded = True
levelName = ''

if isinstance(statsParticipant.activePhase, StatsPhaseLevels):
statsParticipant.activePhase.activeLevel.reloaded = True
levelName = '@' + statsParticipant.activePhase.activeLevel.log_name

ui = getShortPseudo(statsParticipant.pseudonym)
reload_location = statsParticipant.activePhase.phaseType + levelName
logging.warning(f'Participant {ui} reloaded the page at "{reload_location}"')
statsParticipant.reloads.append(reload_location)


def event_quali(self,
statsParticipant: StatsParticipant,
event: QualiEvent
Expand Down Expand Up @@ -321,24 +350,14 @@ def start_phase(self, event: ChronoEvent, statsParticipant: StatsParticipant):
assert event.timeClient is not None
assert event.phase is not None

# Set the reload flag on page reload for phase and if applicable, also level
# Preload events follow directly after event_start_session
if event.phase.activePhase == PhaseType.Preload:
# No need to set the page reload flag, if this is the first launch
if not statsParticipant.game_started:
statsParticipant.game_started = True
return

statsParticipant.activePhase.reloaded = True
levelName = ''

if isinstance(statsParticipant.activePhase, StatsPhaseLevels):
statsParticipant.activePhase.activeLevel.reloaded = True
levelName = '@' + statsParticipant.activePhase.activeLevel.log_name

ui = getShortPseudo(statsParticipant.pseudonym)
reload_location = statsParticipant.activePhase.phaseType + levelName
logging.warning(f'Participant {ui} reloaded the page at "{reload_location}"')
statsParticipant.reloads.append(reload_location)
if not statsParticipant.game_started or statsParticipant.start_time is None:
raise LogValidationError('Preload Scene must follow directly after an event_start_phase')

# The Preload event contains the time limit
if event.limit is not None:
statsParticipant.time_limit = timedelta(seconds=event.limit)

# If this is not a preload phase, start the phase as usual
else:
Expand Down Expand Up @@ -413,7 +432,7 @@ def click_continue(self,
assert event.object == ClickableObjects.CONTINUE

if statsParticipant.activePhase.phaseType == PhaseType.AltTask:
logging.info('End of Phase AltTask')
logging.debug('End of Phase AltTask') # TODO
return

# If it is a level continue
Expand All @@ -428,7 +447,7 @@ def click_continue(self,

# Else this must be the end of a Phase
else:
logging.info(f'End of Phase {statsParticipant.activePhase.phaseType}')
logging.debug(f'End of Phase {statsParticipant.activePhase.phaseType}') # TODO


def click_skip(self,
Expand Down
12 changes: 5 additions & 7 deletions app/statistics3/StatsCircuit.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
from dataclasses import dataclass
from typing import override
from app.statistics3.StatsSlide import StatsSlide
from app.statistics3.statisticsUtils import TIME_TOLERANCE, TIMESTAMP_MS, CurrentLevelState, LogValidationError
from app.utilsGame import LevelType


@dataclass
class StatsCircuit(StatsSlide):

def __init__(self, type_slide: LevelType, log_name: str, time_load: TIMESTAMP_MS) -> None:
super().__init__(type_slide, log_name, time_load)

self.switchClicks: int = 0
self.minSwitchClicks: int|None = None
self.confirmClicks: int = 0
switchClicks: int = 0
minSwitchClicks: int|None = None
confirmClicks: int = 0


def click_switch(self):
Expand Down
34 changes: 17 additions & 17 deletions app/statistics3/StatsParticipant.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
from dataclasses import dataclass, field
from datetime import timedelta
from app.gameConfig import PHASES_WITH_LEVELS
from app.statistics3.StatsPhase import StatsPhase
from app.statistics3.StatsPhaseLevels import StatsPhaseLevels
from app.statistics3.statisticsUtils import TIMESTAMP_MS, LogValidationError
from app.utilsGame import PhaseType


@dataclass
class StatsParticipant:

@property
def activePhase(self) -> StatsPhase:
assert self.phaseIdx >= 0 and self.phaseIdx < len(self.phases)
return self.phases[self.phaseIdx]

pseudonym: str
is_debug: bool

def __init__(self, pseudonym: str, is_debug: bool) -> None:
self.pseudonym = pseudonym
self.is_debug = is_debug
groups: list[str] = field(default_factory=list[str])

self.is_debug: bool
self.groups: list[str] = []
phases: list[StatsPhase] = field(default_factory=list[StatsPhase])
phaseIdx: int = -1

self.phases: list[StatsPhase] = []
self.phaseIdx: int = -1
quali_fails: int = 0

self.quali_fails: int = 0
game_started: bool = False
reloads: list[str] = field(default_factory=list[str])

self.game_started = False
self.reloads: list[str] = []
start_time: TIMESTAMP_MS|None = None
finish_time: TIMESTAMP_MS|None = None
time_limit: timedelta|None = None

self.time_limit: timedelta|None = None
@property
def activePhase(self) -> StatsPhase:
assert self.phaseIdx >= 0 and self.phaseIdx < len(self.phases)
return self.phases[self.phaseIdx]


def load_phase(self, type_phase: str, time_loaded: TIMESTAMP_MS):
Expand Down
21 changes: 10 additions & 11 deletions app/statistics3/StatsPhase.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

from dataclasses import dataclass
import logging
from datetime import timedelta

Expand All @@ -10,21 +11,19 @@
)
from app.utilsGame import PhaseType


@dataclass
class StatsPhase:
phaseType: PhaseType
time_load: TIMESTAMP_MS

def __init__(self, type_phase: PhaseType, time_load: TIMESTAMP_MS) -> None:
self.phaseType = type_phase

self.time_load: TIMESTAMP_MS = time_load
self.time_start: TIMESTAMP_MS|None = None
self.time_start_levels: TIMESTAMP_MS|None = None
self.time_finish: TIMESTAMP_MS|None = None
time_start: TIMESTAMP_MS|None = None
time_start_levels: TIMESTAMP_MS|None = None
time_finish: TIMESTAMP_MS|None = None

self.time_limit: timedelta|None = None
time_limit: timedelta|None = None

self.status: CurrentState = CurrentState.LOADED
self.reloaded = False
status: CurrentState = CurrentState.LOADED
reloaded = False


def start(self, time_start: TIMESTAMP_MS, time_limit: float|None):
Expand Down
9 changes: 7 additions & 2 deletions app/statistics3/StatsPhaseLevels.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dataclasses import dataclass, field
from typing import override

from app.gameConfig import LEVEL_FILETYPES_WITH_TASK, PHASES_WITH_LEVELS
Expand All @@ -8,8 +9,12 @@
from app.utilsGame import LevelType, PhaseType


@dataclass
class StatsPhaseLevels(StatsPhase):

levels: list[StatsSlide] = field(default_factory=list[StatsSlide])
levelIdx: int = -1

@property
def activeLevel(self) -> StatsSlide:
assert self.levelIdx >= 0 and self.levelIdx < len(self.levels)
Expand All @@ -20,8 +25,8 @@ def __init__(self, type_phase: PhaseType, time_load: TIMESTAMP_MS) -> None:
super().__init__(type_phase, time_load)
assert type_phase in PHASES_WITH_LEVELS

self.levels: list[StatsSlide] = []
self.levelIdx: int = -1
self.levels = []
self.levelIdx = -1


def load_level(self, type_level: str, log_name: str, time_load: TIMESTAMP_MS):
Expand Down
23 changes: 10 additions & 13 deletions app/statistics3/StatsSlide.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dataclasses import dataclass
import logging
from datetime import timedelta

Expand All @@ -10,24 +11,20 @@
from app.utilsGame import LevelType


@dataclass
class StatsSlide:

def __init__(self,
type_slide: LevelType,
log_name: str,
time_load: TIMESTAMP_MS
) -> None:
self.slide_type = type_slide
self.log_name = log_name
slide_type: LevelType
log_name: str

self.time_load: TIMESTAMP_MS = time_load
self.time_start: TIMESTAMP_MS|None = None
self.time_finish: TIMESTAMP_MS|None = None
time_load: TIMESTAMP_MS
time_start: TIMESTAMP_MS|None = None
time_finish: TIMESTAMP_MS|None = None

self.time_limit: timedelta|None = None
time_limit: timedelta|None = None

self.status: CurrentLevelState = CurrentLevelState.LOADED
self.reloaded = False
status: CurrentLevelState = CurrentLevelState.LOADED
reloaded = False


def start(self, time_start: TIMESTAMP_MS, time_limit: float|None):
Expand Down
Loading