Restore the installer functions dropped by the Caddy commit
ed4a6f3rewrote the middle of install-wg-ui.sh and deleted seven function definitions while leaving every call site in place. The runtime only stopped at the first one, so `update wg-ui` died with: install-wg-ui.sh: line 202: clone_or_update: command not found Fixing that line alone would have moved the failure to install_dependencies three lines down, and with install_systemd_unit / install_openrc_unit gone the script could never have written a service unit again. Restore all seven from0f94004, keeping the Caddy work that landed in the same commit: - clone_or_update, install_dependencies, ensure_env_file, setup_wireguard, install_wg_quick_helper: byte-identical to0f94004. - install_systemd_unit / install_openrc_unit: identical, except wg-ui now starts after docker, since Caddy ingress reloads go through docker exec. Add install-wg-ui.test.ts so this class of bug cannot come back silently: it asserts the script parses, that every invoked function has a definition (indented calls included, which is how clone_or_update hid), and that the update flow runs in order. Checked against the pre-fix script, the definition test fails with exactly the seven missing names.
This commit is contained in:
@@ -122,6 +122,63 @@ setup_lib_dirs() {
|
||||
chmod 755 "$WGUI_LIB_DIR"
|
||||
}
|
||||
|
||||
clone_or_update() {
|
||||
mkdir -p "$(dirname "$INSTALL_DIR")"
|
||||
if [ -d "$INSTALL_DIR/.git" ]; then
|
||||
UPDATE=true
|
||||
log "updating existing install at $INSTALL_DIR ..."
|
||||
git -C "$INSTALL_DIR" fetch --depth 1 origin "$BRANCH"
|
||||
local db_backup="$INSTALL_DIR/db/.wgui-pre-update.db"
|
||||
if [ -f "$INSTALL_DIR/db/wgui" ]; then
|
||||
cp -a "$INSTALL_DIR/db/wgui" "$db_backup"
|
||||
fi
|
||||
git -C "$INSTALL_DIR" reset --hard "origin/$BRANCH"
|
||||
if [ -f "$db_backup" ]; then
|
||||
mv -f "$db_backup" "$INSTALL_DIR/db/wgui"
|
||||
fi
|
||||
else
|
||||
log "cloning wg-ui ($REPO_URL, branch $BRANCH) ..."
|
||||
git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$INSTALL_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
log "installing app dependencies with bun ..."
|
||||
cd "$INSTALL_DIR"
|
||||
"$BUN_BIN" install
|
||||
"$BUN_BIN" bun-sqlite schema
|
||||
}
|
||||
|
||||
ensure_env_file() {
|
||||
if [ ! -f "$INSTALL_DIR/.env" ]; then
|
||||
log "generating $INSTALL_DIR/.env with fresh encryption secrets ..."
|
||||
{
|
||||
echo "NODE_ENV=${NODE_ENV:-production}"
|
||||
echo "ENCRYPTION_KEY=$(openssl rand -base64 32 | tr -d '\n')"
|
||||
echo "ENCRYPTION_SALT=$(openssl rand -base64 32 | tr -d '\n')"
|
||||
echo "DATA_DIR=$INSTALL_DIR/.data"
|
||||
} > "$INSTALL_DIR/.env"
|
||||
chmod 600 "$INSTALL_DIR/.env"
|
||||
fi
|
||||
}
|
||||
|
||||
setup_wireguard() {
|
||||
local setup_script="$INSTALL_DIR/src/scripts/setup-wireguard.sh"
|
||||
if [ -x "$setup_script" ] && [ "${SKIP_WIREGUARD_SETUP:-0}" != "1" ]; then
|
||||
log "running $(basename "$setup_script") to install wireguard on this host ..."
|
||||
"$setup_script"
|
||||
fi
|
||||
}
|
||||
|
||||
install_wg_quick_helper() {
|
||||
local helper_src="$INSTALL_DIR/src/scripts/wg-quick-manage.sh"
|
||||
if [ ! -f "$helper_src" ]; then
|
||||
fail "missing $helper_src — cannot install the wg-quick helper"
|
||||
fi
|
||||
log "installing wg-quick helper to $WG_QUICK_HELPER ..."
|
||||
install -m 755 -o root -g root "$helper_src" "$WG_QUICK_HELPER"
|
||||
}
|
||||
|
||||
install_caddy_stack() {
|
||||
local install_script="$INSTALL_DIR/src/scripts/install-caddy.sh"
|
||||
|
||||
@@ -190,6 +247,75 @@ print_firewall_instructions() {
|
||||
log " and review all firewall rules under /admin/setup -> Firewall"
|
||||
}
|
||||
|
||||
install_systemd_unit() {
|
||||
local unit="/etc/systemd/system/$SERVICE_NAME.service"
|
||||
log "writing systemd unit $unit ..."
|
||||
cat > "$unit" <<EOF
|
||||
[Unit]
|
||||
Description=Wireguard UI
|
||||
After=network-online.target docker.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
Environment=NODE_ENV=production
|
||||
ExecStart=$BUN_BIN src/server.ts
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
TimeoutStartSec=300
|
||||
PrivateTmp=true
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
log "enabling and starting $SERVICE_NAME ..."
|
||||
systemctl enable --now "$SERVICE_NAME.service"
|
||||
if [ "$UPDATE" = true ] && [ -z "${SKIP_SERVICE_RESTART:-}" ]; then
|
||||
log "restarting $SERVICE_NAME to load updated code ..."
|
||||
systemctl restart "$SERVICE_NAME.service"
|
||||
fi
|
||||
}
|
||||
|
||||
install_openrc_unit() {
|
||||
local init_script="/etc/init.d/$SERVICE_NAME"
|
||||
log "writing openrc init script $init_script ..."
|
||||
cat > "$init_script" <<EOF
|
||||
#!/sbin/openrc-run
|
||||
name="$SERVICE_NAME"
|
||||
description="Wireguard UI"
|
||||
command="$BUN_BIN"
|
||||
command_args="src/server.ts"
|
||||
command_user="root"
|
||||
directory="$INSTALL_DIR"
|
||||
output_log="/var/log/$SERVICE_NAME.log"
|
||||
error_log="/var/log/$SERVICE_NAME.log"
|
||||
pidfile="/run/\${RC_SVCNAME}.pid"
|
||||
export NODE_ENV="production"
|
||||
|
||||
start_pre() {
|
||||
umask 077
|
||||
}
|
||||
|
||||
depend() {
|
||||
need net
|
||||
after docker
|
||||
}
|
||||
EOF
|
||||
chmod +x "$init_script"
|
||||
log "adding $SERVICE_NAME to default runlevel and starting ..."
|
||||
rc-update add "$SERVICE_NAME" default
|
||||
if [ "$UPDATE" = true ] && [ -z "${SKIP_SERVICE_RESTART:-}" ]; then
|
||||
rc-service "$SERVICE_NAME" restart
|
||||
else
|
||||
rc-service "$SERVICE_NAME" start
|
||||
fi
|
||||
}
|
||||
|
||||
install_deps
|
||||
install_bun
|
||||
setup_lib_dirs
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
const SCRIPT_PATH = path.join(import.meta.dir, "install-wg-ui.sh");
|
||||
const SCRIPT = fs.readFileSync(SCRIPT_PATH, "utf-8");
|
||||
|
||||
const BASH_KEYWORDS = new Set([
|
||||
"fi",
|
||||
"else",
|
||||
"then",
|
||||
"do",
|
||||
"done",
|
||||
"esac",
|
||||
"EOF",
|
||||
"in",
|
||||
"{",
|
||||
"}",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Functions declared with `name() {`. `install_openrc_unit` embeds an
|
||||
* OpenRC init script in a heredoc, so `start_pre` / `depend` show up here
|
||||
* too — that is fine, they are only ever compared against definitions.
|
||||
*/
|
||||
function grabDefinedFunctions(): Set<string> {
|
||||
const defined = new Set<string>();
|
||||
|
||||
for (const match of SCRIPT.matchAll(/^([A-Za-z_][A-Za-z0-9_]*)\(\) *\{/gm)) {
|
||||
const name = match[1];
|
||||
|
||||
if (name) {
|
||||
defined.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
return defined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bare word on its own line is a function call — the installer always
|
||||
* invokes helpers that way, both at top level and from inside another
|
||||
* function. Catching the indented ones too is what surfaces a call whose
|
||||
* definition was deleted.
|
||||
*/
|
||||
function grabInvocations(): string[] {
|
||||
const invocations: string[] = [];
|
||||
|
||||
for (const match of SCRIPT.matchAll(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$/gm)) {
|
||||
const name = match[1];
|
||||
|
||||
if (!name || BASH_KEYWORDS.has(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
invocations.push(name);
|
||||
}
|
||||
|
||||
return invocations;
|
||||
}
|
||||
|
||||
describe("install-wg-ui.sh", () => {
|
||||
test("parses as valid bash", () => {
|
||||
expect(() =>
|
||||
execFileSync("bash", ["-n", SCRIPT_PATH], { stdio: "pipe" }),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
test("every invoked function has a definition", () => {
|
||||
const defined = grabDefinedFunctions();
|
||||
const undefined_invocations = grabInvocations().filter(
|
||||
(name) => !defined.has(name),
|
||||
);
|
||||
|
||||
expect(undefined_invocations).toEqual([]);
|
||||
});
|
||||
|
||||
test("runs the update flow in the documented order", () => {
|
||||
const invocations = grabInvocations();
|
||||
const expected_order = [
|
||||
"install_deps",
|
||||
"install_bun",
|
||||
"setup_lib_dirs",
|
||||
"clone_or_update",
|
||||
"install_caddy_stack",
|
||||
"install_dependencies",
|
||||
"ensure_env_file",
|
||||
"setup_wireguard",
|
||||
"install_wg_quick_helper",
|
||||
];
|
||||
|
||||
let cursor = -1;
|
||||
|
||||
for (const name of expected_order) {
|
||||
const index = invocations.indexOf(name);
|
||||
|
||||
expect(index).toBeGreaterThan(cursor);
|
||||
cursor = index;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user