import type { ReconfigureContext, ReconfigureOptions } from "../types/context"; import { exists, readText, writeText } from "../lib/fs"; import { info, step } from "../lib/log"; import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env"; import { preflight } from "../steps/preflight"; import { generateConfig } from "../steps/config"; import { deploySystemd } from "../steps/systemd"; import { applyFirewall, cancelFirewallRollback, rollbackFirewallNow } from "../steps/firewall"; import { writePostInstallEnv } from "../steps/env"; import { smoke } from "../steps/smoke"; import { runVisible } from "../lib/process"; import { readInstalledHysteriaVersion, readPackageValue } from "../lib/packageMeta"; const INSTALL_STATE_PATH = "/var/lib/hy2xs/install-state.json"; type InstallState = { installed?: boolean; }; async function backupCurrentState(): Promise { await runVisible`mkdir -p /etc/hy2xs/backups`; await runVisible`cp -a /etc/hysteria/config.yaml /etc/hy2xs/backups/config.yaml.bak 2>/dev/null || true`; await runVisible`cp -a /etc/systemd/system/hy2xs-admin.service /etc/hy2xs/backups/hy2xs-admin.service.bak 2>/dev/null || true`; await runVisible`cp -a /etc/systemd/system/hysteria-server.service /etc/hy2xs/backups/hysteria-server.service.bak 2>/dev/null || true`; await runVisible`cp -a /etc/hy2xs/hy2xs.env /etc/hy2xs/backups/hy2xs.env.bak 2>/dev/null || true`; await runVisible`cp -a /etc/hysteria/post-install.env /etc/hy2xs/backups/post-install.env.bak 2>/dev/null || true`; await runVisible`cp -a /etc/nftables.conf /etc/hy2xs/backups/nftables.conf.bak 2>/dev/null || true`; await runVisible`cp -a /etc/nftables.d/hy2xs.nft /etc/hy2xs/backups/hy2xs.nft.bak 2>/dev/null || true`; await runVisible`test -f /etc/hy2xs/hy2xs.env && echo 1 > /etc/hy2xs/backups/hy2xs.env.existed || rm -f /etc/hy2xs/backups/hy2xs.env.existed`; await runVisible`test -f /etc/hysteria/post-install.env && echo 1 > /etc/hy2xs/backups/post-install.env.existed || rm -f /etc/hy2xs/backups/post-install.env.existed`; await runVisible`test -f /etc/nftables.conf && echo 1 > /etc/hy2xs/backups/nftables.conf.existed || rm -f /etc/hy2xs/backups/nftables.conf.existed`; await runVisible`test -f /etc/nftables.d/hy2xs.nft && echo 1 > /etc/hy2xs/backups/hy2xs.nft.existed || rm -f /etc/hy2xs/backups/hy2xs.nft.existed`; } async function rollbackCurrentState(): Promise { await runVisible`cp -a /etc/hy2xs/backups/config.yaml.bak /etc/hysteria/config.yaml 2>/dev/null || true`; await runVisible`cp -a /etc/hy2xs/backups/hy2xs-admin.service.bak /etc/systemd/system/hy2xs-admin.service 2>/dev/null || true`; await runVisible`cp -a /etc/hy2xs/backups/hysteria-server.service.bak /etc/systemd/system/hysteria-server.service 2>/dev/null || true`; await runVisible`if [ -f /etc/hy2xs/backups/hy2xs.env.existed ]; then cp -a /etc/hy2xs/backups/hy2xs.env.bak /etc/hy2xs/hy2xs.env 2>/dev/null || true; else rm -f /etc/hy2xs/hy2xs.env; fi`; await runVisible`if [ -f /etc/hy2xs/backups/post-install.env.existed ]; then cp -a /etc/hy2xs/backups/post-install.env.bak /etc/hysteria/post-install.env 2>/dev/null || true; else rm -f /etc/hysteria/post-install.env; fi`; await runVisible`if [ -f /etc/hy2xs/backups/nftables.conf.existed ]; then cp -a /etc/hy2xs/backups/nftables.conf.bak /etc/nftables.conf 2>/dev/null || true; fi`; await runVisible`if [ -f /etc/hy2xs/backups/hy2xs.nft.existed ]; then cp -a /etc/hy2xs/backups/hy2xs.nft.bak /etc/nftables.d/hy2xs.nft 2>/dev/null || true; else rm -f /etc/nftables.d/hy2xs.nft; fi`; await runVisible`nft -f /etc/nftables.conf >/dev/null 2>&1 || true`; await runVisible`systemctl daemon-reload`; await runVisible`systemctl restart hysteria-server hy2xs-admin || true`; } async function ensureInstallStateExists(): Promise { if (!(await exists(INSTALL_STATE_PATH))) { throw new Error(`install state marker is missing: ${INSTALL_STATE_PATH}. Run install first.`); } const raw = await readText(INSTALL_STATE_PATH); let parsed: InstallState; try { parsed = JSON.parse(raw) as InstallState; } catch { throw new Error(`invalid install state marker format: ${INSTALL_STATE_PATH}`); } if (!parsed.installed) { throw new Error(`install state marker does not indicate successful installation: ${INSTALL_STATE_PATH}`); } } async function warnBootstrapDrift(nextConfigRaw: string): Promise { if (!(await exists("/etc/hy2xs/hy2xs.env"))) { return; } const prev = parseRuntimeEnv(await readText("/etc/hy2xs/hy2xs.env")); const next = parseRuntimeEnv(nextConfigRaw); if (prev.adminInitialPassword !== next.adminInitialPassword || prev.adminConPass !== next.adminConPass) { info("warning: Admin bootstrap fields are install-only and will not rotate existing credentials."); info("warning: Use a dedicated password rotation flow in application/account layer."); } } export async function reconfigure(options: ReconfigureOptions): Promise { const configRaw = await readText(options.sourceConfigPath); const config = parseRuntimeEnv(configRaw); const context: ReconfigureContext = { mode: "reconfigure", options, config, packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"), packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"), installDate: new Date().toISOString(), hysteriaVersion: await readInstalledHysteriaVersion() }; step("preflight"); await preflight(context); step("install state marker"); await ensureInstallStateExists(); await warnBootstrapDrift(configRaw); if (options.dryRun) { info("reconfigure dry-run: validated config and execution graph"); info(`config source: ${options.sourceConfigPath}`); info(`runtime file: ${options.runtimeConfigPath}`); info(`ui bind: ${config.uiBindHost}:${config.uiPort}`); info(`hysteria bind: ${config.hysteriaBindHost}:${config.hysteriaPort}`); info(`public endpoint: ${config.publicHost}:${config.publicPort}`); return; } step("backup"); await backupCurrentState(); try { step("config generation"); await generateConfig(context); step("systemd units"); await deploySystemd(context); step("firewall"); await applyFirewall(context); step("write env artifacts"); await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600); await runVisible`chown root:root ${options.runtimeConfigPath}`; await runVisible`chmod 0600 ${options.runtimeConfigPath}`; await writePostInstallEnv(context); step("smoke checks"); await smoke(context); step("finalize firewall rollback guard"); await cancelFirewallRollback(context); } catch (error) { info("reconfigure failed, rollback in progress"); await rollbackFirewallNow(context); await rollbackCurrentState(); throw error; } }