diff --git a/install/install.js b/install/install.js
deleted file mode 100644
index 4bd88dcb3..000000000
--- a/install/install.js
+++ /dev/null
@@ -1,337 +0,0 @@
-/* eslint-disable no-unreachable */
-/* eslint-disable consistent-return */
-/* eslint-disable no-undef */
-///
-
-const locales = {
- zh: {
- 'install.start': '开始运行 Hydro 安装工具',
- 'info.mirror': '将首选 %s 镜像。可以使用 MIRROR=tsinghua|tencent|official 更改。',
- 'warn.avx2': '检测到您的 CPU 不支持 avx2 指令集,将使用 mongodb@v4.4',
- 'error.rootRequired': '请先使用 sudo su 切换到 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.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.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.',
- },
-};
-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);
-
-if (__user !== 'root') log.fatal('error.rootRequired');
-if (__arch !== 'amd64') log.fatal('error.unsupportedArch', __arch);
-if (__os !== 'linux') log.fatal('error.unsupportedOS', __os);
-if (!__env.HOME) log.fatal('$HOME not found');
-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];
-}
-let avx2 = true;
-const cpuInfoFile = fs.readfile('/proc/cpuinfo');
-if (!cpuInfoFile.includes('avx2')) {
- avx2 = false;
- log.warn('warn.avx2');
-}
-let migration;
-let retry = 0;
-log.info('install.start');
-let DATABASE_PASSWORD = randomstring(32);
-// TODO read from args
-const CN = true;
-
-const nixBin = `${__env.HOME}/.nix-profile/bin`;
-const mount = `\
-mount:
- - type: bind
- source: ${nixBin}
- target: /bin
- readonly: true
- - type: bind
- source: ${nixBin}
- target: /usr/bin
- readonly: true
- - type: bind
- source: /nix
- target: /nix
- readonly: true
- - type: bind
- source: /dev/null
- target: /dev/null
- - type: bind
- source: /dev/urandom
- target: /dev/urandom
- - type: tmpfs
- target: /w
- data: size=512m,nr_inodes=8k
- - type: tmpfs
- target: /tmp
- data: size=512m,nr_inodes=8k
-proc: true
-workDir: /w
-hostName: executor_server
-domainName: executor_server
-uid: 1536
-gid: 1536
-`;
-
-function removeOptionalEsbuildDeps() {
- const yarnGlobalPath = exec('yarn global dir').output?.trim() || '';
- if (!yarnGlobalPath) return false;
- const file = fs.readfile(`${yarnGlobalPath}/package.json`);
- const data = JSON.parse(file);
- data.resolutions = data.resolutions || {};
- Object.assign(data.resolutions, Object.fromEntries([
- '@esbuild/linux-loong64',
- ...['android', 'windows', 'darwin', 'freebsd'].flatMap((i) => [`${i}-64`, `${i}-arm64`, `${i}-32`]),
- ...['32', 'arm', 'arm64', 'mips64', 'ppc64', 'riscv64', 's390x'].map((i) => `esbuild-linux-${i}`),
- ...['netbsd', 'openbsd', 'sunos'].map((i) => `esbuild-${i}-64`),
- ].map((i) => [i, 'link:/dev/null'])));
- fs.writefile(`${yarnGlobalPath}/package.json`, JSON.stringify(data, null, 2));
- return true;
-}
-
-const steps = [
- {
- init: 'install.preparing',
- operations: [
- 'mkdir -p /data/db /data/file ~/.hydro',
- () => {
- 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);
- }
- log.info(`Hydro includes anonymous system telemetry,
-which helps developers figure out the most commonly used operating system and platform.
-To disable this feature, checkout our sourcecode.`);
- },
- 'bash -c "bash <(curl https://hydro.ac/nix.sh)"',
- () => {
- setenv('PATH', `${__env.HOME}/.nix-profile/bin:${__env.PATH}`);
- },
- 'nix-env -iA nixpkgs.coreutils nixpkgs.bash nixpkgs.unzip nixpkgs.zip nixpkgs.diffutils nixpkgs.qrencode',
- () => {
- 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: () => !exec('mongod --version').code,
- operations: [
- `nix-env -iA hydro.mongodb${avx2 ? 5 : 4}${CN ? '-cn' : ''}`,
- ],
- },
- {
- init: 'install.mongodb-tools',
- skip: () => !exec('mongoexport --version').code,
- operations: [
- 'nix-env -iA nixpkgs.mongodb-tools',
- ],
- },
- {
- init: 'install.mongo',
- skip: () => !exec('mongo --version').code,
- operations: [
- `nix-env -iA hydro.mongosh${avx2 ? 5 : 4}${CN ? '-cn' : ''}`,
- ],
- },
- {
- init: 'install.nodejs',
- skip: () => !exec('node -v').code && !exec('yarn -v').code,
- operations: [
- 'nix-env -iA nixpkgs.nodejs nixpkgs.yarn nixpkgs.esbuild',
- ],
- },
- {
- init: 'install.pm2',
- skip: () => !exec('pm2 -v').code,
- operations: [
- ['yarn global add pm2', { retry: true }],
- ],
- },
- {
- init: 'install.compiler',
- operations: [
- 'nix-env -iA nixpkgs.gcc nixpkgs.fpc nixpkgs.python3',
- ],
- },
- {
- init: 'install.sandbox',
- skip: () => !exec('hydro-sandbox --help').code,
- operations: [
- 'nix-env -iA hydro.sandbox',
- ],
- },
- {
- init: 'install.hydro',
- operations: [
- () => removeOptionalEsbuildDeps(),
- ['yarn global add hydrooj @hydrooj/ui-default @hydrooj/hydrojudge', { retry: true }],
- () => fs.writefile(`${__env.HOME}/.hydro/addon.json`, '["@hydrooj/ui-default","@hydrooj/hydrojudge"]'),
- ],
- },
- {
- init: 'install.createDatabaseUser',
- skip: () => fs.exist(`${__env.HOME}/.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(`${__env.HOME}/.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.starting',
- operations: [
- ['pm2 stop all', { ignore: true }],
- () => fs.writefile(`${__env.HOME}/.hydro/mount.yaml`, mount),
- 'pm2 start mongod --name mongodb -- --auth --bind_ip 0.0.0.0',
- () => sleep(1000),
- `pm2 start bash --name hydro-sandbox -- -c "ulimit -s unlimited && hydro-sandbox -mount-conf ${__env.HOME}/.hydro/mount.yaml"`,
- 'pm2 start hydrooj',
- 'pm2 startup',
- 'pm2 save',
- ],
- },
- {
- init: 'install.migrateHustoj',
- skip: () => migration !== 'hustoj',
- silent: true,
- operations: [
- ['yarn global add @hydrooj/migrate', { retry: true }],
- 'hydrooj addon add @hydrooj/migrate',
- () => {
- 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(`${__env.HOME}/.hydro/config.json`).password;
- },
- () => log.info('extra.restartTerm'),
- () => log.info('extra.dbUser'),
- () => log.info('extra.dbPassword', DATABASE_PASSWORD),
- ],
- },
-];
-
-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 (op[0].toString().startsWith('nix-env')) op[1].retry = true;
- if (typeof op[0] === 'string') {
- retry = 0;
- exec(op[0], op[1]);
- while (__code !== 0 && op[1].ignore !== true) {
- 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);
diff --git a/install/install.sh b/install/install.sh
new file mode 100644
index 000000000..fc92ca638
--- /dev/null
+++ b/install/install.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+set -e
+echo "Executing Hydro install script v3.0.0"
+echo "Hydro includes anonymous system telemetry,
+which helps developers figure out the most commonly used operating system and platform.
+To disable this feature, checkout our sourcecode."
+mkdir -p /data/db /data/file ~/.hydro
+bash <(curl https://hydro.ac/nix.sh)
+export PATH=$HOME/.nix-profile/bin:$PATH
+nix-env -iA nixpkgs.nodejs nixpkgs.pm2 nixpkgs.yarn nixpkgs.esbuild nixpkgs.coreutils nixpkgs.bash nixpkgs.unzip nixpkgs.zip nixpkgs.diffutils nixpkgs.qrencode
+echo "扫码加入QQ群:"
+echo https://qm.qq.com/cgi-bin/qm/qr\?k\=0aTZfDKURRhPBZVpTYBohYG6P6sxABTw | qrencode -o - -m 2 -t UTF8
+echo "// File created by Hydro install script\n" >/tmp/install.js
+cat >/tmp/install.b64 << EOF123
+%PLACEHOLDER%
+EOF123
+cat /tmp/install.b64 | base64 -d >>/tmp/install.js
+node /tmp/install.js
+set +e
diff --git a/install/install.ts b/install/install.ts
new file mode 100644
index 000000000..97cc866ea
--- /dev/null
+++ b/install/install.ts
@@ -0,0 +1,295 @@
+/* eslint-disable import/no-dynamic-require */
+/* eslint-disable no-sequences */
+import { execSync, ExecSyncOptions } from 'child_process';
+import { existsSync, readFileSync, writeFileSync } from 'fs';
+
+const exec = (command: string, args?: ExecSyncOptions) => {
+ try {
+ return {
+ output: execSync(command, args).toString(),
+ code: 0,
+ };
+ } catch (e) {
+ return {
+ code: e.status,
+ message: e.message,
+ };
+ }
+};
+const sleep = (t: number) => new Promise((r) => { setTimeout(r, t); });
+const locales = {
+ zh: {
+ 'install.start': '开始运行 Hydro 安装工具',
+ 'warn.avx2': '检测到您的 CPU 不支持 avx2 指令集,将使用 mongodb@v4.4',
+ 'error.rootRequired': '请先使用 sudo su 切换到 root 用户后再运行该工具。',
+ 'error.unsupportedArch': '不支持的架构 %s ,请尝试手动安装。',
+ 'error.osreleaseNotFound': '无法获取系统版本信息(/etc/os-release 文件未找到),请尝试手动安装。',
+ 'error.unsupportedOS': '不支持的操作系统 %s ,请尝试手动安装,',
+ 'install.preparing': '正在初始化安装...',
+ 'install.mongodb': '正在安装 mongodb...',
+ 'install.createDatabaseUser': '正在创建数据库用户...',
+ 'install.compiler': '正在安装编译器...',
+ 'install.hydro': '正在安装 Hydro...',
+ 'install.done': 'Hydro 安装成功!',
+ 'extra.dbUser': '数据库用户名: hydro',
+ 'extra.dbPassword': '数据库密码: %s',
+ 'info.skip': '步骤已跳过。',
+ },
+ en: {
+ 'install.start': 'Starting Hydro installation tool',
+ '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.createDatabaseUser': 'Creating database user...',
+ 'install.compiler': 'Installing compiler...',
+ 'install.hydro': 'Installing Hydro...',
+ 'install.done': 'Hydro installation completed!',
+ 'extra.dbUser': 'Database username: hydro',
+ 'extra.dbPassword': 'Database password: %s',
+ 'info.skip': 'Step skipped.',
+ },
+};
+let locale = process.env.LANG?.includes('zh') ? 'zh' : 'en';
+if (process.env.TERM === 'linux') locale = 'en';
+const processLog = (orig) => (str, ...args) => (orig(locales[locale][str] || str, ...args), 0);
+const log = {
+ info: processLog(console.log),
+ warn: processLog(console.warn),
+ fatal: (str, ...args) => (processLog(console.error)(str, ...args), process.exit(1)),
+};
+
+if (!process.getuid) log.fatal('error.unsupportedOs');
+else if (process.getuid() !== 0) log.fatal('error.rootRequired');
+if (!['x64', 'arm64'].includes(process.arch)) log.fatal('error.unsupportedArch', process.arch);
+if (!process.env.HOME) log.fatal('$HOME not found');
+if (!existsSync('/etc/os-release')) log.fatal('error.osreleaseNotFound');
+const osinfoFile = readFileSync('/etc/os-release', 'utf-8');
+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];
+}
+let avx2 = true;
+const cpuInfoFile = readFileSync('/proc/cpuinfo', 'utf-8');
+if (!cpuInfoFile.includes('avx2')) {
+ avx2 = false;
+ log.warn('warn.avx2');
+}
+let migration;
+let retry = 0;
+log.info('install.start');
+const defaultDict = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
+String.random = function random(digit = 32, dict = defaultDict) {
+ let str = '';
+ for (let i = 1; i <= digit; i++) str += dict[Math.floor(Math.random() * dict.length)];
+ return str;
+};
+let DATABASE_PASSWORD = String.random(32);
+// TODO read from args
+const CN = true;
+
+const nixBin = `${process.env.HOME}/.nix-profile/bin`;
+const entry = (source: string, target = source, ro = true) => `\
+ - type: bind
+ source: ${source}
+ target: ${target}${ro ? '\n readonly: true' : ''}`;
+const mount = `mount:
+${entry(nixBin, '/bin')}
+${entry(nixBin, '/usr/bin')}
+${entry('/nix', '/nix')}
+${entry('/dev/null', '/dev/null', false)}
+${entry('/dev/urandom', '/dev/urandom', false)}
+ - type: tmpfs
+ target: /w
+ data: size=512m,nr_inodes=8k
+ - type: tmpfs
+ target: /tmp
+ data: size=512m,nr_inodes=8k
+proc: true
+workDir: /w
+hostName: executor_server
+domainName: executor_server
+uid: 1536
+gid: 1536
+`;
+
+function removeOptionalEsbuildDeps() {
+ const yarnGlobalPath = exec('yarn global dir').output?.trim() || '';
+ if (!yarnGlobalPath) return false;
+ const pkgjson = `${yarnGlobalPath}/package.json`;
+ const data = existsSync(pkgjson) ? require(pkgjson) : {};
+ data.resolutions = data.resolutions || {};
+ Object.assign(data.resolutions, Object.fromEntries([
+ '@esbuild/linux-loong64',
+ ...['android', 'windows', 'darwin', 'freebsd'].flatMap((i) => [`${i}-64`, `${i}-arm64`, `${i}-32`]),
+ ...['32', 'arm', 'mips64', 'ppc64', 'riscv64', 's390x'].map((i) => `esbuild-linux-${i}`),
+ ...['netbsd', 'openbsd', 'sunos'].map((i) => `esbuild-${i}-64`),
+ ].map((i) => [i, 'link:/dev/null'])));
+ exec(`mkdir -p ${yarnGlobalPath}`);
+ writeFileSync(pkgjson, JSON.stringify(data, null, 2));
+ return true;
+}
+
+const steps = [
+ {
+ init: 'install.preparing',
+ operations: [
+ () => {
+ // Not implemented yet
+ // if (fs.existsSync('/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',
+ operations: [
+ `nix-env -iA hydro.mongodb${avx2 ? 5 : 4}${CN ? '-cn' : ''}`,
+ 'nix-env -iA nixpkgs.mongodb-tools',
+ `nix-env -iA hydro.mongosh${avx2 ? 5 : 4}${CN ? '-cn' : ''}`,
+ ],
+ },
+ {
+ init: 'install.compiler',
+ operations: [
+ 'nix-env -iA nixpkgs.gcc nixpkgs.fpc nixpkgs.python3',
+ ],
+ },
+ {
+ init: 'install.sandbox',
+ skip: () => !exec('hydro-sandbox --help').code,
+ operations: [
+ 'nix-env -iA hydro.sandbox',
+ ],
+ },
+ {
+ init: 'install.hydro',
+ operations: [
+ () => removeOptionalEsbuildDeps(),
+ ['yarn global add hydrooj @hydrooj/ui-default @hydrooj/hydrojudge', { retry: true }],
+ () => writeFileSync(`${process.env.HOME}/.hydro/addon.json`, '["@hydrooj/ui-default","@hydrooj/hydrojudge"]'),
+ ],
+ },
+ {
+ init: 'install.createDatabaseUser',
+ skip: () => existsSync(`${process.env.HOME}/.hydro/config.json`),
+ operations: [
+ 'pm2 start mongod',
+ () => sleep(3000),
+ () => writeFileSync('/tmp/createUser.js', `db.createUser(${JSON.stringify({
+ user: 'hydro',
+ pwd: DATABASE_PASSWORD,
+ roles: [{ role: 'readWrite', db: 'hydro' }],
+ })})`),
+ ['mongo 127.0.0.1:27017/hydro /tmp/createUser.js', { retry: true }],
+ () => writeFileSync(`${process.env.HOME}/.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.starting',
+ operations: [
+ ['pm2 stop all', { ignore: true }],
+ () => writeFileSync(`${process.env.HOME}/.hydro/mount.yaml`, mount),
+ 'pm2 start mongod --name mongodb -- --auth --bind_ip 0.0.0.0',
+ () => sleep(1000),
+ `pm2 start bash --name hydro-sandbox -- -c "ulimit -s unlimited && hydro-sandbox -mount-conf ${process.env.HOME}/.hydro/mount.yaml"`,
+ 'pm2 start hydrooj',
+ 'pm2 startup',
+ 'pm2 save',
+ ],
+ },
+ {
+ init: 'install.migrateHustoj',
+ skip: () => migration !== 'hustoj',
+ silent: true,
+ operations: [
+ ['yarn global add @hydrooj/migrate', { retry: true }],
+ 'hydrooj addon add @hydrooj/migrate',
+ () => {
+ 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',
+ };
+ exec(`hydrooj cli script migrateHustoj ${JSON.stringify(config)}`);
+ },
+ 'pm2 restart hydrooj',
+ ],
+ },
+ {
+ init: 'install.done',
+ operations: [
+ () => {
+ DATABASE_PASSWORD = require(`${process.env.HOME}/.hydro/config.json`).password;
+ },
+ () => log.info('extra.dbUser'),
+ () => log.info('extra.dbPassword', DATABASE_PASSWORD),
+ ],
+ },
+];
+
+async function main() {
+ 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, {}] as any;
+ if (op[0].toString().startsWith('nix-env')) op[1].retry = true;
+ if (typeof op[0] === 'string') {
+ retry = 0;
+ let res = exec(op[0], { stdio: 'inherit' });
+ while (res.code && op[1].ignore !== true) {
+ if (op[1].retry && retry < 30) {
+ log.warn('Retry... (%s)', op[0]);
+ res = exec(op[0], { stdio: 'inherit' });
+ 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...');
+ // eslint-disable-next-line no-await-in-loop
+ res = await op[0](op[1]);
+ retry++;
+ } else log.fatal('Error installing');
+ }
+ }
+ }
+ } else if (!step.silent) log.info('info.skip');
+ }
+}
+main().catch(log.fatal);
diff --git a/install/jssh.d.ts b/install/jssh.d.ts
deleted file mode 100644
index 6d59b33a6..000000000
--- a/install/jssh.d.ts
+++ /dev/null
@@ -1,778 +0,0 @@
-/** 全局变量对象 */
-declare var global: any;
-
-/** 是否 self-contained 方式执行 */
-declare const __selfcontained: boolean;
-
-/** CPU数量 */
-declare const __cpucount: number;
-
-/** 操作系统类型,darwin, freebsd, linux, windows */
-declare const __os: string;
-
-/** 处理器架构,386, amd64, arm, s390x */
-declare const __arch: string;
-
-/** JSSH版本号 */
-declare const __version: string;
-
-/** JSSH二进制文件路径 */
-declare const __bin: string;
-
-/** 当前进程PID */
-declare const __pid: number;
-
-/** 临时文件目录 */
-declare const __tmpdir: string;
-
-/** 当前用户HOME目录 */
-declare const __homedir: string;
-
-/** 当前用户名 */
-declare const __user: string;
-
-/** 当前主机名 */
-declare const __hostname: string;
-
-/** 当前脚本目录名 */
-declare var __dirname: string;
-
-/** 当前脚本文件名 */
-declare var __filename: string;
-
-/** 当前命令行参数 */
-declare const __args: string[];
-
-/** 当前环境变量 */
-declare const __env: Record;
-
-/** 最近一次执行命令输出的内容 */
-declare const __output: string;
-
-/** 最近一次执行命令输出内容的字节数 */
-declare const __outputbytes: number;
-
-/** 最近一次执行命令进程退出code */
-declare const __code: number;
-
-/** 已自动加载的全局脚本文件列表 */
-declare const __globalfiles: string[];
-
-/**
- * 加载 CommonJS 模块
- * @param modulename 模块名称
- * @return 模块内容
- */
-// @ts-ignore
-declare function require(modulename: string): any;
-
-/**
- * 以eval方式执行指定脚本文件
- * @param filename 文件路径或URL
- * @param content 文件内容,若指定此参数则无需实际读取文件内容
- * @return 返回结果
- */
-declare function evalfile(filename: string, content?: string): any;
-
-/**
- * 获取指定字符串的字节长度
- * @param data 字符串
- * @return 字节长度
- */
-declare function bytesize(data: string): number;
-
-/**
- * 读取标准输入的内容,返回字符串
- */
-declare function stdin(): string;
-
-/**
- * 读取标准输入的内容,返回 Uint8Array
- */
-declare function stdinbytes(): Uint8Array;
-
-/**
- * 格式化文本内容
- * @param format 模板,支持%s等格式
- * @param args 参数列表
- * @return 格式化后的文本内容
- */
-declare function format(format: any, ...args: any[]): string;
-
-/**
- * 输出到控制台
- * @param format 模板,支持%s等格式
- * @param args 参数列表
- * @return 是否成功
- */
-declare function print(format: any, ...args: any[]): boolean;
-
-/**
- * 输出一行到控制台
- * @param format 模板,支持%s等格式
- * @param args 参数列表
- * @return 是否成功
- */
-declare function println(format: any, ...args: any[]): boolean;
-
-/**
- * 在stdout输出一行日志
- * @param message
- * @return 是否成功
- */
-declare function stdoutlog(message: string): boolean;
-
-/**
- * 在stderr输出一行日志
- * @param message
- * @return 是否成功
- */
-declare function stderrlog(message: string): boolean;
-
-/**
- * 从控制台获取用户一行的字符串输入
- * @return 用户输入的字符串
- */
-declare function readline(): string;
-
-/**
- * 睡眠
- * @param milliseconds 毫秒
- * @return 毫秒
- */
-declare function sleep(milliseconds: number): number;
-
-/**
- * 退出进程
- * @param code 进程退出code
- */
-declare function exit(code?: number): void;
-
-/**
- * 加载配置文件,支持*.json,*.yaml,*.toml格式
- * @param file 文件名
- * @param format 格式,如果没有指定则自动根据文件后缀名判断
- * @return 配置内容
- */
-declare function loadconfig(
- file: string,
- format?: "json" | "yaml" | "toml"
-): any;
-
-/**
- * base64编码
- * @param data 原始内容
- * @return 编码后的内容
- */
-declare function base64encode(data: string): string;
-
-/**
- * base64解码
- * @param data 编码后的内容
- * @return 原始内容
- */
-declare function base64decode(data: string): string;
-
-/**
- * MD5编码
- * @param data 原始内容
- * @return 编码后的内容
- */
-declare function md5(data: string): string;
-
-/**
- * SHA1编码
- * @param data 原始内容
- * @return 编码后的内容
- */
-declare function sha1(data: string): string;
-
-/**
- * SAH256编码
- * @param data 原始内容
- * @return 编码后的内容
- */
-declare function sha256(data: string): string;
-
-/**
- * 生成随机字符串
- * @param size 长度
- * @param chars 可用的字符集,默认为 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
- * @return 生成的随机字符串
- */
-declare function randomstring(size: number, chars?: string): string;
-
-/**
- * 格式化日期时间,格式参考PHP的 date() 函数,文档参考 https://locutus.io/php/datetime/date/
- * @param format 格式
- * @param timestamp 秒时间戳或Date对象
- * @return 日期时间字符串
- */
-declare function formatdate(format: string, timestamp?: number | Date): string;
-
-/**
- * 深度合并两个对象
- * @param target 第一个对象
- * @param src 第二个对象,仅当第一个对象不存在该键才覆盖
- * @return 合并后的新对象
- */
-declare function deepmerge(target: any, src: any): any;
-
-/**
- * 设置环境变量
- * @param name 环境变量名称
- * @param value 环境变量值
- * @return 是否成功
- */
-declare function setenv(name: string, value: string): boolean;
-
-/**
- * 执行命令
- * @param cmd 命令
- * @param env 额外的环境变量
- * @param mode 输出模式,0=直接pipe输出结果到标准输出,1=执行中途不直接输出,存储到返回结果{output}中,2=pipe输出并且存储到返回结果{output}中
- * @return 进程信息
- */
-declare function exec(
- cmd: string,
- env?: Record,
- mode?: number
-): ExecResult;
-
-/**
- * 执行命令 mode=1
- * @param cmd 命令
- * @param env 额外的环境变量
- * @return 进程信息
- */
-declare function exec1(cmd: string, env?: Record): ExecResult;
-
-/**
- * 执行命令 mode=2
- * @param cmd 命令
- * @param env 额外的环境变量
- * @return 进程信息
- */
-declare function exec2(cmd: string, env?: Record): ExecResult;
-
-/**
- * 后台执行命令
- * @param cmd 命令
- * @param env 额外的环境变量
- * @param mode 输出模式,0=直接pipe输出结果到标准输出,1=执行中途不直接输出,存储到返回结果{output}中,2=pipe输出并且存储到返回结果{output}中
- * @return 进程信息
- */
-declare function bgexec(
- cmd: string,
- env?: Record,
- mode?: number
-): ExecResult;
-
-/**
- * 改变当前工作目录
- * @param dir 目录路径
- * @return 是否成功
- */
-declare function chdir(dir: string): boolean;
-
-/**
- * 改变当前工作目录
- * @param dir 目录路径
- * @return 是否成功
- */
-declare function cd(dir: string): boolean;
-
-/**
- * 获取当前工作目录
- * @return 当前工作目录路径
- */
-declare function cwd(): string;
-
-/**
- * 获取当前工作目录
- * @return 当前工作目录路径
- */
-declare function pwd(): string;
-
-/**
- * 获得网络接口信息
- * @return 网络接口数组
- */
-declare function networkinterfaces(): Record;
-
-interface NetworkInterface {
- index: number;
- mac: string;
- list: {
- address: string;
- netmask: string;
- family: string;
- cidr: string;
- internal: boolean;
- multicast: boolean;
- unspecified: boolean;
- }[];
-}
-
-/** 文件相关操作模块 */
-declare const fs: FsModule;
-
-/** 文件路径相关操作模块 */
-declare const path: PathModule;
-
-/** 命令行参数相关操作模块 */
-declare const cli: CliModule;
-
-/** HTTP相关操作模块 */
-declare const http: HttpModule;
-
-/** 日志相关操作模块 */
-declare const log: LogModule;
-
-/** SSH相关操作模块 */
-declare const ssh: SshModule;
-
-/** Socket相关操作模块 */
-declare const socket: SocketModule;
-
-interface FsModule {
- /**
- * 读取目录
- * @param path 路径
- * @return 文件信息列表
- */
- readdir(path: string): FileStat[];
-
- /**
- * 读取文件内容
- * @param path 路径
- * @return 文件内容
- */
- readfile(path: string): string;
-
- /**
- * 读取文件内容
- * @param path 路径
- * @return 文件内容
- */
- readfilebytes(path: string): Uint8Array;
-
- /**
- * 读取文件信息
- * @param path 路径
- * @return 文件信息
- */
- stat(path: string): FileStat;
-
- /**
- * 判断文件是否存在
- * @param path 路径
- * @return 是否存在
- */
- exist(path: string): boolean;
-
- /**
- * 写入文件
- * @param path 路径
- * @param data 内容
- * @return 是否成功
- */
- writefile(path: string, data: string | Uint8Array): boolean;
-
- /**
- * 追加内容到文件末尾
- * @param path 路径
- * @param data 内容
- * @return 是否成功
- */
- appendfile(path: string, data: string | Uint8Array): boolean;
-}
-
-interface FileStat {
- /** 文件名 */
- name: string;
- /** 是否为目录 */
- isdir: boolean;
- /** 文件mode,如0644 */
- mode: number;
- /** 文件最后修改秒时间戳 */
- modtime: number;
- /** 文件大小 */
- size: number;
-}
-
-interface PathModule {
- /**
- * 拼接文件路径
- * @param args 子路径列表
- * @return 拼接后的路径
- */
- join(...args: string[]): string;
-
- /**
- * 获取绝对路径
- * @param path 原始路径
- * @return 绝对路径
- */
- abs(path: string): string;
-
- /**
- * 获取文件名
- * @param path 路径
- * @return 文件名
- */
- base(path: string): string;
-
- /**
- * 获取文件扩展名
- * @param path 路径
- * @return 文件扩展名
- */
- ext(path: string): string;
-
- /**
- * 获取路径的上级目录
- * @param path 路径
- * @return 上级目录
- */
- dir(path: string): string;
-}
-
-interface CliModule {
- /**
- * 获取指定名称的参数
- * @param flag 参数名称
- * @return 参数值
- */
- get(flag: string): string;
-
- /**
- * 获取指定索引的参数
- * @param index 参数索引
- * @return 参数值
- */
- get(index: number): string;
-
- /**
- * 获取指定名称的参数的布尔值,f,false,0表示假,其余为真
- * @param flag 参数名称
- * @return 参数值
- */
- bool(flag: string): boolean;
-
- /**
- * 获取args参数列表
- * @return 参数列表
- */
- args(): string[];
-
- /**
- * 获取opts参数
- * @return 参数Map
- */
- opts(): Record;
-
- /**
- * 等待用户输入一行,按Enter确认
- * @param message 提示
- * @return 输入的内容
- */
- prompt(message?: string): string;
-
- /**
- * 注册子命令处理函数
- * @param name 命令名称,* 表示其他情况
- * @param callback 回调函数
- */
- subcommand(name: string, callback: () => void): void;
-
- /**
- * 开始解析执行子命令
- */
- subcommandstart(): void;
-}
-
-interface HttpModule {
- /**
- * 设置HTTP请求的超时时间
- * @param milliseconds 毫秒
- * @return 毫秒
- */
- timeout(milliseconds: number): number;
-
- /**
- * 发送HTTP请求
- * @param method 请求方法
- * @param url 请求URL
- * @param headers 请求头
- * @param body 请求体
- * @return 响应结果
- */
- request(
- method: string,
- url: String,
- headers?: Record,
- body?: string
- ): HttpResponse;
-
- /**
- * 通过HTTP下载文件
- * @param url 文件地址
- * @param filename 目标文件名,如果未指定则创建临时文件
- * @return 存储的文件名
- */
- download(url: string, filename?: string): string;
-
- /**
- * 发送HTTP GET请求
- * @param url 请求URL
- * @param headers 请求头
- * @return 响应结果
- */
- get(url: String, headers?: Record): HttpResponse;
-
- /**
- * 发送HTTP HEAD请求
- * @param url 请求URL
- * @param headers 请求头
- * @return 响应结果
- */
- head(url: String, headers?: Record): HttpResponse;
-
- /**
- * 发送HTTP OPTIONS请求
- * @param url 请求URL
- * @param headers 请求头
- * @return 响应结果
- */
- options(url: String, headers?: Record): HttpResponse;
-
- /**
- * 发送HTTP POST请求
- * @param url 请求URL
- * @param headers 请求头
- * @param body 请求体
- * @return 响应结果
- */
- post(
- url: String,
- headers?: Record,
- body?: string
- ): HttpResponse;
-
- /**
- * 发送HTTP PUT请求
- * @param url 请求URL
- * @param headers 请求头
- * @param body 请求体
- * @return 响应结果
- */
- put(
- url: String,
- headers?: Record,
- body?: string
- ): HttpResponse;
-
- /**
- * 发送HTTP DELETE请求
- * @param url 请求URL
- * @param headers 请求头
- * @param body 请求体
- * @return 响应结果
- */
- delete(
- url: String,
- headers?: Record,
- body?: string
- ): HttpResponse;
-}
-
-interface HttpResponse {
- /** 状态码 */
- status: number;
- /** 响应头 */
- headers: Record;
- /** 响应体 */
- body: string;
-}
-
-interface LogModule {
- /**
- * 日志输出到控制台
- * @param format 模板,支持%s等格式
- * @param args 参数列表
- * @return 是否成功
- */
- debug(format: any, ...args: any[]): boolean;
-
- /**
- * 日志输出到控制台
- * @param format 模板,支持%s等格式
- * @param args 参数列表
- * @return 是否成功
- */
- info(format: any, ...args: any[]): boolean;
-
- /**
- * 日志输出到控制台
- * @param format 模板,支持%s等格式
- * @param args 参数列表
- * @return 是否成功
- */
- warn(format: any, ...args: any[]): boolean;
-
- /**
- * 日志输出到控制台
- * @param format 模板,支持%s等格式
- * @param args 参数列表
- * @return 是否成功
- */
- error(format: any, ...args: any[]): boolean;
-
- /**
- * 日志输出到控制台,并结束进程
- * @param format 模板,支持%s等格式
- * @param args 参数列表
- * @return 是否成功
- */
- fatal(format: any, ...args: any[]): boolean;
-}
-
-interface ExecResult {
- /**
- * 进程PID
- */
- pid: number;
- /**
- * 进程退出code
- */
- code?: number;
- /**
- * 进程输出内容,仅当combineOutput=true时有效
- */
- output?: string;
- /**
- * 进出输出内容字节数,仅当combineOutput=true时有效
- */
- outputbytes?: number;
-}
-
-interface SshModule {
- /**
- * 设置SSH配置
- * @param name 配置名
- * @param value 配置值
- * @return 是否成功
- */
- set(
- name: "user" | "password" | "key" | "keypass" | "auth" | "timeout" | "port",
- value: any
- ): boolean;
-
- /**
- * 连接到指定服务器
- * @param host 服务器地址
- * @return 是否成功
- */
- open(host: string): boolean;
-
- /**
- * 关闭服务器连接
- * @return 是否成功
- */
- close(): boolean;
-
- /**
- * 设置环境变量
- * @param name 环境变量名称
- * @param value 环境变量值
- * @return 是否成功
- */
- setenv(name: string, value: string): boolean;
-
- /**
- * 执行命令
- * @param cmd 命令
- * @param env 额外的环境变量
- * @param mode 输出模式,0=直接pipe输出结果到标准输出,1=执行中途不直接输出,存储到返回结果{output}中,2=pipe输出并且存储到返回结果{output}中
- * @return 进程信息
- */
- exec(cmd: string, env?: Record, mode?: number): SshExecResult;
-
- /**
- * 执行命令 mode=1
- * @param cmd 命令
- * @param env 额外的环境变量
- * @return 进程信息
- */
- exec1(cmd: string, env?: Record): SshExecResult;
-
- /**
- * 执行命令 mode=2
- * @param cmd 命令
- * @param env 额外的环境变量
- * @return 进程信息
- */
- exec2(cmd: string, env?: Record): SshExecResult;
-
- /**
- * 模拟TTY设备执行命令
- * @param cmd 命令
- * @param env 额外的环境变量
- * @return 进程信息
- */
- pty(cmd: string, env?: Record): SshExecResult;
-}
-
-interface SshExecResult {
- /**
- * 进程退出code
- */
- code?: number;
- /**
- * 进程输出内容,仅当combineOutput=true时有效
- */
- output?: string;
- /**
- * 进出输出内容字节数,仅当combineOutput=true时有效
- */
- outputbytes?: number;
-}
-
-interface SocketModule {
- /**
- * 设置超时时间
- * @param milliseconds 毫秒
- * @return 毫秒
- */
- timeout(milliseconds: number): number;
-
- /**
- * TCP发送消息
- * @param host 地址
- * @param port 端口
- * @param data 数据
- * @return 服务器返回的内容
- */
- tcpsend(host: string, port: number, data: string): string;
-
- /**
- * TCP端口连接测试
- * @param host 地址
- * @param port 端口
- * @return 是否成功
- */
- tcptest(host: string, port: number): boolean;
-}
-
-interface SqlExecResult {
- lastInsertId: number;
- rowsAffected: number;
-}
-
-/**
- * 简单断言
- * @param ok 要断言的值
- * @param message 提示信息
- */
-declare function assert(ok: boolean, message?: string);
diff --git a/package.json b/package.json
index 15f3837e2..dcf8cce84 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"main": "package.json",
"scripts": {
"build": "node build/prepare.js && tsc -b --verbose",
+ "build:installer": "esbuild ./install/install.js --minify --format=cjs | base64",
"build:watch": "node build/prepare.js && tsc -b --watch",
"build:ui": "yarn build:ui:gulp && node packages/ui-default/build",
"build:ui:gulp": "node packages/ui-default/build --gulp",