diff --git a/denis/configure.py b/denis/configure.py index c18060d9..7e5962c0 100755 --- a/denis/configure.py +++ b/denis/configure.py @@ -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') @@ -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) @@ -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'` @@ -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): @@ -86,12 +112,12 @@ 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): @@ -99,7 +125,7 @@ def remove(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): @@ -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__': diff --git a/denis/start.py b/denis/start.py index f9fff509..00bf85c4 100755 --- a/denis/start.py +++ b/denis/start.py @@ -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]) @@ -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: @@ -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()