You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Hydro/install/install.js

375 lines
16 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* eslint-disable no-unreachable */
/* eslint-disable consistent-return */
/* eslint-disable no-undef */
/// <reference types="./jssh" />
const locales = {
zh: {
'install.start': '开始运行 Hydro 安装工具',
'info.mirror': '将首选 %s 镜像。可以使用 MIRROR=tsinghua|tencent|official 更改。',
'warn.avx2': '检测到您的 CPU 不支持 avx2 指令集,将使用 mongodb@v4.4',
'error.rootRequired': '请使用 root 用户运行该工具。',
'error.unsupportedArch': '不支持的架构 %s ,请尝试手动安装。',
'error.osreleaseNotFound': '无法获取系统版本信息(/etc/os-release 文件未找到),请尝试手动安装。',
'error.unsupportedOS': '不支持的操作系统 %s ,请尝试手动安装,',
'install.preparing': '正在初始化安装...',
'install.mongodb': '正在安装 mongodb...',
'install.nvm': '正在安装 NVM...',
'error.nodeWithoutNVMDetected': '检测到您的系统中安装了 Node但未使用 NVM请尝试手动安装或卸载当前 Node 后再试。',
'install.nodejs': '正在安装 NodeJS...',
'error.nodeVersionPraseFail': '无法解析 Node 版本号,请尝试手动安装。',
'install.pm2': '正在安装 PM2...',
'install.createDatabaseUser': '正在创建数据库用户...',
'install.minio': '正在安装 MinIO...',
'install.compiler': '正在安装编译器...',
'install.hydro': '正在安装 Hydro...',
'install.done': 'Hydro 安装成功!',
'extra.restartTerm': '请重启终端或重新连接ssh并切换到 root 用户执行剩下的操作。',
'extra.dbUser': '数据库用户名: hydro',
'extra.dbPassword': '数据库密码: %s',
'info.skip': '步骤已跳过。',
},
en: {
'install.start': 'Starting Hydro installation tool',
'info.mirror': 'Using preferred %s mirror. You can use MIRROR=tsinghua|tencent|official to change.',
'warn.avx2': 'Your CPU does not support avx2, will use mongodb@v4.4',
'error.rootRequired': 'Please run this tool as root user.',
'error.unsupportedArch': 'Unsupported architecture %s, please try to install manually.',
'error.osreleaseNotFound': 'Unable to get system version information (/etc/os-release file not found), please try to install manually.',
'error.unsupportedOS': 'Unsupported operating system %s, please try to install manually.',
'install.preparing': 'Initializing installation...',
'install.mongodb': 'Installing mongodb...',
'install.nvm': 'Installing NVM...',
'error.nodeWithoutNVMDetected': 'Detected Node installation without NVM, please try to install manually or uninstall current Node first.',
'install.nodejs': 'Installing NodeJS...',
'error.nodeVersionPraseFail': 'Unable to parse Node version, please try to install manually.',
'install.pm2': 'Installing PM2...',
'install.createDatabaseUser': 'Creating database user...',
'install.minio': 'Installing MinIO...',
'install.compiler': 'Installing compiler...',
'install.hydro': 'Installing Hydro...',
'install.done': 'Hydro installation completed!',
'extra.restartTerm': 'Please restart your terminal (or reconnect ssh) and switch to root user to execute the remaining operations.',
'extra.dbUser': 'Database username: hydro',
'extra.dbPassword': 'Database password: %s',
'info.skip': 'Step skipped.',
},
};
const preferredMirror = __env.MIRROR || 'tsinghua';
const mirrors = {
node: {
tsinghua: 'https://mirrors.tuna.tsinghua.edu.cn/nodejs-release',
tencent: 'https://mirrors.cloud.tencent.com/nodejs-release',
official: 'https://nodejs.org/dist',
},
mongodb: {
tsinghua: 'https://mirrors.tuna.tsinghua.edu.cn/mongodb/apt/ubuntu',
tencent: 'https://mirrors.cloud.tencent.com/mongodb/apt/ubuntu',
official: 'https://repo.mongodb.org/apt/ubuntu',
},
minio: {
hydro: 'https://kr.hydro.ac/download/minio',
// xiaoheiban: 'https://pro-file.xiaoheiban.cn/minio', // UNSAFE
undefined: 'https://s3.undefined.moe/public/minio',
official: 'https://dl.min.io/server/minio/release/linux-amd64/minio',
},
sandbox: {
hydro: 'https://kr.hydro.ac/download/sandbox',
undefined: 'https://s3.undefined.moe/file/executor-amd64',
official: 'https://github.com/criyle/go-judge/releases/download/v1.4.0/executorserver-amd64',
},
};
let retry = 0;
/** @argument {keyof typeof mirrors} target */
function getMirror(target) {
if (!mirrors[target]) log.fatal('Unknown resource:', target);
const res = [];
if (mirrors[target][preferredMirror]) res.push(mirrors[target][preferredMirror]);
res.push(...Object.keys(mirrors[target]).map((i) => mirrors[target][i]));
return res[retry % res.length];
}
let locale = __env.LANG?.includes('zh') ? 'zh' : 'en';
if (__env.TERM === 'linux') locale = 'en';
log.info = ((orig) => (str, ...args) => orig(locales[locale][str] || str, ...args) && 0)(log.info);
log.warn = ((orig) => (str, ...args) => orig(locales[locale][str] || str, ...args) && 0)(log.warn);
log.fatal = ((orig) => (str, ...args) => orig(locales[locale][str] || str, ...args) && 0)(log.fatal);
log.info('install.start');
const MINIO_ACCESS_KEY = randomstring(32);
const MINIO_SECRET_KEY = randomstring(32);
let DATABASE_PASSWORD = randomstring(32);
const source_nvm = `
# load nvm env (by hydro installer)
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
`;
if (__user !== 'root') log.fatal('error.rootRequired');
if (__arch !== 'amd64') log.fatal('error.unsupportedArch', __arch);
const dev = !!cli.get('dev');
if (!fs.exist('/etc/os-release')) log.fatal('error.osreleaseNotFound');
const osinfoFile = fs.readfile('/etc/os-release');
const lines = osinfoFile.split('\n');
const values = {};
for (const line of lines) {
if (!line.trim()) continue;
const d = line.split('=');
if (d[1].startsWith('"')) values[d[0].toLowerCase()] = d[1].substr(1, d[1].length - 2);
else values[d[0].toLowerCase()] = d[1];
}
if (!['ubuntu', 'arch'].includes(values.id)) log.fatal('error.unsupportedOS', values.id);
const Arch = values.id === 'arch';
const cpuInfoFile = fs.readfile('/proc/cpuinfo');
let mongodbVersion = __env.MONGODB_VERSION || '5.0';
if (!cpuInfoFile.includes('avx2')) {
log.warn('warn.avx2');
mongodbVersion = '4.4';
}
let migration;
const steps = [
{
init: 'install.preparing',
operations: [
() => log.info('info.mirror', preferredMirror),
'mkdir -p /data/db /data/file ~/.hydro',
Arch ? 'pacman --needed --quiet --noconfirm -Sy' : 'apt-get -qq update',
Arch
? 'pacman --needed --quiet --noconfirm -S gnupg curl qrencode'
: 'apt-get install -qy unzip zip curl wget gnupg qrencode ca-certificates',
() => {
if (locale === 'zh') {
log.info('扫码加入QQ群');
exec('echo https://qm.qq.com/cgi-bin/qm/qr\\?k\\=0aTZfDKURRhPBZVpTYBohYG6P6sxABTw | qrencode -o - -m 2 -t UTF8', {}, 0);
}
},
() => {
return; // Not implemented yet
if (fs.exist('/home/judge/src')) {
const res = cli.prompt('migrate.hustojFound');
if (res.toLowerCase().trim() === 'y') migration = 'hustoj';
}
const docker = !exec1('docker -v').code;
if (!docker) return;
// TODO check more places
if (fs.exist('/root/OnlineJudgeDeploy/docker-compose.yml')) {
const res = cli.prompt('migrate.qduojFound');
if (res.toLowerCase().trim() === 'y') migration = 'qduoj';
}
},
],
},
{
init: 'install.mongodb',
skip: () => fs.exist('/usr/bin/mongo'),
operations: Arch
? [
['curl -fSLO https://s3.undefined.moe/hydro/arch/libcurl-openssl-1.0-7.76.0-1-x86_64.pkg.tar.zst', { retry: true }],
['curl -fSLO https://s3.undefined.moe/hydro/arch/mongodb-bin-4.4.5-1-x86_64.pkg.tar.zst', { retry: true }],
['curl -fSLO https://s3.undefined.moe/hydro/arch/mongodb-tools-bin-100.3.1-1-x86_64.pkg.tar.zst', { retry: true }],
'pacman --noconfirm -U libcurl-openssl-1.0-7.76.0-1-x86_64.pkg.tar.zst'
+ 'mongodb-bin-4.4.5-1-x86_64.pkg.tar.zst mongodb-tools-bin-100.3.1-1-x86_64.pkg.tar.zst',
]
: [
// https://letsencrypt.org/docs/dst-root-ca-x3-expiration-september-2021/
['apt-get upgrade openssl ca-certificates -y', { retry: true }],
[`wget -qO - https://www.mongodb.org/static/pgp/server-${mongodbVersion}.asc | apt-key add -`, { retry: true }],
[`echo "deb ${getMirror('mongodb')} ${values.ubuntu_codename}\
/mongodb-org/${mongodbVersion} multiverse" >/etc/apt/sources.list.d/mongodb-org-${mongodbVersion}.list && \
apt-get -qq update && apt-get -q install -y mongodb-org`, { retry: true }],
],
},
{
init: 'install.nvm',
skip: () => {
const nvm = fs.exist('/root/.nvm');
const node = !exec('node -v').code;
if (node && !nvm) log.warn('error.nodeWithoutNVMDetected');
return nvm;
},
operations: [
() => {
const resp = http.request('GET', 'https://hydro.ac/nvm.sh');
const script = resp.body
.replace(/raw\.githubusercontent\.com/g, 'raw.fastgit.org')
.replace(/github\.com\/nvm-sh\/nvm\.git/g, 'gitee.com/imirror/nvm');
fs.writefile('/tmp/install-nvm.sh', script);
},
['bash /tmp/install-nvm.sh', { retry: true }],
],
},
{
init: 'install.nodejs',
operations: [
() => {
const res = exec1('bash -c "source /root/.nvm/nvm.sh && nvm install 14"', {
NVM_NODEJS_ORG_MIRROR: getMirror('node'),
});
let ver;
try {
ver = res.output.split('Now using node v')[1].split(' ')[0];
} catch (e) {
log.error('error.nodeVersionParseFail');
return 'retry';
}
setenv('PATH', `/root/.nvm/versions/node/v${ver}/bin:${__env.PATH}`);
const shell = __env.SHELL ? __env.SHELL.split('/') : ['bash'];
const rc = `/root/.${shell[shell.length - 1]}rc`;
if (!fs.exist(rc)) fs.writefile(rc, source_nvm);
else {
const file = fs.readfile(rc);
if (!file.includes(source_nvm)) fs.appendfile(rc, source_nvm);
}
},
['npm i yarn -g', { retry: true }],
],
},
{
init: 'install.pm2',
skip: () => fs.exist('/usr/local/bin/pm2'),
operations: ['yarn global add pm2'],
},
{
init: 'install.createDatabaseUser',
skip: () => fs.exist('/root/.hydro/config.json'),
operations: [
'pm2 start mongod',
() => sleep(5000),
() => fs.writefile('/tmp/createUser.js', `\
db.createUser({
user: 'hydro',
pwd: '${DATABASE_PASSWORD}',
roles: [{ role: 'readWrite', db: 'hydro' }]
})`),
'mongo 127.0.0.1:27017/hydro /tmp/createUser.js',
() => fs.writefile('/root/.hydro/config.json', JSON.stringify({
host: '127.0.0.1',
port: 27017,
name: 'hydro',
username: 'hydro',
password: DATABASE_PASSWORD,
})),
'pm2 stop mongod',
'pm2 del mongod',
],
},
{
init: 'install.minio',
skip: () => __env.SKIP_MINIO || fs.exist('/root/.hydro/env'),
operations: [
[`curl -fSL ${getMirror('minio')} -o /usr/bin/minio`, { retry: true }],
'chmod +x /usr/bin/minio',
],
},
{
init: 'install.compiler',
operations: [
Arch ? 'pacman --needed --quiet --noconfirm -S gcc fpc' : 'apt-get install -y g++ fp-compiler >/dev/null',
],
},
{
init: 'install.sandbox',
operations: [
[`curl -fSL ${getMirror('sandbox')} -o /usr/bin/hydro-sandbox`, { retry: true }],
'chmod +x /usr/bin/hydro-sandbox',
],
},
{
init: 'install.hydro',
operations: [
...(dev
? [
['rm -rf /root/Hydro && git clone https://github.com/hydro-dev/Hydro.git /root/Hydro', { retry: true }],
['cd /root/Hydro && yarn', { retry: true }],
'cd /root/Hydro && yarn build:ui',
['yarn global add npx', { retry: true }],
]
: [['yarn global add hydrooj @hydrooj/ui-default @hydrooj/hydrojudge', { retry: true }]]),
() => fs.writefile('/root/.hydro/addon.json', '["@hydrooj/ui-default","@hydrooj/hydrojudge"]'),
],
},
{
init: 'install.starting',
operations: [
`echo "MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}\nMINIO_SECRET_KEY=${MINIO_SECRET_KEY}" >/root/.hydro/env`,
`pm2 start "MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} MINIO_SECRET_KEY=${MINIO_SECRET_KEY} minio server /data/file" --name minio`,
'pm2 start mongod --name mongodb -- --auth --bind_ip 0.0.0.0',
() => sleep(1000),
'pm2 start hydro-sandbox',
'pm2 start hydrooj',
'pm2 startup',
'pm2 save',
],
},
{
init: 'install.migrateHustoj',
skip: () => migration !== 'hustoj',
silent: true,
operations: [
['yarn global add @hydrooj/migrate-hustoj', { retry: true }],
'hydrooj addon add @hydrooj/migrate-hustoj',
() => {
const config = {
host: 'localhost',
port: 3306,
name: 'jol',
dataDir: '/home/judge/data',
// TODO: auto-read uname&passwd&contestType
username: 'debian-sys-maint',
password: '',
contestType: 'acm',
};
exec2(`hydrooj cli script migrateHustoj ${JSON.stringify(config)}`);
},
'pm2 restart hydrooj',
],
},
{
init: 'install.done',
operations: [
() => {
DATABASE_PASSWORD = loadconfig('/root/.hydro/config.json').password;
},
() => log.info('extra.restartTerm'),
() => log.info('extra.dbUser'),
() => log.info('extra.dbPassword', DATABASE_PASSWORD),
() => log.info('MINIO_ACCESS_KEY=%s', MINIO_ACCESS_KEY),
() => log.info('MINIO_SECRET_KEY=%s', MINIO_SECRET_KEY),
],
},
];
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
if (!step.silent) log.info(step.init);
if (!(step.skip && step.skip())) {
for (let op of step.operations) {
if (!(op instanceof Array)) op = [op, {}];
if (typeof op[0] === 'string') {
retry = 0;
exec(op[0], op[1]);
while (__code !== 0) {
if (op[1].retry && retry < 30) {
log.warn('Retry... (%s)', op[0]);
exec(op[0], op[1]);
retry++;
} else log.fatal('Error when running %s', op[0]);
}
} else {
retry = 0;
let res = op[0](op[1]);
while (res === 'retry') {
if (retry < 30) {
log.warn('Retry...');
res = op[0](op[1]);
retry++;
} else log.fatal('Error installing');
}
}
}
} else if (!step.silent) log.info('info.skip');
}
exit(0);