Browse Source

Merge branch 'master' into socketio

master
Menci 6 years ago
parent
commit
179474a844
  1. 2
      README.en.md
  2. 2
      README.md
  3. 64
      app.js
  4. 4
      libs/judger.js
  5. 32
      modules/admin.js
  6. 27
      package.json
  7. 46
      views/admin_restart.ejs
  8. 1
      views/header.ejs
  9. 88
      views/problem.ejs

2
README.en.md

@ -48,3 +48,5 @@ ALTER TABLE `user` ADD `prefer_formatted_code` TINYINT(1) NOT NULL DEFAULT 1 AFT
To make code formatting work, `clang-format` needs to be installed. [migrates/format-old-codes.js](migrates/format-old-codes.js) may help formating old submissions' codes.
Who upgraded from a commit BEFORE [c192e8001ac81cab132ae033b39f09a094587633](https://github.com/syzoj/syzoj/commit/c192e8001ac81cab132ae033b39f09a094587633) (Mar 23, 2019) **MUST** install `redis-server` and [pygments](http://pygments.org/) on the web server. Markdown contents may be broken by switching to new renderer, [migrates/html-table-merge-cell-to-md.js](migrates/html-table-merge-cell-to-md.js) may help the migration。
Who upgraded from a commit BEFORE [7b03706821c604f59fe8263286203d57d634c421](https://github.com/syzoj/syzoj/commit/c192e8001ac81cab132ae033b39f09a094587633) (Mar 27, 2019) **MUST** add `RemainAfterExit=yes` to the systemd config file `syzoj-web.service`'s `[Service]` section to make sure that restart service can work properly.

2
README.md

@ -48,3 +48,5 @@ ALTER TABLE `user` ADD `prefer_formatted_code` TINYINT(1) NOT NULL DEFAULT 1 AFT
为使代码格式化功能正常工作,`clang-format` 需要被安装。[migrates/format-old-codes.js](migrates/format-old-codes.js) 可能对格式化旧提交记录的代码有帮助。
从该 commit [c192e8001ac81cab132ae033b39f09a094587633](https://github.com/syzoj/syzoj/commit/c192e8001ac81cab132ae033b39f09a094587633)(2019 年 3 月 23 日)前更新的用户**必须**在网站服务器上安装 `redis-server` 与 [pygments](http://pygments.org/)。旧的 Markdown 内容可能因切换到新渲染器被破坏,[migrates/html-table-merge-cell-to-md.js](migrates/html-table-merge-cell-to-md.js) 可能对迁移有所帮助。
从该 commit [7b03706821c604f59fe8263286203d57d634c421](https://github.com/syzoj/syzoj/commit/c192e8001ac81cab132ae033b39f09a094587633)(2019 年 3 月 27 日)前更新的用户**必须**在其 systemd 配置文件 `syzoj-web.service` 中的 `[Service]` 中加入一行 `RemainAfterExit=yes`,以使得重启服务功能正常工作。

64
app.js

@ -1,9 +1,11 @@
let fs = require('fs'),
path = require('path'),
util = require('util');
const serializejs = require('serialize-javascript');
const fs = require('fs'),
path = require('path'),
util = require('util'),
http = require('http'),
serializejs = require('serialize-javascript'),
UUID = require('uuid'),
commandLineArgs = require('command-line-args');
const commandLineArgs = require('command-line-args');
const optionDefinitions = [
{ name: 'config', alias: 'c', type: String, defaultValue: './config.json' },
];
@ -18,6 +20,7 @@ global.syzoj = {
models: [],
modules: [],
db: null,
serviceID: UUID(),
log(obj) {
if (obj instanceof ErrorMessage) return;
console.log(obj);
@ -25,7 +28,7 @@ global.syzoj = {
async run() {
// Check config
if (syzoj.config.session_secret === '@SESSION_SECRET@'
|| syzoj.config.email_jwt_secret === '@EMAIL_JWT_SECRET@'
|| (syzoj.config.email_jwt_secret === '@EMAIL_JWT_SECRET@' && syzoj.config.register_mail)
|| syzoj.config.db.password === '@DATABASE_PASSWORD@') {
console.log('Please generate and fill the secrets in config!');
process.exit();
@ -38,15 +41,6 @@ global.syzoj = {
let winstonLib = require('./libs/winston');
winstonLib.configureWinston(!syzoj.production);
app.server = require('http').createServer(app);
if (!module.parent) {
// Loaded by `require()`, not node CLI.
app.server.listen(parseInt(syzoj.config.port), syzoj.config.hostname, () => {
this.log(`SYZOJ is listening on ${syzoj.config.hostname}:${parseInt(syzoj.config.port)}...`);
});
}
// Set assets dir
app.use(Express.static(__dirname + '/static', { maxAge: syzoj.production ? '1y' : 0 }));
@ -78,7 +72,10 @@ global.syzoj = {
return router;
})());
app.server = http.createServer(app);
await this.connectDatabase();
this.loadModules();
// redis and redisCache is for syzoj-renderer
const redis = require('redis');
@ -89,10 +86,38 @@ global.syzoj = {
};
if (!module.parent) {
// Loaded by node CLI, not by `require()`.
if (process.send) {
// if it's started by child_process.fork(), it must be requested to restart
// wait until parent process quited.
await new Promise((resolve, reject) => {
process.on('message', message => {
if (message === 'quited') resolve();
});
process.send('quit');
});
}
await this.lib('judger').connect();
app.server.listen(parseInt(syzoj.config.port), syzoj.config.hostname, () => {
this.log(`SYZOJ is listening on ${syzoj.config.hostname}:${parseInt(syzoj.config.port)}...`);
});
}
},
restart() {
console.log('Will now fork a new process.');
const child = require('child_process').fork(__filename, ['-c', options.config]);
child.on('message', (message) => {
if (message !== 'quit') return;
this.loadModules();
console.log('Child process requested "quit".')
child.send('quited', err => {
if (err) console.error('Error sending "quited" to child process:', err);
process.exit();
});
});
},
async connectDatabase() {
let Sequelize = require('sequelize');
@ -144,7 +169,7 @@ global.syzoj = {
global.Promise = Sequelize.Promise;
this.db.countQuery = async (sql, options) => (await this.db.query(`SELECT COUNT(*) FROM (${sql}) AS \`__tmp_table\``, options))[0][0]['COUNT(*)'];
this.loadModels();
await this.loadModels();
},
loadModules() {
fs.readdir('./modules/', (err, files) => {
@ -156,7 +181,7 @@ global.syzoj = {
.forEach((file) => this.modules.push(require(`./modules/${file}`)));
});
},
loadModels() {
async loadModels() {
fs.readdir('./models/', (err, files) => {
if (err) {
this.log(err);
@ -164,9 +189,8 @@ global.syzoj = {
}
files.filter((file) => file.endsWith('.js'))
.forEach((file) => require(`./models/${file}`));
this.db.sync();
});
await this.db.sync();
},
lib(name) {
return require(`./libs/${name}`);

4
libs/judger.js

@ -6,8 +6,6 @@ const fs = Promise.promisifyAll(require('fs-extra'));
const interface = require('./judger_interfaces');
const judgeResult = require('./judgeResult');
const JudgeState = syzoj.model('judge_state');
const judgeStateCache = new Map();
const progressPusher = require('../modules/socketio');
@ -27,6 +25,8 @@ function getRunningTaskStatusString(result) {
let judgeQueue;
async function connect() {
const JudgeState = syzoj.model('judge_state');
judgeQueue = {
redisZADD: util.promisify(syzoj.redis.zadd).bind(syzoj.redis),
redisBZPOPMAX: util.promisify(syzoj.redis.bzpopmax).bind(syzoj.redis),

32
modules/admin.js

@ -442,3 +442,35 @@ app.post('/admin/raw', async (req, res) => {
})
}
});
app.post('/admin/restart', async (req, res) => {
try {
if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。');
syzoj.restart();
res.render('admin_restart', {
data: JSON.stringify(syzoj.config, null, 2)
});
} catch (e) {
syzoj.log(e);
res.render('error', {
err: e
})
}
});
app.get('/admin/serviceID', async (req, res) => {
try {
if (!res.locals.user || !res.locals.user.is_admin) throw new ErrorMessage('您没有权限进行此操作。');
res.send({
serviceID: syzoj.serviceID
});
} catch (e) {
syzoj.log(e);
res.render('error', {
err: e
})
}
});

27
package.json

@ -24,12 +24,12 @@
"homepage": "https://github.com/syzoj/syzoj#readme",
"dependencies": {
"amqplib": "^0.5.2",
"ansi-to-html": "^0.6.9",
"async-lock": "^1.1.3",
"ansi-to-html": "^0.6.10",
"async-lock": "^1.2.0",
"body-parser": "^1.15.2",
"cheerio": "^1.0.0-rc.1",
"command-line-args": "^5.0.2",
"cookie-parser": "^1.4.3",
"command-line-args": "^5.1.0",
"cookie-parser": "^1.4.4",
"cssfilter": "0.0.10",
"download": "^7.1.0",
"ejs": "^2.5.2",
@ -39,11 +39,11 @@
"fs-extra": "^7.0.1",
"gravatar": "^1.8.0",
"javascript-time-ago": "^1.0.30",
"js-yaml": "^3.9.0",
"js-yaml": "^3.13.0",
"jsdom": "^14.0.0",
"jsondiffpatch": "0.2.5",
"jsonwebtoken": "^8.4.0",
"mariadb": "^2.0.2-rc",
"jsonwebtoken": "^8.5.1",
"mariadb": "^2.0.3",
"moment": "^2.24.0",
"msgpack-lite": "^0.1.26",
"multer": "^1.2.0",
@ -54,19 +54,20 @@
"randomstring": "^1.1.5",
"redis": "^2.8.0",
"request": "^2.74.0",
"request-promise": "^4.1.1",
"request-promise": "^4.2.4",
"sendmail": "^1.1.1",
"sequelize": "^5.0.0-beta.15",
"sequelize": "^5.1.1",
"serialize-javascript": "^1.6.1",
"session-file-store": "^1.0.0",
"socket.io": "^2.2.0",
"stream-to-string": "^1.1.0",
"stream-to-string": "^1.2.0",
"syzoj-divine": "^1.0.2",
"syzoj-renderer": "^1.0.4",
"syzoj-renderer": "^1.0.5",
"tempfile": "^2.0.0",
"tmp-promise": "^1.0.3",
"uuid": "^3.3.2",
"waliyun": "^3.1.1",
"winston": "^3.1.0",
"xss": "^1.0.3"
"winston": "^3.2.1",
"xss": "^1.0.6"
}
}

46
views/admin_restart.ejs

@ -0,0 +1,46 @@
<% this.title = '重启服务' %>
<% include header %>
<div class="ui success icon message">
<i class="redo icon"></i>
<div class="content">
<div class="header" style="margin-bottom: 10px; ">
服务重启中
</div>
<p>已等待 <span id="elapsed-wait-time">0</span> 秒。</p>
</div>
</div>
<script>
var span = $('#elapsed-wait-time'),
time = 0;
setInterval(function () {
span.text((++time).toString());
}, 1000);
var checkInterval = 500,
delayBeforeRedirect = 1000,
currentServiceID = <%- serializejs(syzoj.serviceID) %>;
function checkServiceUp() {
function retry() {
setTimeout(checkServiceUp, 500);
}
$.ajax({
url: '/admin/serviceID',
success: function (data, textStatus, xhr) {
if (data.serviceID && data.serviceID !== currentServiceID) {
setTimeout(function () {
location = <%- serializejs(req.query.url || '/') %>;
}, delayBeforeRedirect);
} else retry();
},
complete: function (xhr, textStatus) {
retry();
}
});
}
checkServiceUp();
</script>
<% include footer %>

1
views/header.ejs

@ -72,6 +72,7 @@
<a class="item" href="<%= syzoj.utils.makeUrl(['user', user.id, 'edit']) %>"><i class="edit icon"></i>修改资料</a>
<% if (user.is_admin) { %>
<a class="item" href="<%= syzoj.utils.makeUrl(['admin', 'info']) %>"><i class="settings icon"></i>后台管理</a>
<a class="item" href-post="<%= syzoj.utils.makeUrl(['admin', 'restart'], { url: req.query['url'] || req.originalUrl }) %>"><i class="redo icon"></i>重启服务</a>
<% } %>
<a class="item" href-post="<%= syzoj.utils.makeUrl(['logout'], { url: req.originalUrl }) %>"><i class="power icon"></i>注销</a>
</div>

88
views/problem.ejs

@ -336,7 +336,6 @@ div[class*=ace_br] {
</div>
<div class="ui center aligned vertical segment" style="padding-bottom: 0; ">
<button type="submit" class="ui labeled icon button"><i class="ui edit icon"></i>提交</button>
<!--div onclick="show_custom_test()" class="ui positive button">自定义测试</div-->
</div>
<% } %>
</form>
@ -384,93 +383,6 @@ div[class*=ace_br] {
});
</script>
<script src="https://cdn.staticfile.org/css-element-queries/0.4.0/ResizeSensor.min.js"></script>
<div class="ui modal" id="modal-custom-test">
<div class="header">
自定义测试
</div>
<div class="content" style="padding-bottom: 0; ">
<div class="ui form">
<form id="form-custom-test" action="<%= syzoj.utils.makeUrl(['problem', problem.id, 'custom-test']) %>">
<input type="hidden" name="code">
<input type="hidden" name="language">
<div class="field">
<label>输入文件</label>
<textarea name="input_file_textarea" style="font-family: 'Roboto Mono', 'Bitstream Vera Sans Mono', 'Menlo', 'Consolas', 'Lucida Console', monospace; "></textarea>
</div>
<div class="inline fields" style="width: 100%; ">
<div class="field" style="margin: 0 auto; ">
<label for="answer">或者,上传输入文件</label>
<input type="file" name="input_file">
</div>
</div>
</form>
</div>
<div id="custom-test-result"></div>
</div>
<div class="actions" style="text-align: center; ">
<div class="ui green button" id="submit-custom-test">提交</div>
</div>
</div>
<script>
var custom_test_id;
$('#submit-custom-test').click(function () {
$('#submit-custom-test').addClass('disabled');
var form = document.getElementById('form-custom-test');
$(form).find('[name=language]').val($('#languages-menu .item.active').data('value'));
$(form).find('[name=code]').val(editor.getValue());
var code_upload = $('#answer')[0].cloneNode(true);
code_upload.style.display = 'none';
code_upload.name = 'code_upload';
form.appendChild(code_upload);
$.ajax({
url: form.action,
type: 'post',
data: new FormData(form),
cache: false,
contentType: false,
processData: false,
success: function (data) {
custom_test_id = data.id;
update_custom_test_result();
}
});
form.removeChild(code_upload);
});
function show_custom_test() {
$('#modal-custom-test').modal('show');
new ResizeSensor($('#modal-custom-test'), function () {
$(window).resize();
});
}
function update_custom_test_result() {
$.get('/custom-test/' + custom_test_id + '/ajax', function (data) {
if (data.err) {
alert(data.err);
}
if ($('#custom-test-result').html() !== data.html) {
$('#custom-test-result').html(data.html);
}
if (data.pending) {
setTimeout(function () {
update_custom_test_result();
}, 500);
} else {
$('#submit-custom-test').removeClass('disabled');
}
});
}
</script>
<% } else { %>
<script>
function submit_code() {

Loading…
Cancel
Save