Skip to content
Open
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
2 changes: 2 additions & 0 deletions cms/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ def __init__(self):
self.overview_listen_address = "127.0.0.1"
self.overview_listen_port = 8891
self.task_repository = None
self.tasks_folders = []
self.contests_folders = []
self.auto_sync = False
self.max_compilations = 1000

Expand Down
46 changes: 46 additions & 0 deletions cms/io/BackgroundScheduler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Programming contest management system
# Copyright © 2026 Erik Sünderhauf <erik.suenderhauf@gmx.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import logging

from sched import scheduler
from sys import exc_info
from traceback import format_exception
from typing import Callable

logger = logging.getLogger(__name__)


class BackgroundScheduler(scheduler):
def __init__(self):
super().__init__()

def every(self, interval: float, func: Callable, priority: int = 0, skip_first: bool = True, args=(), **kwargs) -> None:
def wrapped():
try:
func(*args, **kwargs)
except Exception:
logger.error("Failed to execute background task",
"\n".join(format_exception(*exc_info())))
finally:
self.enter(interval, priority, wrapped)
delay = 0
if skip_first:
delay = interval
self.enter(delay, priority, wrapped)
65 changes: 34 additions & 31 deletions cms/io/Repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

from multiprocessing import Manager
from subprocess import check_output
from typing import Optional

from cmscontrib.gerpythonformat.LocationStack import chdir

Expand Down Expand Up @@ -91,46 +92,48 @@ def _push(self):
logger.info("Finished pushing: " +
"{}".format(gitout))

# For GerTranslate
# For GerTranslate/cmsTaskOverview
# TODO Show errors in web overview
def commit(self, file_path, file_identifier):
def commit(self, file_path, commit_message="", author="") -> Optional[str]:
'''Commits the changes in file_path as `author` with given `commit_message`.
If an error occurs, the error message will be returned.

'''
if commit_message == "" or author == "":
raise Exception("Missing commit message or author")
# TODO Only do this if it's a git repository
# if self.auto_sync:
logger.info("Committing {} in {}".format(file_path, self.path))

with chdir(self.path):
gitout = ""

try:
gitout = check_output(["git", "add",
file_path])
except:
logger.error("Couldn't add file to git staging area: " +
"{}".format(gitout))
else:
gitout = check_output(["git", "add", file_path])
except Exception as e:
logger.error("Couldn't add file to git staging area: {}".format(e))
return str(e)
try:
gitout = ""
# NOTE file_path is relative to self.path, which isn't
# necessarily the root of the git repo. So the commit
# message might be confusing.
gitout = \
check_output(
["git", "commit",
"-o", file_path,
"-m", commit_message,
"--author", author]
)
except Exception as e:
logger.error("Couldn't commit in repository: {}".format(e))
try:
gitout = ""
# NOTE file_path is relative to self.path, which isn't
# necessarily the root of the git repo. So the commit
# message might be confusing.
gitout = \
check_output(
["git", "commit",
"-o", file_path,
# TODO Provide meaningful commit message and
# author
"-m", "Changes to " +
file_identifier +
", uploaded via GerTranslate web "
"interface",
"--author", '"GerTranslate <GerTranslate@localhost>"']
)
except:
logger.error("Couldn't commit in repository: " +
"{}".format(gitout))
else:
logger.info("Committed: " +
"{}".format(gitout))
# try to unstage files if committing failed
check_output(["git", "restore", "--staged", file_path])
except Exception as e:
logger.warning("unable to unstage files: {}".format(e))
return str(e)
logger.info("Committed: {}".format(gitout))


# For GerTranslate
# TODO Show errors in web overview
Expand Down
21 changes: 16 additions & 5 deletions cms/io/TaskAccess.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from six import StringIO
from ansi2html import Ansi2HTMLConverter

from cms.io.Repository import Repository
from cms.io.TaskTranslateInfo import TaskTranslateInfo

from cmscontrib.gerpythonformat.ContestConfig import MyGroup
Expand Down Expand Up @@ -333,7 +334,7 @@ def get(self):

class TaskTeXReceiver:
def __init__(self, repository, name):
self.repository = repository
self.repository: Repository = repository
self.name = name

def receive(self, f):
Expand Down Expand Up @@ -364,15 +365,21 @@ def receive(self, f):
else:
with open(tex_file, "wb") as target_file:
target_file.write(f)
# TODO Provide meaningful commit message and
# author
self.repository.commit(
str(tex_file.resolve()), str(_repository_code))
str(tex_file.resolve()),
commit_message=f"Changes to {str(_repository_code)}, "
"uploaded via GerTranslate web interface",
author='"GerTranslate <GerTranslate@localhost>"',
)

return result


class TaskMarker:
def __init__(self, repository, name):
self.repository = repository
self.repository: Repository = repository
self.name = name

def mark(self):
Expand All @@ -385,8 +392,12 @@ def mark(self):

with open(lock_file, "w") as target_file:
target_file.write("The translation in this language is locked.")
self.repository.commit(str(lock_file.resolve()),
str(_repository_lock_file_code))
self.repository.commit(
str(lock_file.resolve()),
commit_message=f"Changes to {str(_repository_lock_file_code)}, "
"uploaded via GerTranslate web interface",
author='"GerTranslate <GerTranslate@localhost>"',
)


class TaskGitLog:
Expand Down
41 changes: 25 additions & 16 deletions cms/io/TaskFetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@
from __future__ import unicode_literals

import logging
from pathlib import Path
import sys

from sys import exc_info
from traceback import format_exception
from multiprocessing import Process, Manager
from typing import Optional
from six import StringIO
from ansi2html import Ansi2HTMLConverter

from cms.io.Repository import Repository
from cms.io.TaskInfo import TaskInfo

from cmscontrib.gerpythonformat.GerMakeTask import GerMakeTask
Expand All @@ -39,9 +42,10 @@


class TaskCompileJob:
def __init__(self, repository, name, balancer):
def __init__(self, repository: Repository, name: str, task_folder: str, balancer):
self.repository = repository
self.name = name
self.task_folder = task_folder
self.balancer = balancer

self.current_handle = 1
Expand All @@ -61,10 +65,10 @@ def join(self):

def _compile(self):
self._reset_status()
logger.info("loading task {} in {}".format(self.name,
self.repository.path))
directory = Path(self.repository.path) / self.task_folder
logger.info("loading task {} in {}".format(self.name, str(directory)))

def do(status, repository, balancer):
def do(status, repository: Repository, directory: str, balancer):
# stdout is process local in Python, so we can simply use this
# to redirect all output from GerMakeTask to a string
sys.stdout = StringIO()
Expand All @@ -73,16 +77,16 @@ def do(status, repository, balancer):

with balancer:
try:
comp = GerMakeTask(odir = repository.path,
task = self.name,
minimal = True,
no_test = True,
submission = None,
no_latex = False,
verbose_latex = True,
language = None,
clean = False,
ntcimp = True)
comp = GerMakeTask(odir=directory,
task=self.name,
minimal=True,
no_test=True,
submission=None,
no_latex=False,
verbose_latex=True,
language=None,
clean=False,
ntcimp=True)

with repository:
comp.prepare()
Expand Down Expand Up @@ -112,6 +116,7 @@ def do(status, repository, balancer):

self.compilation_process = Process(target=do, args=(self.status,
self.repository,
str(directory),
self.balancer))
self.compilation_process.daemon = True
self.compilation_process.start()
Expand Down Expand Up @@ -174,7 +179,7 @@ def get(self):

class TaskFetch:
jobs = {}
repository = None
repository: Optional[Repository] = None
balancer = None

@staticmethod
Expand All @@ -187,10 +192,14 @@ def init(repository, max_compilations):

@staticmethod
def compile(name):
if TaskFetch.repository is None:
raise Exception("tasks repository not initialized")
if name not in TaskInfo.tasks:
raise KeyError("No such task")
if name not in TaskFetch.jobs:
TaskFetch.jobs[name] = TaskCompileJob(TaskFetch.repository, name,
TaskFetch.jobs[name] = TaskCompileJob(TaskFetch.repository,
name,
TaskInfo.tasks[name]["folder"],
TaskFetch.balancer)
return TaskFetch.jobs[name].join()

Expand Down
Loading
Loading