import type { InstallContext, InstallOptions } from "../types/context"; import { exists, readText, writeText } from "../lib/fs"; import { runVisible } from "../lib/process"; import { step } from "../lib/log"; import { readPackageValue } from "../lib/packageMeta"; import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env"; import { preflight } from "../steps/preflight"; import { installDeps } from "../steps/deps"; import { prepareFilesystem } from "../steps/filesystem"; import { deployUi } from "../steps/ui"; import { installHysteria } from "../steps/hysteria"; import { generateConfig } from "../steps/config"; import { deploySystemd } from "../steps/systemd"; import { applyFirewall, cancelFirewallRollback, rollbackFirewallNow } from "../steps/firewall"; import { writeBootstrapAdminSecret, writePostInstallEnv } from "../steps/env"; import { smoke } from "../steps/smoke"; const INSTALL_STATE_PATH = "/var/lib/hy2xs/install-state.json"; async function markInstallSuccessful(context: InstallContext): Promise { await runVisible`install -d -m 0755 -o root -g root /var/lib/hy2xs`; const state = JSON.stringify( { installed: true, version: context.packageVersion, build_id: context.packageBuildId, installed_at: new Date().toISOString() }, null, 2 ); await writeText(INSTALL_STATE_PATH, `${state}\n`, 0o644); await runVisible`chown root:root ${INSTALL_STATE_PATH}`; } async function rollbackFailedInstall(context: InstallContext, state: { firewallTouched: boolean }): Promise { if (state.firewallTouched) { await rollbackFirewallNow(context); } await runVisible`systemctl stop hysteria-server hy2xs-admin || true`; await runVisible`systemctl disable hysteria-server hy2xs-admin || true`; await runVisible`systemctl reset-failed hysteria-server hy2xs-admin || true`; } export async function install(options: InstallOptions): Promise { const hasSourceConfig = options.sourceConfigPath ? await exists(options.sourceConfigPath) : false; if (options.sourceConfigPath && !hasSourceConfig) { throw new Error(`config source not found: ${options.sourceConfigPath}`); } const sourceConfigPath = hasSourceConfig ? options.sourceConfigPath : `${options.packageDir}/config/hy2xs.env`; const sourceConfigRaw = await readText(sourceConfigPath); const config = parseRuntimeEnv(sourceConfigRaw); const context: InstallContext = { mode: "install", options, config, packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"), packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"), installDate: new Date().toISOString(), hysteriaVersion: "unknown", hysteriaTargetVersion: await readPackageValue(options.packageDir, "hysteria.version", ""), hysteriaArtifactUrl: await readPackageValue(options.packageDir, "hysteria.url", ""), hysteriaArtifactSha256: await readPackageValue(options.packageDir, "hysteria.sha256", "") }; if (!context.hysteriaTargetVersion || !context.hysteriaArtifactUrl || !context.hysteriaArtifactSha256) { throw new Error("missing Hysteria lock metadata in package: hysteria.version/hysteria.url/hysteria.sha256"); } const state = { firewallTouched: false }; try { step("preflight"); await preflight(context); step("system dependencies"); await installDeps(context); step("filesystem"); await prepareFilesystem(context); step("write runtime env"); await runVisible`mkdir -p /etc/hy2xs`; await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600); await runVisible`chown root:root ${options.runtimeConfigPath}`; await runVisible`chmod 0600 ${options.runtimeConfigPath}`; step("bundled UI"); await deployUi(context); step("Hysteria2 upstream install"); await installHysteria(context); step("config generation"); await generateConfig(context); step("systemd units"); await deploySystemd(context); step("firewall"); await applyFirewall(context); state.firewallTouched = true; step("post-install env"); await writePostInstallEnv(context); step("bootstrap admin secret"); await writeBootstrapAdminSecret(context); step("smoke checks"); await smoke(context); step("finalize firewall rollback guard"); await cancelFirewallRollback(context); step("mark install successful"); await markInstallSuccessful(context); } catch (error) { await rollbackFailedInstall(context, state); throw error; } }