From f6e5fdb64a3b36324a145dfe199bba20eb1611bd Mon Sep 17 00:00:00 2001 From: stqfdyr <89149493+stqfdyr@users.noreply.github.com> Date: Thu, 17 Sep 2026 10:50:00 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix(install):=20=E5=BA=94=E6=80=A5=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E4=B8=8D=E5=86=8D=E4=BE=9D=E8=B5=96=20journal?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=20--reset-password?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首次启动的应急密码只打印到 stdout,安装器从 journal 里读取。journald 不保存 日志的主机上(例如 Storage=none)安装器读不到,事后也无从找回,只能删库重装。 GCP 上的 Ubuntu 24.04 有用户遇到:journalctl -u monitor-hub 没有任何记录。 hub 新增 --reset-password:设置新的随机密码、登出所有会话、打印后退出。运行中 的 hub 每次请求都从数据库读取密码哈希与会话,无需重启。 install-hub.sh 首次安装时在服务启动前以 monitor 用户调用它,直接从输出取得 密码;菜单新增「重置密码」,也可用 --reset-password 非交互调用。 --- install-hub.sh | 39 +++++++++++++++++++++++++++++++++------ src/main.rs | 44 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/install-hub.sh b/install-hub.sh index f3285574..493f7f58 100755 --- a/install-hub.sh +++ b/install-hub.sh @@ -152,8 +152,8 @@ install_hub() { # depends on useradd having created one. install -d -m 0700 -o "$USER_NAME" "$DATA" - # A first run prints the one-time password, and only a missing database - # constitutes one. Checked before anything is installed. + # Only a missing database makes this a first install, which sets the + # password. Checked before anything is installed. first="" [ -f "$DATA/monitor.db" ] || first=1 @@ -238,6 +238,12 @@ MemoryMax=256M WantedBy=multi-user.target UNIT + # Taken from the binary's stdout rather than the journal, which some hosts + # keep nowhere. Set before the service starts, so the hub finds a password in + # place and does not generate a second one. + pw="" + [ -z "$first" ] || pw="$(new_password)" + systemctl daemon-reload systemctl enable "$SERVICE" >/dev/null 2>&1 || true # Not left to set -e: a binary that cannot exec fails the job itself, which is @@ -266,13 +272,11 @@ UNIT printf '\n' field "面板" "${SITE:-http://127.0.0.1:$PORT}/admin" if [ -n "$first" ]; then - pw="$(journalctl -u "$SERVICE" --since '-2 min' --no-pager 2>/dev/null | - sed -n 's/.*Emergency password: //p' | tail -1)" if [ -n "$pw" ]; then field "密码" "$pw" - field " " "${D}只显示这一次,登录后到「设置」里改掉${N}" + field " " "${D}记下来,登录后到「设置」里改掉${N}" else - field "密码" "journalctl -u $SERVICE | grep Emergency" + field "密码" "没取到,重跑安装器选「重置密码」" fi fi field "数据" "$DATA/monitor.db" @@ -344,6 +348,23 @@ ingress: CFD } +# ---- password ---- +# Prints nothing on failure; the hub's own error reaches stderr. Runs as the +# service user because SQLite creates -wal and -shm beside the database, and +# root-owned ones would leave the hub unable to open it. +new_password() { + runuser -u "$USER_NAME" -- "$BIN" --db "$DATA/monitor.db" --reset-password | + sed -n 's/^Emergency password: //p' +} + +reset_password() { + [ -f "$DATA/monitor.db" ] || die "这台机器上没有 hub 的数据库:$DATA/monitor.db" + confirm "重置面板密码?所有已登录的会话都会被登出" || return 0 + pw="$(new_password)" + [ -n "$pw" ] || die "重置失败" + field "密码" "$pw" +} + # ---- uninstall ---- uninstall_hub() { if [ ! -f "$BIN" ] && [ ! -f "$UNIT" ]; then @@ -384,6 +405,7 @@ menu() { printf ' 2 卸载\n' printf ' 3 状态\n' printf ' 4 日志\n' + printf ' 5 重置密码\n' printf ' q 退出\n\n' printf ' %s›%s ' "$B" "$N" read -r choice || exit 0 @@ -405,6 +427,7 @@ menu() { 2) uninstall_hub; press ;; 3) systemctl status "$SERVICE" --no-pager || true; press ;; 4) journalctl -u "$SERVICE" -f --no-pager ;; + 5) reset_password; press ;; q | Q | exit | "") exit 0 ;; *) ;; esac @@ -419,6 +442,8 @@ monitor hub 安装器 sudo ./install-hub.sh --port 8443 指定端口安装 sudo ./install-hub.sh --uninstall 卸载,保留数据 sudo ./install-hub.sh --purge 卸载并删除数据库 + sudo ./install-hub.sh --reset-password + 重置面板密码,登出所有会话 --port 本机监听端口,默认 $PORT --site 一般不用填。面板拼安装命令用的是浏览器地址栏,配好反代 @@ -446,6 +471,7 @@ while [ $# -gt 0 ]; do --site) [ $# -ge 2 ] || die "--site 后面要跟地址"; SITE="$2"; SITE_SET=1; shift 2 ;; --uninstall) ACTION=uninstall; shift ;; --purge) ACTION=uninstall; PURGE=1; shift ;; + --reset-password) ACTION=reset; shift ;; --yes | -y) YES=1; shift ;; -h | --help) usage; exit 0 ;; *) die "未知参数:$1(--help 看用法)" ;; @@ -486,6 +512,7 @@ command -v systemctl >/dev/null 2>&1 || case "$ACTION" in uninstall) banner; uninstall_hub ;; +reset) banner; reset_password ;; *) if [ -t 0 ]; then menu diff --git a/src/main.rs b/src/main.rs index a21c7931..b2578cbc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -262,6 +262,7 @@ struct Args { database: String, site: String, themes: PathBuf, + reset_password: bool, } /// The default listen address. A v6 wildcard also accepts IPv4 through @@ -284,6 +285,7 @@ fn parse_args() -> Result { let mut database = "monitor.db".to_owned(); let mut site = String::new(); let mut themes = None; + let mut reset_password = false; let mut it = std::env::args().skip(1); while let Some(arg) = it.next() { let mut value = || it.next().unwrap_or_default(); @@ -292,17 +294,22 @@ fn parse_args() -> Result { "--db" => database = value(), "--site" => site = value(), "--themes" => themes = Some(PathBuf::from(value())), + "--reset-password" => reset_password = true, "-h" | "--help" => { println!( "monitor-hub {}\n\n\ - Usage: monitor-hub [--listen [::]:28080] [--db monitor.db] [--themes themes] [--site https://hub.example.com]\n\n\ + Usage: monitor-hub [--listen [::]:28080] [--db monitor.db] [--themes themes] [--site https://hub.example.com]\n \ + monitor-hub --db monitor.db --reset-password\n\n\ --listen defaults to [::]:28080, one socket serving IPv6 and IPv4\n\ both; where the kernel has no dual-stack sockets it is 0.0.0.0:28080.\n\ --themes defaults to a themes/ directory beside the database.\n\ --site is only needed behind a reverse proxy, where the address the\n\ panel is reached on is not the one agents should use. Left out, the\n\ hub answers on whatever ip:port it is asked, and the panel builds\n\ - install commands from the address in the browser's bar.", + install commands from the address in the browser's bar.\n\ + --reset-password replaces the emergency password, signs every session\n\ + out, prints the new password and exits. Run it as the user the hub\n\ + runs as, so any file SQLite creates stays readable by the hub.", env!("CARGO_PKG_VERSION") ); std::process::exit(0); @@ -315,7 +322,14 @@ fn parse_args() -> Result { let themes = themes.unwrap_or_else(|| { std::path::Path::new(&database).parent().unwrap_or_else(|| std::path::Path::new(".")).join("themes") }); - Ok(Args { listen, listen_defaulted, database, site: site.trim_end_matches('/').to_owned(), themes }) + Ok(Args { + listen, + listen_defaulted, + database, + site: site.trim_end_matches('/').to_owned(), + themes, + reset_password, + }) } #[tokio::main] @@ -328,6 +342,14 @@ async fn main() -> Result<()> { .init(); let args = parse_args()?; + // Delivered on the terminal rather than through the service log, so a + // password missing from the journal is still recoverable. A running hub + // reads the hash and its sessions from the database on every request, so + // the change applies to it without a restart. + if args.reset_password { + println!("Emergency password: {}", new_password(&Db::open(&args.database)?)?); + return Ok(()); + } std::fs::create_dir_all(&args.themes)?; let (notes, inbox) = tokio::sync::mpsc::channel(notify::QUEUE); let app = Arc::new(App::new(Db::open(&args.database)?, args.site.clone(), args.themes, notes)); @@ -548,8 +570,7 @@ fn first_run(app: &App, url: &str) -> Result<()> { if app.db.get("admin_password_hash").is_some() { return Ok(()); } - let password = auth::random_token()[..24].to_owned(); - app.db.set("admin_password_hash", &auth::hash_password(&password)?)?; + let password = new_password(&app.db)?; println!( "\n Monitor hub is ready.\n\n \ Sign in at {url}/admin\n \ @@ -559,6 +580,14 @@ fn first_run(app: &App, url: &str) -> Result<()> { Ok(()) } +/// Sets a random 24-character admin password and signs every session out. +fn new_password(db: &Db) -> Result { + let password = auth::random_token()[..24].to_owned(); + db.set("admin_password_hash", &auth::hash_password(&password)?)?; + db.drop_all_sessions()?; + Ok(password) +} + /// Billing cycles as whole months. `once` has none, so it never rolls over. fn cycle_months(cycle: &str) -> Option { Some(match cycle { @@ -857,5 +886,10 @@ mod tests { assert!(hash.starts_with("$argon2")); first_run(&app, "http://x").unwrap(); assert_eq!(app.db.get("admin_password_hash").unwrap(), hash, "must not rotate on restart"); + + app.db.create_session("s", i64::MAX).unwrap(); + new_password(&app.db).unwrap(); + assert_ne!(app.db.get("admin_password_hash").unwrap(), hash, "a reset must rotate it"); + assert!(!app.db.session_valid("s"), "a reset must sign every session out"); } } From 75d9bda4189586b02dc1728f7c8da74f871b7029 Mon Sep 17 00:00:00 2001 From: stqfdyr <89149493+stqfdyr@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:03:24 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(install):=20=E9=87=8D=E7=BD=AE=E6=8B=92?= =?UTF-8?q?=E7=BB=9D=E4=B8=8D=E5=AD=98=E5=9C=A8=E7=9A=84=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=EF=BC=8C=E9=A6=96=E6=AC=A1=E5=AE=89=E8=A3=85=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E5=90=8E=E5=8F=AF=E9=87=8D=E6=96=B0=E5=8F=96=E5=AF=86?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - --reset-password 在数据库不存在时报错退出,不再新建空库并打印一个无效的密码 - 写入密码哈希与清空会话合入同一事务,面板改密码共用 Db::replace_password - 首次安装先建属主为 monitor 的空库文件再重置;服务没能启动时停掉服务并删除 该库,重跑仍按首次安装显示密码 - SQLite 以 root 运行时会把新建的 -wal / -shm 交给数据库文件的属主,安装器 不再经 runuser 调用 - 重置前检查二进制是否存在;失败时提示版本过旧需先升级 - 提示里改密码的位置更正为「安全」页 --- install-hub.sh | 39 +++++++++++++++++++++++++++------------ src/api.rs | 3 +-- src/db.rs | 16 ++++++++++++++++ src/main.rs | 17 +++++++++-------- 4 files changed, 53 insertions(+), 22 deletions(-) diff --git a/install-hub.sh b/install-hub.sh index 493f7f58..0202040d 100755 --- a/install-hub.sh +++ b/install-hub.sh @@ -240,9 +240,14 @@ UNIT # Taken from the binary's stdout rather than the journal, which some hosts # keep nowhere. Set before the service starts, so the hub finds a password in - # place and does not generate a second one. + # place and does not generate a second one. A reset refuses a missing + # database, so the file is created first under the service user; SQLite reads + # an empty file as an empty database. pw="" - [ -z "$first" ] || pw="$(new_password)" + if [ -n "$first" ]; then + install -m 0600 -o "$USER_NAME" /dev/null "$DATA/monitor.db" + pw="$(new_password)" + fi systemctl daemon-reload systemctl enable "$SERVICE" >/dev/null 2>&1 || true @@ -251,10 +256,17 @@ UNIT # would exit here with a raw systemd error and leave the hub down on the # binary that just failed. systemctl restart "$SERVICE" || true - # is-active answers before a unit that exits immediately has done so, and the - # first run also computes an argon2 hash. Wait, then query. + # is-active answers before a unit that exits immediately has done so. Wait, + # then query. sleep 3 if ! systemctl is-active --quiet "$SERVICE"; then + # The new database holds nothing but a password never shown. Removed, with + # the service stopped so no restart recreates it, so that a rerun is again + # a first install and shows one. + if [ -n "$first" ]; then + systemctl stop "$SERVICE" 2>/dev/null || true + rm -f "$DATA/monitor.db" "$DATA/monitor.db-wal" "$DATA/monitor.db-shm" + fi if [ -n "$backup" ]; then install -m 0755 "$backup" "$BIN" rm -f "$backup" @@ -274,7 +286,7 @@ UNIT if [ -n "$first" ]; then if [ -n "$pw" ]; then field "密码" "$pw" - field " " "${D}记下来,登录后到「设置」里改掉${N}" + field " " "${D}记下来,登录后到「安全」里改掉${N}" else field "密码" "没取到,重跑安装器选「重置密码」" fi @@ -349,19 +361,22 @@ CFD } # ---- password ---- -# Prints nothing on failure; the hub's own error reaches stderr. Runs as the -# service user because SQLite creates -wal and -shm beside the database, and -# root-owned ones would leave the hub unable to open it. +# Prints nothing on failure; the hub's own error reaches stderr. Running as root +# is safe: SQLite gives the -wal and -shm files it creates the database file's +# owner. new_password() { - runuser -u "$USER_NAME" -- "$BIN" --db "$DATA/monitor.db" --reset-password | - sed -n 's/^Emergency password: //p' + "$BIN" --db "$DATA/monitor.db" --reset-password | sed -n 's/^Emergency password: //p' } reset_password() { - [ -f "$DATA/monitor.db" ] || die "这台机器上没有 hub 的数据库:$DATA/monitor.db" + # The data outlives --uninstall, so the database alone does not mean a hub. + if [ ! -f "$BIN" ] || [ ! -f "$DATA/monitor.db" ]; then + die "这台机器上没有装 monitor hub" + fi confirm "重置面板密码?所有已登录的会话都会被登出" || return 0 pw="$(new_password)" - [ -n "$pw" ] || die "重置失败" + # Versions without the flag report "unknown argument" on stderr. + [ -n "$pw" ] || die "重置失败。上面提示 unknown argument 的话是 hub 版本太旧,先升级" field "密码" "$pw" } diff --git a/src/api.rs b/src/api.rs index 703832c3..5cd5861f 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1466,8 +1466,7 @@ pub async fn save_settings( // receives a replacement. if key == "admin_password" { match hash_password(value).and_then(|h| { - app.db.set("admin_password_hash", &h)?; - app.db.drop_all_sessions()?; + app.db.replace_password(&h)?; issue_session(&app, &headers) }) { Ok(cookie) => reissued = cookie, diff --git a/src/db.rs b/src/db.rs index 7ef3c138..351dc9f9 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1478,6 +1478,22 @@ impl Db { } /// Invalidates every login. Used when the admin password changes. + /// Replaces the admin password hash and signs every session out, both or + /// neither: a reset that stored the hash and then failed would report failure + /// while the old password no longer works. + pub fn replace_password(&self, hash: &str) -> Result<()> { + let mut conn = self.conn(); + let tx = conn.transaction()?; + tx.execute( + "INSERT INTO setting (key, value) VALUES ('admin_password_hash', ?1) + ON CONFLICT(key) DO UPDATE SET value = excluded.value", + [hash], + )?; + tx.execute("DELETE FROM session", [])?; + tx.commit()?; + Ok(()) + } + pub fn drop_all_sessions(&self) -> Result<()> { self.conn().execute("DELETE FROM session", [])?; Ok(()) diff --git a/src/main.rs b/src/main.rs index b2578cbc..363a65ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -308,8 +308,7 @@ fn parse_args() -> Result { hub answers on whatever ip:port it is asked, and the panel builds\n\ install commands from the address in the browser's bar.\n\ --reset-password replaces the emergency password, signs every session\n\ - out, prints the new password and exits. Run it as the user the hub\n\ - runs as, so any file SQLite creates stays readable by the hub.", + out, prints the new password and exits. The database must exist.", env!("CARGO_PKG_VERSION") ); std::process::exit(0); @@ -342,11 +341,14 @@ async fn main() -> Result<()> { .init(); let args = parse_args()?; - // Delivered on the terminal rather than through the service log, so a - // password missing from the journal is still recoverable. A running hub - // reads the hash and its sessions from the database on every request, so - // the change applies to it without a restart. + // Delivered on the terminal rather than through the service log, which some + // hosts do not keep. A running hub reads the hash and its sessions from the + // database on every request, so the change applies without a restart. if args.reset_password { + // A mistyped path would otherwise create an empty database and print a + // password no running hub reads. + anyhow::ensure!(std::path::Path::new(&args.database).is_file(), "no database at {}", args.database); + // install-hub.sh extracts the password by this exact line prefix. println!("Emergency password: {}", new_password(&Db::open(&args.database)?)?); return Ok(()); } @@ -583,8 +585,7 @@ fn first_run(app: &App, url: &str) -> Result<()> { /// Sets a random 24-character admin password and signs every session out. fn new_password(db: &Db) -> Result { let password = auth::random_token()[..24].to_owned(); - db.set("admin_password_hash", &auth::hash_password(&password)?)?; - db.drop_all_sessions()?; + db.replace_password(&auth::hash_password(&password)?)?; Ok(password) } From 6528c720c3133d651996b0cfa6436238747f05de Mon Sep 17 00:00:00 2001 From: stqfdyr <89149493+stqfdyr@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:07:41 +0800 Subject: [PATCH 3/5] =?UTF-8?q?chore(install):=20=E8=A3=85=E5=AE=8C?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E6=89=93=E5=8D=B0=E5=8F=8D=E5=90=91=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E9=85=8D=E7=BD=AE=EF=BC=8C=E6=94=B9=E4=B8=BA=E7=BB=99?= =?UTF-8?q?=E5=87=BA=E6=96=87=E6=A1=A3=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit caddy、nginx、cloudflare 隧道三份配置与文档重复维护,改为在「还差一步」下给出 反向代理文档的地址。文档里的配置按默认端口 28080 写,端口不同时多提示一行 替换。--help 同步改为指向文档。 --- install-hub.sh | 59 +++++--------------------------------------------- 1 file changed, 5 insertions(+), 54 deletions(-) diff --git a/install-hub.sh b/install-hub.sh index 0202040d..5a5a69f5 100755 --- a/install-hub.sh +++ b/install-hub.sh @@ -304,62 +304,13 @@ UNIT printf ' %s还差一步:配个反向代理%s\n' "$B" "$N" printf ' 面板只监听本机,公网访问不到——这是故意的,凭证不会在链路上裸奔。\n' printf ' 用 nginx / caddy / cf tunnel 任选一种,把 hub.example.com 换成你的域名,\n' - printf ' 配好之后用域名访问面板,我相信这难不倒你。\n\n' - proxy_configs - printf '\n %s四点注意与完整说明见 README 的「反向代理」一节。%s\n' "$D" "$N" + printf ' 配好之后用域名访问面板,我相信这难不倒你。\n' + # The documented configurations use the default port. + [ "$PORT" = 28080 ] || printf ' 文档里的 28080 换成 %s。\n' "$PORT" + printf ' 反向代理文档:https://monitor-document.pages.dev/install/reverse-proxy\n' fi } -# The three configurations, printed where they are needed rather than described: -# the hub is unreachable until one of them is in place, so an installer that -# stops at "put a proxy in front of it" leaves the install half done. -# -# The heredocs are unquoted so $PORT lands in them; every variable belonging to -# the proxy is escaped, since nginx and caddy read those themselves. -proxy_configs() { - printf ' %scaddy%s Caddyfile\n' "$B" "$N" - cat < Date: Sat, 19 Sep 2026 15:47:04 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(install):=20=E5=8F=96=E4=B8=8D=E5=88=B0?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E6=97=B6=E5=9B=9E=E9=80=80=E8=AF=BB=20journa?= =?UTF-8?q?l=EF=BC=9B=E9=A6=96=E8=A3=85=E5=A4=B1=E8=B4=A5=E5=9B=9E?= =?UTF-8?q?=E6=BB=9A=E6=97=B6=E4=B8=8D=E5=90=AF=E5=8A=A8=E6=97=A7=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - --reset-password 失败(二进制早于这个参数)时,服务首次启动仍会生成密码并打印到 journal, 改为从那里取。新脚本配旧发布首次安装也能拿到密码,脚本与发版之间不再有先后依赖。 首装那一次调用不再把 unknown argument 打到屏幕上。 - 首次安装失败而回滚到旧二进制时(例如删了数据库重装),不再启动旧版:空库上它会生成一个 只打印进 journal 的密码,下次重跑又被当成升级而不显示。 - 面板拒绝添加节点时的提示原来指向 README 的反代配置,README 没有这一节,改为文档地址。 - drop_all_sessions 的文档注释被 replace_password 挤走了,放回原处,用途更正为恢复备份之后。 --- install-hub.sh | 14 ++++++++++++-- src/api.rs | 3 ++- src/db.rs | 3 ++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/install-hub.sh b/install-hub.sh index 5a5a69f5..4c5376b8 100755 --- a/install-hub.sh +++ b/install-hub.sh @@ -246,7 +246,8 @@ UNIT pw="" if [ -n "$first" ]; then install -m 0600 -o "$USER_NAME" /dev/null "$DATA/monitor.db" - pw="$(new_password)" + # Quiet: a release predating the flag is handled after the start below. + pw="$(new_password 2>/dev/null)" fi systemctl daemon-reload @@ -270,13 +271,22 @@ UNIT if [ -n "$backup" ]; then install -m 0755 "$backup" "$BIN" rm -f "$backup" - systemctl restart "$SERVICE" 2>/dev/null || true + # Not on a first install: there is no data to serve, and on an empty + # database the previous binary would set a password shown only in the + # journal. + [ -n "$first" ] || systemctl restart "$SERVICE" 2>/dev/null || true die "新版本没能启动,已回滚到上一版。日志:journalctl -u $SERVICE -n 50" fi die "服务启动失败。日志:journalctl -u $SERVICE -n 50" fi rm -f "$BIN.old" ok "服务" "已启动并开机自启" + # A release predating --reset-password refuses it, and its first start sets + # the password and prints it to the journal instead. + if [ -n "$first" ] && [ -z "$pw" ]; then + pw="$(journalctl -u "$SERVICE" --since '-2 min' --no-pager 2>/dev/null | + sed -n 's/.*Emergency password: //p' | tail -1)" + fi if [ -n "$first" ]; then done_title="安装完成"; else done_title="升级完成"; fi printf '\n %s%s%s\n' "$B" "$done_title" "$N" diff --git a/src/api.rs b/src/api.rs index 5cd5861f..63aaef3e 100644 --- a/src/api.rs +++ b/src/api.rs @@ -434,7 +434,8 @@ async fn stream_live(app: Shared, mut socket: WebSocket, session: Option /// from an otherwise valid https domain entry. `main` warns about that at /// startup; this is for whoever reads the panel rather than the journal. const PROVISIONING_DENIED: &str = "请通过 HTTPS 域名访问面板后添加或安装节点;\ - 如果已经是域名访问,检查反向代理是否透传了 Host 与 X-Forwarded-Proto(见 README 的反代配置);\ + 如果已经是域名访问,检查反向代理是否透传了 Host 与 X-Forwarded-Proto\ + (见 https://monitor-document.pages.dev/install/reverse-proxy);\ 两者都没问题就检查 hub 的启动参数 --site,它必须是 https:// 加域名,不能是 IP、不能带路径"; pub(crate) fn https_domain(site: &str) -> Option { diff --git a/src/db.rs b/src/db.rs index 351dc9f9..87183832 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1477,7 +1477,6 @@ impl Db { Ok(()) } - /// Invalidates every login. Used when the admin password changes. /// Replaces the admin password hash and signs every session out, both or /// neither: a reset that stored the hash and then failed would report failure /// while the old password no longer works. @@ -1494,6 +1493,8 @@ impl Db { Ok(()) } + /// Invalidates every login. Used after a restore, which would otherwise + /// revive every session the backup holds. pub fn drop_all_sessions(&self) -> Result<()> { self.conn().execute("DELETE FROM session", [])?; Ok(()) From d3f66ef8779ce53a904caba436d0e1b2abe0e988 Mon Sep 17 00:00:00 2001 From: stqfdyr <89149493+stqfdyr@users.noreply.github.com> Date: Sat, 19 Sep 2026 16:18:02 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix(install):=20=E9=A6=96=E6=AC=A1=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E5=A4=B1=E8=B4=A5=E6=97=B6=E5=81=9C=E7=94=A8=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E8=80=8C=E4=B8=8D=E5=9B=9E=E6=BB=9A=EF=BC=9Bjournal?= =?UTF-8?q?=20=E5=8F=AA=E8=AF=BB=E6=9C=AC=E6=AC=A1=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E4=B9=8B=E5=90=8E=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 首次安装没有要保住的服务,失败时不再回滚到旧二进制,改为 disable --now 并删掉新库。 只是不重启旧版还不够:unit 仍开机自启,下次开机旧版照样在空库上生成一个只打印进 journal 的 密码,再跑安装器又被当成升级。备份留给下一次运行。 - 回退读 journal 时只看这次 restart 之后的日志。两分钟内重跑时,上一次失败尝试打印过的 密码(对应的库已删掉)不会被当成这次的。 --- install-hub.sh | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/install-hub.sh b/install-hub.sh index 4c5376b8..72a410a3 100755 --- a/install-hub.sh +++ b/install-hub.sh @@ -256,25 +256,27 @@ UNIT # precisely the case the rollback below exists for. Unguarded, the script # would exit here with a raw systemd error and leave the hub down on the # binary that just failed. + started="$(date '+%Y-%m-%d %H:%M:%S')" systemctl restart "$SERVICE" || true # is-active answers before a unit that exits immediately has done so. Wait, # then query. sleep 3 if ! systemctl is-active --quiet "$SERVICE"; then - # The new database holds nothing but a password never shown. Removed, with - # the service stopped so no restart recreates it, so that a rerun is again - # a first install and shows one. + # A first install has nothing to keep serving, so there is no rollback. + # The new database holds nothing but a password never shown; it is removed + # so that a rerun is again a first install and shows one, and the unit is + # disabled, since any binary it started later -- on a reboot as well -- + # would set a password on an empty database and print it only to the + # journal. A backup, if any, stays for the next run. if [ -n "$first" ]; then - systemctl stop "$SERVICE" 2>/dev/null || true + systemctl disable --now "$SERVICE" >/dev/null 2>&1 || true rm -f "$DATA/monitor.db" "$DATA/monitor.db-wal" "$DATA/monitor.db-shm" + die "服务启动失败。日志:journalctl -u $SERVICE -n 50" fi if [ -n "$backup" ]; then install -m 0755 "$backup" "$BIN" rm -f "$backup" - # Not on a first install: there is no data to serve, and on an empty - # database the previous binary would set a password shown only in the - # journal. - [ -n "$first" ] || systemctl restart "$SERVICE" 2>/dev/null || true + systemctl restart "$SERVICE" 2>/dev/null || true die "新版本没能启动,已回滚到上一版。日志:journalctl -u $SERVICE -n 50" fi die "服务启动失败。日志:journalctl -u $SERVICE -n 50" @@ -282,9 +284,10 @@ UNIT rm -f "$BIN.old" ok "服务" "已启动并开机自启" # A release predating --reset-password refuses it, and its first start sets - # the password and prints it to the journal instead. + # the password and prints it to the journal instead. Only this start's lines: + # an earlier failed attempt printed a password for a database since removed. if [ -n "$first" ] && [ -z "$pw" ]; then - pw="$(journalctl -u "$SERVICE" --since '-2 min' --no-pager 2>/dev/null | + pw="$(journalctl -u "$SERVICE" --since "$started" --no-pager 2>/dev/null | sed -n 's/.*Emergency password: //p' | tail -1)" fi