diff --git a/README.en.md b/README.en.md index 886a02f..8ddde47 100644 --- a/README.en.md +++ b/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. diff --git a/README.md b/README.md index de290c4..eed47d8 100644 --- a/README.md +++ b/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`,以使得重启服务功能正常工作。 diff --git a/app.js b/app.js index 18197c9..056ad4b 100644 --- a/app.js +++ b/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}`); diff --git a/libs/judger.js b/libs/judger.js index d97b112..0ccff81 100644 --- a/libs/judger.js +++ b/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), diff --git a/modules/admin.js b/modules/admin.js index 0505b7b..23372f4 100644 --- a/modules/admin.js +++ b/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 + }) + } +}); diff --git a/package.json b/package.json index c3a4819..d79fdad 100644 --- a/package.json +++ b/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" } } diff --git a/views/admin_restart.ejs b/views/admin_restart.ejs new file mode 100644 index 0000000..e07b767 --- /dev/null +++ b/views/admin_restart.ejs @@ -0,0 +1,46 @@ +<% this.title = '重启服务' %> +<% include header %> + +
+ +
+
+ 服务重启中 +
+

已等待 0 秒。

+
+
+ + + +<% include footer %> diff --git a/views/header.ejs b/views/header.ejs index 9d786c1..f91a8de 100644 --- a/views/header.ejs +++ b/views/header.ejs @@ -72,6 +72,7 @@ 修改资料 <% if (user.is_admin) { %> 后台管理 + 重启服务 <% } %> 注销 diff --git a/views/problem.ejs b/views/problem.ejs index 0655a62..ceb3df4 100644 --- a/views/problem.ejs +++ b/views/problem.ejs @@ -336,7 +336,6 @@ div[class*=ace_br] {
-
<% } %> @@ -384,93 +383,6 @@ div[class*=ace_br] { }); - - - - <% } else { %>