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"
|
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() {
|
install_caddy_stack() {
|
||||||
local install_script="$INSTALL_DIR/src/scripts/install-caddy.sh"
|
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"
|
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_deps
|
||||||
install_bun
|
install_bun
|
||||||
setup_lib_dirs
|
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