边搭边理解OJ是如何写成的

什么是 OJ

OJ 是 Online Judge(在线判题系统)的缩写,通常用于算法代码测试、算法竞赛等场景中,主要包含一个相对简单纯粹的代码运行沙盒环境、与用户交互的在线界面以及运行任务的管理调度等功能。国内比较重视算法竞赛(ACM)的学校都有自己的 OJ,比如有名的杭州电子科大OJ北京大学OJ青岛大学OJ上海大学OJ等等。除此之外,国内外也有不少企业专注于给大家提供一个在线判题平台,比如有名的LeetCodeLeetCode CN牛客网DMOJ等等。其实如果只是想练习算法,这些OJ系统是非常合适的。只要在具有一定名气的OJ上一直刷题,最后找算法岗工作的时候还是很有用的。

为什么选 DMOJ

OJ系统中有一部分是开源的,比如青岛大学OJDMOJ等等。如果想要搭建一个自己玩一玩,可以选择青岛大学OJ,毕竟官方提供了一步到位的 docker-compose 方式安装。只要有 Docker 环境就可以从docker-compose.yml启动一套完整的实例。这样的搭建的确是简单易用,但是也失去了从头开始一步一步部署了解 OJ 是如何构成、运行的机会。除了这之外,还有一个非常重要的原因。大部分的开源OJ支持的编程语言是相当少的,基本上是在CC++JavaPython范围内,少数像 LeetCode 这样的 OJ 支持JavascriptGo等更多种常用编程语言。而 DMOJ 号称可以支持超过60种编程语言,这简直是囊括了绝大多数见过的编程语言了。所以,我最终选择了 DMOJ 来做这次实践学习。

安装 DMOJ

DMOJ 官方提供的文档大致上写得还是比较细致的,所有的文件配置关联与应用启动都在合适的时候进行修改,对于通过 DMOJ 来了解 OJ 的构成与运行非常有帮助。在按照文档安装的过程中,也发现了一些小问题,并通过查找其他的资料一步步解决。以下就让我们来试着完整的安装一次吧。

安装环境

正式安装前的准备工作

由于 DMOJ 是基于 Django 框架和 NodeJS 运行的,因此需要预先安装 Python 和 NodeJS。再者,在代码运行任务调度中需要有消息队列来缓冲提交的任务,所以预先安装 Redis 数据库。

sudo apt update && sudo apt upgrade -y
sudo apt install git gcc g++ make python3-dev python3-pip libxml2-dev libxslt1-dev zlib1g-dev gettext curl redis-server -y
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt install nodejs
sudo npm install -g sass postcss-cli autoprefixer

这里所采用的是 Mariadb 数据库,Ubuntu 安装极为方便。由于现在的 Mariadb 数据库安装完后不会要求用户设置 root 密码,直接在本地登录密码默认为空

sudo apt update
sudo apt install mariadb-server libmysqlclient-dev -y

sudo mysql -u root -p
mariadb> CREATE DATABASE dmoj DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;
mariadb> GRANT ALL PRIVILEGES ON dmoj.* to 'dmoj'@'localhost' IDENTIFIED BY '<password>';
mariadb> exit

由于 DMOJ 设计时数据库用了较长的索引,此处还需要有进一步对数据库进行设置(官方文档中没有提到,可能是操作系统不同的原因)。在/etc/mysql/mariadb.conf.d/50-server.conf文件的innodb注释位置添加如下三行内容,修改完成后重启Mysql数据库生效配置。除此在外,还要在之后下载的 DMOJ 主项目代码文件site/manage.py中添加如下两行。

[mysqld]
...

innodb_file_format = Barracuda
innodb_file_per_table = 1
innodb_large_prefix

#重启Mysql数据库
sudo service mysql restart
#!/usr/bin/env python
import os
import sys

if __name__ == "__main__":
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sampleproj.settings")

    # 增加下面两行
    from django.db.backends.mysql.schema import DatabaseSchemaEditor
    DatabaseSchemaEditor.sql_create_table += " ROW_FORMAT=DYNAMIC"

为了使本项目运行环境与操作系统中的其他 Python 项目环境不相互干扰,采用了venv沙盒工具。不仅在调试的工作中使用,在部署生产的时候也将采用venv沙盒工具。此处三条命令执行完就进入了沙盒模式,在终端提示的最前面会出现(dmojsite)的提示。

sudo apt install -y python3-venv
python3 -m venv dmojsite
. dmojsite/bin/activate

开始安装DMOJ

这一步骤就是从 Github 下载 DMOJ 的主项目代码到本地,并添加子项目的 git 追踪和更新代码。在这里值得注意的是,如果决定之后采用 Docker 方式配置判定服务器的话,就不需要切换分支。如果不然,则必须要切换分支。

git clone https://github.com/DMOJ/site.git
cd site
git checkout v2.1.0  # 这一步只在后面使用pypi方式配置判定服务器的时候需要执行
git submodule init
git submodule update
pip3 install -r requirements.txt
pip3 install mysqlclient

这个步骤是非常重要的,决定了主项目代码是否能正常运行。可以从官方提供的下载地址获取基本文件,也可以直接使用我提供的配置文件local_setting.py,内容如下。

#####################################
########## Django settings ##########
#####################################
# See <https://docs.djangoproject.com/en/1.11/ref/settings/>
# for more info and help. If you are stuck, you can try Googling about
# Django - many of these settings below have external documentation about them.
#
# The settings listed here are of special interest in configuring the site.

# SECURITY WARNING: keep the secret key used in production secret!
# You may use <http://www.miniwebtool.com/django-secret-key-generator/>
# to generate this key.
#  访问前两行的URL生成一个key
SECRET_KEY = 'anpsa4ko6^@b5u-)e9gm$vk(=lqb()-%0n@lr5c^=$feq45jdh'

# SECURITY WARNING: don't run with debug turned on in production!
# 生产环境下一定要修改成False
DEBUG = False # Change to False once you are done with runserver testing.

# Uncomment and set to the domain names this site is intended to serve.
# You must do this once you set DEBUG to False.
ALLOWED_HOSTS = ['10.0.4.9','oj.lep.ac.cn']  #设置成允许访问的ip或域名

# Optional apps that DMOJ can make use of.
INSTALLED_APPS += (
)

# Caching. You can use memcached or redis instead.
# Documentation: <https://docs.djangoproject.com/en/1.11/topics/cache/>
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
    }
}

# Your database credentials. Only MySQL is supported by DMOJ.
# Documentation: <https://docs.djangoproject.com/en/1.11/ref/databases/>
DATABASES = {
     'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'dmoj',
        'USER': 'dmoj',
        'PASSWORD': '<password>',  #需要修改为自己设定的
        'HOST': '127.0.0.1',
        'OPTIONS': {
            'charset': 'utf8mb4',
            'sql_mode': 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION',
        },
    }
}

# Sessions.
# Documentation: <https://docs.djangoproject.com/en/1.11/topics/http/sessions/>
#SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'

# Internationalization.
# Documentation: <https://docs.djangoproject.com/en/1.11/topics/i18n/>
LANGUAGE_CODE = 'zh-hans'  # 可以修改成符合国际规范的语言编码
DEFAULT_USER_TIME_ZONE = 'Asia/Shanghai'  # 设置时区
USE_I18N = True
USE_L10N = True
USE_TZ = True

## django-compressor settings, for speeding up page load times by minifying CSS and JavaScript files.
# Documentation: https://django-compressor.readthedocs.io/en/latest/
COMPRESS_OUTPUT_DIR = 'cache'
COMPRESS_CSS_FILTERS = [
    'compressor.filters.css_default.CssAbsoluteFilter',
    'compressor.filters.cssmin.CSSMinFilter',
]
COMPRESS_JS_FILTERS = ['compressor.filters.jsmin.JSMinFilter']
COMPRESS_STORAGE = 'compressor.storage.GzipCompressorFileStorage'
STATICFILES_FINDERS += ('compressor.finders.CompressorFinder',)


#########################################
########## Email configuration ##########
#########################################
# See <https://docs.djangoproject.com/en/1.11/topics/email/#email-backends>
# for more documentation. You should follow the information there to define
# your email settings.

# Use this if you are just testing.
#EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# The following block is included for your convenience, if you want
# to use Gmail.
#EMAIL_USE_TLS = True
#EMAIL_HOST = 'smtp.gmail.com'
#EMAIL_HOST_USER = '<your account>@gmail.com'
#EMAIL_HOST_PASSWORD = '<your password>'
#EMAIL_PORT = 587

# To use Mailgun, uncomment this block.
# You will need to run `pip install django-mailgun` for to get `MailgunBackend`.
#EMAIL_BACKEND = 'django_mailgun.MailgunBackend'
#MAILGUN_ACCESS_KEY = '<your Mailgun access key>'
#MAILGUN_SERVER_NAME = '<your Mailgun domain>'

# You can also use Sendgrid, with `pip install sendgrid-django`.
#EMAIL_BACKEND = 'sgbackend.SendGridBackend'
#SENDGRID_API_KEY = '<Your SendGrid API Key>'

# The DMOJ site is able to notify administrators of errors via email,
# if configured as shown below.

# A tuple of (name, email) pairs that specifies those who will be mailed
# when the server experiences an error when DEBUG = False.
ADMINS = (
    ('zhonger', 'zhonger@lep.ac.cn'),
)

# The sender for the aforementioned emails.
SERVER_EMAIL = 'LEPOJ: Modern Online Judge <zhonger@lep.ac.cn>'


##################################################
########### Static files configuration. ##########
##################################################
# See <https://docs.djangoproject.com/en/1.11/howto/static-files/>.

# Change this to somewhere more permanent., especially if you are using a
# webserver to serve the static files. This is the directory where all the
# static files DMOJ uses will be collected to.
# You must configure your webserver to serve this directory as /static/ in production.
# 这个目录是用于存放静态文件的地方
STATIC_ROOT = '/tmp/static/'

# URL to access static files.
#STATIC_URL = '/static/'

#STATICFILES_DIRS = (
#    os.path.join(BASE_DIR, "static"),
#)

# Uncomment to use hashed filenames with the cache framework.
#STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.CachedStaticFilesStorage'

############################################
########## DMOJ-specific settings ##########
############################################

## DMOJ site display settings.
SITE_NAME = 'LEPOJ'
SITE_LONG_NAME = 'LEPOJ: Modern Online Judge'
SITE_ADMIN_EMAIL = 'zhonger@lep.ac.cn'
TERMS_OF_SERVICE_URL = '//oj.lep.ac.cn/tos' # Use a flatpage.

## Bridge controls.
# The judge connection address and port; where the judges will connect to the site.
# You should change this to something your judges can actually connect to
# (e.g., a port that is unused and unblocked by a firewall).
BRIDGED_JUDGE_ADDRESS = [('localhost', 9999)]  # 任务调度服务配置

# The bridged daemon bind address and port to communicate with the site.
#BRIDGED_DJANGO_ADDRESS = [('localhost', 9998)]

## DMOJ features.
# Set to True to enable full-text searching for problems.
ENABLE_FTS = True

# Set of email providers to ban when a user registers, e.g., {'throwawaymail.com'}.
BAD_MAIL_PROVIDERS = set()

# The number of submissions that a staff user can rejudge at once without
# requiring the permission 'Rejudge a lot of submissions'.
# Uncomment to change the submission limit.
#DMOJ_SUBMISSIONS_REJUDGE_LIMIT = 10

## Event server.
# Uncomment to enable live updating.
#EVENT_DAEMON_USE = True

# Uncomment this section to use websocket/daemon.js included in the site.
#EVENT_DAEMON_POST = '<ws:// URL to post to>'

# If you are using the defaults from the guide, it is this:
EVENT_DAEMON_POST = 'ws://127.0.0.1:15101/'  # 提交事件

# These are the publicly accessed interface configurations.
# They should match those used by the script.
#EVENT_DAEMON_GET = '<public ws:// URL for clients>'
#EVENT_DAEMON_GET_SSL = '<public wss:// URL for clients>'
#EVENT_DAEMON_POLL = '<public URL to access the HTTP long polling of event server>'
# i.e. the path to /channels/ exposed by the daemon, through whatever proxy setup you have.

# Using our standard nginx configuration, these should be.
EVENT_DAEMON_GET = 'ws://127.0.0.1:15100/event/' # 获取事件
#EVENT_DAEMON_GET_SSL = 'wss://<your domain>/event/' # Optional
EVENT_DAEMON_POLL = '/channels/'  # 频道广播

# If you would like to use the AMQP-based event server from <https://github.com/DMOJ/event-server>,
# uncomment this section instead. This is more involved, and recommended to be done
# only after you have a working event server.
#EVENT_DAEMON_AMQP = '<amqp:// URL to connect to, including username and password>'
#EVENT_DAEMON_AMQP_EXCHANGE = '<AMQP exchange to use>'

## Celery 消息队列服务,需将注释去除
CELERY_BROKER_URL = 'redis://localhost:6379' 
CELERY_RESULT_BACKEND = 'redis://localhost:6379'

## CDN control.
# Base URL for a copy of ace editor.
# Should contain ace.js, along with mode-*.js.
ACE_URL = '//cdnjs.cloudflare.com/ajax/libs/ace/1.2.3/'
JQUERY_JS = '//cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js'
SELECT2_JS_URL = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js'
SELECT2_CSS_URL = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css'

# A map of Earth in Equirectangular projection, for timezone selection.
# Please try not to hotlink this poor site.
TIMEZONE_MAP = 'http://naturalearth.springercarto.com/ne3_data/8192/textures/3_no_ice_clouds_8k.jpg'

## Camo (https://github.com/atmos/camo) usage.
#DMOJ_CAMO_URL = "<URL to your camo install>"
#DMOJ_CAMO_KEY = "<The CAMO_KEY environmental variable you used>"

# Domains to exclude from being camo'd.
#DMOJ_CAMO_EXCLUDE = ("https://dmoj.ml", "https://dmoj.ca")

# Set to True to use https when dealing with protocol-relative URLs.
# See <http://www.paulirish.com/2010/the-protocol-relative-url/> for what they are.
#DMOJ_CAMO_HTTPS = False

# HTTPS level. Affects <link rel='canonical'> elements generated.
# Set to 0 to make http URLs canonical.
# Set to 1 to make the currently used protocol canonical.
# Set to 2 to make https URLs canonical.
#DMOJ_HTTPS = 0

## PDF rendering settings.
# Directory to cache the PDF.
#DMOJ_PDF_PROBLEM_CACHE = '/home/dmoj-uwsgi/pdfcache'

# Path to use for nginx's X-Accel-Redirect feature.
# Should be an internal location mapped to the above directory.
#DMOJ_PDF_PROBLEM_INTERNAL = '/pdfcache'

# Enable Selenium PDF generation
#USE_SELENIUM = True

## Data download settings.
# Uncomment to allow users to download their data
#DMOJ_USER_DATA_DOWNLOAD = True

# Directory to cache user data downloads.
# It is the administrator's responsibility to clean up old files.
#DMOJ_USER_DATA_CACHE = '/home/dmoj-uwsgi/datacache'

# Path to use for nginx's X-Accel-Redirect feature.
# Should be an internal location mapped to the above directory.
#DMOJ_USER_DATA_INTERNAL = '/datacache'
# How often a user can download their data.
#DMOJ_USER_DATA_DOWNLOAD_RATELIMIT = datetime.timedelta(days=1)


## ======== Logging Settings ========
# Documentation: https://docs.djangoproject.com/en/1.9/ref/settings/#logging
#                https://docs.python.org/2/library/logging.config.html#logging-config-dictschema
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'file': {
            'format': '%(levelname)s %(asctime)s %(module)s %(message)s',
        },
        'simple': {
            'format': '%(levelname)s %(message)s',
        },
    },
    'handlers': {
        # You may use this handler as example for logging to other files..
        'bridge': {
            'level': 'INFO',
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': '<desired bridge log path>',
            'maxBytes': 10 * 1024 * 1024,
            'backupCount': 10,
            'formatter': 'file',
        },
        'mail_admins': {
            'level': 'ERROR',
            'class': 'dmoj.throttle_mail.ThrottledEmailHandler',
        },
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'file',
        },
    },
    'loggers': {
        # Site 500 error mails.
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': False,
        },
        # Judging logs as received by bridged.
        'judge.bridge': {
            'handlers': ['bridge', 'mail_admins'],
            'level': 'INFO',
            'propagate': True,
        },
        # Catch all log to stderr.
        '': {
            'handlers': ['console'],
        },
        # Other loggers of interest. Configure at will.
        #  - judge.user: logs naughty user behaviours.
        #  - judge.problem.pdf: PDF generation log.
        #  - judge.html: HTML parsing errors when processing problem statements etc.
        #  - judge.mail.activate: logs for the reply to activate feature.
        #  - event_socket_server
    },
}

## ======== Integration Settings ========
## Python Social Auth
# Documentation: https://python-social-auth.readthedocs.io/en/latest/
# You can define these to enable authentication through the following services.
#SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = ''
#SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = ''
#SOCIAL_AUTH_FACEBOOK_KEY = ''
#SOCIAL_AUTH_FACEBOOK_SECRET = ''
#SOCIAL_AUTH_GITHUB_SECURE_KEY = ''
#SOCIAL_AUTH_GITHUB_SECURE_SECRET = ''
#SOCIAL_AUTH_DROPBOX_OAUTH2_KEY = ''
#SOCIAL_AUTH_DROPBOX_OAUTH2_SECRET = ''

## ======== Custom Configuration ========
# You may add whatever django configuration you would like here.
# Do try to keep it separate so you can quickly patch in new settings.

在这一步,只需要修改应用秘钥、数据库密码、语言、时区、站点基本信息,其他中文标注地方可以在后续步骤进行修改。修改完成后,执行以下命令进行验证。

python3 manage.py check  # 如使用本人提供配置文件无须此步骤

这一步骤会在/tmp/static/目录生成并优化项目需要的静态文件。

./make_style.sh
python3 manage.py collectstatic
python3 manage.py compilemessages
python3 manage.py compilejsi18n
# 迁移所有表
python3 manage.py migrate

# 导入测试数据和配置
python3 manage.py loaddata navbar
python3 manage.py loaddata language_small
python3 manage.py loaddata demo

# 创建管理员用户,需输入用户名、邮箱和密码
python3 manage.py createsuperuser
# 启动redis
sudo service redis-server start

#将项目配置文件中Celery配置去除注释使其生效

# 测试运行主项目代码
python3 manage.py runserver 0.0.0.0:8000

# 运行上一步成功后,运行调度程序,十秒内无任何回显则ctrl+c中止
python3 manage.py runbridged

# 运行Celery任务队列,无错误回显即可
pip3 install redis
celery -A dmoj_celery worker

众所周知,uwsgi 是一个用于长久运行 Python Web 项目的工具。这里的配置文件放在site目录下,官方下载地址,也可以用我提供的配置文件。

[uwsgi]
# Socket and pid file location/permission.
uwsgi-socket = /tmp/dmoj-site.sock
chmod-socket = 666
pidfile = /tmp/dmoj-site.pid

# You should create an account dedicated to running dmoj under uwsgi.
# 为了避免因用户权限和文件夹权限导致的运行失败,这里均设置为初始用户
uid = ubuntu
gid = ubuntu

# Paths. 此处的三个目录需修改为对应目录
chdir = /home/ubuntu/site/  
pythonpath = /home/ubuntu/site/  
virtualenv = /home/ubuntu/dmojsite/ 

# Details regarding DMOJ application.
protocol = uwsgi
master = true
env = DJANGO_SETTINGS_MODULE=dmoj.settings
module = dmoj.wsgi:application
optimize = 2

# Scaling settings. Tune as you like.
memory-report = true
cheaper-algo = backlog
cheaper = 3
cheaper-initial = 5
cheaper-step = 1
cheaper-rss-limit-soft = 201326592
cheaper-rss-limit-hard = 234881024
workers = 7
# 安装依赖
pip3 install uwsgi

# 测试配置文件是否有效
uwsgi --ini uwsgi.ini

此处查看执行命令的回显,未报错误信息并正常启动 worker 即可。验证是否完全有效需要等后续配置 nginx。

其实以上步骤已经对 DMOJ 中非常重要的几个部分完成了部署,包括数据库 Mysql、主项目代码运行 uwsgi、任务队列 Celery、任务调度 Bridged。但是除了数据库是时刻保持在后台的服务以外,其他三项均是前台执行,因此需要用 supervisord 来给他们后台化。

# 安装 supervisord
sudo apt install supervisor -y

安装完成后将以下三个配置文件放在/etc/supervisor/conf.d/文件目录下。为了避免因用户权限和文件夹权限导致的运行失败,这里均设置为初始用户执行程序。

# site.conf
[program:site]
command=/home/ubuntu/dmojsite/bin/uwsgi --ini uwsgi.ini
directory=/home/ubuntu/site/
stopsignal=QUIT
stdout_logfile=/tmp/site.stdout.log
stderr_logfile=/tmp/site.stderr.log
# bridged.conf
[program:bridged]
command=/home/ubuntu/dmojsite/bin/python manage.py runbridged
directory=/home/ubuntu/site/
stopsignal=INT
# You should create a dedicated user for the bridged to run under.
user=ubuntu
group=ubuntu
stdout_logfile=/tmp/bridge.stdout.log
stderr_logfile=/tmp/bridge.stderr.log
# celery.conf
[program:celery]
command=/home/ubuntu/dmojsite/bin/celery -A dmoj_celery worker
directory=/home/ubuntu/site/
# You should create a dedicated user for celery to run under.
user=ubuntu
group=ubuntu
stdout_logfile=/tmp/celery.stdout.log
stderr_logfile=/tmp/celery.stderr.log
# 更新supervisord监控列表并查询状态,但均为running是正常运行
sudo supervisorctl update
sudo supervisorctl status
# 安装nginx
sudo apt install nginx -y

/etc/nginx/sites-available/default删除并新建新的同名文件,填入以下内容。

server {
    listen       80;
    listen       [::]:80;

    # Change port to 443 and do the nginx ssl stuff if you want it.

    # Change server name to the HTTP hostname you are using.
    # You may also make this the default server by listening with default_server,
    # if you disable the default nginx server declared.
    server_name oj.lep.ac.cn;

    add_header X-UA-Compatible "IE=Edge,chrome=1";
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";

    charset utf-8;
    try_files $uri @icons;
    error_page 502 504 /502.html;

    location ~ ^/502\.html$|^/logo\.png$|^/robots\.txt$ {
        root /home/ubuntu/site/;
    }

    location @icons {
        root /home/ubuntu/site/resources/icons;
        error_page 403 = @uwsgi;
        error_page 404 = @uwsgi;
    }

    location @uwsgi {
        uwsgi_read_timeout 600;
        # Change this path if you did so in uwsgi.ini
        uwsgi_pass unix:///tmp/dmoj-site.sock;
        include uwsgi_params;
        uwsgi_param SERVER_SOFTWARE nginx/$nginx_version;
    }

    location /static {
        gzip_static on;
        expires max;
        #root /tmp/static/;
        # Comment out root, and use the following if it doesn't end in /static.
        alias /tmp/static/; # 配置主项目的静态文件地址
    }

    # Uncomment if you are using PDFs and want to serve it faster.
    # This location name should be set to DMOJ_PDF_PROBLEM_INTERNAL.
    #location /pdfcache {
    #    internal;
    #    root <path to pdf cache diretory, without the final /pdfcache>;
    #    # Default from docs:
    #    #root /home/dmoj-uwsgi/;
    #}

    # Uncomment if you are allowing user data downloads and want to serve it faster.
    # This location name should be set to DMOJ_USER_DATA_INTERNAL.
    #location /datacache {
    #    internal;
    #    root <path to data cache diretory, without the final /datacache>;
    #
    #    # Default from docs:
    #    #root /home/dmoj-uwsgi/;
    #}

    # Uncomment these sections if you are using the event server.
    location /event/ {
        proxy_pass http://127.0.0.1:15100/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400;
    }

    location /channels/ {
        proxy_read_timeout          120;
        proxy_pass http://127.0.0.1:15102;
    }
}
# 测试nginx配置文件并使之生效
sudo nginx -t
sudo nginx -s reload

新建文件site/websocket/config.js,内容如下。并根据该文件修改 nginx 的配置文件中eventchannels对应端口。同时修改local_setting.py文件中EVENT_DAEMON_POSTEVENT_DAEMON_GETEVENT_DAEMON_POLL三个变量值。

module.exports = {
    get_host: '127.0.0.1',
    get_port: 15100,
    post_host: '127.0.0.1',
    post_port: 15101,
    http_host: '127.0.0.1',
    http_port: 15102,
    long_poll_timeout: 29000,
};
# 安装ws依赖
npm install qu ws simplesets
pip3 install websocket-client

# 重启程序使修改的配置生效
sudo supervisorctl update
sudo supervisorctl restart bridged
sudo supervisorctl restart site
sudo nginx -s reload

安装判题服务器

sudo apt install python3-dev python3-pip build-essential libseccomp-dev -y
pip3 install dmoj
dmoj-autoconf  # 执行后会打印支持的runtime

# 将刚才打印的runtime复制到新建文件judge.yml的runtime部分
# 此处的id和key配置与后端管理界面中判题服务器设置一直即可
id: <judge name>
key: <judge authentication key>
problem_storage_root:
  - /home/ubunut/problem
runtime:
  ...
# judge.conf
[program:judge]
command=/home/ubuntu/dmojsite/bin/dmoj -c judge.yml localhost
directory=/home/ubuntu/site/
stopsignal=QUIT
stdout_logfile=/tmp/site.stdout.log
stderr_logfile=/tmp/site.stderr.log

使用sudo supervisord update更新配置并生效,在后端管理界面发现判题服务器在线状态为在线

验证和总结

到此,DMOJ 就安装完成了。从整个安装过程来看,一个 OJ 系统所包含的组件的确挺多,各种各样的配置来回修改,很容易让人一不留神就落下某个配置而使得程序无法正常运行。当然也有在测试过程中是可以正常运行的,而使用 supervisord 和 nginx 之后突然就不能了。我在实践过程中,对于官方提供的不同程序使用用户可能造成的文件权限不足,从而导致程序无法运行的问题感受颇深。折腾了很久也没有找出其中哪个步骤少了或者文件路径不对,后来发现主项目目录没有权限访问执行会导致一系列的问题。如果对用户管理和文件权限安全管理非常了解的话,还是按照官方的做法来设置,毕竟这样能保证操作系统的安全。