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
63 changes: 58 additions & 5 deletions denis/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@

from datetime import datetime
from argparse import ArgumentParser as ap
import sys

import db

# FIXME: if you are reading this in the year 9999...
far_future = 253401417420


def errx(msg):
print(msg, file=sys.stderr)
sys.exit(1)


def main():
parser = ap(prog='configure', description='Configure assignments')
Expand Down Expand Up @@ -40,6 +49,9 @@ def add_final(parser, required=True):
add_peer_review(create_parser)
add_final(create_parser)

dummy_parser = command_parsers.add_parser('dummy')
add_assignment(dummy_parser)

alter_parser = command_parsers.add_parser('alter')
add_assignment(alter_parser)
add_initial(alter_parser, required=False)
Expand All @@ -57,6 +69,10 @@ def add_final(parser, required=True):

command_parsers.add_parser('reload')

trigger_parser = command_parsers.add_parser('trigger')
add_assignment(trigger_parser)
trigger_parser.add_argument('-c', '--component', dest='component', required=True)

# Dictionary containing the desired command and all flags with their values
kwargs = vars(parser.parse_args())
# Subparsers store their name in the destination `'command'`
Expand All @@ -74,7 +90,17 @@ def create(assignment, initial, peer_review, final):
peer_review_due_date=peer_review,
final_due_date=final)
except db.peewee.IntegrityError:
print('cannot create assignment with duplicate name')
errx('cannot create assignment with duplicate name')


def dummy(assignment):
try:
db.Assignment.create(name=assignment,
initial_due_date=far_future,
peer_review_due_date=far_future,
final_due_date=far_future)
except db.peewee.IntegrityError:
errx('cannot create assignment with duplicate name')


def alter(assignment, initial, peer_review, final):
Expand All @@ -86,20 +112,20 @@ def alter(assignment, initial, peer_review, final):
if final is not None:
alterations[db.Assignment.final_due_date] = final
if not alterations:
return print('At least one new date must be specified')
errx('At least one new date must be specified')
query = (db.Assignment
.update(alterations)
.where(db.Assignment.name == assignment))
if query.execute() < 1:
print(f'no such assignment {assignment}')
errx(f'no such assignment {assignment}')


def remove(assignment):
query = (db.Assignment
.delete()
.where(db.Assignment.name == assignment))
if query.execute() < 1:
print(f'no such assignment {assignment}')
errx(f'no such assignment {assignment}')


def dump(fmt_iso):
Expand All @@ -118,7 +144,34 @@ def timestamp_to_formatted(timestamp):
def reload():
import os
import signal
os.kill(1, signal.SIGUSR1)
try:
os.kill(1, signal.SIGUSR1)
except OSError as e:
errx(f'kill: {e}')


def trigger(assignment, component):
import signal
import ctypes
libc = ctypes.CDLL(None)
sigqueue = libc.sigqueue
sigqueue.restype = ctypes.c_int
sigqueue.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.c_int)

if not (asn := db.Assignment.get_or_none(db.Assignment.name == assignment)):
errx(f'no such assignment {assignment}')
match component:
case 'initial':
component_id = 0
case 'peer':
component_id = 1
case 'final':
component_id = 2
case _:
errx(f'no such component {component}')

if err := sigqueue(1, signal.SIGRTMIN, asn.id * 3 + component_id):
errx(f'sigqueue error: {err}')


if __name__ == '__main__':
Expand Down
71 changes: 61 additions & 10 deletions denis/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import db

from configure import far_future


def spawn_waiter(timestamp, name, script):
return subprocess.Popen(['/usr/local/bin/run-at', timestamp, script, name])
Expand All @@ -16,14 +18,49 @@ def in_the_future(ts):
return datetime.datetime.now() < datetime.datetime.fromtimestamp(ts)


def handle_trigger(info):
if info.si_code != -1:
print('spurious sigrtmin without queuing info!', file=sys.stderr)
return False
assignment_id, component_id = divmod(info.si_status, 3)
if not (asn := db.Assignment.get_or_none(db.Assignment.id == assignment_id)):
print(f'trigger for non existant assignment id {assignment_id}!', file=sys.stderr)
return
match component_id:
case 0:
component = 'initial submission'
attr = 'initial_due_date'
program = './initial.py'
case 1:
component = 'peer review'
attr = 'peer_review_due_date'
program = './peer_review.py'
case 2:
component = 'final submission'
attr = 'final_due_date'
program = './final.py'
case _:
print(f'invalid component id {component_id}!', file=sys.stderr)
return
if not in_the_future(getattr(asn, attr)):
print(f'{component} for {asn.name} already passed!', file=sys.stderr)
return

# update relevant deadline to current time so that dashboard etc behaves as expected
setattr(asn, attr, int(datetime.datetime.now().timestamp()))
asn.save()
subprocess.Popen([program, asn.name]).wait()


def main():
# neither of these handlers should actually run,
# this handler function should never actually run,
# but because we are pid 1 in container we need
# to register handlers or they wont be delivered
# to register handlers or signals wont be delivered
def signal_handler(*_):
assert False
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGUSR1, signal_handler)
signal.signal(signal.SIGRTMIN, signal_handler)

again = True
while again:
Expand All @@ -34,26 +71,40 @@ def signal_handler(*_):
peer_review = assignment.peer_review_due_date
final = assignment.final_due_date

if in_the_future(initial):
if initial == far_future:
pass
elif in_the_future(initial):
procs.append(spawn_waiter(str(initial), name, './initial.py'))
else:
print(f'skipping initial for {name}', file=sys.stderr)

if in_the_future(peer_review):
if peer_review == far_future:
pass
elif in_the_future(peer_review):
procs.append(spawn_waiter(str(peer_review), name, './peer_review.py'))
else:
print(f'skipping peer review for {name}', file=sys.stderr)

if in_the_future(final):
if final == far_future:
pass
elif in_the_future(final):
procs.append(spawn_waiter(str(final), name, './final.py'))
else:
print(f'skipping final for {name}', file=sys.stderr)

# send SIGUSR1 to reload with new due dates, SIGTERM to exit
if signal.SIGUSR1 == signal.sigwait([signal.SIGUSR1, signal.SIGTERM]):
print('reloading', file=sys.stderr)
else:
again = False
while True:
# send SIGUSR1 to reload with new due dates, SIGTERM to exit, SIGRTMIN to trigger a deadline
info = signal.sigwaitinfo([signal.SIGUSR1, signal.SIGTERM, signal.SIGRTMIN])
match info.si_signo:
case signal.SIGUSR1:
print('reloading', file=sys.stderr)
case signal.SIGTERM:
again = False
case signal.SIGRTMIN:
handle_trigger(info)
# no need to reload
continue
break

for proc in procs:
proc.terminate()
Expand Down