一.介绍

Hydro 是一个现代化、高性能的在线评测系统(Online Judge)核心框架与社区平台。它不像传统 OJ 那样笨重封闭,而是像乐高一样模块化、可自由拼装——你可以用它轻松搭建从算法竞赛、作业提交到在线考试的任何评测场景。其核心设计极致追求效率与公平,采用分布式沙箱确保判题快速安全,同时支持实时榜单、比赛克隆等酷炫功能。

二.安装脚本setup.sh

hydro的github地址: https://github.com/hydro-dev/Hydro

hydro官方在github上提供的安装脚本如下:

LANG=zh . <(curl https://hydro.ac/setup.sh)

这个sh脚本下载下来后如下:

#!/bin/bash
if [ $EUID != 0 ]; then
    echo "This script requires root however you are currently running under another user."
    echo "Please use 'sudo su' to switch to root user before running this script."
    # echo "We will call sudo directly for you."
    # echo "Please input your account password below:"
    # echo "安装脚本需要使用 root 权限,请在下方输入此账号的密码确认授权:"
    echo "安装脚本需要使用 root 权限,请先使用 sudo su 切换到 root 用户后再运行此脚本。"
    # sudo "$0" "$@"
    exit $?
fi
set -e
echo "Executing Hydro install script v3.0.1"
echo "Hydro includes 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.coreutils 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
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
dRn4XjNgDHABAIdr6q4qbwAA
EOF123
cat /tmp/install.b64 | base64 -d | gunzip >/tmp/install.js
node /tmp/install.js "$@"
set +e

对于上面的这个安装脚本,解释如下:

1. 根权限校验段

if [ $EUID != 0 ]; then
    echo "This script requires root however you are currently running under another user."
    echo "Please use 'sudo su' to switch to root user before running this script."
    echo "安装脚本需要使用 root 权限,请先使用 sudo su 切换到 root 用户后再运行此脚本。"
    exit $?
fi
  • ​核心作用​:校验当前执行脚本的用户是否为 root 管理员(EUID​ 是有效用户 ID,root 用户的 EUID 为 0)。
  • ​逻辑说明​:如果当前用户不是 root,会输出中英文提示信息,告知用户需要通过 sudo su​ 切换到 root 权限,随后退出脚本执行,确保后续安装操作拥有足够的系统权限。
  • ​补充​:脚本中注释了自动调用 sudo​ 授权的逻辑,当前采用手动切换 root 的方式。

2. 脚本执行模式配置

set -e
  • ​核心作用​:设置脚本的 “严格执行模式”,​一旦脚本中任何一条命令执行失败(返回非 0 退出码),脚本会立即终止执行​,避免错误累积导致后续无效操作或系统异常。

3. 脚本信息提示与目录创建

echo "Executing Hydro install script v3.0.1"
echo "Hydro includes 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
  • ​信息提示​:输出脚本版本和 Hydro 的系统遥测功能说明(告知用户该功能用于统计系统使用情况,可通过源码禁用)。
  • ​目录创建​:通过 mkdir -p​ 命令创建所需目录(-p​ 确保目录不存在时创建,已存在时不报错):
    • ​/data/db​:Hydro 的数据库存储目录;
    • ​/data/file​:Hydro 的文件存储目录;
    • ​~/.hydro​:Hydro 的用户级配置目录(~​ 代表当前用户主目录)。

4. Nix 包管理器安装

bash <(curl https://hydro.ac/nix.sh)
  • ​核心作用​:通过 curl​ 下载 Hydro 提供的 nix.sh​ 脚本,并直接通过 bash​ 执行,​自动安装 Nix 包管理器​(Nix 是一个跨平台的包管理器,用于快速部署依赖环境,避免依赖冲突)。
  • ​执行方式​:<(curl …)​ 是进程替换语法,将 curl 下载的脚本内容作为 bash 的输入源,无需先保存为本地文件再执行。

5. 环境变量配置与依赖安装

export PATH=$HOME/.nix-profile/bin:$PATH
nix-env -iA nixpkgs.nodejs nixpkgs.coreutils nixpkgs.qrencode
  • ​环境变量配置​:将 Nix 包管理器的二进制程序目录($HOME/.nix-profile/bin​)添加到系统 PATH​ 环境变量的最前面,确保系统优先使用 Nix 安装的工具。
  • ​依赖安装​:通过 nix-env -iA​ 命令从 Nix 软件源(nixpkgs)中安装指定依赖:
    • ​nodejs​:JavaScript 运行环境,Hydro 核心程序依赖;
    • ​coreutils​:基础系统工具集(如 ls、cp 等,确保环境兼容性);
    • ​qrencode​:二维码生成工具,用于后续生成 QQ 群二维码。

6. QQ 群二维码展示

echo "扫码加入QQ群:"
echo https://qm.qq.com/cgi-bin/qm/qr\?k\=0aTZfDKURRhPBZVpTYBohYG6P6sxABTw | qrencode -o - -m 2 -t UTF8
  • ​核心作用​:生成并在终端中以 UTF-8 字符形式展示 Hydro 交流 QQ 群的二维码,方便用户扫码加入。
  • ​参数说明​:
    • ​qrencode -o -​:将二维码输出到标准输出(终端),而非文件;
    • ​-m 2​:设置二维码边距为 2 个字符;
    • ​-t UTF8​:以 UTF-8 字符格式生成二维码(可在终端直接显示);
    • 链接中的 \?​ 和 \&​ 是转义字符,避免 Shell 将 ?​ &​ 解析为特殊符号。

7. 核心安装程序的解码与执行

# 创建安装脚本文件并写入注释
echo "// File created by Hydro install script\n" >/tmp/install.js

# 将 Base64 压缩数据写入临时文件
cat >/tmp/install.b64 << EOF123
# 此处省略大量 Base64 编码内容
EOF123

# 解码+解压后生成可执行的 JS 脚本,再用 Node.js 执行
cat /tmp/install.b64 | base64 -d | gunzip >/tmp/install.js
node /tmp/install.js "$@"

这是脚本的核心执行流程,分为 3 步:

  1. 初始化 JS 文件​:在 /tmp​ 目录(临时目录,重启后自动清理)创建 install.js​,写入注释信息;
  2. ​存储 Base64 压缩数据​:将 Hydro 核心安装逻辑的 Base64 编码压缩数据写入 /tmp/install.b64​(EOF123​ 是 Here Document 语法,用于批量写入多行内容);
  3. ​解码解压 + 执行​:
    • ​base64 -d​:对 Base64 编码数据进行解码,还原为原始压缩数据;
    • ​gunzip​:对解码后的 gzip 压缩数据进行解压,得到完整的 Node.js 安装脚本,并重定向到 /tmp/install.js​;
    • ​node /tmp/install.js “$@”​:用 Node.js 执行解压后的安装脚本,”$@”​ 表示将当前脚本接收的所有命令行参数传递给 install.js​。
8. 脚本收尾
set +e
  • ​核心作用​:关闭之前通过 set -e​ 开启的 “命令失败即终止” 模式,恢复 Shell 脚本的默认执行行为(后续即使有命令失败,脚本也不会强制退出,此处用于收尾清理)。
三、核心安装程序解析

上面的第7步中,有一段核心安装程序,这是使用base64编码的一段数据,这段数据会生成一个install.js的安装脚本在服务器中,我使用了python脚本同样解析一下这段数据。

# 简单示例:解码+解压
import base64
import gzip

def base64_gunzip(input_path, output_path):
    with open(input_path, 'r') as f:
        b64_data = f.read()
    # Base64解码
    compressed_data = base64.b64decode(b64_data)
    # gzip解压
    uncompressed_data = gzip.decompress(compressed_data)
    # 写入输出文件
    with open(output_path, 'wb') as f:
        f.write(uncompressed_data)
    print(f"处理完成:{input_path} → {output_path}")

# 调用示例
base64_gunzip("install.b64", "install.js")

使用方式很简单,把上面第7步省略的内容复制到txt文档中,重命名为install.b64。然后和上面的python文件放在同一个目录下

├── 目标文件夹/          # 任意文件夹均可(桌面、文档文件夹等)
│   ├── base64_gunzip.py  # 你的Python脚本(保存上述代码)
│   ├── install.b64       # 待处理的Base64编码压缩文件(从sh脚本中提取的Base64数据)
│   └── install.js       # 处理后生成的JS文件(无需手动创建,脚本会自动生成)

运行python文件后就可以得到install.js文件,和官方生成的install文件对比一下,看看这个过程是否有问题。

通过上面的文件可以看到,两个文件对比一致,说明解析这段数据没有问题。

四、install.js解析

得到上面的install.js文件如下:

var pe=Object.create;var I=Object.defineProperty;var me=Object.getOwnPropertyDescriptor;var fe=Object.getOwnPropertyNames;var he=Object.getPrototypeOf,ye=Object.prototype.hasOwnProperty;var ge=(e,t)=>{for(var r in t)I(e,r,{get:t[r],enumerable:!0})},D=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of fe(t))!ye.call(e,s)&&s!==r&&I(e,s,{get:()=>t[s],enumerable:!(o=me(t,s))||o.enumerable});return e};var h=(e,t,r)=>(r=e!=null?pe(he(e)):{},D(t||!e||!e.__esModule?I(r,"default",{value:e,enumerable:!0}):r,e)),be=e=>D(I({},"__esModule",{value:!0}),e);var Le={};ge(Le,{link:()=>M});module.exports=be(Le);var ne=require("child_process"),L=h(require("crypto")),a=require("fs"),ie=h(require("net")),j=h(require("os")),ae=require("readline/promises");var O=h(require("node:process"),1);var k=h(require("node:process"),1),B=h(require("node:os"),1),A=h(require("node:tty"),1);function m(e,t=globalThis.Deno?globalThis.Deno.args:k.default.argv){let r=e.startsWith("-")?"":e.length===1?"-":"--",o=t.indexOf(r+e),s=t.indexOf("--");return o!==-1&&(s===-1||o<s)}var{env:l}=k.default,S;m("no-color")||m("no-colors")||m("color=false")||m("color=never")?S=0:(m("color")||m("colors")||m("color=true")||m("color=always"))&&(S=1);function we(){if(!("FORCE_COLOR"in l))return;if(l.FORCE_COLOR==="true")return 1;if(l.FORCE_COLOR==="false")return 0;if(l.FORCE_COLOR.length===0)return 1;let e=Math.min(Number.parseInt(l.FORCE_COLOR,10),3);if([0,1,2,3].includes(e))return e}function xe(e){return e===0?!1:{level:e,hasBasic:!0,has256:e>=2,has16m:e>=3}}function ve(e,{streamIsTTY:t,sniffFlags:r=!0}={}){let o=we();o!==void 0&&(S=o);let s=r?S:o;if(s===0)return 0;if(r){if(m("color=16m")||m("color=full")||m("color=truecolor"))return 3;if(m("color=256"))return 2}if("TF_BUILD"in l&&"AGENT_NAME"in l)return 1;if(e&&!t&&s===void 0)return 0;let c=s||0;if(l.TERM==="dumb")return c;if(k.default.platform==="win32"){let d=B.default.release().split(".");return Number(d[0])>=10&&Number(d[2])>=10586?Number(d[2])>=14931?3:2:1}if("CI"in l)return["GITHUB_ACTIONS","GITEA_ACTIONS","CIRCLECI"].some(d=>d in l)?3:["TRAVIS","APPVEYOR","GITLAB_CI","BUILDKITE","DRONE"].some(d=>d in l)||l.CI_NAME==="codeship"?1:c;if("TEAMCITY_VERSION"in l)return/^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(l.TEAMCITY_VERSION)?1:0;if(l.COLORTERM==="truecolor"||l.TERM==="xterm-kitty")return 3;if("TERM_PROGRAM"in l){let d=Number.parseInt((l.TERM_PROGRAM_VERSION||"").split(".")[0],10);switch(l.TERM_PROGRAM){case"iTerm.app":return d>=3?3:2;case"Apple_Terminal":return 2}}return/-256(color)?$/i.test(l.TERM)?2:/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(l.TERM)||"COLORTERM"in l?1:c}function _(e,t={}){let r=ve(e,{streamIsTTY:e&&e.isTTY,...t});return xe(r)}var Be={stdout:_({isTTY:A.default.isatty(1)}),stderr:_({isTTY:A.default.isatty(2)})};var U=h(require("process"),1);function y(e,t=U.default.argv){let r=e.startsWith("-")?"":e.length===1?"-":"--",o=t.indexOf(r+e),s=t.indexOf("--");return o!==-1&&(s===-1||o<s)}function G(e=""){if(/^\d{3,4}$/.test(e)){let r=/(\d{1,2})(\d{2})/.exec(e)??[];return{major:0,minor:Number.parseInt(r[1],10),patch:Number.parseInt(r[2],10)}}let t=(e??"").split(".").map(r=>Number.parseInt(r,10));return{major:t[0],minor:t[1],patch:t[2]}}function z(e){let{CI:t,CURSOR_TRACE_ID:r,FORCE_HYPERLINK:o,NETLIFY:s,TEAMCITY_VERSION:c,TERM_PROGRAM:d,TERM_PROGRAM_VERSION:$,VTE_VERSION:u,TERM:ue}=O.default.env;if(o)return!(o.length>0&&Number.parseInt(o,10)===0);if(y("no-hyperlink")||y("no-hyperlinks")||y("hyperlink=false")||y("hyperlink=never"))return!1;if(y("hyperlink=true")||y("hyperlink=always")||s)return!0;if(!_(e)||e&&!e.isTTY)return!1;if("WT_SESSION"in O.default.env)return!0;if(O.default.platform==="win32"||t||c)return!1;if(d){let f=G($);switch(d){case"iTerm.app":return f.major===3?f.minor>=1:f.major>3;case"WezTerm":return f.major>=20200620;case"vscode":return r?!0:f.major>1||f.major===1&&f.minor>=72;case"ghostty":return!0}}if(u){if(u==="0.50.0")return!1;let f=G(u);return f.major>0||f.minor>=50}switch(ue){case"alacritty":return!0}return!1}var Oe={stdout:z(O.default.stdout),stderr:z(O.default.stderr)},J=Oe;var Ee=J.stdout,q="\x1B]",Y="\x07",N=";",M=Ee?(e,t)=>[q,"8",N,N,t,Y,e,q,"8",N,N,Y].join(""):(e,t)=>`${e} < ${t} > `,Ce=j.default.freemem(),je=Ce<1024*1024*1024,T=(...e)=>je?e.map(t=>`nix-env -iA ${t.includes(".")?t:`nixpkgs.${t}`}`).join(" && "):`nix-env -iA ${e.map(t=>t.includes(".")?t:`nixpkgs.${t}`).join(" ")}`,b=[],n=(e,t)=>{try{return{output:(0,ne.execSync)(e,t).toString(),code:0}}catch(r){return{code:r.status,message:r.message}}},x=e=>new Promise(t=>{setTimeout(t,e)}),W="https://docs.hydro.ac/FAQ/#%E8%B0%83%E6%95%B4%E4%B8%B4%E6%97%B6%E7%9B%AE%E5%BD%95%E5%A4%A7%E5%B0%8F",Te={zh:{"install.wait":"\u5B89\u88C5\u811A\u672C\u5C06\u7B49\u5F85 %d \u79D2\u540E\u81EA\u52A8\u7EE7\u7EED\u5B89\u88C5\uFF0C\u6216\u6309 Ctrl-C \u9000\u51FA\u3002","install.start":"\u5F00\u59CB\u8FD0\u884C Hydro \u5B89\u88C5\u5DE5\u5177","note.avx":`\u68C0\u6D4B\u5230\u60A8\u7684 CPU \u4E0D\u652F\u6301 avx \u6307\u4EE4\u96C6\uFF0C\u8FD9\u53EF\u80FD\u4F1A\u5F71\u54CD\u7CFB\u7EDF\u8FD0\u884C\u901F\u5EA6\u3002
\u5982\u679C\u60A8\u6B63\u5728\u4F7F\u7528 PVE/VirtualBox \u7B49\u865A\u62DF\u673A\u5E73\u53F0\uFF0C\u8BF7\u5C1D\u8BD5\u5173\u673A\u540E\u5C06\u865A\u62DF\u673A\u7684 CPU \u7C7B\u578B\u8BBE\u7F6E\u4E3A Host\uFF0C\u91CD\u542F\u540E\u518D\u6B21\u8FD0\u884C\u8BE5\u811A\u672C\u3002`,"warn.avx":"\u68C0\u6D4B\u5230\u60A8\u7684 CPU \u4E0D\u652F\u6301 avx \u6307\u4EE4\u96C6\uFF0C\u5C06\u4F7F\u7528 mongodb@v4.4","error.rootRequired":"\u8BF7\u5148\u4F7F\u7528 sudo su \u5207\u6362\u5230 root \u7528\u6237\u540E\u518D\u8FD0\u884C\u8BE5\u5DE5\u5177\u3002","error.unsupportedArch":"\u4E0D\u652F\u6301\u7684\u67B6\u6784 %s ,\u8BF7\u5C1D\u8BD5\u624B\u52A8\u5B89\u88C5\u3002","error.osreleaseNotFound":"\u65E0\u6CD5\u83B7\u53D6\u7CFB\u7EDF\u7248\u672C\u4FE1\u606F\uFF08/etc/os-release \u6587\u4EF6\u672A\u627E\u5230\uFF09\uFF0C\u8BF7\u5C1D\u8BD5\u624B\u52A8\u5B89\u88C5\u3002","error.unsupportedOS":"\u4E0D\u652F\u6301\u7684\u64CD\u4F5C\u7CFB\u7EDF %s \uFF0C\u8BF7\u5C1D\u8BD5\u624B\u52A8\u5B89\u88C5\uFF0C","error.centos":"CentOS \u53CA\u5176\u53D8\u79CD\u7CFB\u7EDF\u56E0\u7CFB\u7EDF\u5185\u6838\u8FC7\u4F4E\uFF0C\u65E0\u6CD5\u5B89\u88C5 Hydro\uFF0C\u5F3A\u70C8\u5EFA\u8BAE\u4F7F\u7528\u5176\u4ED6\u7CFB\u7EDF\u3002\u82E5\u786E\u6709\u9700\u6C42\uFF0C\u8BF7\u5347\u7EA7 Linux \u5185\u6838\u81F3 4.4+ \u540E\u518D\u624B\u52A8\u5B89\u88C5 Hydro\u3002","install.preparing":"\u6B63\u5728\u521D\u59CB\u5316\u5B89\u88C5...","install.mongodb":"\u6B63\u5728\u5B89\u88C5 mongodb...","install.createDatabaseUser":"\u6B63\u5728\u521B\u5EFA\u6570\u636E\u5E93\u7528\u6237...","install.compiler":"\u6B63\u5728\u5B89\u88C5\u7F16\u8BD1\u5668...","install.hydro":"\u6B63\u5728\u5B89\u88C5 Hydro...","install.done":"Hydro \u5B89\u88C5\u6210\u529F\uFF01","install.alldone":"\u5B89\u88C5\u5DF2\u5168\u90E8\u5B8C\u6210\u3002","install.editJudgeConfigAndStart":"\u8BF7\u7F16\u8F91 ~/.hydro/judge.yaml \u540E\u4F7F\u7528 pm2 start hydrojudge && pm2 save \u542F\u52A8\u3002","extra.dbUser":"\u6570\u636E\u5E93\u7528\u6237\u540D\uFF1A hydro","extra.dbPassword":"\u6570\u636E\u5E93\u5BC6\u7801\uFF1A %s","port.80":"\u7AEF\u53E3 80 \u5DF2\u88AB\u5360\u7528\uFF0CCaddy \u65E0\u6CD5\u6B63\u5E38\u76D1\u542C\u6B64\u7AEF\u53E3\u3002","shm.readFail":"\u8BFB\u53D6 /dev/shm \u5927\u5C0F\u5931\u8D25\u3002\u8BF7\u68C0\u67E5\u7CFB\u7EDF\u662F\u5426\u5728\u6B64\u6302\u8F7D\u4E86 tmpfs\u3002","shm.sizeTooSmall":`\u60A8\u7684\u7CFB\u7EDF /dev/shm \u5927\u5C0F\u4E3A %d MB\uFF0C\u5728\u9AD8\u5E76\u53D1\u8BC4\u6D4B\u65F6\u53EF\u80FD\u4EA7\u751F\u95EE\u9898\u3002
\u5EFA\u8BAE\u53C2\u7167\u6587\u6863 ${M("FAQS",W)} \u8FDB\u884C\u8C03\u6574\u3002`,"info.skip":"\u6B65\u9AA4\u5DF2\u8DF3\u8FC7\u3002","error.bt":`\u68C0\u6D4B\u5230\u5B9D\u5854\u9762\u677F\uFF0C\u5B89\u88C5\u811A\u672C\u5F88\u53EF\u80FD\u65E0\u6CD5\u6B63\u5E38\u5DE5\u4F5C\u3002\u5EFA\u8BAE\u60A8\u4F7F\u7528\u7EAF\u51C0\u7684 Debian 12 \u7CFB\u7EDF\u8FDB\u884C\u5B89\u88C5\u3002
\u8981\u5FFD\u7565\u8BE5\u8B66\u544A\uFF0C\u8BF7\u4F7F\u7528 --shamefully-unsafe-bt-panel \u53C2\u6570\u91CD\u65B0\u8FD0\u884C\u6B64\u811A\u672C\u3002`,"warn.bt":`\u68C0\u6D4B\u5230\u5B9D\u5854\u9762\u677F\uFF0C\u8FD9\u4F1A\u5BF9\u7CFB\u7EDF\u5B89\u5168\u6027\u4E0E\u7A33\u5B9A\u6027\u9020\u6210\u5F71\u54CD\u3002\u5EFA\u8BAE\u4F7F\u7528\u7EAF\u51C0 Debian 12 \u7CFB\u7EDF\u8FDB\u884C\u5B89\u88C5\u3002
\u5F00\u53D1\u8005\u5BF9\u56E0\u4E3A\u4F7F\u7528\u5B9D\u5854\u9762\u677F\u7684\u6570\u636E\u4E22\u5931\u4E0D\u627F\u62C5\u4EFB\u4F55\u8D23\u4EFB\u3002
\u8981\u53D6\u6D88\u5B89\u88C5\uFF0C\u8BF7\u4F7F\u7528 Ctrl-C \u9000\u51FA\u3002\u5B89\u88C5\u7A0B\u5E8F\u5C06\u5728\u4E94\u79D2\u540E\u7EE7\u7EED\u3002`,"migrate.hustojFound":`\u68C0\u6D4B\u5230 HustOJ\u3002\u5B89\u88C5\u7A0B\u5E8F\u53EF\u4EE5\u5C06 HustOJ \u4E2D\u7684\u5168\u90E8\u6570\u636E\u5BFC\u5165\u5230 Hydro\u3002\uFF08\u539F\u6709\u6570\u636E\u4E0D\u4F1A\u4E22\u5931\uFF0C\u60A8\u53EF\u968F\u65F6\u5207\u6362\u56DE HustOJ\uFF09
\u8BE5\u529F\u80FD\u652F\u6301\u539F\u7248 HustOJ \u548C\u90E8\u5206\u4FEE\u6539\u7248\uFF0C\u8F93\u5165 y \u786E\u8BA4\u8BE5\u64CD\u4F5C\u3002
\u8FC1\u79FB\u8FC7\u7A0B\u6709\u4EFB\u4F55\u95EE\u9898\uFF0C\u6B22\u8FCE\u52A0QQ\u7FA4 1085853538 \u54A8\u8BE2\u7BA1\u7406\u5458\u3002`,"install.restartRequired":"\u5B89\u88C5\u5B8C\u6210\uFF0C\u8BF7\u4F7F\u7528 sudo reboot \u91CD\u542F\u7CFB\u7EDF\u3002\u5728\u6B64\u4E4B\u524D\u7CFB\u7EDF\u7684\u90E8\u5206\u529F\u80FD\u53EF\u80FD\u65E0\u6CD5\u6B63\u5E38\u4F7F\u7528\u3002","install.warnings":"\u5B89\u88C5\u8FC7\u7A0B\u4E2D\u4EA7\u751F\u4E86\u4EE5\u4E0B\u8B66\u544A\uFF1A"},en:{"install.wait":`The installation script will wait for %d seconds before continuing.
Press Ctrl-C to exit.`,"install.start":"Starting Hydro installation tool","note.avx":`Your CPU does not support avx, this may affect system performance.
If you are using a virtual machine platform such as PVE/VirtualBox,
try shutting down and setting the CPU type of the virtual machine to Host,
then restart and run the script again.`,"warn.avx":"Your CPU does not support avx, 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.","error.centos":`CentOS and its derivatives are not supported due to low system kernel versions.
It is strongly recommended to use other systems. If you really need it, please upgrade the Linux kernel to 4.4+ and manually install Hydro.`,"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!","install.alldone":"Hydro installation completed.","install.editJudgeConfigAndStart":`Please edit config at ~/.hydro/judge.yaml than start hydrojudge with:
pm2 start hydrojudge && pm2 save.`,"extra.dbUser":"Database username: hydro","extra.dbPassword":"Database password: %s","port.80":"Port 80 is already in use, Caddy cannot listen to this port.","shm.readFail":"Failed to read /dev/shm size.","shm.sizeTooSmall":`Your system /dev/shm size is %d MB, which may cause problems in high concurrency testing.
Please refer to ${M("FAQS",W)} for adjustments.`,"info.skip":"Step skipped.","error.bt":`BT-Panel detected, this script may not work properly. It is recommended to use a clean Debian 12 OS.
To ignore this warning, please run this script again with '--shamefully-unsafe-bt-panel' flag.`,"warn.bt":`BT-Panel detected, this will affect system security and stability. It is recommended to use a clean Debian 12 OS.
The developer is not responsible for any data loss caused by using BT-Panel.
To cancel the installation, please use Ctrl-C to exit. The installation program will continue in five seconds.`,"migrate.hustojFound":`HustOJ detected. The installation program can migrate all data from HustOJ to Hydro.
The original data will not be lost, and you can switch back to HustOJ at any time.
This feature supports the original version of HustOJ and some modified versions. Enter y to confirm this operation.
If you have any questions about the migration process, please add QQ group 1085853538 to consult the administrator.`,"install.restartRequired":"Please reboot the system. Some functions may not work properly before the restart.","install.warnings":"The following warnings occurred during the installation:"}},p=process.argv.includes("--judge"),V=process.argv.includes("--no-caddy"),Re=process.argv.includes("--expose-db"),le=["@hydrooj/ui-default","@hydrooj/hydrojudge","@hydrooj/fps-importer","@hydrooj/a11y"],H=p?"@hydrooj/hydrojudge":`hydrooj ${le.join(" ")}`,Q=process.argv.find(e=>e.startsWith("--substituters=")),K=Q?Q.split("=")[1].split(","):[],Z=process.argv.find(e=>e.startsWith("--migration=")),w=Z?Z.split("=")[1]:"",X=!1,ce=process.env.LANG?.includes("zh")||process.env.LOCALE?.includes("zh")?"zh":"en";process.env.TERM==="linux"&&(ce="en");var P=e=>(t,...r)=>(e(Te[ce][t]||t,...r),0),i={info:P(console.log),warn:P(console.warn),fatal:(e,...t)=>(P(console.error)(e,...t),process.exit(1))};process.getuid?process.getuid()!==0&&i.fatal("error.rootRequired"):i.fatal("error.unsupportedOs");["x64","arm64"].includes(process.arch)||i.fatal("error.unsupportedArch",process.arch);process.env.HOME||i.fatal("$HOME not found");(0,a.existsSync)("/etc/os-release")||i.fatal("error.osreleaseNotFound");var $e=(0,a.readFileSync)("/etc/os-release","utf-8"),Ie=$e.split(`
`),ee={};for(let e of Ie){if(!e.trim())continue;let t=e.split("=");t[1].startsWith('"')?ee[t[0].toLowerCase()]=t[1].substring(1,t[1].length-2):ee[t[0].toLowerCase()]=t[1]}var F=!0,Se=(0,a.readFileSync)("/proc/cpuinfo","utf-8");!Se.includes("avx")&&!p&&(F=!1,i.warn("warn.avx"),b.push(["warn.avx"]));var E=0;i.info("install.start");var v=L.default.randomBytes(32).toString("hex"),C=!0,R=`${process.env.HOME}/.nix-profile/`,g=(e,t=e,r=!0)=>`  - type: bind
    source: ${e}
    target: ${t}${r?`
    readonly: true`:""}`,_e=`mount:
${g(`${R}bin`,"/bin")}
${g(`${R}bin`,"/usr/bin")}
${g(`${R}lib`,"/lib")}
${g(`${R}share`,"/share")}
${g(`${R}etc`,"/etc")}
${g("/nix","/nix")}
${g("/dev/null","/dev/null",!1)}
${g("/dev/urandom","/dev/urandom",!1)}
  - 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
`,ke=`# \u5982\u679C\u4F60\u5E0C\u671B\u4F7F\u7528\u5176\u4ED6\u7AEF\u53E3\u6216\u4F7F\u7528\u57DF\u540D\uFF0C\u4FEE\u6539\u6B64\u5904 :80 \u7684\u503C\u540E\u5728 ~/.hydro \u76EE\u5F55\u4E0B\u4F7F\u7528 caddy reload \u91CD\u8F7D\u914D\u7F6E\u3002
# \u5982\u679C\u4F60\u5728\u5F53\u524D\u914D\u7F6E\u4E0B\u80FD\u591F\u901A\u8FC7 http://\u4F60\u7684\u57DF\u540D/ \u6B63\u5E38\u8BBF\u95EE\u5230\u7F51\u7AD9\uFF0C\u82E5\u9700\u5F00\u542F ssl\uFF0C
# \u4EC5\u9700\u5C06 :80 \u6539\u4E3A\u4F60\u7684\u57DF\u540D\uFF08\u5982 hydro.ac\uFF09\u540E\u4F7F\u7528 caddy reload \u91CD\u8F7D\u914D\u7F6E\u5373\u53EF\u81EA\u52A8\u7B7E\u53D1 ssl \u8BC1\u4E66\u3002
# \u586B\u5199\u5B8C\u6574\u57DF\u540D\uFF0C\u6CE8\u610F\u533A\u5206\u6709\u65E0 www \uFF08www.hydro.ac \u548C hydro.ac \u4E0D\u540C\uFF0C\u8BF7\u68C0\u67E5 DNS \u8BBE\u7F6E\uFF09
# \u8BF7\u6CE8\u610F\u5728\u9632\u706B\u5899/\u5B89\u5168\u7EC4\u4E2D\u653E\u884C\u7AEF\u53E3\uFF0C\u4E14\u90E8\u5206\u8FD0\u8425\u5546\u4F1A\u62E6\u622A\u672A\u7ECF\u5907\u6848\u7684\u57DF\u540D\u3002
# \u5176\u4ED6\u9700\u6C42\u6E05\u53C2\u7167 https://caddyserver.com/docs/ \u8BF4\u660E\u8FDB\u884C\u8BBE\u7F6E\u3002
# For more information, refer to caddy v2 documentation.
:80 {
  encode zstd gzip
  log {
    output file /data/access.log {
      roll_size 1gb
      roll_keep_for 72h
    }
    format json
  }
  # Handle static files directly, for better performance.
  root * /root/.hydro/static
  @static {
    file {
      try_files {path}
    }
  }
  handle @static {
    file_server
  }
  handle {
    reverse_proxy http://127.0.0.1:8888
  }
}

# \u5982\u679C\u4F60\u9700\u8981\u540C\u65F6\u914D\u7F6E\u5176\u4ED6\u7AD9\u70B9\uFF0C\u53EF\u53C2\u8003\u4E0B\u65B9\u8BBE\u7F6E\uFF1A
# \u8BF7\u6CE8\u610F\uFF1A\u5982\u679C\u591A\u4E2A\u7AD9\u70B9\u9700\u8981\u5171\u4EAB\u540C\u4E00\u4E2A\u7AEF\u53E3\uFF08\u5982 80/443\uFF09\uFF0C\u8BF7\u786E\u4FDD\u4E3A\u6BCF\u4E2A\u7AD9\u70B9\u90FD\u586B\u5199\u4E86\u57DF\u540D\uFF01
# \u52A8\u6001\u7AD9\u70B9\uFF1A
# xxx.com {
#    reverse_proxy http://127.0.0.1:1234
# }
# \u9759\u6001\u7AD9\u70B9\uFF1A
# xxx.com {
#    root * /www/xxx.com
#    file_server
# }
`,Ne=`hosts:
  local:
    host: localhost
    type: hydro
    server_url: https://hydro.ac/
    uname: judge
    password: examplepassword
    detail: true
    concurrency: 2
tmpfs_size: 512m
stdio_size: 256m
memoryMax: ${Math.min(1024,j.default.totalmem()/4)}m
processLimit: 128
testcases_max: 120
total_time_limit: 600
retry_delay_sec: 3
parallelism: ${Math.max(1,Math.floor((0,j.cpus)().length/4))}
singleTaskParallelism: 2
rate: 1.00
rerun: 2
secret: ${L.default.randomBytes(32).toString("hex")}
env: |
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    HOME=/w
`,te=`
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= hydro.ac:EytfvyReWHFwhY9MCGimCIn46KQNfmv9y8E2NqlNfxQ=
connect-timeout = 10
experimental-features = nix-command flakes
`,Me=async e=>{let t=ie.default.createServer(),r=await new Promise(o=>{t.once("error",()=>o(!1)),t.once("listening",()=>o(!0)),t.listen(e)});return t.close(),r};function Ae(){let e=n("yarn global dir").output?.trim()||"";if(!e)return!1;let t=`${e}/package.json`,r=(0,a.existsSync)(t)?require(t):{};return r.resolutions||={},Object.assign(r.resolutions,Object.fromEntries(["@esbuild/linux-loong64","esbuild-windows-32",...["android","darwin","freebsd","windows"].flatMap(o=>[`${o}-64`,`${o}-arm64`]).map(o=>`esbuild-${o}`),...["32","arm","mips64","ppc64","riscv64","s390x"].map(o=>`esbuild-linux-${o}`),...["netbsd","openbsd","sunos"].map(o=>`esbuild-${o}-64`)].map(o=>[o,"link:/dev/null"]))),n(`mkdir -p ${e}`),(0,a.writeFileSync)(t,JSON.stringify(r,null,2)),!0}function He(){let e=n("yarn global dir").output?.trim()||"";if(!e)return!1;let t=`${e}/package.json`,r=JSON.parse((0,a.readFileSync)(t,"utf-8"));return delete r.resolutions,(0,a.writeFileSync)(t,JSON.stringify(r,null,2)),!0}var Pe=j.default.totalmem()/1024/1024/1024,re=Math.max(.25,Math.floor(Pe/6*100)/100),oe="https://qm.qq.com/cgi-bin/qm/qr?k=0aTZfDKURRhPBZVpTYBohYG6P6sxABTw",se=[()=>console.log(`\u626B\u7801\u6216\u70B9\u51FB${M("\u94FE\u63A5",oe)}\u52A0\u5165QQ\u7FA4\uFF1A`),`echo '${oe}' | qrencode -o - -m 2 -t UTF8`,()=>{if(p)return;let e=require(`${process.env.HOME}/.hydro/config.json`);e.uri?v=new URL(e.uri).password||"(No password)":v=e.password||"(No password)",i.info("extra.dbUser"),i.info("extra.dbPassword",v)}],Fe=()=>[{init:"install.preparing",operations:[async()=>{if(process.env.IGNORE_BT)return;n("bt default").code||(process.argv.includes("--shamefully-unsafe-bt-panel")?(i.warn("warn.bt"),b.push(["warn.bt"]),i.info("install.wait",5),await x(5e3)):(i.warn("error.bt"),process.exit(1)))},async()=>{process.env.IGNORE_CENTOS||n("yum -h").code||(process.argv.includes("--unsupported-centos")?(i.warn("warn.centos"),b.push(["warn.centos"])):(i.warn("error.centos"),process.exit(1)))},async()=>{!F&&!p&&(i.warn("note.avx"),i.info("install.wait",60),await x(6e4))},async()=>{let e=n("df --output=avail -k /dev/shm").output?.split(`
`)[1];if(!e||!+e){i.warn("shm.readFail"),b.push(["shm.readFail"]);return}let t=+e/1024;t<250&&(i.warn("shm.sizeTooSmall",t),b.push(["shm.sizeTooSmall",t]))},async()=>{if(process.arch!=="arm64"||!["rpi","raspberrypi"].some(c=>(0,a.readFileSync)("/proc/cpuinfo","utf-8").toLowerCase().includes(c)))return;let t=(0,a.readFileSync)("/proc/cgroups","utf-8").split(`
`).find(c=>c.includes("memory"))?.trim();if(t&&!t.endsWith("0"))return;let o="/boot/cmdline.txt",s=(0,a.readFileSync)(o,"utf-8");s.includes("has moved to /boot/firmware/cmdline.txt")&&(o="/boot/firmware/cmdline.txt"),s=(0,a.readFileSync)(o,"utf-8"),!s.includes("cgroup_enable=memory")&&((0,a.writeFileSync)(o,s.replace(" console="," cgroup_enable=memory cgroup_memory=1 console=")),X=!0)},()=>{K.length?(0,a.writeFileSync)("/etc/nix/nix.conf",`substituters = ${K.join(" ")}
${te}`):C||(0,a.writeFileSync)("/etc/nix/nix.conf",`substituters = https://cache.nixos.org/ https://nix.hydro.ac/cache
${te}`),!C&&(n("nix-channel --remove nixpkgs",{stdio:"inherit"}),n("nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs",{stdio:"inherit"}),n("nix-channel --update",{stdio:"inherit"}))},T("pm2","yarn","esbuild","bash","unzip","zip","diffutils","patch","screen","gawk"),"yarn config set disable-self-update-check true",async()=>{let e=(0,ae.createInterface)(process.stdin,process.stdout);try{if((0,a.existsSync)("/home/judge/src")&&(i.info("migrate.hustojFound"),(await e.question(">")).toLowerCase().trim()==="y"&&(w="hustoj")),w||!!n("docker -v").code)return;(n("docker ps -a --format json").output?.split(`
`).map(s=>s.trim()).filter(s=>s).map(s=>JSON.parse(s))||[]).find(s=>s.Image.toLowerCase()==="universaloj/uoj-system"&&s.State==="running")&&(i.info("migrate.uojFound"),(await e.question(">")).toLowerCase().trim()==="y"&&(w="uoj"))}catch{console.error("Failed migration detection")}finally{e.close()}}]},{init:"install.mongodb",skip:()=>p,hidden:p,operations:[()=>(0,a.writeFileSync)(`${process.env.HOME}/.config/nixpkgs/config.nix`,`{
    permittedInsecurePackages = [
        "openssl-1.1.1t"
        "openssl-1.1.1u"
        "openssl-1.1.1v"
        "openssl-1.1.1w"
        "openssl-1.1.1x"
        "openssl-1.1.1y"
        "openssl-1.1.1z"
    ];
}`),T(`hydro.mongodb${F?7:4}${C?"-cn":""}`,"mongosh","mongodb-tools")]},{init:"install.compiler",operations:[T("gcc","python3")]},{init:"install.sandbox",skip:()=>!n("hydro-sandbox --help").code,operations:[T("go-judge"),"ln -sf $(which go-judge) /usr/local/bin/hydro-sandbox"]},{init:"install.caddy",skip:()=>p||V||(0,a.existsSync)(`${process.env.HOME}/.hydro/Caddyfile`),hidden:p,operations:[T("caddy"),()=>(0,a.writeFileSync)(`${process.env.HOME}/.hydro/Caddyfile`,ke)]},{init:"install.hydro",operations:[()=>Ae(),C?()=>{let e=null;try{n("yarn config set registry https://registry.npmmirror.com/",{stdio:"inherit"}),e=n(`yarn global add ${H}`,{stdio:"inherit"})}catch{console.log("Failed to install from npmmirror, fallback to yarnpkg")}finally{n("yarn config set registry https://registry.yarnpkg.com",{stdio:"inherit"})}try{n(`yarn global add ${H}`,{timeout:6e4})}catch{if(console.warn("Failed to check update from yarnpkg"),e?.code!==0)return"retry"}return null}:[`yarn global add ${H}`,{retry:!0}],()=>{p?(0,a.writeFileSync)(`${process.env.HOME}/.hydro/judge.yaml`,Ne):(0,a.writeFileSync)(`${process.env.HOME}/.hydro/addon.json`,JSON.stringify(le))},()=>He()]},{init:"install.createDatabaseUser",skip:()=>(0,a.existsSync)(`${process.env.HOME}/.hydro/config.json`)||p,hidden:p,operations:["pm2 start mongod",()=>x(3e3),async()=>{let{MongoClient,WriteConcern}=eval("require")("/usr/local/share/.config/yarn/global/node_modules/mongodb"),client=await MongoClient.connect("mongodb://127.0.0.1",{readPreference:"nearest",writeConcern:new WriteConcern("majority")});await client.db("hydro").command({createUser:"hydro",pwd:v,roles:[{role:"readWrite",db:"hydro"}]}),await client.close()},()=>(0,a.writeFileSync)(`${process.env.HOME}/.hydro/config.json`,JSON.stringify({uri:`mongodb://hydro:${v}@127.0.0.1:27017/hydro`})),"pm2 stop mongod","pm2 del mongod"]},{init:"install.starting",operations:[["pm2 stop all",{ignore:!0}],()=>(0,a.writeFileSync)(`${process.env.HOME}/.hydro/mount.yaml`,_e),`pm2 start bash --name hydro-sandbox -- -c "ulimit -s unlimited && hydro-sandbox -mount-conf ${process.env.HOME}/.hydro/mount.yaml -http-addr=localhost:5050"`,...p?[]:[()=>console.log(`WiredTiger cache size: ${re}GB`),`pm2 start mongod --name mongodb -e /dev/null -- --auth ${Re?"--bind_ip=0.0.0.0 ":""}--wiredTigerCacheSizeGB=${re}`,()=>x(1e3),async()=>{if(V){n("hydrooj cli system set server.host 0.0.0.0");return}w==="hustoj"&&(n("systemctl stop nginx || true"),n("systemctl disable nginx || true"),n("/etc/init.d/nginx stop || true"),await x(1e3)),await Me(80)||(i.warn("port.80"),b.push(["port.80"])),n("pm2 start caddy -- run",{cwd:`${process.env.HOME}/.hydro`}),n("hydrooj cli system set server.xff x-forwarded-for"),n("hydrooj cli system set server.xhost x-forwarded-host"),n("hydrooj cli system set server.xproxy true")},"pm2 start hydrooj"],"pm2 startup","pm2 save"]},{init:"install.migrate",skip:()=>!w,silent:!0,operations:[["yarn global add @hydrooj/migrate",{retry:!0}],"hydrooj addon add @hydrooj/migrate"]},{init:"install.migrateHustoj",skip:()=>w!=="hustoj",silent:!0,operations:["pm2 restart hydrooj",()=>{let t=(0,a.readFileSync)("/home/judge/src/web/include/db_info.inc.php","utf-8").split(`
`);function r(s){let c=t.find(d=>d.includes(`$${s}`))?.split("=",2)[1].split(";")[0].trim();return c?c.startsWith('"')&&c.endsWith('"')?c.slice(1,-1):c==="false"?!1:c==="true"?!0:+c:null}let o={host:r("DB_HOST"),port:3306,name:r("DB_NAME"),dataDir:r("OJ_DATA"),username:r("DB_USER"),password:r("DB_PASS"),contestType:r("OJ_OI_MODE")?"oi":"acm",domainId:"system"};console.log(o),n(`hydrooj cli script migrateHustoj '${JSON.stringify(o)}'`,{stdio:"inherit"}),r("OJ_REGISTER")||n("hydrooj cli user setPriv 0 0")},"pm2 restart hydrooj"]},{init:"install.migrateUoj",skip:()=>w!=="uoj",silent:!0,operations:[()=>{let t=(n("docker ps -a --format json").output?.split(`
`).map(u=>u.trim()).filter(u=>u).map(u=>JSON.parse(u))).find(u=>u.Image.toLowerCase()==="universaloj/uoj-system"&&u.State==="running"),r=t.Id||t.ID,o=JSON.parse(n(`docker inspect ${r}`).output),s=o[0].GraphDriver.Data.MergedDir;n(`sed s/127.0.0.1/0.0.0.0/g -i ${s}/etc/mysql/mysql.conf.d/mysqld.cnf`),n(`docker exec -i ${r} /etc/init.d/mysql restart`);let c=(0,a.readFileSync)(`${s}/etc/mysql/debian.cnf`,"utf-8").split(`
`).find(u=>u.startsWith("password"))?.split("=")[1].trim(),d=[`CREATE USER 'hydromigrate'@'%' IDENTIFIED BY '${v}';`,"GRANT ALL PRIVILEGES ON *.* TO 'hydromigrate'@'%' WITH GRANT OPTION;","FLUSH PRIVILEGES;",""].join(`
`);n(`docker exec -i ${r} mysql -u debian-sys-maint -p${c} -e "${d}"`);let $={host:o[0].NetworkSettings.IPAddress,port:3306,name:"app_uoj233",dataDir:`${s}/var/uoj_data`,username:"hydromigrate",password:v,domainId:"system"};console.log($),n(`hydrooj cli script migrateUniversaloj '${JSON.stringify($)}'`,{stdio:"inherit"})}]},{init:"install.done",skip:()=>p,operations:se},{init:"install.postinstall",operations:['echo "layout=2" >/etc/HYDRO_INSTALLER','echo "vm.swappiness = 1" >>/etc/sysctl.conf',"sysctl -p",'screen -d -m "pm2 install pm2-logrotate && pm2 set pm2-logrotate:max_size 64M"']},{init:"install.alldone",operations:[...se,()=>i.info("install.alldone"),()=>p&&i.info("install.editJudgeConfigAndStart"),()=>X&&i.info("install.restartRequired"),()=>{if(b.length){i.warn("install.warnings");for(let e of b)i.warn(e[0],...e.slice(1))}}]}];async function de(){try{if(process.env.REGION)process.env.REGION!=="CN"&&(C=!1);else{console.log("Getting IP info to find best mirror:");let t=await fetch("https://ipinfo.io",{headers:{accept:"application/json"}}).then(r=>r.json());delete t.readme,console.log(t),t.country!=="CN"&&(C=!1)}}catch(t){console.error(t),console.log("Cannot find the best mirror. Fallback to default.")}let e=Fe();for(let t=0;t<e.length;t++){let r=e[t];if(r.silent||i.info(r.init),r.skip?.())r.silent||i.info("info.skip");else for(let o of r.operations)if(o instanceof Array||(o=[o,{}]),o[0].toString().startsWith("nix-env")&&(o[1].retry=!0),typeof o[0]=="string"){E=0;let s=n(o[0],{stdio:"inherit"});for(;s.code&&o[1].ignore!==!0;)o[1].retry&&E<30?(i.warn("Retry in 3 secs... (%s)",o[0]),await x(3e3),s=n(o[0],{stdio:"inherit"}),E++):i.fatal("Error when running %s",o[0])}else{E=0;let s=await o[0](o[1]);for(;s==="retry";)E<30?(i.warn("Retry in 3 secs..."),await x(3e3),s=await o[0](o[1]),E++):i.fatal("Error installing")}}}de().catch(i.fatal);global.main=de;0&&(module.exports={link});

上面的这个js文件格式化后如下:

/**
 * Hydro 在线评测系统(OJ)自动化安装核心脚本
 * 功能:完成环境校验、依赖安装、配置生成、服务启动、数据迁移全流程
 */
var pe = Object.create;
var I = Object.defineProperty;
var me = Object.getOwnPropertyDescriptor;
var fe = Object.getOwnPropertyNames;
var he = Object.getPrototypeOf,
    ye = Object.prototype.hasOwnProperty;

// 工具函数:对象属性挂载
var ge = (e, t) => {
    for (var r in t)
        I(e, r, {
            get: t[r],
            enumerable: !0
        })
};

// 工具函数:对象属性继承与描述符复制
var D = (e, t, r, o) => {
    if (t && (typeof t == "object" || typeof t == "function"))
        for (let s of fe(t))
            !ye.call(e, s) && s !== r && I(e, s, {
                get: () => t[s],
                enumerable: !(o = me(t, s)) || o.enumerable
            });
    return e
};

// 工具函数:模块默认导出处理
var h = (e, t, r) => (
    r = e != null ? pe(he(e)) : {},
    D(t || !e || !e.__esModule ? I(r, "default", {
        value: e,
        enumerable: !0
    }) : r, e)
);

// 工具函数:标记 ES 模块
var be = e => D(I({}, "__esModule", {
    value: !0
}), e);

// 模块导出初始化
var Le = {};
ge(Le, {
    link: () => M
});
module.exports = be(Le);

// 依赖模块导入
var ne = require("child_process");
var L = h(require("crypto"));
var a = require("fs");
var ie = h(require("net"));
var j = h(require("os"));
var ae = require("readline/promises");
var O = h(require("node:process"), 1);
var k = h(require("node:process"), 1);
var B = h(require("node:os"), 1);
var A = h(require("node:tty"), 1);

// 命令行参数检测工具(短参数/长参数)
function m(e, t = globalThis.Deno ? globalThis.Deno.args : k.default.argv) {
    let r = e.startsWith("-") ? "" : e.length === 1 ? "-" : "--";
    let o = t.indexOf(r + e);
    let s = t.indexOf("--");
    return o !== -1 && (s === -1 || o < s)
}

// 终端颜色输出配置
var {
    env: l
} = k.default;
var S;
if (m("no-color") || m("no-colors") || m("color=false") || m("color=never")) {
    S = 0;
} else if (m("color") || m("colors") || m("color=true") || m("color=always")) {
    S = 1;
}

// FORCE_COLOR 环境变量处理
function we() {
    if (!("FORCE_COLOR" in l)) return;
    if (l.FORCE_COLOR === "true") return 1;
    if (l.FORCE_COLOR === "false") return 0;
    if (l.FORCE_COLOR.length === 0) return 1;
    let e = Math.min(Number.parseInt(l.FORCE_COLOR, 10), 3);
    if ([0, 1, 2, 3].includes(e)) return e;
}

// 颜色支持判断结果格式化
function xe(e) {
    return e === 0 ? !1 : {
        level: e,
        hasBasic: !0,
        has256: e >= 2,
        has16m: e >= 3
    }
}

// 终端颜色支持检测
function ve(e, {
    streamIsTTY: t,
    sniffFlags: r = !0
} = {}) {
    let o = we();
    o !== void 0 && (S = o);
    let s = r ? S : o;
    if (s === 0) return 0;
    if (r) {
        if (m("color=16m") || m("color=full") || m("color=truecolor")) return 3;
        if (m("color=256")) return 2;
    }
    if ("TF_BUILD" in l && "AGENT_NAME" in l) return 1;
    if (e && !t && s === void 0) return 0;
    let c = s || 0;
    if (l.TERM === "dumb") return c;
    if (k.default.platform === "win32") {
        let d = B.default.release().split(".");
        return Number(d[0]) >= 10 && Number(d[2]) >= 10586 ? Number(d[2]) >= 14931 ? 3 : 2 : 1;
    }
    if ("CI" in l) {
        return ["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some(d => d in l) ? 3 :
            ["TRAVIS", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some(d => d in l) || l.CI_NAME === "codeship" ? 1 : c;
    }
    if ("TEAMCITY_VERSION" in l) {
        return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(l.TEAMCITY_VERSION) ? 1 : 0;
    }
    if (l.COLORTERM === "truecolor" || l.TERM === "xterm-kitty") return 3;
    if ("TERM_PROGRAM" in l) {
        let d = Number.parseInt((l.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
        switch (l.TERM_PROGRAM) {
            case "iTerm.app":
                return d >= 3 ? 3 : 2;
            case "Apple_Terminal":
                return 2;
        }
    }
    return /-256(color)?$/i.test(l.TERM) ? 2 :
        /^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(l.TERM) || "COLORTERM" in l ? 1 : c;
}

// 终端颜色支持初始化
function _(e, t = {}) {
    let r = ve(e, {
        streamIsTTY: e && e.isTTY,
        ...t
    });
    return xe(r)
}
var Be = {
    stdout: _({
        isTTY: A.default.isatty(1)
    }),
    stderr: _({
        isTTY: A.default.isatty(2)
    })
};

// 进程相关依赖与工具函数
var U = h(require("process"), 1);

// 超链接参数检测
function y(e, t = U.default.argv) {
    let r = e.startsWith("-") ? "" : e.length === 1 ? "-" : "--";
    let o = t.indexOf(r + e);
    let s = t.indexOf("--");
    return o !== -1 && (s === -1 || o < s)
}

// 版本号解析工具
function G(e = "") {
    if (/^\d{3,4}$/.test(e)) {
        let r = /(\d{1,2})(\d{2})/.exec(e) ?? [];
        return {
            major: 0,
            minor: Number.parseInt(r[1], 10),
            patch: Number.parseInt(r[2], 10)
        }
    }
    let t = (e ?? "").split(".").map(r => Number.parseInt(r, 10));
    return {
        major: t[0],
        minor: t[1],
        patch: t[2]
    }
}

// 终端超链接支持检测
function z(e) {
    let {
        CI: t,
        CURSOR_TRACE_ID: r,
        FORCE_HYPERLINK: o,
        NETLIFY: s,
        TEAMCITY_VERSION: c,
        TERM_PROGRAM: d,
        TERM_PROGRAM_VERSION: $,
        VTE_VERSION: u,
        TERM: ue
    } = O.default.env;
    if (o) return !(o.length > 0 && Number.parseInt(o, 10) === 0);
    if (y("no-hyperlink") || y("no-hyperlinks") || y("hyperlink=false") || y("hyperlink=never")) return !1;
    if (y("hyperlink=true") || y("hyperlink=always") || s) return !0;
    if (!_(e) || e && !e.isTTY) return !1;
    if ("WT_SESSION" in O.default.env) return !0;
    if (O.default.platform === "win32" || t || c) return !1;
    if (d) {
        let f = G($);
        switch (d) {
            case "iTerm.app":
                return f.major === 3 ? f.minor >= 1 : f.major > 3;
            case "WezTerm":
                return f.major >= 20200620;
            case "vscode":
                return r ? !0 : f.major > 1 || f.major === 1 && f.minor >= 72;
            case "ghostty":
                return !0;
        }
    }
    if (u) {
        if (u === "0.50.0") return !1;
        let f = G(u);
        return f.major > 0 || f.minor >= 50;
    }
    switch (ue) {
        case "alacritty":
            return !0;
    }
    return !1
}

// 超链接配置初始化
var Oe = {
    stdout: z(O.default.stdout),
    stderr: z(O.default.stderr)
}, J = Oe;
var Ee = J.stdout,
    q = "\x1B]",
    Y = "\x07",
    N = ";",
    M = Ee ? (e, t) => [q, "8", N, N, t, Y, e, q, "8", N, N, Y].join("") : (e, t) => `${e} < ${t} > `;

// 系统资源检测与工具函数
var Ce = j.default.freemem(),
    je = Ce < 1024 * 1024 * 1024;

// Nix 命令拼接工具(根据内存大小拆分命令)
function T(...e) {
    return je ? e.map(t => `nix-env -iA ${t.includes(".") ? t : `nixpkgs.${t}`}`).join(" && ") :
        `nix-env -iA ${e.map(t => t.includes(".") ? t : `nixpkgs.${t}`).join(" ")}`
}

var b = [];

// 系统命令执行封装(捕获异常并返回结果)
function n(e, t) {
    try {
        return {
            output: (0, ne.execSync)(e, t).toString(),
            code: 0
        }
    } catch (r) {
        return {
            code: r.status,
            message: r.message
        }
    }
}

// 异步延时工具
function x(e) {
    return new Promise(t => {
        setTimeout(t, e)
    })
}

// 常量定义
var W = "https://docs.hydro.ac/FAQ/#%E8%B0%83%E6%95%B4%E4%B8%B4%E6%97%B6%E7%9B%AE%E5%BD%95%E5%A4%A7%E5%B0%8F";

// 国际化文案配置
var Te = {
    zh: {
        "install.wait": "\u5B89\u88C5\u811A\u672C\u5C06\u7B49\u5F85 %d \u79D2\u540E\u81EA\u52A8\u7EE7\u7EED\u5B89\u88C5\uFF0C\u6216\u6309 Ctrl-C \u9000\u51FA\u3002",
        "install.start": "\u5F00\u59CB\u8FD0\u884C Hydro \u5B89\u88C5\u5DE5\u5177",
        "note.avx": `\u68C0\u6D4B\u5230\u60A8\u7684 CPU \u4E0D\u652F\u6301 avx \u6307\u4EE4\u96C6\uFF0C\u8FD9\u53EF\u80FD\u4F1A\u5F71\u54CD\u7CFB\u7EDF\u8FD0\u884C\u901F\u5EA6\u3002
\u5982\u679C\u60A8\u6B63\u5728\u4F7F\u7528 PVE/VirtualBox \u7B49\u865A\u62DF\u673A\u5E73\u53F0\uFF0C\u8BF7\u5C1D\u8BD5\u5173\u673A\u540E\u5C06\u865A\u62DF\u673A\u7684 CPU \u7C7B\u578B\u8BBE\u7F6E\u4E3A Host\uFF0C\u91CD\u542F\u540E\u518D\u6B21\u8FD0\u884C\u8BE5\u811A\u672C\u3002`,
        "warn.avx": "\u68C0\u6D4B\u5230\u60A8\u7684 CPU \u4E0D\u652F\u6301 avx \u636E\u4EE4\u96C6\uFF0C\u5C06\u4F7F\u7528 mongodb@v4.4",
        "error.rootRequired": "\u8BF7\u5148\u4F7F\u7528 sudo su \u5207\u6362\u5230 root \u7528\u6237\u540E\u518D\u8FD0\u884C\u8BE5\u5DE5\u5177\u3002",
        "error.unsupportedArch": "\u4E0D\u652F\u6301\u7684\u67B6\u6784 %s ,\u8BF7\u5C1D\u8BD5\u624B\u52A8\u5B89\u88C5\u3002",
        "error.osreleaseNotFound": "\u65E0\u6CD5\u83B7\u53D6\u7CFB\u7EDF\u7248\u672C\u4FE1\u606F\uFF08/etc/os-release \u6587\u4EF6\u672A\u627E\u5230\uFF09\uFF0C\u8BF7\u5C1D\u8BD5\u624B\u52A8\u5B89\u88C5\u3002",
        "error.unsupportedOS": "\u4E0D\u652F\u6301\u7684\u64CD\u4F5C\u7CFB\u7EDF %s \uFF0C\u8BF7\u5C1D\u8BD5\u624B\u52A8\u5B89\u88C5\uFF0C",
        "error.centos": "CentOS \u53CA\u5176\u53D8\u79CD\u7CFB\u7EDF\u56E0\u7CFB\u7EDF\u5185\u6838\u8FC7\u4F4E\uFF0C\u65E0\u6CD5\u5B89\u88C5 Hydro\uFF0C\u5F3A\u70C8\u5EFA\u8BAE\u4F7F\u7528\u5176\u4ED6\u7CFB\u7EDF\u3002\u82E5\u786E\u6709\u9700\u6C42\uFF0C\u8BF7\u5347\u7EA7 Linux \u5185\u6838\u81F3 4.4+ \u540E\u518D\u624B\u52A8\u5B89\u88C5 Hydro\u3002",
        "install.preparing": "\u6B63\u5728\u521D\u59CB\u5316\u5B89\u88C5...",
        "install.mongodb": "\u6B63\u5728\u5B89\u88C5 mongodb...",
        "install.createDatabaseUser": "\u6B63\u5728\u521B\u5EFA\u6570\u636E\u5E93\u7528\u6237...",
        "install.compiler": "\u6B63\u5728\u5B89\u88C5\u7F16\u8BD1\u5668...",
        "install.hydro": "\u6B63\u5728\u5B89\u88C5 Hydro...",
        "install.done": "Hydro \u5B89\u88C5\u6210\u529F\uFF01",
        "install.alldone": "\u5B89\u88C5\u5DF2\u5168\u90E8\u5B8C\u6210\u3002",
        "install.editJudgeConfigAndStart": "\u8BF7\u7F16\u8F91 ~/.hydro/judge.yaml \u540E\u4F7F\u7528 pm2 start hydrojudge && pm2 save \u542F\u52A8\u3002",
        "extra.dbUser": "\u6570\u636E\u5E93\u7528\u6237\u540D\uFF1A hydro",
        "extra.dbPassword": "\u6570\u636E\u5E93\u5BC6\u7801\uFF1A %s",
        "port.80": "\u7AEF\u53E3 80 \u5DF2\u88AB\u5360\u7528\uFF0CCaddy \u65E0\u6CD5\u6B63\u5E38\u76D1\u542C\u6B64\u7AEF\u53E3\u3002",
        "shm.readFail": "\u8BFB\u53D6 /dev/shm \u5927\u5C0F\u5931\u8D25\u3002\u8BF7\u68C0\u67E5\u7CFB\u7EDF\u662F\u5426\u5728\u6B64\u6302\u8F7D\u4E86 tmpfs\u3002",
        "shm.sizeTooSmall": `\u60A8\u7684\u7CFB\u7EDF /dev/shm \u5927\u5C0F\u4E3A %d MB\uFF0C\u5728\u9AD8\u5E76\u53D1\u8BC4\u6D4B\u65F6\u53EF\u80FD\u4EA7\u751F\u95EE\u9898\u3002
\u5EFA\u8BAE\u53C2\u7167\u6587\u6863 ${M("FAQS", W)} \u8FDB\u884C\u8C03\u6574\u3002`,
        "info.skip": "\u6B65\u9AA4\u5DF2\u8DF3\u8FC7\u3002",
        "error.bt": `\u68C0\u6D4B\u5230\u5B9D\u5854\u9762\u677F\uFF0C\u5B89\u88C5\u811A\u672C\u5F88\u53EF\u80FD\u65E0\u6CD5\u6B63\u5E38\u5DE5\u4F5C\u3002\u5EFA\u8BAE\u60A8\u4F7F\u7528\u7EAF\u51C0\u7684 Debian 12 \u7CFB\u7EDF\u8FDB\u884C\u5B89\u88C5\u3002
\u8981\u5FFD\u7565\u8BE5\u8B66\u544A\uFF0C\u8BF7\u4F7F\u7528 --shamefully-unsafe-bt-panel \u53C2\u6570\u91CD\u65B0\u8FD0\u884C\u6B64\u811A\u672C\u3002`,
        "warn.bt": `\u68C0\u6D4B\u5230\u5B9D\u5854\u9762\u677F\uFF0C\u8FD9\u4F1A\u5BF9\u7CFB\u7EDF\u5B89\u5168\u6027\u4E0E\u7A33\u5B9A\u6027\u9020\u6210\u5F71\u54CD\u3002\u5EFA\u8BAE\u4F7F\u7528\u7EAF\u51C0 Debian 12 \u7CFB\u7EDF\u8FDB\u884C\u5B89\u88C5\u3002
\u5F00\u53D1\u8005\u5BF9\u56E0\u4E3A\u4F7F\u7528\u5B9D\u5854\u9762\u677F\u7684\u6570\u636E\u4E22\u5931\u4E0D\u627F\u62C5\u4EFB\u4F55\u8D23\u4EFB\u3002
\u8981\u53D6\u6D88\u5B89\u88C5\uFF0C\u8BF7\u4F7F\u7528 Ctrl-C \u9000\u51FA\u3002\u5B89\u88C5\u7A0B\u5E8F\u5C06\u5728\u4E94\u79D2\u540E\u7EE7\u7EED\u3002`,
        "migrate.hustojFound": `\u68C0\u6D4B\u5230 HustOJ\u3002\u5B89\u88C5\u7A0B\u5E8F\u53EF\u4EE5\u5C06 HustOJ \u4E2D\u7684\u5168\u90E8\u6570\u636E\u5BFC\u5165\u5230 Hydro\u3002\uFF08\u539F\u6709\u6570\u636E\u4E0D\u4F1A\u4E22\u5931\uFF0C\u60A8\u53EF\u968F\u65F6\u5207\u6362\u56DE HustOJ\uFF09
\u8BE5\u529F\u80FD\u652F\u6301\u539F\u7248 HustOJ \u548C\u90E8\u5206\u4FEE\u6539\u7248\uff0c\u8F93\u5165 y \u786E\u8BA4\u8BE5\u64CD\u4F5C\u3002
\u8FC1\u79FB\u8FC7\u7A0B\u6709\u4EFB\u4F55\u95EE\u9898\uff0c\u6B22\u8FCE\u52A0QQ\u7FA4 1085853538 \u54A8\u8BE2\u7BA1\u7406\u5458\u3002`,
        "install.restartRequired": "\u5B89\u88C5\u5B8C\u6210\uff0c\u8BF7\u4F7F\u7528 sudo reboot \u91CD\u542F\u7CFB\u7EDF\u3002\u5728\u6B64\u4E4B\u524D\u7CFB\u7EDF\u7684\u90E8\u5206\u529F\u80FD\u53EF\u80FD\u65E0\u6CD5\u6B63\u5E38\u4F7F\u7528\u3002",
        "install.warnings": "\u5B89\u88C5\u8FC7\u7A0B\u4E2D\u4EA7\u751F\u4E86\u4EE5\u4E0B\u8B66\u544A\uFF1A"
    },
    en: {
        "install.wait": `The installation script will wait for %d seconds before continuing.
Press Ctrl-C to exit.`,
        "install.start": "Starting Hydro installation tool",
        "note.avx": `Your CPU does not support avx, this may affect system performance.
If you are using a virtual machine platform such as PVE/VirtualBox,
try shutting down and setting the CPU type of the virtual machine to Host,
then restart and run the script again.`,
        "warn.avx": "Your CPU does not support avx, 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.",
        "error.centos": `CentOS and its derivatives are not supported due to low system kernel versions.
It is strongly recommended to use other systems. If you really need it, please upgrade the Linux kernel to 4.4+ and manually install Hydro.`,
        "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!",
        "install.alldone": "Hydro installation completed.",
        "install.editJudgeConfigAndStart": `Please edit config at ~/.hydro/judge.yaml than start hydrojudge with:
pm2 start hydrojudge && pm2 save.`,
        "extra.dbUser": "Database username: hydro",
        "extra.dbPassword": "Database password: %s",
        "port.80": "Port 80 is already in use, Caddy cannot listen to this port.",
        "shm.readFail": "Failed to read /dev/shm size.",
        "shm.sizeTooSmall": `Your system /dev/shm size is %d MB, which may cause problems in high concurrency testing.
Please refer to ${M("FAQS", W)} for adjustments.`,
        "info.skip": "Step skipped.",
        "error.bt": `BT-Panel detected, this script may not work properly. It is recommended to use a clean Debian 12 OS.
To ignore this warning, please run this script again with '--shamefully-unsafe-bt-panel' flag.`,
        "warn.bt": `BT-Panel detected, this will affect system security and stability. It is recommended to use a clean Debian 12 OS.
The developer is not responsible for any data loss caused by using BT-Panel.
To cancel the installation, please use Ctrl-C to exit. The installation program will continue in five seconds.`,
        "migrate.hustojFound": `HustOJ detected. The installation program can migrate all data from HustOJ to Hydro.
The original data will not be lost, and you can switch back to HustOJ at any time.
This feature supports the original version of HustOJ and some modified versions. Enter y to confirm this operation.
If you have any questions about the migration process, please add QQ group 1085853538 to consult the administrator.`,
        "install.restartRequired": "Please reboot the system. Some functions may not work properly before the restart.",
        "install.warnings": "The following warnings occurred during the installation:"
    }
};

// 全局参数解析与初始化
var p = process.argv.includes("--judge"); // 是否仅安装评测机
var V = process.argv.includes("--no-caddy"); // 是否不安装Caddy
var Re = process.argv.includes("--expose-db"); // 是否暴露MongoDB
var le = ["@hydrooj/ui-default", "@hydrooj/hydrojudge", "@hydrooj/fps-importer", "@hydrooj/a11y"]; // Hydro附加组件
var H = p ? "@hydrooj/hydrojudge" : `hydrooj ${le.join(" ")}`; // 要安装的Hydro包
var Q = process.argv.find(e => e.startsWith("--substituters=")); // Nix镜像源参数
var K = Q ? Q.split("=")[1].split(",") : []; // 镜像源列表
var Z = process.argv.find(e => e.startsWith("--migration=")); // 数据迁移类型参数
var w = Z ? Z.split("=")[1] : ""; // 迁移类型(hustoj/uoj)
var X = !1; // 树莓派是否需要重启标记
var ce = process.env.LANG?.includes("zh") || process.env.LOCALE?.includes("zh") ? "zh" : "en"; // 语言切换
process.env.TERM === "linux" && (ce = "en"); // Linux终端强制英文

// 日志输出工具(封装国际化文案)
var P = e => (t, ...r) => (e(Te[ce][t] || t, ...r), 0);
var i = {
    info: P(console.log),
    warn: P(console.warn),
    fatal: (e, ...t) => (P(console.error)(e, ...t), process.exit(1))
};

// 前置环境校验(权限、架构、环境变量)
process.getuid ? process.getuid() !== 0 && i.fatal("error.rootRequired") : i.fatal("error.unsupportedOs");
["x64", "arm64"].includes(process.arch) || i.fatal("error.unsupportedArch", process.arch);
process.env.HOME || i.fatal("$HOME not found");
(0, a.existsSync)("/etc/os-release") || i.fatal("error.osreleaseNotFound");

// 读取操作系统信息
var $e = (0, a.readFileSync)("/etc/os-release", "utf-8");
var Ie = $e.split(`
`);
var ee = {};
for (let e of Ie) {
    if (!e.trim()) continue;
    let t = e.split("=");
    t[1].startsWith('"') ? ee[t[0].toLowerCase()] = t[1].substring(1, t[1].length - 2) : ee[t[0].toLowerCase()] = t[1];
}

// CPU AVX指令集检测
var F = !0;
var Se = (0, a.readFileSync)("/proc/cpuinfo", "utf-8");
!Se.includes("avx") && !p && (F = !1, i.warn("warn.avx"), b.push(["warn.avx"]));

// 安装流程初始化
var E = 0;
i.info("install.start");
var v = L.default.randomBytes(32).toString("hex"); // 随机生成MongoDB密码
var C = !0; // 是否使用国内镜像
var R = `${process.env.HOME}/.nix-profile/`; // Nix安装目录

// 沙箱挂载配置模板生成工具
function g(e, t = e, r = !0) {
    return `  - type: bind
    source: ${e}
    target: ${t}${r ? `
    readonly: true` : ""}`
}

// 配置模板定义
var _e = `mount:
${g(`${R}bin`, "/bin")}
${g(`${R}bin`, "/usr/bin")}
${g(`${R}lib`, "/lib")}
${g(`${R}share`, "/share")}
${g(`${R}etc`, "/etc")}
${g("/nix", "/nix")}
${g("/dev/null", "/dev/null", !1)}
${g("/dev/urandom", "/dev/urandom", !1)}
  - 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
`; // 沙箱挂载配置

var ke = `# \u5982\u679C\u4F60\u5E0C\u671B\u4F7F\u7528\u5176\u4ED6\u7AEF\u53E3\u6216\u4F7F\u7528\u57DF\u540D\uff0c\u4FEE\u6539\u6B64\u5904 :80 \u7684\u503C\u540E\u5728 ~/.hydro \u76EE\u5F55\u4E0B\u4F7F\u7528 caddy reload \u91CD\u8F7D\u914D\u7F6E\u3002
# \u5982\u679C\u4F60\u5728\u5F53\u524D\u914D\u7F6E\u4E0B\u80FD\u591F\u901A\u8FC7 http://\u4F60\u7684\u57DF\u540D/ \u6B63\u5E38\u8BBF\u95EE\u5230\u7F51\u7AD9\uff0c\u82E5\u9700\u5F00\u542F ssl\uff0c
# \u4EC5\u9700\u5C06 :80 \u6539\u4E3A\u4F60\u7684\u57DF\u540D\uff08\u5982 hydro.ac\uff09\u540E\u4F7F\u7528 caddy reload \u91CD\u8F7D\u914D\u7F6E\u5373\u53EF\u81EA\u52A8\u7B7E\u53D1 ssl \u8BC1\u4E66\u3002
# \u586B\u5199\u5B8C\u6574\u57DF\u540D\uff0c\u6CE8\u610F\u533A\u5206\u6709\u65E0 www \uff08www.hydro.ac \u548C hydro.ac \u4E0D\u540C\uff0c\u8BF7\u68C0\u67E5 DNS \u8BBE\u7F6E\uff09
# \u8BF7\u6CE8\u610F\u5728\u9632\u706B\u5899/\u5B89\u5168\u7EC4\u4E2D\u653E\u884C\u7AEF\u53E3\uff0c\u4E14\u90E8\u5206\u8FD0\u8425\u5546\u4F1A\u62E6\u622A\u672A\u7ECF\u5907\u6848\u7684\u57DF\u540D\u3002
# \u5176\u4ED6\u9700\u6C42\u6E05\u53C2\u7167 https://caddyserver.com/docs/ \u8BF4\u660E\u8FDB\u884C\u8BBE\u7F6E\u3002
# For more information, refer to caddy v2 documentation.
:80 {
  encode zstd gzip
  log {
    output file /data/access.log {
      roll_size 1gb
      roll_keep_for 72h
    }
    format json
  }
  # Handle static files directly, for better performance.
  root * /root/.hydro/static
  @static {
    file {
      try_files {path}
    }
  }
  handle @static {
    file_server
  }
  handle {
    reverse_proxy http://127.0.0.1:8888
  }
}

# \u5982\u679C\u4F60\u9700\u8981\u540C\u65F6\u914D\u7F6E\u5176\u4ED6\u7AD9\u70B9\uff0c\u53EF\u53C2\u8003\u4E0B\u65B9\u8BBE\u7F6E\uff1A
# \u8BF7\u6CE8\u610F\uff1A\u5982\u679C\u591A\u4E2A\u7AD9\u70B9\u9700\u8981\u5171\u4EAB\u540C\u4E00\u4E2A\u7AEF\u53E3\uff08\u5982 80/443\uff09\uff0c\u8BF7\u786E\u4FDD\u4E3A\u6BCF\u4E2A\u7AD9\u70B9\u90FD\u586B\u5199\u4E86\u57DF\u540D\uff01
# \u52A8\u6001\u7AD9\u70B9\uff1A
# xxx.com {
#    reverse_proxy http://127.0.0.1:1234
# }
# \u9759\u6001\u7AD9\u70B9\uff1A
# xxx.com {
#    root * /www/xxx.com
#    file_server
# }
`; // Caddy配置模板

var Ne = `hosts:
  local:
    host: localhost
    type: hydro
    server_url: https://hydro.ac/
    uname: judge
    password: examplepassword
    detail: true
    concurrency: 2
tmpfs_size: 512m
stdio_size: 256m
memoryMax: ${Math.min(1024, j.default.totalmem() / 4)}m
processLimit: 128
testcases_max: 120
total_time_limit: 600
retry_delay_sec: 3
parallelism: ${Math.max(1, Math.floor((0, j.cpus)().length / 4))}
singleTaskParallelism: 2
rate: 1.00
rerun: 2
secret: ${L.default.randomBytes(32).toString("hex")}
env: |
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    HOME=/w
`; // 评测机配置模板

var te = `
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= hydro.ac:EytfvyReWHFwhY9MCGimCIn46KQNfmv9y8E2NqlNfxQ=
connect-timeout = 10
experimental-features = nix-command flakes
`; // Nix配置模板

// Yarn依赖优化工具(忽略无用平台依赖)
function Ae() {
    let e = n("yarn global dir").output?.trim() || "";
    if (!e) return !1;
    let t = `${e}/package.json`;
    let r = (0, a.existsSync)(t) ? require(t) : {};
    return r.resolutions || {},
        Object.assign(r.resolutions, Object.fromEntries(["@esbuild/linux-loong64", "esbuild-windows-32",
            ...["android", "darwin", "freebsd", "windows"].flatMap(o => [`${o}-64`, `${o}-arm64`]).map(o => `esbuild-${o}`),
            ...["32", "arm", "mips64", "ppc64", "riscv64", "s390x"].map(o => `esbuild-linux-${o}`),
            ...["netbsd", "openbsd", "sunos"].map(o => `esbuild-${o}-64`)
        ].map(o => [o, "link:/dev/null"]))),
        n(`mkdir -p ${e}`),
        (0, a.writeFileSync)(t, JSON.stringify(r, null, 2)), !0
}

// 恢复Yarn原始配置
function He() {
    let e = n("yarn global dir").output?.trim() || "";
    if (!e) return !1;
    let t = `${e}/package.json`;
    let r = JSON.parse((0, a.readFileSync)(t, "utf-8"));
    return delete r.resolutions,
        (0, a.writeFileSync)(t, JSON.stringify(r, null, 2)), !0
}

// 系统资源计算
var Pe = j.default.totalmem() / 1024 / 1024 / 1024;
var re = Math.max(.25, Math.floor(Pe / 6 * 100) / 100); // MongoDB缓存大小
var oe = "https://qm.qq.com/cgi-bin/qm/qr?k=0aTZfDKURRhPBZVpTYBohYG6P6sxABTw"; // QQ群链接

// 安装完成后执行的附加操作
var se = [
    () => console.log(`\u626B\u7801\u6216\u70B9\u51FB${M("\u94FE\u63A5", oe)}\u52A0\u5165QQ\u7FA4\uff1a`),
    `echo '${oe}' | qrencode -o - -m 2 -t UTF8`,
    () => {
        if (p) return;
        let e = require(`${process.env.HOME}/.hydro/config.json`);
        e.uri ? v = new URL(e.uri).password || "(No password)" : v = e.password || "(No password)";
        i.info("extra.dbUser");
        i.info("extra.dbPassword", v);
    }
];

// 核心安装流程定义
function Fe() {
    return [{
            // 步骤1:初始化准备
            init: "install.preparing",
            operations: [
                // 检测宝塔面板
                async () => {
                    if (process.env.IGNORE_BT) return;
                    n("bt default").code || (process.argv.includes("--shamefully-unsafe-bt-panel") ?
                        (i.warn("warn.bt"), b.push(["warn.bt"]), i.info("install.wait", 5), await x(5e3)) :
                        (i.warn("error.bt"), process.exit(1)))
                },
                // 检测CentOS系统
                async () => {
                    process.env.IGNORE_CENTOS || n("yum -h").code || (process.argv.includes("--unsupported-centos") ?
                        (i.warn("warn.centos"), b.push(["warn.centos"])) :
                        (i.warn("error.centos"), process.exit(1)))
                },
                // AVX指令集缺失提示
                async () => {
                    !F && !p && (i.warn("note.avx"), i.info("install.wait", 60), await x(6e4))
                },
                // 检测/dev/shm大小
                async () => {
                    let e = n("df --output=avail -k /dev/shm").output?.split(`
`)[1];
                    if (!e || !+e) {
                        i.warn("shm.readFail");
                        b.push(["shm.readFail"]);
                        return;
                    }
                    let t = +e / 1024;
                    t < 250 && (i.warn("shm.sizeTooSmall", t), b.push(["shm.sizeTooSmall", t]))
                },
                // 树莓派cgroup配置
                async () => {
                    if (process.arch !== "arm64" || !["rpi", "raspberrypi"].some(c =>
                            (0, a.readFileSync)("/proc/cpuinfo", "utf-8").toLowerCase().includes(c))) return;
                    let t = (0, a.readFileSync)("/proc/cgroups", "utf-8").split(`
`).find(c => c.includes("memory"))?.trim();
                    if (t && !t.endsWith("0")) return;
                    let o = "/boot/cmdline.txt";
                    let s = (0, a.readFileSync)(o, "utf-8");
                    s.includes("has moved to /boot/firmware/cmdline.txt") && (o = "/boot/firmware/cmdline.txt");
                    s = (0, a.readFileSync)(o, "utf-8");
                    !s.includes("cgroup_enable=memory") && (
                        (0, a.writeFileSync)(o, s.replace(" console=", " cgroup_enable=memory cgroup_memory=1 console=")),
                        X = !0
                    )
                },
                // 写入Nix配置
                () => {
                    K.length ?
                        (0, a.writeFileSync)("/etc/nix/nix.conf", `substituters = ${K.join(" ")}
${te}`) :
                        C || (0, a.writeFileSync)("/etc/nix/nix.conf", `substituters = https://cache.nixos.org/ https://nix.hydro.ac/cache
${te}`);
                    !C && (
                        n("nix-channel --remove nixpkgs", {
                            stdio: "inherit"
                        }),
                        n("nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs", {
                            stdio: "inherit"
                        }),
                        n("nix-channel --update", {
                            stdio: "inherit"
                        })
                    )
                },
                // 安装基础依赖
                T("pm2", "yarn", "esbuild", "bash", "unzip", "zip", "diffutils", "patch", "screen", "gawk"),
                // 禁用Yarn自更新检查
                "yarn config set disable-self-update-check true",
                // 检测是否需要数据迁移
                async () => {
                    let e = (0, ae.createInterface)(process.stdin, process.stdout);
                    try {
                        if ((0, a.existsSync)("/home/judge/src") && (
                                i.info("migrate.hustojFound"),
                                (await e.question(">")).toLowerCase().trim() === "y" && (w = "hustoj")
                            ));
                        if (w || !!n("docker -v").code) return;
                        (n("docker ps -a --format json").output?.split(`
`).map(s => s.trim()).filter(s => s).map(s => JSON.parse(s)) || []).find(s =>
                            s.Image.toLowerCase() === "universaloj/uoj-system" && s.State === "running"
                        ) && (
                            i.info("migrate.uojFound"),
                            (await e.question(">")).toLowerCase().trim() === "y" && (w = "uoj")
                        )
                    } catch {
                        console.error("Failed migration detection")
                    } finally {
                        e.close()
                    }
                }
            ]
        },
        {
            // 步骤2:安装MongoDB
            init: "install.mongodb",
            skip: () => p,
            hidden: p,
            operations: [
                () => (0, a.writeFileSync)(`${process.env.HOME}/.config/nixpkgs/config.nix`, `{
    permittedInsecurePackages = [
        "openssl-1.1.1t"
        "openssl-1.1.1u"
        "openssl-1.1.1v"
        "openssl-1.1.1w"
        "openssl-1.1.1x"
        "openssl-1.1.1y"
        "openssl-1.1.1z"
    ];
}`),
                T(`hydro.mongodb${F ? 7 : 4}${C ? "-cn" : ""}`, "mongosh", "mongodb-tools")
            ]
        },
        {
            // 步骤3:安装编译器
            init: "install.compiler",
            operations: [T("gcc", "python3")]
        },
        {
            // 步骤4:安装评测沙箱
            init: "install.sandbox",
            skip: () => !n("hydro-sandbox --help").code,
            operations: [T("go-judge"), "ln -sf $(which go-judge) /usr/local/bin/hydro-sandbox"]
        },
        {
            // 步骤5:安装Caddy
            init: "install.caddy",
            skip: () => p || V || (0, a.existsSync)(`${process.env.HOME}/.hydro/Caddyfile`),
            hidden: p,
            operations: [T("caddy"), () => (0, a.writeFileSync)(`${process.env.HOME}/.hydro/Caddyfile`, ke)]
        },
        {
            // 步骤6:安装Hydro核心
            init: "install.hydro",
            operations: [
                () => Ae(),
                // 根据镜像源选择安装方式
                C ? () => {
                    let e = null;
                    try {
                        n("yarn config set registry https://registry.npmmirror.com/", {
                            stdio: "inherit"
                        });
                        e = n(`yarn global add ${H}`, {
                            stdio: "inherit"
                        })
                    } catch {
                        console.log("Failed to install from npmmirror, fallback to yarnpkg")
                    } finally {
                        n("yarn config set registry https://registry.yarnpkg.com", {
                            stdio: "inherit"
                        })
                    }
                    try {
                        n(`yarn global add ${H}`, {
                            timeout: 6e4
                        })
                    } catch {
                        if (console.warn("Failed to check update from yarnpkg"), e?.code !== 0) return "retry"
                    }
                    return null
                } : [
                    `yarn global add ${H}`, {
                        retry: !0
                    }
                ],
                // 生成Hydro配置文件
                () => p ?
                    (0, a.writeFileSync)(`${process.env.HOME}/.hydro/judge.yaml`, Ne) :
                    (0, a.writeFileSync)(`${process.env.HOME}/.hydro/addon.json`, JSON.stringify(le)),
                () => He()
            ]
        },
        {
            // 步骤7:创建MongoDB用户
            init: "install.createDatabaseUser",
            skip: () => (0, a.existsSync)(`${process.env.HOME}/.hydro/config.json`) || p,
            hidden: p,
            operations: [
                "pm2 start mongod",
                () => x(3e3),
                async () => {
                    let {
                        MongoClient,
                        WriteConcern
                    } = eval("require")("/usr/local/share/.config/yarn/global/node_modules/mongodb");
                    let client = await MongoClient.connect("mongodb://127.0.0.1", {
                        readPreference: "nearest",
                        writeConcern: new WriteConcern("majority")
                    });
                    await client.db("hydro").command({
                        createUser: "hydro",
                        pwd: v,
                        roles: [{
                            role: "readWrite",
                            db: "hydro"
                        }]
                    });
                    await client.close()
                },
                () => (0, a.writeFileSync)(`${process.env.HOME}/.hydro/config.json`, JSON.stringify({
                    uri: `mongodb://hydro:${v}@127.0.0.1:27017/hydro`
                })),
                "pm2 stop mongod",
                "pm2 del mongod"
            ]
        },
        {
            // 步骤8:启动所有服务
            init: "install.starting",
            operations: [
                ["pm2 stop all", {
                    ignore: !0
                }],
                () => (0, a.writeFileSync)(`${process.env.HOME}/.hydro/mount.yaml`, _e),
                `pm2 start bash --name hydro-sandbox -- -c "ulimit -s unlimited && hydro-sandbox -mount-conf ${process.env.HOME}/.hydro/mount.yaml -http-addr=localhost:5050"`,
                ...p ? [] : [
                    () => console.log(`WiredTiger cache size: ${re}GB`),
                    `pm2 start mongod --name mongodb -e /dev/null -- --auth ${Re ? "--bind_ip=0.0.0.0 " : ""}--wiredTigerCacheSizeGB=${re}`,
                    () => x(1e3),
                    async () => {
                        if (V) {
                            n("hydrooj cli system set server.host 0.0.0.0");
                            return;
                        }
                        w === "hustoj" && (
                            n("systemctl stop nginx || true"),
                            n("systemctl disable nginx || true"),
                            n("/etc/init.d/nginx stop || true"),
                            await x(1e3)
                        );
                        await Me(80) || (i.warn("port.80"), b.push(["port.80"]));
                        n("pm2 start caddy -- run", {
                            cwd: `${process.env.HOME}/.hydro`
                        });
                        n("hydrooj cli system set server.xff x-forwarded-for");
                        n("hydrooj cli system set server.xhost x-forwarded-host");
                        n("hydrooj cli system set server.xproxy true");
                    }
                ],
                "pm2 start hydrooj",
                "pm2 startup",
                "pm2 save"
            ]
        },
        {
            // 步骤9:安装迁移工具
            init: "install.migrate",
            skip: () => !w,
            silent: !0,
            operations: [
                ["yarn global add @hydrooj/migrate", {
                    retry: !0
                }],
                "hydrooj addon add @hydrooj/migrate"
            ]
        },
        {
            // 步骤10:HustOJ数据迁移
            init: "install.migrateHustoj",
            skip: () => w !== "hustoj",
            silent: !0,
            operations: [
                "pm2 restart hydrooj",
                () => {
                    let t = (0, a.readFileSync)("/home/judge/src/web/include/db_info.inc.php", "utf-8").split(`
`);

                    function r(s) {
                        let c = t.find(d => d.includes(`$${s}`))?.split("=", 2)[1].split(";")[0].trim();
                        return c ? c.startsWith('"') && c.endsWith('"') ? c.slice(1, -1) :
                            c === "false" ? !1 : c === "true" ? !0 : +c : null
                    }

                    let o = {
                        host: r("DB_HOST"),
                        port: 3306,
                        name: r("DB_NAME"),
                        dataDir: r("OJ_DATA"),
                        username: r("DB_USER"),
                        password: r("DB_PASS"),
                        contestType: r("OJ_OI_MODE") ? "oi" : "acm",
                        domainId: "system"
                    };
                    console.log(o);
                    n(`hydrooj cli script migrateHustoj '${JSON.stringify(o)}'`, {
                        stdio: "inherit"
                    });
                    r("OJ_REGISTER") || n("hydrooj cli user setPriv 0 0")
                },
                "pm2 restart hydrooj"
            ]
        },
        {
            // 步骤11:UniversalOJ数据迁移
            init: "install.migrateUoj",
            skip: () => w !== "uoj",
            silent: !0,
            operations: [
                () => {
                    let t = (n("docker ps -a --format json").output?.split(`
`).map(u => u.trim()).filter(u => u).map(u => JSON.parse(u))).find(u =>
                        u.Image.toLowerCase() === "universaloj/uoj-system" && u.State === "running"
                    );
                    let r = t.Id || t.ID;
                    let o = JSON.parse(n(`docker inspect ${r}`).output);
                    let s = o[0].GraphDriver.Data.MergedDir;
                    n(`sed s/127.0.0.1/0.0.0.0/g -i ${s}/etc/mysql/mysql.conf.d/mysqld.cnf`);
                    n(`docker exec -i ${r} /etc/init.d/mysql restart`);
                    let c = (0, a.readFileSync)(`${s}/etc/mysql/debian.cnf`, "utf-8").split(`
`).find(u => u.startsWith("password"))?.split("=")[1].trim();
                    let d = [`CREATE USER 'hydromigrate'@'%' IDENTIFIED BY '${v}';`,
                        "GRANT ALL PRIVILEGES ON *.* TO 'hydromigrate'@'%' WITH GRANT OPTION;",
                        "FLUSH PRIVILEGES;",
                        ""
                    ].join(`
`);
                    n(`docker exec -i ${r} mysql -u debian-sys-maint -p${c} -e "${d}"`);
                    let $ = {
                        host: o[0].NetworkSettings.IPAddress,
                        port: 3306,
                        name: "app_uoj233",
                        dataDir: `${s}/var/uoj_data`,
                        username: "hydromigrate",
                        password: v,
                        domainId: "system"
                    };
                    console.log($);
                    n(`hydrooj cli script migrateUniversaloj '${JSON.stringify($)}'`, {
                        stdio: "inherit"
                    })
                }
            ]
        },
        {
            // 步骤12:安装完成提示
            init: "install.done",
            skip: () => p,
            operations: se
        },
        {
            // 步骤13:系统后续优化
            init: "install.postinstall",
            operations: [
                'echo "layout=2" >/etc/HYDRO_INSTALLER',
                'echo "vm.swappiness = 1" >>/etc/sysctl.conf',
                "sysctl -p",
                'screen -d -m "pm2 install pm2-logrotate && pm2 set pm2-logrotate:max_size 64M"'
            ]
        },
        {
            // 步骤14:最终汇总提示
            init: "install.alldone",
            operations: [
                ...se,
                () => i.info("install.alldone"),
                () => p && i.info("install.editJudgeConfigAndStart"),
                () => X && i.info("install.restartRequired"),
                () => {
                    if (b.length) {
                        i.warn("install.warnings");
                        for (let e of b) i.warn(e[0], ...e.slice(1))
                    }
                }
            ]
        }
    ];
}

// 端口检测工具
function Me(e) {
    let t = ie.default.createServer();
    return new Promise(o => {
        t.once("error", () => o(!1));
        t.once("listening", () => o(!0));
        t.listen(e);
    }).finally(() => t.close());
}

// 脚本入口函数
async function de() {
    try {
        // 自动检测镜像源(国内/国外)
        if (process.env.REGION) {
            process.env.REGION !== "CN" && (C = !1);
        } else {
            console.log("Getting IP info to find best mirror:");
            let t = await fetch("https://ipinfo.io", {
                headers: {
                    accept: "application/json"
                }
            }).then(r => r.json());
            delete t.readme;
            console.log(t);
            t.country !== "CN" && (C = !1);
        }
    } catch (t) {
        console.error(t);
        console.log("Cannot find the best mirror. Fallback to default.");
    }

    // 执行安装流程
    let e = Fe();
    for (let t = 0; t < e.length; t++) {
        let r = e[t];
        if (r.silent || i.info(r.init));
        if (r.skip?.()) {
            r.silent || i.info("info.skip");
        } else {
            for (let o of r.operations) {
                if (o instanceof Array || (o = [o, {}])) {
                    o[0].toString().startsWith("nix-env") && (o[1].retry = !0);
                    if (typeof o[0] === "string") {
                        E = 0;
                        let s = n(o[0], {
                            stdio: "inherit"
                        });
                        for (; s.code && o[1].ignore !== !0;) {
                            if (o[1].retry && E < 30) {
                                i.warn("Retry in 3 secs... (%s)", o[0]);
                                await x(3e3);
                                s = n(o[0], {
                                    stdio: "inherit"
                                });
                                E++;
                            } else {
                                i.fatal("Error when running %s", o[0]);
                            }
                        }
                    } else {
                        E = 0;
                        let s = await o[0](o[1]);
                        for (; s === "retry";) {
                            if (E < 30) {
                                i.warn("Retry in 3 secs...");
                                await x(3e3);
                                s = await o[0](o[1]);
                                E++;
                            } else {
                                i.fatal("Error installing");
                            }
                        }
                    }
                }
            }
        }
    }
}

// 执行入口并捕获异常
de().catch(i.fatal);
global.main = de;
0 && (module.exports = {
    link
});

该脚本完成的主要功能如下:

1.环境校验(权限、系统架构、操作系统兼容性);
2.镜像源自动适配(根据 IP 地区选择国内 / 国际镜像);
3.依赖组件安装(MongoDB、编译器、Caddy、Hydro 核心包等);
4.配置文件生成(数据库、沙箱、Web 服务、评测机配置);
5.服务进程管理(通过 PM2 托管所有相关服务);
6.数据迁移(支持从 HustOJ/UniversalOJ 迁移至 Hydro);
7.后续优化(系统参数调整、日志轮转配置)。

1. 头部依赖导入与工具函数封装(至 var Te = 前)

这部分是脚本的基础支撑层,所有后续功能都依赖该部分提供的能力,代码位置与功能一一对应:

(1)模块导入

专门引入 Node.js 核心模块和相关依赖,为后续系统交互、数据处理提供基础能力,对应代码如下:

// 依赖模块导入
var ne = require("child_process");
var L = h(require("crypto"));
var a = require("fs");
var ie = h(require("net"));
var j = h(require("os"));
var ae = require("readline/promises");
var O = h(require("node:process"), 1);
var k = h(require("node:process"), 1);
var B = h(require("node:os"), 1);
var A = h(require("node:tty"), 1);

各模块核心用途:

  • child_process(别名 ne):执行系统命令(如安装软件、启动 PM2 服务),是脚本与操作系统交互的核心;
  • crypto(别名 L):生成 32 位十六进制随机字符串,用于 MongoDB 数据库密码、评测机通信密钥;
  • fs(别名 a):完成各类配置文件的读写(如写入 Caddy 配置、生成 Hydro 评测机配置);
  • net(别名 ie):创建临时网络服务器,实现端口占用检测(核心用于检测 80 端口是否被占用);
  • os(别名 j/B):获取系统硬件与环境信息(内存大小、CPU 核心数、操作系统版本、主机架构);
  • readline/promises(别名 ae):提供交互式命令行询问能力,用于确认用户是否需要进行 HustOJ/UOJ 数据迁移;
  • process(别名 O/k/U):获取进程相关信息(命令行参数、当前用户权限、系统环境变量、运行平台);
  • tty(别名 A):检测终端特性(是否为 TTY 终端、是否支持彩色输出、是否支持超链接展示)。

(2)工具函数封装

封装各类通用工具函数,避免重复代码,提升脚本可维护性,各函数对应代码位置与功能:

  1. 终端输出优化工具
    • m(e, t):命令行参数检测,用于判断是否传入 --no-color 等终端样式控制参数;
    • _()ve(e, opts):综合判断终端颜色支持能力,适配不同终端的彩色输出;
    • z(e):检测终端是否支持超链接展示,不支持则降级为「文本 <链接>」格式;
    • M(e, t):生成终端超链接,兼容支持与不支持超链接的终端环境。
  2. 命令执行封装
    • n(e, t):封装 child_process.execSync,执行系统命令并捕获异常,统一返回 {output, code, message} 格式结果,避免单个命令失败导致脚本崩溃。
  3. 延时函数
    • x(e):封装 setTimeout 为 Promise 格式,用于等待服务初始化(如 MongoDB 启动后延时 3 秒再创建用户)。
  4. Nix 命令拼接
    • T(...e):根据系统可用内存(je 变量判断是否小于 1GB),决定是否拆分 Nix 安装命令,内存不足时拆分执行避免内存溢出。
  5. 端口占用检测
    • Me(e):通过创建临时 TCP 服务器,检测指定端口(如 80 端口)是否被占用,返回布尔值。
  6. Yarn 配置优化
    • Ae():配置 Yarn 忽略无用平台依赖包(如非当前系统的 esbuild 依赖),减少安装体积和时间;
    • He():安装完成后恢复 Yarn 原始配置,删除无用依赖忽略规则。

2. 国际化配置与全局参数定义(var Te = 至 var ce =

该部分完成多语言适配与安装模式配置,为后续流程提供全局参数支撑,对应代码位置清晰:

(1)国际化配置(var Te = 定义)

// 国际化文案配置
var Te = {
    zh: { ... }, // 中文文案
    en: { ... }  // 英文文案
};

包含中文(zh)和英文(en)两套完整文案,覆盖安装提示、警告、错误、迁移确认等全场景。脚本会根据系统环境变量 LANG/LOCALE 自动切换语言,Linux 纯终端环境强制使用英文,确保输出兼容性。

(2)全局参数定义

通过解析命令行参数,定义安装模式和功能开关,核心参数对应代码位置与含义:

// 全局参数解析与初始化
var p = process.argv.includes("--judge"); // 是否仅安装评测机
var V = process.argv.includes("--no-caddy"); // 是否不安装Caddy Web服务器
var Re = process.argv.includes("--expose-db"); // 是否暴露MongoDB(允许外部访问)
var le = ["@hydrooj/ui-default", "@hydrooj/hydrojudge", "@hydrooj/fps-importer", "@hydrooj/a11y"]; // Hydro附加组件
var H = p ? "@hydrooj/hydrojudge" : `hydrooj ${le.join(" ")}`; // 要安装的Hydro包
var Q = process.argv.find(e => e.startsWith("--substituters=")); // Nix镜像源参数
var K = Q ? Q.split("=")[1].split(",") : []; // 镜像源列表
var Z = process.argv.find(e => e.startsWith("--migration=")); // 数据迁移类型参数
var w = Z ? Z.split("=")[1] : ""; // 迁移类型(hustoj/uoj)
var X = !1; // 树莓派是否需要重启标记
var ce = process.env.LANG?.includes("zh") || process.env.LOCALE?.includes("zh") ? "zh" : "en"; // 当前语言
process.env.TERM === "linux" && (ce = "en"); // Linux终端强制英文

关键参数补充:

  • v:随机生成的 MongoDB 数据库密码(32 位 16 进制字符串,由 crypto.randomBytes 生成);
  • 日志工具 i:封装国际化文案,实现多语言日志输出(i.info/i.warn/i.fatal)。

3. 系统环境校验(process.getuid? 至 var $e =

安装前的前置校验流程,确保系统满足安装条件,不满足则直接退出,对应代码位置如下:

// 前置环境校验
process.getuid ? process.getuid() !== 0 && i.fatal("error.rootRequired") : i.fatal("error.unsupportedOs"); // 权限校验
["x64", "arm64"].includes(process.arch) || i.fatal("error.unsupportedArch", process.arch); // 架构校验
process.env.HOME || i.fatal("$HOME not found"); // 环境变量校验
(0, a.existsSync)("/etc/os-release") || i.fatal("error.osreleaseNotFound"); // 系统版本文件校验

// 读取操作系统信息
var $e = (0, a.readFileSync)("/etc/os-release", "utf-8");
var Ie = $e.split(`
`);
var ee = {};
for (let e of Ie) {
    if (!e.trim()) continue;
    let t = e.split("=");
    t[1].startsWith('"') ? ee[t[0].toLowerCase()] = t[1].substring(1, t[1].length - 2) : ee[t[0].toLowerCase()] = t[1];
}

// CPU AVX指令集检测
var F = !0;
var Se = (0, a.readFileSync)("/proc/cpuinfo", "utf-8");
!Se.includes("avx") && !p && (F = !1, i.warn("warn.avx"), b.push(["warn.avx"]));

各校验环节对应代码:

  1. 权限校验:检测当前用户是否为 root(process.getuid() === 0),非 root 输出错误并退出;
  2. 架构校验:仅支持 x64/arm64 架构,其他架构(如 x86)直接退出;
  3. 环境变量校验:检测 $HOME 环境变量是否存在,不存在则退出;
  4. 系统版本文件校验:检测 /etc/os-release 文件是否存在(用于识别操作系统),不存在则退出;
  5. CPU 特性校验:读取 /proc/cpuinfo 检测 CPU 是否支持 AVX 指令集,不支持则给出警告(将使用 MongoDB 4.4 版本,性能略有下降);
  6. 操作系统识别:读取 /etc/os-release 文件,解析操作系统名称、版本等信息,存入 ee 对象,后续用于 CentOS 等不兼容系统的判断。

4. 核心配置模板定义(var _e = 至 var Ne =

定义 Hydro 运行所需的各类配置文件模板,后续会写入对应目录,各模板对应代码位置与功能:

(1)沙箱挂载配置(var _e = 定义)

// 沙箱挂载配置模板生成工具与配置
function g(e, t = e, r = !0) { ... } // 挂载配置片段生成函数

var _e = `mount:
${g(`${R}bin`, "/bin")}
${g(`${R}bin`, "/usr/bin")}
... // 其他挂载配置
`; // Hydro评测沙箱挂载配置

这是 Hydro 评测沙箱(hydro-sandbox)的挂载配置模板,核心功能:

  • 绑定 Nix 安装目录(~/.nix-profile)、系统核心目录(/bin//lib 等),确保沙箱内可调用编译器、系统工具;
  • 挂载 /w/tmp 为 tmpfs 临时文件系统,提升评测时的文件读写性能;
  • 设置沙箱进程的 UID/GID(1536)、工作目录、主机名,实现评测环境与主机系统的权限隔离。

(2)Caddy Web 服务配置(var ke = 定义)

var ke = `# \u5982\u679C\u4F60\u5E0C\u671B\u4F7F\u7528\u5176\u4ED6\u7AEF\u53E3\u6216\u4F7F\u7528\u57DF\u540D\uff0c\u4FEE\u6539\u6B64\u5904 :80 \u7684\u503C\u540E\u5728 ~/.hydro \u76EE\u5F55\u4E0B\u4F7F\u7528 caddy reload \u91CD\u8F7D\u914D\u7F6E\u3002
:80 {
  encode zstd gzip
  log { ... }
  @static { ... }
  handle @static { ... }
  handle {
    reverse_proxy http://127.0.0.1:8888
  }
}
`; // Caddy配置模板

Caddy Web 服务器的配置文件模板,核心功能:

  • 监听 80 端口,修改为域名后可自动申请并续期 SSL 证书,实现 HTTPS 访问;
  • 开启 zstd/gzip 压缩,减小网页传输体积,提升加载速度;
  • 日志记录:输出到 /data/access.log,按 1GB 大小分割,保留 72 小时日志;
  • 静态文件托管:直接返回 /root/.hydro/static 目录下的静态资源(如图片、CSS),动态请求反向代理到 Hydro 核心服务(127.0.0.1:8888)。

(3)评测机配置(var Ne = 定义)

var Ne = `hosts:
  local:
    host: localhost
    type: hydro
    ... // 服务器连接信息
tmpfs_size: 512m
stdio_size: 256m
memoryMax: ${Math.min(1024, j.default.totalmem() / 4)}m
... // 其他资源限制
secret: ${L.default.randomBytes(32).toString("hex")}
`; // 评测机配置模板

Hydro 评测机(hydrojudge)的配置文件模板,核心参数:

  • hosts:配置评测机连接的 Hydro 服务器信息(默认本地服务器,包含用户名、密码、并发数);
  • 资源限制:临时目录大小(512m)、内存上限(总内存的 1/4,最大 1024m)、进程数限制(128)、评测总时间限制(600 秒);
  • 并发配置:根据 CPU 核心数自动计算 parallelism(并发数 = CPU 核心数 / 4,最小为 1);
  • secret:随机生成的 32 位密钥,用于评测机与 Hydro 服务器之间的通信认证;
  • 环境变量:设置 PATH,确保评测环境中可调用 gcc、python3 等编译 / 运行工具。

此外,该部分还包含 Nix 配置模板(var te =),用于配置 Nix 镜像源、信任公钥和实验性功能。

5. 安装流程定义(var Fe = 至 async function de()

这是脚本的核心执行逻辑,通过 Fe() 函数定义了有序的安装步骤,每个步骤对应一个安装阶段,按执行顺序对应代码如下:

(1)初始化准备(init: "install.preparing"

// 步骤1:初始化准备
{
    init: "install.preparing",
    operations: [
        async () => { ... }, // 检测宝塔面板(BT-Panel),避免环境冲突
        async () => { ... }, // 检测CentOS系统,内核过低默认不支持
        async () => { ... }, // AVX指令集缺失提示,延时60秒
        async () => { ... }, // 检测/dev/shm大小,小于250MB给出警告
        async () => { ... }, // 树莓派专属配置,开启内存cgroup支持
        () => { ... }, // 写入Nix配置文件,配置镜像源和实验性功能
        T("pm2", "yarn", ...), // 安装基础依赖(pm2、yarn等)
        "yarn config set disable-self-update-check true", // 禁用Yarn自更新检查
        async () => { ... } // 交互式检测数据迁移,询问用户是否迁移HustOJ/UOJ数据
    ]
}

该步骤是安装前的前置准备,解决环境兼容性问题,为后续安装铺路。

(2)安装 MongoDB(init: "install.mongodb"

// 步骤2:安装MongoDB
{
    init: "install.mongodb",
    skip: () => p, // 仅安装评测机时跳过
    hidden: p,
    operations: [
        () => { ... }, // 写入Nix配置,允许安装不安全的OpenSSL版本
        T(`hydro.mongodb${F ? 7 : 4}${C ? "-cn" : ""}`, "mongosh", "mongodb-tools") // 安装MongoDB及配套工具
    ]
}

根据 CPU 是否支持 AVX 指令集,选择安装 MongoDB 7.x(支持 AVX)或 4.4(不支持 AVX),同时安装数据库客户端 mongosh 和工具集 mongodb-tools

(3)安装编译器(init: "install.compiler"

// 步骤3:安装编译器
{
    init: "install.compiler",
    operations: [T("gcc", "python3")] // 通过Nix安装gcc和python3
}

安装代码评测所需的编译 / 运行环境,gcc 用于编译 C/C++ 代码,python3 用于执行 Python 代码。

(4)安装沙箱(init: "install.sandbox"

// 步骤4:安装沙箱
{
    init: "install.sandbox",
    skip: () => !n("hydro-sandbox --help").code, // 已安装则跳过
    operations: [T("go-judge"), "ln -sf $(which go-judge) /usr/local/bin/hydro-sandbox"]
}

安装 go-judge(Hydro 沙箱核心),并创建软链接 /usr/local/bin/hydro-sandbox,使系统可全局调用沙箱命令。

(5)安装 Caddy(init: "install.caddy"

// 步骤5:安装Caddy
{
    init: "install.caddy",
    skip: () => p || V || (0, a.existsSync)(`${process.env.HOME}/.hydro/Caddyfile`), // 满足任一条件则跳过
    hidden: p,
    operations: [T("caddy"), () => (0, a.writeFileSync)(`${process.env.HOME}/.hydro/Caddyfile`, ke)]
}

通过 Nix 安装 Caddy Web 服务器,并将配置模板 ke 写入 ~/.hydro/Caddyfile,已存在配置文件则跳过。

(6)安装 Hydro 核心(init: "install.hydro"

// 步骤6:安装Hydro核心
{
    init: "install.hydro",
    operations: [
        () => Ae(), // 优化Yarn配置,忽略无用依赖
        C ? () => { ... } : [...], // 优先国内镜像安装,失败回退官方镜像
        () => p ? ... : ..., // 生成评测机/核心服务配置文件
        () => He() // 恢复Yarn原始配置
    ]
}

通过 Yarn 全局安装 Hydro 核心包及附加组件,根据安装模式(完整服务 / 仅评测机)生成对应的配置文件,安装前后优化并恢复 Yarn 配置。

(6)安装 Hydro 核心(init: "install.hydro"

// 步骤7:创建数据库用户
    init: "install.createDatabaseUser",
    skip: () => (0, a.existsSync)(`${process.env.HOME}/.hydro/config.json`) || p, // 已存在配置或仅装评测机则跳过
    hidden: p,
    operations: [
        "pm2 start mongod", // 临时启动MongoDB
        () => x(3e3), // 延时3秒等待初始化
        async () => { ... }, // 创建hydro数据库用户,设置随机密码
        () => (0, a.writeFileSync)(...), // 生成Hydro数据库配置文件
        "pm2 stop mongod", // 停止临时MongoDB进程
        "pm2 del mongod" // 删除临时进程
    ]
}

为 Hydro 创建专属 MongoDB 用户,配置访问权限,并生成数据库连接配置文件,确保 Hydro 能正常连接数据库。

(8)启动服务(init: "install.starting"

// 步骤8:启动所有服务
{
    init: "install.starting",
    operations: [
        ["pm2 stop all", { ignore: !0 }], // 停止所有已有PM2进程
        () => (0, a.writeFileSync)(...), // 写入沙箱挂载配置
        `pm2 start bash --name hydro-sandbox -- ...`, // 启动沙箱服务
        ...p ? [] : [ // 非评测机模式,启动额外服务
            () => console.log(...), // 输出MongoDB缓存大小
            `pm2 start mongod --name mongodb ...`, // 启动MongoDB并配置缓存
            () => x(1e3), // 延时1秒
            async () => { ... } // 检测80端口,启动Caddy并配置反向代理
        ],
        "pm2 start hydrooj", // 启动Hydro核心服务
        "pm2 startup", // 配置PM2开机自启
        "pm2 save" // 保存当前进程列表
    ]
}

通过 PM2 托管所有服务,实现进程守护和开机自启,核心服务包括:hydro-sandbox(沙箱)、mongodb(数据库)、caddy(Web 服务器)、hydrooj(Hydro 核心)。

(9)数据迁移(init: "install.migrate"/install.migrateHustoj/install.migrateUoj

// 步骤9:安装迁移工具
{
    init: "install.migrate",
    skip: () => !w, // 无迁移需求则跳过
    silent: !0,
    operations: [["yarn global add @hydrooj/migrate", { retry: !0 }], ...]
}

// 步骤10:HustOJ数据迁移
{
    init: "install.migrateHustoj",
    skip: () => w !== "hustoj", // 非HustOJ迁移则跳过
    silent: !0,
    operations: [
        "pm2 restart hydrooj",
        () => { ... } // 读取HustOJ配置,执行数据迁移
    ]
}

// 步骤11:UniversalOJ数据迁移
{
    init: "install.migrateUoj",
    skip: () => w !== "uoj", // 非UOJ迁移则跳过
    silent: !0,
    operations: [
        () => { ... } // 通过Docker获取UOJ配置,执行数据迁移
    ]
}

先安装 Hydro 迁移工具 @hydrooj/migrate,再根据迁移类型(HustOJ/UOJ),自动读取原 OJ 配置,将用户、题目、提交记录等数据迁移到 Hydro,迁移后不影响原 OJ 使用。

(10)安装完成(init: "install.done"/install.alldone

// 步骤12:安装完成提示
{
    init: "install.done",
    skip: () => p, // 仅安装评测机则跳过
    operations: se // 输出数据库密码、QQ群二维码等信息
}

// 步骤13:最终汇总提示
{
    init: "install.alldone",
    operations: [
        ...se,
        () => i.info("install.alldone"), // 输出安装完成提示
        () => p && i.info(...), // 提醒评测机用户编辑配置并启动
        () => X && i.info(...), // 树莓派用户需重启系统
        () => { ... } // 汇总输出所有警告信息
    ]
}

输出安装完成相关信息,包括 MongoDB 用户名密码、Hydro 交流群二维码,汇总安装过程中的所有警告(如端口被占用、/dev/shm 过小),并针对评测机用户、树莓派用户给出专属提示。

(11)后续优化(init: "install.postinstall"

// 步骤14:系统后续优化
{
    init: "install.postinstall",
    operations: [
        'echo "layout=2" >/etc/HYDRO_INSTALLER', // 写入Hydro安装标识
        'echo "vm.swappiness = 1" >>/etc/sysctl.conf', // 减少内存交换
        "sysctl -p", // 生效系统参数
        'screen -d -m "pm2 install pm2-logrotate && ..."' // 配置PM2日志轮转
    ]
}

完成系统参数优化和日志配置,减少内存交换提升性能,配置 PM2 日志自动分割,避免日志文件过大占用磁盘空间。

6. 脚本入口执行(de().catch(i.fatal)

// 脚本入口函数
async function de() {
    try {
        // 自动检测IP地区,适配最优镜像源
        if (process.env.REGION) {
            process.env.REGION !== "CN" && (C = !1);
        } else {
            console.log("Getting IP info to find best mirror:");
            let t = await fetch("https://ipinfo.io", ...).then(r => r.json());
            t.country !== "CN" && (C = !1);
        }
    } catch (t) {
        console.error(t);
        console.log("Cannot find the best mirror. Fallback to default.");
    }

    // 遍历执行安装步骤
    let e = Fe();
    for (let t = 0; t < e.length; t++) {
        let r = e[t];
        // 步骤执行逻辑,包含重试机制
        ...
    }
}

// 执行入口并捕获异常
de().catch(i.fatal);
global.main = de;
0 && (module.exports = { link });

de() 函数是脚本的入口,核心流程:

  1. 镜像源适配:自动检测 IP 地区(国内 / 国外),国内环境使用国内镜像源,国外环境使用官方镜像源,检测失败则使用默认配置;
  2. 流程执行:遍历 Fe() 定义的所有安装步骤,逐步骤执行操作,包含重试机制(命令执行失败时最多重试 30 次,每次间隔 3 秒);
  3. 异常捕获:通过 catch(i.fatal) 捕获执行过程中的所有异常,输出错误信息并退出进程,避免脚本异常挂起。
五、说明

上面的js文件是经过压缩和混淆以后的结果,并不是真正的原代码,不然不会出现这么多没有意义的变量,混淆不影响代码的执行。

hydro官方的github上还给我们提供了更加ts的原代码,这个比上面解析出来的结果更好理解。

/* eslint-disable no-await-in-loop */
/* eslint-disable import/no-dynamic-require */
/* eslint-disable no-sequences */
import { execSync, ExecSyncOptions } from 'child_process';
import crypto from 'crypto';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import net from 'net';
import os, { cpus } from 'os';
import { createInterface } from 'readline/promises';
import supportsHyperlink from 'supports-hyperlinks';

const isSupported = supportsHyperlink.stdout;
const OSC = '\u001B]';
const BEL = '\u0007';
const SEP = ';';
export const link = isSupported ? (text: string, url: string) => [
    OSC, '8', SEP, SEP, url, BEL, text, OSC, '8', SEP, SEP, BEL,
].join('') : (text: string, url: string) => `${text} < ${url} > `;
const freemem = os.freemem();
const smallMemory = (freemem < 1024 * 1024 * 1024);

const nixInstall = (...packages: string[]) => (smallMemory
    ? packages.map((t) => `nix-env -iA ${t.includes('.') ? t : `nixpkgs.${t}`}`).join(' && ')
    : `nix-env -iA ${packages.map((t) => (t.includes('.') ? t : `nixpkgs.${t}`)).join(' ')}`);

const warnings: [string, ...any[]][] = [];

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 shmFAQ = 'https://docs.hydro.ac/FAQ/#%E8%B0%83%E6%95%B4%E4%B8%B4%E6%97%B6%E7%9B%AE%E5%BD%95%E5%A4%A7%E5%B0%8F';
const locales = {
    zh: {
        'install.wait': '安装脚本将等待 %d 秒后自动继续安装,或按 Ctrl-C 退出。',
        'install.start': '开始运行 Hydro 安装工具',
        'note.avx': `检测到您的 CPU 不支持 avx 指令集,这可能会影响系统运行速度。
如果您正在使用 PVE/VirtualBox 等虚拟机平台,请尝试关机后将虚拟机的 CPU 类型设置为 Host,重启后再次运行该脚本。`,
        'warn.avx': '检测到您的 CPU 不支持 avx 指令集,将使用 mongodb@v4.4',
        'error.rootRequired': '请先使用 sudo su 切换到 root 用户后再运行该工具。',
        'error.unsupportedArch': '不支持的架构 %s ,请尝试手动安装。',
        'error.osreleaseNotFound': '无法获取系统版本信息(/etc/os-release 文件未找到),请尝试手动安装。',
        'error.unsupportedOS': '不支持的操作系统 %s ,请尝试手动安装,',
        'error.centos': 'CentOS 及其变种系统因系统内核过低,无法安装 Hydro,强烈建议使用其他系统。若确有需求,请升级 Linux 内核至 4.4+ 后再手动安装 Hydro。',
        'install.preparing': '正在初始化安装...',
        'install.mongodb': '正在安装 mongodb...',
        'install.createDatabaseUser': '正在创建数据库用户...',
        'install.compiler': '正在安装编译器...',
        'install.hydro': '正在安装 Hydro...',
        'install.done': 'Hydro 安装成功!',
        'install.alldone': '安装已全部完成。',
        'install.editJudgeConfigAndStart': '请编辑 ~/.hydro/judge.yaml 后使用 pm2 start hydrojudge && pm2 save 启动。',
        'extra.dbUser': '数据库用户名: hydro',
        'extra.dbPassword': '数据库密码: %s',
        'port.80': '端口 80 已被占用,Caddy 无法正常监听此端口。',
        'shm.readFail': '读取 /dev/shm 大小失败。请检查系统是否在此挂载了 tmpfs。',
        'shm.sizeTooSmall': `您的系统 /dev/shm 大小为 %d MB,在高并发评测时可能产生问题。
建议参照文档 ${link('FAQS', shmFAQ)} 进行调整。`,
        'info.skip': '步骤已跳过。',
        'error.bt': `检测到宝塔面板,安装脚本很可能无法正常工作。建议您使用纯净的 Debian 12 系统进行安装。
要忽略该警告,请使用 --shamefully-unsafe-bt-panel 参数重新运行此脚本。`,
        'warn.bt': `检测到宝塔面板,这会对系统安全性与稳定性造成影响。建议使用纯净 Debian 12 系统进行安装。
开发者对因为使用宝塔面板的数据丢失不承担任何责任。
要取消安装,请使用 Ctrl-C 退出。安装程序将在五秒后继续。`,
        'migrate.hustojFound': `检测到 HustOJ。安装程序可以将 HustOJ 中的全部数据导入到 Hydro。(原有数据不会丢失,您可随时切换回 HustOJ)
该功能支持原版 HustOJ 和部分修改版,输入 y 确认该操作。
迁移过程有任何问题,欢迎加QQ群 1085853538 咨询管理员。`,
        'install.restartRequired': '安装完成,请使用 sudo reboot 重启系统。在此之前系统的部分功能可能无法正常使用。',
        'install.warnings': '安装过程中产生了以下警告:',
    },
    en: {
        'install.wait': `The installation script will wait for %d seconds before continuing.
Press Ctrl-C to exit.`,
        'install.start': 'Starting Hydro installation tool',
        'note.avx': `Your CPU does not support avx, this may affect system performance.
If you are using a virtual machine platform such as PVE/VirtualBox,
try shutting down and setting the CPU type of the virtual machine to Host,
then restart and run the script again.`,
        'warn.avx': 'Your CPU does not support avx, 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.',
        'error.centos': `CentOS and its derivatives are not supported due to low system kernel versions.
It is strongly recommended to use other systems. If you really need it, please upgrade the Linux kernel to 4.4+ and manually install Hydro.`,
        '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!',
        'install.alldone': 'Hydro installation completed.',
        'install.editJudgeConfigAndStart': 'Please edit config at ~/.hydro/judge.yaml than start hydrojudge with:\npm2 start hydrojudge && pm2 save.',
        'extra.dbUser': 'Database username: hydro',
        'extra.dbPassword': 'Database password: %s',
        'port.80': 'Port 80 is already in use, Caddy cannot listen to this port.',
        'shm.readFail': 'Failed to read /dev/shm size.',
        'shm.sizeTooSmall': `Your system /dev/shm size is %d MB, which may cause problems in high concurrency testing.
Please refer to ${link('FAQS', shmFAQ)} for adjustments.`,
        'info.skip': 'Step skipped.',
        'error.bt': `BT-Panel detected, this script may not work properly. It is recommended to use a clean Debian 12 OS.
To ignore this warning, please run this script again with '--shamefully-unsafe-bt-panel' flag.`,
        'warn.bt': `BT-Panel detected, this will affect system security and stability. It is recommended to use a clean Debian 12 OS.
The developer is not responsible for any data loss caused by using BT-Panel.
To cancel the installation, please use Ctrl-C to exit. The installation program will continue in five seconds.`,
        'migrate.hustojFound': `HustOJ detected. The installation program can migrate all data from HustOJ to Hydro.
The original data will not be lost, and you can switch back to HustOJ at any time.
This feature supports the original version of HustOJ and some modified versions. Enter y to confirm this operation.
If you have any questions about the migration process, please add QQ group 1085853538 to consult the administrator.`,
        'install.restartRequired': 'Please reboot the system. Some functions may not work properly before the restart.',
        'install.warnings': 'The following warnings occurred during the installation:',
    },
};

const installAsJudge = process.argv.includes('--judge');
const noCaddy = process.argv.includes('--no-caddy');
const exposeDb = process.argv.includes('--expose-db');
const addons = ['@hydrooj/ui-default', '@hydrooj/hydrojudge', '@hydrooj/fps-importer', '@hydrooj/a11y'];
const installTarget = installAsJudge ? '@hydrooj/hydrojudge' : `hydrooj ${addons.join(' ')}`;
const substitutersArg = process.argv.find((i) => i.startsWith('--substituters='));
const substituters = substitutersArg ? substitutersArg.split('=')[1].split(',') : [];
const migrationArg = process.argv.find((i) => i.startsWith('--migration='));
let migration = migrationArg ? migrationArg.split('=')[1] : '';

let needRestart = false;

let locale = (process.env.LANG?.includes('zh') || process.env.LOCALE?.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].substring(1, d[1].length - 2);
    else values[d[0].toLowerCase()] = d[1];
}
let avx = true;
const cpuInfoFile = readFileSync('/proc/cpuinfo', 'utf-8');
if (!cpuInfoFile.includes('avx') && !installAsJudge) {
    avx = false;
    log.warn('warn.avx');
    warnings.push(['warn.avx']);
}
let retry = 0;
log.info('install.start');
let password = crypto.randomBytes(32).toString('hex');
// eslint-disable-next-line
let CN = true;

const nixProfile = `${process.env.HOME}/.nix-profile/`;
const entry = (source: string, target = source, ro = true) => `\
  - type: bind
    source: ${source}
    target: ${target}${ro ? '\n    readonly: true' : ''}`;
const mount = `mount:
${entry(`${nixProfile}bin`, '/bin')}
${entry(`${nixProfile}bin`, '/usr/bin')}
${entry(`${nixProfile}lib`, '/lib')}
${entry(`${nixProfile}share`, '/share')}
${entry(`${nixProfile}etc`, '/etc')}
${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
`;
const Caddyfile = `\
# 如果你希望使用其他端口或使用域名,修改此处 :80 的值后在 ~/.hydro 目录下使用 caddy reload 重载配置。
# 如果你在当前配置下能够通过 http://你的域名/ 正常访问到网站,若需开启 ssl,
# 仅需将 :80 改为你的域名(如 hydro.ac)后使用 caddy reload 重载配置即可自动签发 ssl 证书。
# 填写完整域名,注意区分有无 www (www.hydro.ac 和 hydro.ac 不同,请检查 DNS 设置)
# 请注意在防火墙/安全组中放行端口,且部分运营商会拦截未经备案的域名。
# 其他需求清参照 https://caddyserver.com/docs/ 说明进行设置。
# For more information, refer to caddy v2 documentation.
:80 {
  encode zstd gzip
  log {
    output file /data/access.log {
      roll_size 1gb
      roll_keep_for 72h
    }
    format json
  }
  # Handle static files directly, for better performance.
  root * /root/.hydro/static
  @static {
    file {
      try_files {path}
    }
  }
  handle @static {
    file_server
  }
  handle {
    reverse_proxy http://127.0.0.1:8888
  }
}

# 如果你需要同时配置其他站点,可参考下方设置:
# 请注意:如果多个站点需要共享同一个端口(如 80/443),请确保为每个站点都填写了域名!
# 动态站点:
# xxx.com {
#    reverse_proxy http://127.0.0.1:1234
# }
# 静态站点:
# xxx.com {
#    root * /www/xxx.com
#    file_server
# }
`;

const judgeYaml = `\
hosts:
  local:
    host: localhost
    type: hydro
    server_url: https://hydro.ac/
    uname: judge
    password: examplepassword
    detail: full
    concurrency: 2
tmpfs_size: 512m
stdio_size: 256m
memoryMax: ${Math.min(1024, os.totalmem() / 4)}m
processLimit: 128
testcases_max: 120
total_time_limit: 600
retry_delay_sec: 3
parallelism: ${Math.max(1, Math.floor(cpus().length / 4))}
singleTaskParallelism: 2
rate: 1.00
rerun: 2
secret: ${crypto.randomBytes(32).toString('hex')}
env: |
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    HOME=/w
`;

const nixConfBase = `
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= hydro.ac:EytfvyReWHFwhY9MCGimCIn46KQNfmv9y8E2NqlNfxQ=
connect-timeout = 10
experimental-features = nix-command flakes
`;

const isPortFree = async (port: number) => {
    const server = net.createServer();
    const res = await new Promise((resolve) => {
        server.once('error', () => resolve(false));
        server.once('listening', () => resolve(true));
        server.listen(port);
    });
    server.close();
    return res;
};

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 ||= {};
    Object.assign(data.resolutions, Object.fromEntries([
        '@esbuild/linux-loong64',
        'esbuild-windows-32',
        ...['android', 'darwin', 'freebsd', 'windows']
            .flatMap((i) => [`${i}-64`, `${i}-arm64`])
            .map((i) => `esbuild-${i}`),
        ...['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;
}

function rollbackResolveField() {
    const yarnGlobalPath = exec('yarn global dir').output?.trim() || '';
    if (!yarnGlobalPath) return false;
    const pkgjson = `${yarnGlobalPath}/package.json`;
    const data = JSON.parse(readFileSync(pkgjson, 'utf-8'));
    delete data.resolutions;
    writeFileSync(pkgjson, JSON.stringify(data, null, 2));
    return true;
}

const mem = os.totalmem() / 1024 / 1024 / 1024; // In GiB
// TODO: refuse to install if mem < 1.5
const wtsize = Math.max(0.25, Math.floor((mem / 6) * 100) / 100);

const inviteLink = 'https://qm.qq.com/cgi-bin/qm/qr?k=0aTZfDKURRhPBZVpTYBohYG6P6sxABTw';
const printInfo = [
    () => console.log(`扫码或点击${link('链接', inviteLink)}加入QQ群:`),
    `echo '${inviteLink}' | qrencode -o - -m 2 -t UTF8`,
    () => {
        if (installAsJudge) return;
        const config = require(`${process.env.HOME}/.hydro/config.json`);
        if (config.uri) password = new URL(config.uri).password || '(No password)';
        else password = config.password || '(No password)';
        log.info('extra.dbUser');
        log.info('extra.dbPassword', password);
    },
];

const Steps = () => [
    {
        init: 'install.preparing',
        operations: [
            async () => {
                if (process.env.IGNORE_BT) return;
                const res = exec('bt default');
                if (!res.code) {
                    if (!process.argv.includes('--shamefully-unsafe-bt-panel')) {
                        log.warn('error.bt');
                        process.exit(1);
                    } else {
                        log.warn('warn.bt');
                        warnings.push(['warn.bt']);
                        log.info('install.wait', 5);
                        await sleep(5000);
                    }
                }
            },
            async () => {
                if (process.env.IGNORE_CENTOS) return;
                const res = exec('yum -h');
                if (res.code) return;
                if (!process.argv.includes('--unsupported-centos')) {
                    log.warn('error.centos');
                    process.exit(1);
                } else {
                    log.warn('warn.centos');
                    warnings.push(['warn.centos']);
                }
            },
            async () => {
                if (!avx && !installAsJudge) {
                    log.warn('note.avx');
                    log.info('install.wait', 60);
                    await sleep(60000);
                }
            },
            async () => {
                const shm = exec('df --output=avail -k /dev/shm').output?.split('\n')[1];
                if (!shm || !+shm) {
                    log.warn('shm.readFail');
                    warnings.push(['shm.readFail']);
                    return;
                }
                const size = (+shm) / 1024;
                if (size < 250) {
                    log.warn('shm.sizeTooSmall', size);
                    warnings.push(['shm.sizeTooSmall', size]);
                }
            },
            async () => {
                // Enable memory cgroup for Raspberry Pi
                if (process.arch !== 'arm64') return;
                const isRpi = ['rpi', 'raspberrypi'].some((i) => readFileSync('/proc/cpuinfo', 'utf-8').toLowerCase().includes(i));
                if (!isRpi) return;
                const memoryLine = readFileSync('/proc/cgroups', 'utf-8').split('\n').find((i) => i.includes('memory'))?.trim();
                const memoryCgroupEnabled = memoryLine && !memoryLine.endsWith('0');
                if (memoryCgroupEnabled) return;
                let targetFile = '/boot/cmdline.txt';
                let content = readFileSync(targetFile, 'utf-8');
                if (content.includes('has moved to /boot/firmware/cmdline.txt')) targetFile = '/boot/firmware/cmdline.txt';
                content = readFileSync(targetFile, 'utf-8');
                if (content.includes('cgroup_enable=memory')) return;
                writeFileSync(targetFile, content.replace(' console=', ' cgroup_enable=memory cgroup_memory=1 console='));
                needRestart = true;
            },
            () => {
                if (substituters.length) {
                    writeFileSync('/etc/nix/nix.conf', `substituters = ${substituters.join(' ')}
${nixConfBase}`);
                } else if (!CN) {
                    writeFileSync('/etc/nix/nix.conf', `substituters = https://cache.nixos.org/ https://nix.hydro.ac/cache
${nixConfBase}`);
                }
                if (CN) return;
                // rollback mirrors
                exec('nix-channel --remove nixpkgs', { stdio: 'inherit' });
                exec('nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs', { stdio: 'inherit' });
                exec('nix-channel --update', { stdio: 'inherit' });
            },
            nixInstall('pm2', 'yarn', 'esbuild', 'bash', 'unzip', 'zip', 'diffutils', 'patch', 'screen', 'gawk'),
            'yarn config set disable-self-update-check true',
            async () => {
                const rl = createInterface(process.stdin, process.stdout);
                try {
                    if (existsSync('/home/judge/src')) {
                        log.info('migrate.hustojFound');
                        const res = await rl.question('>');
                        if (res.toLowerCase().trim() === 'y') migration = 'hustoj';
                    }
                    if (migration) return;
                    const docker = !exec('docker -v').code;
                    if (!docker) return;
                    const containers = exec('docker ps -a --format json').output?.split('\n')
                        .map((i) => i.trim()).filter((i) => i).map((i) => JSON.parse(i)) || [];
                    const uoj = containers.find((i) => i.Image.toLowerCase() === 'universaloj/uoj-system' && i.State === 'running');
                    if (uoj) {
                        log.info('migrate.uojFound');
                        const res = await rl.question('>');
                        if (res.toLowerCase().trim() === 'y') migration = 'uoj';
                    }
                    // // 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';
                    // }
                } catch (e) {
                    console.error('Failed migration detection');
                } finally {
                    rl.close();
                }
            },
        ],
    },
    {
        init: 'install.mongodb',
        skip: () => installAsJudge,
        hidden: installAsJudge,
        operations: [
            () => writeFileSync(`${process.env.HOME}/.config/nixpkgs/config.nix`, `\
{
    permittedInsecurePackages = [
        "openssl-1.1.1t"
        "openssl-1.1.1u"
        "openssl-1.1.1v"
        "openssl-1.1.1w"
        "openssl-1.1.1x"
        "openssl-1.1.1y"
        "openssl-1.1.1z"
    ];
}`),
            nixInstall(`hydro.mongodb${avx ? 7 : 4}${CN ? '-cn' : ''}`, 'mongosh', 'mongodb-tools'),
        ],
    },
    {
        init: 'install.compiler',
        operations: [
            nixInstall('gcc', 'python3'),
        ],
    },
    {
        init: 'install.sandbox',
        skip: () => !exec('hydro-sandbox --help').code,
        operations: [
            nixInstall('go-judge'),
            'ln -sf $(which go-judge) /usr/local/bin/hydro-sandbox',
        ],
    },
    {
        init: 'install.caddy',
        skip: () => installAsJudge || noCaddy || existsSync(`${process.env.HOME}/.hydro/Caddyfile`),
        hidden: installAsJudge,
        operations: [
            nixInstall('caddy'),
            () => writeFileSync(`${process.env.HOME}/.hydro/Caddyfile`, Caddyfile),
        ],
    },
    {
        init: 'install.hydro',
        operations: [
            () => removeOptionalEsbuildDeps(),
            (CN ? () => {
                let res: any = null;
                try {
                    exec('yarn config set registry https://registry.npmmirror.com/', { stdio: 'inherit' });
                    res = exec(`yarn global add ${installTarget}`, { stdio: 'inherit' });
                } catch (e) {
                    console.log('Failed to install from npmmirror, fallback to yarnpkg');
                } finally {
                    exec('yarn config set registry https://registry.yarnpkg.com', { stdio: 'inherit' });
                }
                try {
                    exec(`yarn global add ${installTarget}`, { timeout: 60000 });
                } catch (e) {
                    console.warn('Failed to check update from yarnpkg');
                    if (res?.code !== 0) return 'retry';
                }
                return null;
            } : [`yarn global add ${installTarget}`, { retry: true }]),
            () => {
                if (installAsJudge) writeFileSync(`${process.env.HOME}/.hydro/judge.yaml`, judgeYaml);
                else writeFileSync(`${process.env.HOME}/.hydro/addon.json`, JSON.stringify(addons));
            },
            () => rollbackResolveField(),
        ],
    },
    {
        init: 'install.createDatabaseUser',
        skip: () => existsSync(`${process.env.HOME}/.hydro/config.json`) || installAsJudge,
        hidden: installAsJudge,
        operations: [
            'pm2 start mongod',
            () => sleep(3000),
            async () => {
                // eslint-disable-next-line
                const { MongoClient, WriteConcern } = eval('require')('/usr/local/share/.config/yarn/global/node_modules/mongodb') as typeof import('mongodb');
                const client = await MongoClient.connect('mongodb://127.0.0.1', {
                    readPreference: 'nearest',
                    writeConcern: new WriteConcern('majority'),
                });
                await client.db('hydro').command({
                    createUser: 'hydro',
                    pwd: password,
                    roles: [{ role: 'readWrite', db: 'hydro' }],
                });
                await client.close();
            },
            () => writeFileSync(`${process.env.HOME}/.hydro/config.json`, JSON.stringify({
                uri: `mongodb://hydro:${password}@127.0.0.1:27017/hydro`,
            })),
            'pm2 stop mongod',
            'pm2 del mongod',
        ],
    },
    {
        init: 'install.starting',
        operations: [
            ['pm2 stop all', { ignore: true }],
            () => writeFileSync(`${process.env.HOME}/.hydro/mount.yaml`, mount),
            // eslint-disable-next-line max-len
            `pm2 start bash --name hydro-sandbox -- -c "ulimit -s unlimited && hydro-sandbox -mount-conf ${process.env.HOME}/.hydro/mount.yaml -http-addr=localhost:5050"`,
            ...installAsJudge ? [] : [
                () => console.log(`WiredTiger cache size: ${wtsize}GB`),
                // The only thing mongod writes to stderr is 'libcurl no version information available'
                `pm2 start mongod --name mongodb -e /dev/null -- --auth ${exposeDb ? '--bind_ip=0.0.0.0 ' : ''}--wiredTigerCacheSizeGB=${wtsize}`,
                () => sleep(1000),
                async () => {
                    if (noCaddy) {
                        exec('hydrooj cli system set server.host 0.0.0.0');
                        return;
                    }
                    if (migration === 'hustoj') {
                        exec('systemctl stop nginx || true');
                        exec('systemctl disable nginx || true');
                        exec('/etc/init.d/nginx stop || true');
                        await sleep(1000);
                    }
                    if (!await isPortFree(80)) {
                        log.warn('port.80');
                        warnings.push(['port.80']);
                    }
                    exec('pm2 start caddy -- run', { cwd: `${process.env.HOME}/.hydro` });
                    exec('hydrooj cli system set server.xff x-forwarded-for');
                    exec('hydrooj cli system set server.xhost x-forwarded-host');
                    exec('hydrooj cli system set server.xproxy true');
                },
                'pm2 start hydrooj',
            ],
            'pm2 startup',
            'pm2 save',
        ],
    },
    {
        init: 'install.migrate',
        skip: () => !migration,
        silent: true,
        operations: [
            ['yarn global add @hydrooj/migrate', { retry: true }],
            'hydrooj addon add @hydrooj/migrate',
        ],
    },
    {
        init: 'install.migrateHustoj',
        skip: () => migration !== 'hustoj',
        silent: true,
        operations: [
            'pm2 restart hydrooj',
            () => {
                const dbInc = readFileSync('/home/judge/src/web/include/db_info.inc.php', 'utf-8');
                const l = dbInc.split('\n');
                function getConfig(key) {
                    const t = l.find((i) => i.includes(`$${key}`))?.split('=', 2)[1].split(';')[0].trim();
                    if (!t) return null;
                    if (t.startsWith('"') && t.endsWith('"')) return t.slice(1, -1);
                    if (t === 'false') return false;
                    if (t === 'true') return true;
                    return +t;
                }
                const config = {
                    host: getConfig('DB_HOST'),
                    port: 3306,
                    name: getConfig('DB_NAME'),
                    dataDir: getConfig('OJ_DATA'),
                    username: getConfig('DB_USER'),
                    password: getConfig('DB_PASS'),
                    contestType: getConfig('OJ_OI_MODE') ? 'oi' : 'acm',
                    domainId: 'system',
                };
                console.log(config);
                exec(`hydrooj cli script migrateHustoj '${JSON.stringify(config)}'`, { stdio: 'inherit' });
                if (!getConfig('OJ_REGISTER')) exec('hydrooj cli user setPriv 0 0');
            },
            'pm2 restart hydrooj',
        ],
    },
    {
        init: 'install.migrateUoj',
        skip: () => migration !== 'uoj',
        silent: true,
        operations: [
            () => {
                const containers = exec('docker ps -a --format json').output?.split('\n')
                    .map((i) => i.trim()).filter((i) => i).map((i) => JSON.parse(i));
                const uoj = containers!.find((i) => i.Image.toLowerCase() === 'universaloj/uoj-system' && i.State === 'running')!;
                const id = uoj.Id || uoj.ID;
                const info = JSON.parse(exec(`docker inspect ${id}`).output!);
                const dir = info[0].GraphDriver.Data.MergedDir;
                exec(`sed s/127.0.0.1/0.0.0.0/g -i ${dir}/etc/mysql/mysql.conf.d/mysqld.cnf`);
                exec(`docker exec -i ${id} /etc/init.d/mysql restart`);
                const passwd = readFileSync(`${dir}/etc/mysql/debian.cnf`, 'utf-8')
                    .split('\n').find((i) => i.startsWith('password'))?.split('=')[1].trim();
                const script = [
                    `CREATE USER 'hydromigrate'@'%' IDENTIFIED BY '${password}';`,
                    'GRANT ALL PRIVILEGES ON *.* TO \'hydromigrate\'@\'%\' WITH GRANT OPTION;',
                    'FLUSH PRIVILEGES;',
                    '',
                ].join('\n');
                exec(`docker exec -i ${id} mysql -u debian-sys-maint -p${passwd} -e "${script}"`);
                const config = {
                    host: info[0].NetworkSettings.IPAddress,
                    port: 3306,
                    name: 'app_uoj233',
                    dataDir: `${dir}/var/uoj_data`,
                    username: 'hydromigrate',
                    password,
                    domainId: 'system',
                };
                console.log(config);
                // TODO mail config
                exec(`hydrooj cli script migrateUniversaloj '${JSON.stringify(config)}'`, { stdio: 'inherit' });
            },
        ],
    },
    {
        init: 'install.done',
        skip: () => installAsJudge,
        operations: printInfo,
    },
    {
        init: 'install.postinstall',
        operations: [
            'echo "layout=2" >/etc/HYDRO_INSTALLER',
            'echo "vm.swappiness = 1" >>/etc/sysctl.conf',
            'sysctl -p',
            // dont retry this as it usually fails
            'screen -d -m "pm2 install pm2-logrotate && pm2 set pm2-logrotate:max_size 64M"',
        ],
    },
    {
        init: 'install.alldone',
        operations: [
            ...printInfo,
            () => log.info('install.alldone'),
            () => installAsJudge && log.info('install.editJudgeConfigAndStart'),
            () => needRestart && log.info('install.restartRequired'),
            () => {
                if (warnings.length) {
                    log.warn('install.warnings');
                    for (const warning of warnings) {
                        log.warn(warning[0], ...warning.slice(1));
                    }
                }
            },
        ],
    },
];

async function main() {
    try {
        if (process.env.REGION) {
            if (process.env.REGION !== 'CN') CN = false;
        } else {
            console.log('Getting IP info to find best mirror:');
            const res = await fetch('https://ipinfo.io', { headers: { accept: 'application/json' } }).then((r) => r.json());
            delete res.readme;
            console.log(res);
            if (res.country !== 'CN') CN = false;
        }
    } catch (e) {
        console.error(e);
        console.log('Cannot find the best mirror. Fallback to default.');
    }
    const steps = Steps();
    for (let i = 0; i < steps.length; i++) {
        const step = steps[i];
        if (!step.silent) log.info(step.init);
        if (!(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 in 3 secs... (%s)', op[0]);

                            await sleep(3000);
                            res = exec(op[0], { stdio: 'inherit' });
                            retry++;
                        } else log.fatal('Error when running %s', op[0]);
                    }
                } else {
                    retry = 0;
                    let res = await op[0](op[1]);
                    while (res === 'retry') {
                        if (retry < 30) {
                            log.warn('Retry in 3 secs...');

                            await sleep(3000);

                            res = await op[0](op[1]);
                            retry++;
                        } else log.fatal('Error installing');
                    }
                }
            }
        } else if (!step.silent) log.info('info.skip');
    }
}
main().catch(log.fatal);
global.main = main;