113 lines
4.5 KiB
TypeScript
113 lines
4.5 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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;
|
|
}
|
|
}
|