1.0.0-preview

This commit is contained in:
masnn committed 2020-05-13 12:25:18 +08:00
1 parent 90b06d6aa7
commit 8cd84f29e8
18 files changed
+152 -401

No files matched your search

+6 -2
View File
@@ -22,8 +22,12 @@ jobs:
yarn build:production
- uses: actions/upload-artifact@v1
with:
name: hydro
path: .build
name: development.js
path: .build/development.js
- uses: actions/upload-artifact@v1
with:
name: module
path: .build/module
- uses: elgohr/Publish-Docker-Github-Action@master
with:
name: hydrooj/hydro:alpine
+1 -1
View File
@@ -269,7 +269,7 @@ class ProblemSolutionHandler extends ProblemDetailHandler {
['problem_solution', null],
];
this.response.body = {
path, psdocs, page, pcount, pscount, udict, pssdict,
path, psdocs, page, pcount, pscount, udict, pssdict, pdoc: this.pdoc,
};
}
+13 -17
View File
@@ -1,22 +1,6 @@
const nodemailer = require('nodemailer');
const options = require('../options');
const { SendMailError } = require('../error');
let transporter = null;
try {
transporter = nodemailer.createTransport({
host: options.VJ_SMTP_HOST,
port: options.VJ_DB_PORT,
secure: options.VJ_SMTP_SECURE,
auth: {
user: options.VJ_SMTP_USER,
pass: options.VJ_SMTP_PASSWORD,
},
});
} catch (e) {
console.error(e);
}
module.exports = {
/**
* @param {string} to
@@ -27,9 +11,21 @@ module.exports = {
async sendMail(to, subject, text, html) {
let t;
try {
const system = require('../model/system');
const [host, port, secure, user, pass, from] = await Promise.all([
system.get('smtp.host'),
system.get('smtp.port'),
system.get('smtp.secure'),
system.get('smtp.user'),
system.get('smtp.pass'),
system.get('smtp.from'),
]);
const transporter = nodemailer.createTransport({
host, port, secure, auth: { user, pass },
});
t = await new Promise((resolve, reject) => {
transporter.sendMail({
from: options.VJ_SMTP_FROM,
from,
to,
subject,
text,
+17 -37
View File
@@ -1,3 +1,4 @@
/* eslint-disable import/no-dynamic-require */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-eval */
const fs = require('fs');
@@ -19,6 +20,7 @@ function ensureDir(dir) {
}
const active = [];
const fail = [];
try {
// Let webpack pack builtin module together.
// eslint-disable-next-line import/no-unresolved
@@ -59,7 +61,7 @@ async function preload() {
for (const i of active) {
try {
if (i.os) {
if (!i.os.includes(os.platform())) throw new Error('Unsupported OS');
if (!i.os.includes(os.platform().toLowerCase())) throw new Error('Unsupported OS');
}
if (i.file) {
i.files = {};
@@ -87,6 +89,7 @@ async function handler() {
for (const i of active) {
if (i.handler && !i.fail) {
try {
console.log(`Handler init: ${i.id}`);
console.time(`Handler init: ${i.id}`);
const exports = {};
// eslint-disable-next-line no-unused-vars
@@ -136,6 +139,7 @@ async function model() {
for (const i of active) {
if (i.model && !i.fail) {
try {
console.log(`Model init: ${i.id}`);
console.time(`Model init: ${i.id}`);
const exports = {};
const module = { exports, file: i.files || {} };
@@ -155,6 +159,7 @@ async function lib() {
for (const i of active) {
if (i.lib && !i.fail) {
try {
console.log(`Lib init: ${i.id}`);
console.time(`Lib init: ${i.id}`);
const exports = {};
// eslint-disable-next-line no-unused-vars
@@ -184,44 +189,15 @@ async function service() {
i.fail = true;
fail.push(i.id);
console.error(`Service Load Fail: ${i.id}`);
console.error(e);
}
}
}
}
async function install() {
await Promise.all([lib(), locale(), template()]);
require('./service/setup');
}
async function installDb() {
const system = require('./model/system');
const def = {
PROBLEM_PER_PAGE: 100,
RECORD_PER_PAGE: 100,
SOLUTION_PER_PAGE: 20,
CONTEST_PER_PAGE: 20,
TRAINING_PER_PAGE: 10,
DISCUSSION_PER_PAGE: 50,
REPLY_PER_PAGE: 50,
CONTESTS_ON_MAIN: 5,
TRAININGS_ON_MAIN: 5,
DISCUSSIONS_ON_MAIN: 20,
'db.ver': 1,
'listen.https': false,
'listen.port': 8888,
'session.keys': ['Hydro'],
'session.secure': false,
'session.saved_expire_seconds': 3600 * 24,
'session.unsaved_expire_seconds': 600,
changemail_token_expire_seconds: 3600 * 24,
registration_token_expire_seconds: 600,
};
const tasks = [];
for (const key in def) {
tasks.push(system.set(key, def[key]));
}
await Promise.all(tasks);
const setup = require('./service/setup');
await setup.setup();
}
async function load() {
@@ -243,11 +219,12 @@ async function load() {
require('./utils');
require('./error');
require('./permission');
await Promise.all([locale(), template()]);
try {
require('./options');
} catch (e) {
await install();
return;
require('./options');
}
const bus = require('./service/bus');
await new Promise((resolve) => {
@@ -265,7 +242,7 @@ async function load() {
'template', 'validator', 'nav',
];
for (const i of builtinLib) require(`./lib/${i}`);
await Promise.all([lib(), locale(), template()]);
await lib();
require('./service/gridfs');
const server = require('./service/server');
await server.prepare();
@@ -281,7 +258,10 @@ async function load() {
}
const system = require('./model/system');
const dbVer = await system.get('db.ver');
if (dbVer !== 1) await installDb();
if (dbVer !== 1) {
const ins = require('./script/install');
await ins.run();
}
const builtinHandler = [
'home', 'problem', 'record', 'judge', 'user',
'contest', 'training', 'discussion', 'manage', 'import',
@@ -300,4 +280,4 @@ async function load() {
await server.start();
}
module.exports = { load, active };
module.exports = { load, active, fail };
+8 -1
View File
@@ -160,7 +160,13 @@ async function setTestdata(_id, readStream) {
return await edit(_id, { data: f.id }); // eslint-disable-line no-return-await
}
module.exports = {
async function getData(pid) {
const pdoc = await get(pid);
if (!pdoc.data) return null;
return gridfs.openDownloadStream(pdoc.data);
}
global.Hydro.model.problem = module.exports = {
add,
inc,
get,
@@ -175,4 +181,5 @@ module.exports = {
getMultiStatus,
setTestdata,
updateStatus,
getData,
};
+2 -2
View File
@@ -21,7 +21,7 @@ function del(_id) {
async function getFirst(query) {
const res = await coll.find(query).sort('_id', 1).limit(1).toArray();
if (res) {
if (res.length) {
await coll.deleteOne({ _id: res[0]._id });
return res[0];
}
@@ -35,6 +35,6 @@ async function consume(query, cb) {
}, 100);
}
module.exports = {
global.Hydro.model.task = module.exports = {
add, get, del, count, getFirst, consume,
};
+7 -46
View File
@@ -1,59 +1,20 @@
const fs = require('fs');
const os = require('os');
const path = require('path');
const { defaultsDeep } = require('lodash');
const { defaults } = require('lodash');
let options = {
db: {
name: 'hydro',
host: '127.0.0.1',
port: 27017,
username: '',
password: '',
},
template: {
path: path.join(process.cwd(), 'templates'),
},
listen: {
host: '127.0.0.1',
port: 8888,
domain: '',
https: false,
},
smtp: {
from: '',
host: '',
port: 465,
user: '',
pass: '',
secure: false,
},
session: {
keys: ['Hydro'],
secure: false,
domain: '127.0.0.1',
saved_expire_seconds: 3600 * 24,
unsaved_expire_seconds: 600,
registration_token_expire_seconds: 600,
},
constants: {
PROBLEM_PER_PAGE: 100,
RECORD_PER_PAGE: 100,
SOLUTION_PER_PAGE: 20,
CONTEST_PER_PAGE: 20,
TRAINING_PER_PAGE: 10,
DISCUSSION_PER_PAGE: 50,
REPLY_PER_PAGE: 50,
CONTESTS_ON_MAIN: 5,
TRAININGS_ON_MAIN: 5,
DISCUSSIONS_ON_MAIN: 20,
},
name: 'hydro',
host: '127.0.0.1',
port: 27017,
username: '',
password: '',
};
let f = path.resolve(process.cwd(), 'config.json');
if (!fs.existsSync(f)) f = path.resolve(os.homedir(), '.config', 'hydro', 'config.json');
if (!fs.existsSync(f)) f = path.resolve('/config/config.json');
const t = JSON.parse(fs.readFileSync(f));
options = defaultsDeep(t, options);
options = defaults(t, options);
module.exports = options;
+68 -32
View File
@@ -1,46 +1,82 @@
const { defaults } = require('lodash');
const db = require('../service/db');
const builtin = require('../model/builtin');
const system = require('../model/system');
const pwhash = require('../lib/pwhash');
const { udoc } = require('../interface');
const collUser = db.collection('user');
const collRole = db.collection('role');
const collSystem = db.collection('system');
async function run() {
const salt = pwhash.salt();
await collUser.insertMany([
defaults({
_id: 0,
uname: 'Hydro',
unameLower: 'hydro',
mail: 'hydro@hydro',
mailLower: 'hydro@hydro',
role: 'guest',
}, udoc),
defaults({
_id: 1,
mail: 'guest@hydro',
mailLower: 'guest@hydro',
uname: 'Guest',
unameLower: 'guest',
role: 'guest',
}, udoc),
defaults({
_id: -1,
mail: 'root@hydro',
mailLower: 'root@hydro',
uname: 'Root',
unameLower: 'root',
hash: pwhash.hash('rootroot', salt),
salt,
gravatar: 'root@hydro',
role: 'root',
}, udoc),
]);
const def = {
PROBLEM_PER_PAGE: 100,
RECORD_PER_PAGE: 100,
SOLUTION_PER_PAGE: 20,
CONTEST_PER_PAGE: 20,
TRAINING_PER_PAGE: 10,
DISCUSSION_PER_PAGE: 50,
REPLY_PER_PAGE: 50,
CONTESTS_ON_MAIN: 5,
TRAININGS_ON_MAIN: 5,
DISCUSSIONS_ON_MAIN: 20,
'smtp.host': '',
'smtp.port': 465,
'smtp.from': '',
'smtp.user': '',
'smtp.pass': '',
'smtp.secure': false,
'db.ver': 1,
'listen.https': false,
'listen.port': 8888,
'session.keys': ['Hydro'],
'session.secure': false,
'session.saved_expire_seconds': 3600 * 24,
'session.unsaved_expire_seconds': 600,
changemail_token_expire_seconds: 3600 * 24,
registration_token_expire_seconds: 600,
};
const tasks = [];
for (const key in def) {
tasks.push(system.set(key, def[key]));
}
await Promise.all(tasks);
try {
const salt = pwhash.salt();
await collUser.insertMany([
defaults({
_id: 0,
uname: 'Hydro',
unameLower: 'hydro',
mail: 'hydro@hydro',
mailLower: 'hydro@hydro',
role: 'guest',
}, udoc),
defaults({
_id: 1,
mail: 'guest@hydro',
mailLower: 'guest@hydro',
uname: 'Guest',
unameLower: 'guest',
role: 'guest',
}, udoc),
defaults({
_id: -1,
mail: 'root@hydro',
mailLower: 'root@hydro',
uname: 'Root',
unameLower: 'root',
hash: pwhash.hash('rootroot', salt),
salt,
gravatar: 'root@hydro',
role: 'root',
}, udoc),
]);
} catch (e) {
// Ignore user already exist error
}
await collRole.deleteMany({ _id: { $in: builtin.BUILTIN_ROLES.map((role) => role._id) } });
await collRole.insertMany(builtin.BUILTIN_ROLES);
console.log('Installed');
}
module.exports = { run };
+3 -3
View File
@@ -3,15 +3,15 @@ const bus = require('./bus');
const options = require('../options');
let mongourl = 'mongodb://';
if (options.db.username) mongourl += `${options.db.username}:${options.db.password}@`;
mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`;
if (options.username) mongourl += `${options.username}:${options.password}@`;
mongourl += `${options.host}:${options.port}/${options.name}`;
/**
* @type {import('mongodb').Db}
*/
let db = null;
Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true })
.then((Database) => {
db = Database.db(options.db.name);
db = Database.db(options.name);
db.Db = Database;
bus.publish('system_database_connected', null);
});
+22 -19
View File
@@ -249,27 +249,30 @@ function Route(route, RouteHandler) {
const args = { ...ctx.params, ...ctx.query, ...ctx.request.body };
if (h.___prepare) await h.___prepare(args);
for (const l of check) {
if (args[l]) {
args[l] = new ObjectID(args[l]);
if (!args[l]) throw new ValidationError(l);
try {
for (const l of check) {
if (args[l]) {
args[l] = new ObjectID(args[l]);
if (!args[l]) throw new ValidationError(l);
}
}
if (args.content) validator.checkContent(args.content);
if (args.title) validator.checkContent(args.title);
if (args.uid) args.uid = parseInt(validator.checkUid(args.uid));
if (args.password) validator.checkPassword(args.password);
if (args.mail) validator.checkEmail(args.mail);
if (args.uname) validator.checkUname(args.uname);
if (args.page) args.page = parseInt(args.page);
if (args.duration) args.duration = parseFloat(args.duration);
if (args.pids) args.pids = args.pids.split(',').map((i) => i.trim());
if (args.role) validator.checkRole(args.role);
if (args.roles) {
for (const i of args.roles) validator.checkRole(i);
}
} catch (e) {
if (e instanceof ValidationError) throw e;
throw new ValidationError('Argument check failed');
}
if (args.content) validator.checkContent(args.content);
if (args.title) validator.checkContent(args.title);
if (args.uid) args.uid = parseInt(validator.checkUid(args.uid));
if (args.password) validator.checkPassword(args.password);
if (args.mail) validator.checkEmail(args.mail);
if (args.uname) validator.checkUname(args.uname);
if (args.page) args.page = parseInt(args.page);
if (args.duration) args.duration = parseFloat(args.duration);
if (args.pids) args.pids = args.pids.split(',').map((i) => i.trim());
if (args.role) validator.checkRole(args.role);
if (args.roles) {
for (const i of args.roles) validator.checkRole(i);
}
if (h.__prepare) await h.__prepare(args);
if (h._prepare) await h._prepare(args);
if (h.prepare) await h.prepare(args);
-20
View File
@@ -1,20 +0,0 @@
const Mongo = require('mongodb');
const options = require('./options');
async function run() {
let mongourl = 'mongodb://';
if (options.db.username) mongourl += `${options.db.username}:${options.db.password}@`;
mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`;
const Database = await Mongo.MongoClient.connect(mongourl, {
useNewUrlParser: true, useUnifiedTopology: true,
});
const db = Database.db(options.db.name);
await db.dropDatabase();
console.log('Dropped');
process.exit(0);
}
run().catch((e) => {
console.error(e);
process.exit(1);
});
-14
View File
@@ -1,14 +0,0 @@
const fs = require('fs');
const path = require('path');
const child = require('child_process');
const yaml = require('js-yaml');
exports.prebuild = async function prebuild() {
if (!fs.existsSync(path.resolve(__dirname, 'HydroJudger'))) {
const res = child.spawnSync('git clone https://github.com/hydro-dev/HydroJudger.git');
if (res.error) throw res.error;
}
let lang = fs.readFileSync(path.resolve(__dirname, 'langs.yaml')).toString();
lang = yaml.safeLoad(lang);
fs.writeFileSync(path.resolve(__dirname, '__lang.json'), JSON.stringify(lang));
};
-5
View File
@@ -1,5 +0,0 @@
{
"name": "judger",
"version": "1.0.0",
"description": "HydroJudger"
}
-167
View File
@@ -1,167 +0,0 @@
const fs = require('fs');
const path = require('path');
const child = require('child_process');
async function postInit() {
const config = require('./HydroJudger/judger/config');
// eslint-disable-next-line import/no-unresolved
config.LANGS = require('./__langs.json');
const { mkdirp, rmdir, compilerText } = require('./HydroJudger/judger/utils');
const log = require('./HydroJudger/judger/log');
const tmpfs = require('./HydroJudger/judger/tmpfs');
const { FormatError, CompileError, SystemError } = require('./HydroJudger/judger/error');
const { STATUS_COMPILE_ERROR, STATUS_SYSTEM_ERROR } = require('./HydroJudger/judger/status');
const readCases = require('./HydroJudger/judger/cases');
const judger = require('./HydroJudger/judger/judger');
const fsp = fs.promises;
const { problem, task } = global.Hydro.model;
const { judge } = global.Hydro.handler;
async function problemData(pid, savePath) {
const tmpFilePath = path.resolve(config.CACHE_DIR, `download_${pid}`);
const res = await problem.getData(pid);
const w = await fs.createWriteStream(tmpFilePath);
res.data.pipe(w);
await new Promise((resolve, reject) => {
w.on('finish', resolve);
w.on('error', reject);
});
mkdirp(path.dirname(savePath));
await new Promise((resolve, reject) => {
child.exec(`unzip ${tmpFilePath} -d ${savePath}`, (e) => {
if (e) reject(e);
else resolve();
});
});
await fsp.unlink(tmpFilePath);
await this.processData(savePath).catch();
return savePath;
}
async function cacheOpen(pid, version) {
const filePath = path.join(config.CACHE_DIR, pid);
if (fs.existsSync(filePath)) {
let ver;
try {
ver = fs.readFileSync(path.join(filePath, 'version')).toString();
} catch (e) { /* ignore */ }
if (version === ver) return filePath;
rmdir(filePath);
}
mkdirp(filePath);
await problemData(pid, filePath);
fs.writeFileSync(path.join(filePath, 'version'), version);
return filePath;
}
function getNext(that) {
that.nextId = 1;
that.nextWaiting = [];
return (data, id) => {
data.key = 'next';
data.rid = that.rid;
if (id) {
if (id === that.nextId) {
judge.next(data);
that.nextId++;
let t = true;
while (t) {
t = false;
for (const i in that.nextWaiting) {
if (that.nextId === that.nextWaiting[i].id) {
judge.next(that.nextWaiting[i].data);
that.nextId++;
that.nextWaiting.splice(i, 1);
t = true;
}
}
}
} else that.nextWaiting.push({ data, id });
} else judge.next(data);
};
}
function getEnd(rid) {
return (data) => {
data.key = 'end';
data.rid = rid;
log.log({
status: data.status,
score: data.score,
time_ms: data.time_ms,
memory_kb: data.memory_kb,
});
judge.end(data);
};
}
class JudgeTask {
constructor(request) {
this.stat = {};
this.stat.receive = new Date();
this.request = request;
}
async handle() {
try {
this.stat.handle = new Date();
this.event = this.request.event;
this.pid = this.request.pid;
this.rid = this.request.rid;
this.lang = this.request.lang;
this.code = this.request.code;
this.data = this.request.data;
this.next = getNext(this);
this.end = getEnd(this.rid);
this.tmpdir = path.resolve(config.TEMP_DIR, 'tmp', this.rid);
this.clean = [];
mkdirp(this.tmpdir);
tmpfs.mount(this.tmpdir, '64m');
log.submission(`${this.rid}`, { pid: this.pid });
if (!this.event) await this.submission();
else throw new SystemError(`Unsupported type: ${this.type}`);
} catch (e) {
if (e instanceof CompileError) {
this.next({ compiler_text: compilerText(e.stdout, e.stderr) });
this.end({
status: STATUS_COMPILE_ERROR, score: 0, time_ms: 0, memory_kb: 0,
});
} else if (e instanceof FormatError) {
this.next({ judge_text: `${e.message}\n${JSON.stringify(e.params)}` });
this.end({
status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0,
});
} else {
log.error(e);
this.next({ judge_text: `${e.message}\n${e.stack}\n${JSON.stringify(e.params)}` });
this.end({
status: STATUS_SYSTEM_ERROR, score: 0, time_ms: 0, memory_kb: 0,
});
}
}
// eslint-disable-next-line no-await-in-loop
for (const clean of this.clean) await clean().catch();
tmpfs.umount(this.tmpdir);
await rmdir(this.tmpdir);
}
async submission() {
this.stat.cache_start = new Date();
this.folder = await cacheOpen(this.pid, this.data);
this.stat.read_cases = new Date();
this.config = await readCases(
this.folder,
{ detail: true },
{ next: this.next },
);
this.stat.judge = new Date();
await judger[this.config.type || 'default'].judge(this);
}
}
task.consume({ type: 'judge' }, (t) => {
(new JudgeTask(t)).handle();
});
}
module.exports = { postInit };
+3 -1
View File
@@ -1,6 +1,8 @@
{
"name": "sandbox-linux",
"version": "1.0.0",
"os": "Linux",
"os": [
"linux"
],
"description": "Sandbox service for linux"
}
+1 -1
View File
@@ -2,7 +2,7 @@ const os = require('os');
const path = require('path');
const child = require('child_process');
const p = child.spawn(path.resolve(os.tmpdir(), 'hydro', 'sandbox-linux-amd64', 'executorserver', ['-silent']));
const p = child.spawn(path.resolve(os.tmpdir(), 'hydro', 'sandbox-linux-amd64', 'executorserver'), ['-silent']);
if (!p.stdout) throw new Error('Cannot start executorserver');
else {
p.stdout.on('data', (data) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hydro",
"version": "0.0.0",
"version": "1.0.0-preview",
"main": "hydro/development.js",
"repository": "https://github.com/hydro-dev/Hydro.git",
"author": "masnn",
-32
View File
@@ -1,32 +0,0 @@
<!DOCTYPE html>
<html data-page="setup" data-layout="immersive" class="layout--immersive page--setup nojs">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="chrome=1"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta name="msapplication-TileColor" content="#579e9a">
<meta name="theme-color" content="#56758f">
<link rel="stylesheet" media="all" href="{{ static_url('vj4.css') }}">
<title>{{ _('Setup') }}</title>
<script>
var _htmlNode = document.documentElement;
_htmlNode.className = _htmlNode.className.replace(' nojs', ' hasjs');
</script>
</head>
<body>
<div class="slideout-panel" id="panel">
<div class="main">
<div class="row"><div class="columns">
<div class="immersive--content immersive--center">
<h1>{{ _('Setup') }}</h1>
<div class="text-center">
{{ _('Done. Please restart the program.') }}
</div>
</div>
</div></div>
</div>
</div>
<script type="text/javascript" src="{{ static_url('vj4.js') }}"></script>
</body>
</html>