Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/WINDOWS-PORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Port each Mac engine verb to a `BHServe.Core` method + a `bhserve.exe` subcomman
- [ ] `init` — create `%LOCALAPPDATA%\BHServe\{config,nginx\sites,bin,run,logs,sites,certs,tmp}`
- [ ] `install <tool>` — download+extract a pinned portable build (php@x.y / nginx / mariadb / node / mkcert / mailpit / adminer / phpmyadmin)
- [ ] `update <tool>` / `uninstall <tool>`
- [ ] `site add <name> [--php 8.4] [--root] [--server nginx] [--type wordpress|php|others]` — render vhost, add hosts line, auto-create DB, WP download for `--type wordpress`
- [ ] `site add <name> [--php 8.4] [--root] [--server nginx] [--type wordpress|php|laravel|others]` — render vhost, add hosts line, auto-create DB, WP download for `--type wordpress`
- [ ] `site list|rm|php|server|root`
- [ ] `secure <domain>` — mkcert into Windows store + re-render vhost ssl block
- [ ] `dns` — hosts-file management (and/or Acrylic config)
Expand Down
93 changes: 73 additions & 20 deletions engine/bhserve
Original file line number Diff line number Diff line change
Expand Up @@ -1261,7 +1261,7 @@ vhost_server(){ local s; s="$(sed -n 's/.*server=\([^ ]*\).*/\1/p' "$1" | head -

site_add() {
local name="" root="" phpv="" server="" type="others"
[ $# -ge 1 ] || die "usage: bhserve site add <name> [--php 8.4] [--root path] [--server nginx|apache] [--type wordpress|php|others]"
[ $# -ge 1 ] || die "usage: bhserve site add <name> [--php 8.4] [--root path] [--server nginx|apache] [--type wordpress|php|laravel|others]"
name="$1"; shift
while [ $# -gt 0 ]; do case "$1" in
--php) phpv="$2"; shift 2;;
Expand All @@ -1271,12 +1271,14 @@ site_add() {
*) die "unknown flag: $1";;
esac; done
valid_site_name "$name"
case "$type" in wordpress|php|others) ;; *) die "--type must be wordpress|php|others" ;; esac
case "$type" in wordpress|php|laravel|others) ;; *) die "--type must be wordpress|php|laravel|others" ;; esac
local tld; tld="$(jget tld test)"
local domain="$name.$tld"
[ -n "$root" ] || root="$(jget sites_root "$HOME/BHServe/www")/$name"
[ -n "$server" ] || server="$(jget default_web nginx)"
local phpkey; phpkey="$(php_key "$phpv")"
local vhost_root="$root"
[ "$type" = "laravel" ] && vhost_root="$root/public"

mkdir -p "$root"
# Branded default landing page (identical to the Windows build). Quoted heredoc
Expand Down Expand Up @@ -1317,7 +1319,7 @@ PHP

render_fpm_pool "$phpkey" || true
fpm_start "$phpkey" >/dev/null 2>&1 || true # ensure the site's PHP pool is up (no 502)
render_site_vhost "$name" "$domain" "$root" "$phpkey" "$server"
render_site_vhost "$name" "$domain" "$vhost_root" "$phpkey" "$server"
# apache_start no-ops when httpd is already up, so the FIRST apache site worked but every
# LATER one was never loaded (requests fell through to the first vhost). Reload when running.
if [ "$server" = apache ]; then
Expand All @@ -1326,6 +1328,7 @@ PHP
hdr "Site '$name' added"
info "domain : http://$domain"
info "root : $root"
[ "$vhost_root" != "$root" ] && info "vhost : $vhost_root"
info "php : $phpkey server: $server type: $type"

site_provision "$name" "$type" "$root" "$domain"
Expand Down Expand Up @@ -1650,18 +1653,18 @@ nginx_start(){
grep -q "syntax is ok" <<<"$t" || { printf '%s\n' "$t"; die "nginx config test failed"; }
$pre "$bin" -c "$BH_HOME/nginx/nginx.conf" -p "$BH_HOME/nginx" && ok "nginx started${pre:+ (sudo)}" || no "nginx failed to start"
}
# Graceful reload of the running nginx (picks up new/changed vhosts).
nginx_reload(){
nginx_running || return 0
local bin pre=""; bin="$(NGINX_BIN)"; needs_root_ports && pre="sudo"
$pre "$bin" -s reload -c "$BH_HOME/nginx/nginx.conf" -p "$BH_HOME/nginx" 2>/dev/null \
&& ok "nginx reloaded" || warn "reload failed — run: bhserve restart nginx"
nginx_restart >/dev/null 2>&1 && ok "nginx restarted" || {
local bin pre=""; bin="$(NGINX_BIN)"; needs_root_ports && pre="sudo"
$pre "$bin" -s reload -c "$BH_HOME/nginx/nginx.conf" -p "$BH_HOME/nginx" 2>/dev/null \
&& ok "nginx reloaded" || warn "reload failed — run: bhserve restart nginx"
}
}
# Auto-apply config changes: reload interactively (one sudo prompt); from the GUI
# (no tty) just note it — the GUI issues its own privileged restart.
# Auto-apply config changes
maybe_reload_nginx(){
nginx_running || return 0
if [ -t 1 ]; then nginx_reload; else info "reload nginx to serve changes (bhserve restart nginx)"; fi
nginx_reload
}

nginx_stop(){
Expand Down Expand Up @@ -1806,7 +1809,7 @@ cmd_restart() {
}

# ── databases (mysql/mariadb + postgresql) ──────────────────────────────────
MYSQL_CLI(){ echo "$BREW_PREFIX/bin/mysql"; }
MYSQL_CLI(){ [ -x "$BREW_PREFIX/bin/mysql" ] && echo "$BREW_PREFIX/bin/mysql" || command -v mysql 2>/dev/null || command -v mariadb 2>/dev/null || echo /usr/bin/mysql; }
PSQL_CLI(){ echo "$BREW_PREFIX/opt/postgresql@17/bin/psql"; }
PG_BIN(){ echo "$BREW_PREFIX/opt/postgresql@17/bin/$1"; }

Expand All @@ -1818,8 +1821,14 @@ valid_site_name(){ [[ "$1" =~ ^[a-z0-9][a-z0-9.-]*$ ]] || die "invalid site name
# unix_socket account), then passwordless -u root (Oracle MySQL / older setups).
mysql_run(){
local c; c="$(MYSQL_CLI)"; [ -x "$c" ] || return 127
local saved_pw; saved_pw="$(jget root_password "")"
if "$c" -N -e "SELECT 1;" >/dev/null 2>&1; then "$c" "$@"
elif "$c" -u root -N -e "SELECT 1;" >/dev/null 2>&1; then "$c" -u root "$@"
elif [ -n "${BHSERVE_OLD_DB_PASSWORD:-}" ] && MYSQL_PWD="$BHSERVE_OLD_DB_PASSWORD" "$c" -u root -N -e "SELECT 1;" >/dev/null 2>&1; then MYSQL_PWD="$BHSERVE_OLD_DB_PASSWORD" "$c" -u root "$@"
elif [ -n "${DB_OLD_PASSWORD:-}" ] && MYSQL_PWD="$DB_OLD_PASSWORD" "$c" -u root -N -e "SELECT 1;" >/dev/null 2>&1; then MYSQL_PWD="$DB_OLD_PASSWORD" "$c" -u root "$@"
elif [ -n "${DB_PASSWORD:-}" ] && MYSQL_PWD="$DB_PASSWORD" "$c" -u root -N -e "SELECT 1;" >/dev/null 2>&1; then MYSQL_PWD="$DB_PASSWORD" "$c" -u root "$@"
elif [ -n "${BHSERVE_DB_PASSWORD:-}" ] && MYSQL_PWD="$BHSERVE_DB_PASSWORD" "$c" -u root -N -e "SELECT 1;" >/dev/null 2>&1; then MYSQL_PWD="$BHSERVE_DB_PASSWORD" "$c" -u root "$@"
elif [ -n "$saved_pw" ] && MYSQL_PWD="$saved_pw" "$c" -u root -N -e "SELECT 1;" >/dev/null 2>&1; then MYSQL_PWD="$saved_pw" "$c" -u root "$@"
else return 1; fi
}

Expand Down Expand Up @@ -1868,18 +1877,59 @@ cmd_db(){

# Whether mysql/mariadb root@localhost has a password set. Prints set|blank|unavailable.
db_root_status(){
local r
local r out cli
r="$(mysql_run -N -e "SELECT IF(LENGTH(authentication_string)>0,'set','blank') FROM mysql.user WHERE user='root' AND host='localhost' LIMIT 1;" 2>/dev/null)" || true
[ -n "$r" ] && printf '%s\n' "$r" || echo unavailable
if [ -n "$r" ]; then
printf '%s\n' "$r"
return 0
fi
cli="$(MYSQL_CLI 2>/dev/null || echo mysql)"
out="$("$cli" -u root -N -e "SELECT 1;" 2>&1 || true)"
if grep -qi "Access denied" <<<"$out"; then
echo set
else
echo unavailable
fi
}

# Set (or, with empty password, clear) the mysql/mariadb root@localhost password.
# Only touches root — the OS-user socket account we operate through is untouched.
db_root_passwd(){
local epw; epw="$(mysql_esc "$DB_PASSWORD")"
mysql_run -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '$epw'; FLUSH PRIVILEGES;" \
&& ok "root password $([ -n "$DB_PASSWORD" ] && echo set || echo "cleared (blank)")" \
|| die "failed (server running? do you have privilege?)"
local sql="ALTER USER 'root'@'localhost' IDENTIFIED BY '$epw'; FLUSH PRIVILEGES;"
if mysql_run -e "$sql" 2>/dev/null; then
json_set "root_password" "$DB_PASSWORD" 2>/dev/null || true
ok "root password $([ -n "$DB_PASSWORD" ] && echo set || echo "cleared (blank)")"
return 0
fi
# Fallback 1: Privileged execution via pkexec/sudo if unprivileged connect failed
local cli; cli="$(MYSQL_CLI 2>/dev/null || echo mysql)"
if [ -x "$cli" ]; then
if [ "$(id -u)" != 0 ] && command -v pkexec >/dev/null 2>&1 && [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]; then
if pkexec "$cli" -e "$sql" 2>/dev/null; then
json_set "root_password" "$DB_PASSWORD" 2>/dev/null || true
ok "root password $([ -n "$DB_PASSWORD" ] && echo set || echo "cleared (blank)")"
return 0
fi
elif [ "$(id -u)" = 0 ] || command -v sudo >/dev/null 2>&1; then
if $SUDO "$cli" -e "$sql" 2>/dev/null; then
json_set "root_password" "$DB_PASSWORD" 2>/dev/null || true
ok "root password $([ -n "$DB_PASSWORD" ] && echo set || echo "cleared (blank)")"
return 0
fi
fi
fi
# Fallback 2: Legacy MariaDB/MySQL SQL syntax
for alt_sql in \
"ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING '$epw'; FLUSH PRIVILEGES;" \
"SET PASSWORD FOR 'root'@'localhost' = PASSWORD('$epw'); FLUSH PRIVILEGES;"; do
if mysql_run -e "$alt_sql" 2>/dev/null; then
json_set "root_password" "$DB_PASSWORD" 2>/dev/null || true
ok "root password $([ -n "$DB_PASSWORD" ] && echo set || echo "cleared (blank)")"
return 0
fi
done
die "failed (server running? do you have privilege?)"
}

db_list(){
Expand Down Expand Up @@ -2016,14 +2066,15 @@ cmd_config(){
# Rewrite bhserve.json from current values, overriding one key.
json_set(){
local k="$1" v="$2"
local tld httpp httpsp dphp dweb bp sroot autostart
local tld httpp httpsp dphp dweb bp sroot autostart rootpw
tld="$(jget tld test)"; httpp="$(jget http_port 80)"; httpsp="$(jget https_port 443)"
dphp="$(jget default_php 8.4)"; dweb="$(jget default_web nginx)"
bp="$(jget brew_prefix "$BREW_PREFIX")"; sroot="$(jget sites_root "$HOME/BHServe/www")"
autostart="$(jget autostart false)"
autostart="$(jget autostart false)"; rootpw="$(jget root_password "")"
case "$k" in
tld) tld="$v";; http_port) httpp="$v";; https_port) httpsp="$v";;
default_php) dphp="$v";; default_web) dweb="$v";; sites_root) sroot="$v";; autostart) autostart="$v";;
root_password) rootpw="$v";;
esac
cat > "$BH_HOME/config/bhserve.json" <<JSON
{
Expand All @@ -2034,7 +2085,8 @@ json_set(){
"default_web": "$dweb",
"brew_prefix": "$bp",
"sites_root": "$sroot",
"autostart": $autostart
"autostart": $autostart,
"root_password": "$rootpw"
}
JSON
}
Expand Down Expand Up @@ -2069,6 +2121,7 @@ config_set(){
default_php) val="$(php_key "$val")" ;;
default_web) [[ "$val" =~ ^(nginx|apache)$ ]] || die "default_web must be nginx|apache" ;;
autostart) [[ "$val" =~ ^(true|false)$ ]] || die "autostart must be true|false" ;;
root_password) ;;
*) die "unknown/uneditable key: $key" ;;
esac
json_set "$key" "$val"
Expand Down Expand Up @@ -2684,7 +2737,7 @@ BHServe — usage:
bhserve install <svc|all> brew install a service
bhserve update <svc|all> brew upgrade a service
bhserve uninstall <svc> brew uninstall a service
bhserve site add <name> [--php 8.4] [--root path] [--server nginx|apache] [--type wordpress|php|others]
bhserve site add <name> [--php 8.4] [--root path] [--server nginx|apache] [--type wordpress|php|laravel|others]
bhserve site list|rm <name>
bhserve site php <name> <ver> switch a site's PHP version
bhserve db {list|create|drop|passwd} [name] [--engine mysql|pg] [--password PW]
Expand Down
34 changes: 21 additions & 13 deletions engine/platform-linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -661,21 +661,21 @@ hosts_sync_all(){
# go through here; in non-tty (GUI) mode it still syncs hosts before deferring the reload.
maybe_reload_nginx(){
hosts_sync_all
# ALWAYS (re)load nginx when a site changes. The old `[ -t 1 ]` tty gate meant the GUI (which runs
# us non-interactively) never actually reloaded — so a GUI-added site's vhost wasn't served until a
# manual restart. And if nginx is down, START it (else the new site 502s / doesn't load at all).
if nginx_running; then nginx_reload; else nginx_start >/dev/null 2>&1 && ok "nginx started" || warn "start nginx to serve the site (bhserve start nginx)"; fi
if nginx_running; then
nginx_restart >/dev/null 2>&1 || nginx_reload
else
nginx_start >/dev/null 2>&1 && ok "nginx started" || warn "start nginx to serve the site (bhserve start nginx)"
fi
}

# nodesite/pysite add + `secure` call nginx_reload DIRECTLY (not via maybe_reload_nginx),
# so sync /etc/hosts here too — this is the real choke point every reload passes through.
# hosts_sync_all is idempotent (no-op + no sudo when nothing changed), so double calls are free.
nginx_reload(){
hosts_sync_all
nginx_running || return 0
local bin pre=""; bin="$(NGINX_BIN)"; needs_root_ports && pre="sudo"
$pre "$bin" -s reload -c "$BH_HOME/nginx/nginx.conf" -p "$BH_HOME/nginx" 2>/dev/null \
&& ok "nginx reloaded" || warn "reload failed — run: bhserve restart nginx"
nginx_restart >/dev/null 2>&1 && ok "nginx restarted" || {
local bin pre=""; bin="$(NGINX_BIN)"; needs_root_ports && pre="sudo"
$pre "$bin" -s reload -c "$BH_HOME/nginx/nginx.conf" -p "$BH_HOME/nginx" 2>/dev/null \
&& ok "nginx reloaded" || warn "reload failed — run: bhserve restart nginx"
}
}

# `bhserve dns` — default: (re)sync /etc/hosts for all sites. `bhserve dns wildcard` —
Expand Down Expand Up @@ -831,19 +831,27 @@ PG_BIN(){ local v; for v in 17 16 15; do [ -x "/usr/lib/postgresql/$v/bin/$1" ]
# then -u root, so db verbs work however the server is configured.
mysql_run(){
local c; c="$(MYSQL_CLI)"; [ -x "$c" ] || return 127
local saved_pw; saved_pw="$(jget root_password "")"
if "$c" -N -e "SELECT 1;" >/dev/null 2>&1; then "$c" "$@"
elif $SUDO "$c" -N -e "SELECT 1;" >/dev/null 2>&1; then $SUDO "$c" "$@"
elif "$c" -u root -N -e "SELECT 1;" >/dev/null 2>&1; then "$c" -u root "$@"
elif [ -n "${BHSERVE_OLD_DB_PASSWORD:-}" ] && MYSQL_PWD="$BHSERVE_OLD_DB_PASSWORD" "$c" -u root -N -e "SELECT 1;" >/dev/null 2>&1; then MYSQL_PWD="$BHSERVE_OLD_DB_PASSWORD" "$c" -u root "$@"
elif [ -n "${DB_OLD_PASSWORD:-}" ] && MYSQL_PWD="$DB_OLD_PASSWORD" "$c" -u root -N -e "SELECT 1;" >/dev/null 2>&1; then MYSQL_PWD="$DB_OLD_PASSWORD" "$c" -u root "$@"
elif [ -n "${DB_PASSWORD:-}" ] && MYSQL_PWD="$DB_PASSWORD" "$c" -u root -N -e "SELECT 1;" >/dev/null 2>&1; then MYSQL_PWD="$DB_PASSWORD" "$c" -u root "$@"
elif [ -n "${BHSERVE_DB_PASSWORD:-}" ] && MYSQL_PWD="$BHSERVE_DB_PASSWORD" "$c" -u root -N -e "SELECT 1;" >/dev/null 2>&1; then MYSQL_PWD="$BHSERVE_DB_PASSWORD" "$c" -u root "$@"
elif [ -n "$saved_pw" ] && MYSQL_PWD="$saved_pw" "$c" -u root -N -e "SELECT 1;" >/dev/null 2>&1; then MYSQL_PWD="$saved_pw" "$c" -u root "$@"
elif command -v sudo >/dev/null 2>&1 && $SUDO "$c" -N -e "SELECT 1;" >/dev/null 2>&1; then $SUDO "$c" "$@"
elif command -v sudo >/dev/null 2>&1 && $SUDO "$c" -u root -N -e "SELECT 1;" >/dev/null 2>&1; then $SUDO "$c" -u root "$@"
else return 1; fi
}

# Make root@localhost a BLANK-password native account — same posture as the Mac (root has
# no password; the server is bound to loopback only) so the engine + WordPress connect over
# TCP as root. Tries MariaDB then MySQL syntax.
# TCP as root. Tries MySQL 8.x, MariaDB, then legacy syntax.
_db_open_root(){
local cli i; cli="$(MYSQL_CLI)"
for i in 1 2 3 4 5 6; do $SUDO "$cli" -e "SELECT 1" >/dev/null 2>&1 && break; sleep 1; done
$SUDO "$cli" -e "ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING ''; FLUSH PRIVILEGES;" >/dev/null 2>&1 \
$SUDO "$cli" -e "ALTER USER 'root'@'localhost' IDENTIFIED BY ''; FLUSH PRIVILEGES;" >/dev/null 2>&1 \
|| $SUDO "$cli" -e "ALTER USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING ''; FLUSH PRIVILEGES;" >/dev/null 2>&1 \
|| $SUDO "$cli" -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY ''; FLUSH PRIVILEGES;" >/dev/null 2>&1 \
|| $SUDO "$cli" -e "SET PASSWORD FOR 'root'@'localhost' = ''; FLUSH PRIVILEGES;" >/dev/null 2>&1 || true
db_secure_bind
Expand Down
20 changes: 19 additions & 1 deletion linux/app/bhserve/pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,23 @@ def site_change_php(win, s: dict) -> None:
("ols", "Switch to OpenLiteSpeed")]


def site_change_root(win, s: dict) -> None:
def on_pick(dialog, result):
try:
f = dialog.select_folder_finish(result)
if f:
path = f.get_path()
win.run_verb(["site", "root", s["name"], path], f"Changing root for {s['name']} → {path}…")
except Exception:
pass

dlg = Gtk.FileDialog()
dlg.set_title(f"Select new root directory for {s['name']}")
if s.get("root") and os.path.isdir(s["root"]):
dlg.set_initial_folder(Gio.File.new_for_path(s["root"]))
dlg.select_folder(win, None, on_pick)


def _site_menu(win, s: dict) -> Gtk.Popover:
pop = Gtk.Popover()
v = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2, margin_top=6, margin_bottom=6,
Expand All @@ -144,6 +161,7 @@ def item(label, icon, cb, destructive=False):
continue
item(_label, "network-server-symbolic",
lambda v=_val: win.run_verb(["site", "server", name, v], f"Switching {name} → {v}…"))
item("Change root directory…", "folder-symbolic", lambda: site_change_root(win, s))
dom = s["domain"]
if not s.get("secure"):
item("Install SSL (HTTPS)", "security-high-symbolic",
Expand All @@ -169,7 +187,7 @@ def item(label, icon, cb, destructive=False):
srv = s.get("server", "nginx")
_conf_path = f"{os.path.expanduser('~/.bhserve')}/{'apache' if srv == 'apache' else 'nginx'}/sites/{name}.conf"
item(f"Open {'Apache' if srv == 'apache' else 'nginx'} config", "text-editor-symbolic",
lambda: _open_editor(_conf_path))
lambda: _open(_conf_path))
item("Delete site…", "user-trash-symbolic", lambda: win.confirm(
f"Delete site “{name}”?", "Removes the vhost. Tick purge in the next step to also drop files + DB.",
lambda: win.run_verb(["site", "rm", name], f"Removing {name}…")), destructive=True)
Expand Down
Loading