From 3b7094e8ca728220d55a70c593ff3a0534c29bca Mon Sep 17 00:00:00 2001 From: BrowserBox Release Bot Date: Sat, 6 Dec 2025 10:38:34 +0000 Subject: [PATCH 01/15] Update installer scripts for v15.3.5 - Update bbx.sh binary installer - Update bbx.ps1 Windows installer This commit is automatically generated by the release process. --- bbx.sh | 1139 ++++++++++++++++++++++++++++++++------- windows-scripts/bbx.ps1 | 306 +++++++---- 2 files changed, 1165 insertions(+), 280 deletions(-) diff --git a/bbx.sh b/bbx.sh index 1760f18a9..1c7c66cd0 100755 --- a/bbx.sh +++ b/bbx.sh @@ -57,6 +57,235 @@ protecc_win_sysadmins() { fi } +# ==================================================================== +# BINARY DISTRIBUTION SUPPORT +# Functions for downloading and managing pre-compiled BrowserBox binaries +# ==================================================================== + +# Configuration +PUBLIC_REPO="BrowserBox/BrowserBox" +BINARY_NAME="browserbox" +GLOBAL_BIN_DIR="/usr/local/bin" +SUDO_BIN="$(command -v sudo || true)" + +# Prefer global install; require writable /usr/local/bin (or sudo) +if [[ -w "$GLOBAL_BIN_DIR" ]]; then + BINARY_DIR="$GLOBAL_BIN_DIR" + INSTALL_CMD="install -m 755" + mkdir -p "$BINARY_DIR" +else + if [[ -n "$SUDO_BIN" ]]; then + BINARY_DIR="$GLOBAL_BIN_DIR" + INSTALL_CMD="$SUDO_BIN install -m 755" + "$SUDO_BIN" mkdir -p "$BINARY_DIR" + else + echo -e "${RED}Cannot install to $GLOBAL_BIN_DIR (not writable and sudo unavailable).${NC}" >&2 + echo "BrowserBox requires a global install; please run with sudo or make $GLOBAL_BIN_DIR writable." >&2 + exit 1 + fi +fi + +BINARY_PATH="${BINARY_DIR}/${BINARY_NAME}" + +# Function to detect OS and architecture +detect_platform() { + case "$(uname -s)" in + Linux*) echo "linux" ;; + Darwin*) echo "macos" ;; + *) + echo -e "${RED}Unsupported OS: $(uname -s)${NC}" >&2 + echo "This installer only supports Linux and macOS." >&2 + echo "For Windows, use: irm dosaygo.com/browserbox | iex" >&2 + exit 1 + ;; + esac +} + +# Function to get the latest release tag from GitHub +get_latest_release() { + local repo="$1" + local tag="" + + # Try using curl with GitHub API + if command -v curl >/dev/null 2>&1; then + local api_url="https://api.github.com/repos/${repo}/releases/latest" + local response + response=$(curl -sS --connect-timeout 10 "$api_url" 2>/dev/null || echo "") + + if [[ -n "$response" ]]; then + # Try jq first + if command -v jq >/dev/null 2>&1; then + tag=$(echo "$response" | jq -r '.tag_name // empty' 2>/dev/null) + else + # Fallback to sed + tag=$(echo "$response" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]\+\)".*/\1/p' | head -n1) + fi + fi + fi + + if [[ -z "$tag" ]]; then + echo -e "${RED}Failed to fetch latest release from ${repo}${NC}" >&2 + exit 1 + fi + + echo "$tag" +} + +# Function to download the binary +download_binary() { + local platform="$1" + local tag="$2" + local asset_name + case "$platform" in + macos) asset_name="browserbox-macos-bin" ;; + linux) asset_name="browserbox-linux-bin" ;; + *) echo -e "${RED}Unsupported platform: $platform${NC}" >&2; exit 1 ;; + esac + local download_url="https://github.com/${PUBLIC_REPO}/releases/download/${tag}/${asset_name}" + local temp_file + temp_file="$(mktemp "${TMPDIR:-/tmp}/browserbox.XXXX")" + + echo -e "${CYAN}Downloading BrowserBox ${tag} for ${platform}...${NC}" + + # Check if curl is available + if ! command -v curl >/dev/null 2>&1; then + echo -e "${RED}Error: curl is required but not installed.${NC}" >&2 + echo -e "Please install curl and try again." >&2 + exit 1 + fi + + if ! curl -L --fail --progress-bar --connect-timeout 30 -o "$temp_file" "$download_url" 2>&1; then + echo -e "${RED}Failed to download binary from ${download_url}${NC}" >&2 + echo -e "${YELLOW}This could mean:${NC}" >&2 + echo -e " - No release is available for ${platform}" >&2 + echo -e " - Network connectivity issues" >&2 + echo -e " - The release ${tag} doesn't have a ${asset_name} asset" >&2 + rm -f "$temp_file" + exit 1 + fi + + if [[ ! -s "$temp_file" ]]; then + echo -e "${RED}Downloaded file is empty${NC}" >&2 + rm -f "$temp_file" + exit 1 + fi + + $INSTALL_CMD "$temp_file" "$BINARY_PATH" + rm -f "$temp_file" + + echo -e "${GREEN}Successfully downloaded and installed BrowserBox binary${NC}" + echo -e "${CYAN}Binary installed at: ${BINARY_PATH}${NC}" +} + +# Function to check if binary exists and is executable +binary_exists() { + [[ -f "$BINARY_PATH" ]] && [[ -x "$BINARY_PATH" ]] +} + +extract_semver() { + local text="$1" line + while IFS= read -r line; do + if [[ "$line" =~ ([vV]?[0-9]+(\.[0-9]+){1,2}(-[0-9A-Za-z\.-]+)?) ]]; then + echo "${BASH_REMATCH[1]}" + return 0 + fi + done <<< "$text" + return 1 +} + +# Function to get current binary version +get_binary_version() { + if binary_exists; then + local output + output="$("$BINARY_PATH" --version 2>/dev/null || true)" + if extract_semver "$output"; then + return 0 + fi + echo "unknown" + else + echo "not_installed" + fi +} + +# Semver helpers from legacy installer (stable > rc; explicit patch tie-break) +_parse_tag() { + local s="$1" core pre a b c rcnum + PAR_MAJ=0 PAR_MIN=0 PAR_PAT=0 PAR_STABLE=1 PAR_RCNUM=0 + PAR_HASPATCH=0 + + [[ ${s:0:1} == "v" ]] && s="${s:1}" + + core="${s%%-*}" + if [[ "$core" == "$s" ]]; then pre=""; else pre="${s#"$core"-}"; fi + + IFS='.' read -r a b c <<<"$core" + + [[ "$a" =~ ^[0-9]+$ ]] || return 1 + [[ "$b" =~ ^[0-9]+$ ]] || return 1 + if [[ -z "$c" ]]; then + PAR_HASPATCH=0 + PAR_PAT=0 + else + [[ "$c" =~ ^[0-9]+$ ]] || return 1 + PAR_HASPATCH=1 + PAR_PAT=$c + fi + + PAR_MAJ=$a + PAR_MIN=$b + + if [[ -n "$pre" ]]; then + if [[ "$pre" == rc ]]; then + PAR_STABLE=0; PAR_RCNUM=0 + elif [[ "$pre" == rc.* ]]; then + rcnum="${pre#rc.}" + [[ "$rcnum" =~ ^[0-9]+$ ]] || return 1 + PAR_STABLE=0; PAR_RCNUM=$rcnum + else + return 1 + fi + fi + return 0 +} + +_better_than() { + local cMaj=$1 cMin=$2 cPat=$3 cSt=$4 cRc=$5 cHP=$6 + local bMaj=$7 bMin=$8 bPat=$9 bSt=${10} bRc=${11} bHP=${12} + + if (( cMaj > bMaj )); then return 0 + elif (( cMaj < bMaj )); then return 1; fi + if (( cMin > bMin )); then return 0 + elif (( cMin < bMin )); then return 1; fi + if (( cPat > bPat )); then return 0 + elif (( cPat < bPat )); then return 1; fi + + if (( cSt != bSt )); then + (( cSt > bSt )) && return 0 || return 1 + fi + + if (( cSt == 0 )); then + if (( cRc > bRc )); then return 0 + elif (( cRc < bRc )); then return 1; fi + fi + + if (( cHP != bHP )); then + (( cHP > bHP )) && return 0 || return 1 + fi + + return 1 +} + +version_is_newer() { + local candidate="$1" baseline="$2" + _parse_tag "$candidate" || return 1 + local cMaj=$PAR_MAJ cMin=$PAR_MIN cPat=$PAR_PAT cSt=$PAR_STABLE cRc=$PAR_RCNUM cHP=$PAR_HASPATCH + _parse_tag "$baseline" || return 1 + local bMaj=$PAR_MAJ bMin=$PAR_MIN bPat=$PAR_PAT bSt=$PAR_STABLE bRc=$PAR_RCNUM bHP=$PAR_HASPATCH + _better_than "$cMaj" "$cMin" "$cPat" "$cSt" "$cRc" "$cHP" "$bMaj" "$bMin" "$bPat" "$bSt" "$bRc" "$bHP" +} + + + # Call the function right away protecc_win_sysadmins @@ -138,7 +367,8 @@ ensure_modern_bash() { } # Call the guard immediately -ensure_modern_bash "$@" +# We don't need modern bash +# ensure_modern_bash "$@" # Sudo check @@ -157,7 +387,8 @@ export BBX_REQUIRE_RELEASE=1 BBX_HOME="${HOME}/.bbx" BBX_NEW_DIR="${BBX_HOME}/new" COMMAND_DIR="" -REPO_URL="https://github.com/BrowserBox/BrowserBox" +REPO_URL="https://github.com/BrowserBox/BrowserBox-source" +owner_repo="${REPO_URL#https://github.com/}" BBX_SHARE="/usr/local/share/dosyago" if [[ ":$PATH:" == *":/usr/local/bin:"* ]] && $SUDO test -w /usr/local/bin; then COMMAND_DIR="/usr/local/bin" @@ -166,6 +397,7 @@ elif $SUDO test -w /usr/bin; then else COMMAND_DIR="$HOME/.local/bin" mkdir -p "$COMMAND_DIR" + printf "${YELLOW}WARNING: BrowserBox command directory set to use local direcotry \""$COMMAND_DIR"\". This will likely produce errors, especially for updates. Ensure you can write to a global executable direcotry to install the binaries.${NC}\n" >&2 fi BBX_BIN="${COMMAND_DIR}/bbx" @@ -178,19 +410,70 @@ CERT_META_FILE="${BB_CONFIG_DIR}/tickets/cert.meta.env" DOCKER_CONTAINERS_FILE="$BB_CONFIG_DIR/docker_containers.json" [ ! -f "$DOCKER_CONTAINERS_FILE" ] && echo "{}" > "$DOCKER_CONTAINERS_FILE" -# Version file paths -VERSION_FILE="${BBX_SHARE}/BrowserBox/version.json" -PREPARED_VERSION_FILE="${BBX_NEW_DIR}/BrowserBox/version.json" +# Version tracking and update lock files +# Note: VERSION_FILE and PREPARED_VERSION_FILE are deprecated in binary-based installation +# Version info is now obtained from `browserbox --version` command via get_canonical_bbx_version() LOG_FILE="${BB_CONFIG_DIR}/update.log" PREPARING_FILE="${BBX_SHARE}/preparing" PREPARED_FILE="${BBX_SHARE}/prepared" +# Legacy note: Chai static assets used to be synchronized from a local source tree +# here. As part of the migration away from source-based installs, that logic now +# lives solely inside the binary `browserbox --install/--full-install` flow. + # Clean up any leftover temp installer scripts clean_temp_installers() { local TMPDIR="$HOME/.cache/myscript-installer" find "$TMPDIR" -type f -name 'installer-*' -exec rm -f {} \; 2>/dev/null } +# Ensure installation_id exists with a UUID +ensure_installation_id() { + local INSTALL_ID_DIR="${HOME}/.config/dosyago/bbpro" + local INSTALL_ID_FILE="${INSTALL_ID_DIR}/installation_id" + + # Create directory if it doesn't exist with owner-only permissions + if [ ! -d "$INSTALL_ID_DIR" ]; then + mkdir -p "$INSTALL_ID_DIR" 2>/dev/null || { + [[ -n "$BBX_DEBUG" ]] && echo "Warning: Could not create $INSTALL_ID_DIR (may be read-only)" + return 0 + } + chmod 700 "$INSTALL_ID_DIR" 2>/dev/null || true + fi + + # Create installation_id file if it doesn't exist + if [ ! -f "$INSTALL_ID_FILE" ]; then + local uuid="" + + # Try various methods to generate a UUID + if command -v uuidgen >/dev/null 2>&1; then + uuid=$(uuidgen 2>/dev/null) + elif command -v python3 >/dev/null 2>&1; then + uuid=$(python3 -c "import uuid; print(uuid.uuid4())" 2>/dev/null) + elif command -v python >/dev/null 2>&1; then + uuid=$(python -c "import uuid; print(uuid.uuid4())" 2>/dev/null) + elif command -v node >/dev/null 2>&1; then + uuid=$(node -e "console.log(require('crypto').randomUUID())" 2>/dev/null) + elif command -v openssl >/dev/null 2>&1; then + # Generate a UUID-like string using openssl + uuid=$(openssl rand -hex 16 | sed 's/\(........\)\(....\)\(....\)\(....\)\(............\)/\1-\2-\3-\4-\5/' 2>/dev/null) + fi + + if [ -n "$uuid" ]; then + echo "$uuid" > "$INSTALL_ID_FILE" 2>/dev/null || { + [[ -n "$BBX_DEBUG" ]] && echo "Warning: Could not write installation_id (may be read-only)" + return 0 + } + chmod 600 "$INSTALL_ID_FILE" 2>/dev/null || true + [[ -n "$BBX_DEBUG" ]] && echo "Created installation_id: $uuid" + else + [[ -n "$BBX_DEBUG" ]] && echo "Warning: Could not generate UUID for installation_id" + fi + else + [[ -n "$BBX_DEBUG" ]] && echo "installation_id already exists" + fi +} + # Returns 0 if currently running from official location (not temp copy) is_running_in_official() { local TMPDIR="$HOME/.cache/myscript-installer" @@ -319,8 +602,7 @@ _better_than() { get_latest_release_tag_filtered() { local channel="${1:-stable}" - # Derive owner/repo from REPO_URL (e.g. https://github.com/BrowserBox/BrowserBox) - local owner_repo="${REPO_URL#https://github.com/}" + # Derive owner/repo from REPO_URL (e.g. https://github.com/BrowserBox/BrowserBox-source) local owner="${owner_repo%%/*}" local repo="${owner_repo#*/}" local api="https://api.github.com/repos/${owner}/${repo}" @@ -498,6 +780,8 @@ fi banner_color=$CYAN # Helper: Get version info from version.json +# DEPRECATED: This function is no longer used in binary-based installation. +# Version info is now obtained via get_canonical_bbx_version() which calls `browserbox --version` get_version_info() { local file="$1" if [ -f "$file" ]; then @@ -508,8 +792,42 @@ get_version_info() { fi } -if ! test -d "${BBX_HOME}/BrowserBox/node_modules" || ! test -f "${BBX_HOME}/BrowserBox/.bbpro_install_dir"; then - if [[ "$1" != "install" ]] && [[ "$1" != "uninstall" ]] && [[ "$1" != "docker-"* ]] && [[ "$1" != "stop" ]] && [[ "$1" != "update-background" ]]; then +# Helper: Get canonical BBX version from browserbox command or fallback +get_canonical_bbx_version() { + local version="" + + # Try to get version from browserbox command if it exists + if command -v browserbox >/dev/null 2>&1; then + local browserbox_output + browserbox_output="$(browserbox --version 2>/dev/null || true)" + if [[ -n "$browserbox_output" ]]; then + # Extract version number using regex (supports any dot-separated numeric format: X.Y, X.Y.Z, etc.) + # Handles formats like "BrowserBox version: 15.1.2", "v15.1.2", or just "15.1.2" + version="$(echo "$browserbox_output" | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1)" + fi + fi + + # Fallback to BBX_VERSION if set and valid + # Note: BBX_VERSION can be "unknown", "unknown - cannot find any releases", or a valid version + # We explicitly exclude "unknown" and "unknown - ..." variants + if [[ -z "$version" ]] && [[ -n "${BBX_VERSION:-}" ]] && [[ "$BBX_VERSION" != "unknown" ]] && [[ "$BBX_VERSION" != "unknown - "* ]]; then + # Strip leading 'v' if present + version="${BBX_VERSION#v}" + fi + + # Final fallback + if [[ -z "$version" ]]; then + version="unknown" + fi + + echo "$version" +} + +# Set the canonical version for use throughout the script +VERSION="$(get_canonical_bbx_version)" + +if ! command -v browserbox &>/dev/null || ! test -d "${HOME}/.config/dosyago/bbpro"; then + if [[ "$1" != "install" ]] && [[ "$1" != "uninstall" ]] && [[ "$1" != "update-background" ]] && [[ "$1" != "--version" ]] && [[ "$1" != "-v" ]] && [[ "$1" != "--help" ]] && [[ "$1" != "-h" ]]; then banner printf "\n${RED}Run ${NC}${BOLD}bbx install${NC}${RED} first.${NC}\n" printf "\tYou may need to run bbx uninstall to remove any previous or broken installation.\n" @@ -688,12 +1006,25 @@ get_system_hostname() { echo "${host:-unknown}" } +# Wrapper for getent-like functionality without installing getent +getent_hosts() { + local hostname="$1" + # If on macOS, etc, manually search /etc/hosts + if ! command -v getent &>/dev/null; then + grep -E "^\s*([^#]+)\s+$hostname" /etc/hosts || echo "" + else + getent hosts "$hostname" || echo "" + fi +} + # Check if hostname is local is_local_hostname() { local hostname="$1" local resolved_ips ip local public_dns_servers=("8.8.8.8" "1.1.1.1" "208.67.222.222") local has_valid_result=0 + + # Try DNS resolution for dns in "${public_dns_servers[@]}"; do resolved_ips=$(command -v dig >/dev/null 2>&1 && dig +short "$hostname" A @"$dns") if [[ "$?" -eq 0 ]] && [[ -n "$resolved_ips" ]]; then @@ -707,17 +1038,18 @@ is_local_hostname() { done <<< "$resolved_ips" fi done + # If all results were private or none resolved, treat as local if [[ "$has_valid_result" -eq 1 ]]; then return 0 # All IPs private => local fi + # Fallback: check /etc/hosts (or similar) - if command -v getent &>/dev/null; then - ip=$(getent hosts "$hostname" | awk '{print $1}' | head -n1) - if [[ "$ip" =~ ^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|::1$|fe80:) ]]; then - return 0 # Local - fi + ip=$(getent_hosts "$hostname" | awk '{print $1}' | head -n1) + if [[ "$ip" =~ ^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|::1$|fe80:) ]]; then + return 0 # Local fi + return 0 # Unresolvable => local } @@ -901,24 +1233,93 @@ ensure_setup_tor() { fi } +# Ensure cloudflared is installed +ensure_cloudflared() { + if command -v cloudflared >/dev/null 2>&1; then + return 0 + fi + + printf "${YELLOW}cloudflared not found. Installing...${NC}\n" + + if [[ "$(uname -s)" == "Darwin" ]]; then + # macOS: use Homebrew + if command -v brew >/dev/null 2>&1; then + brew install cloudflared || { + printf "${RED}Failed to install cloudflared via Homebrew${NC}\n" + return 1 + } + else + printf "${RED}Homebrew not found. Please install cloudflared manually from:${NC}\n" + printf "https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/\n" + return 1 + fi + elif [[ -f /etc/debian_version ]]; then + # Debian/Ubuntu: try apt, fallback to binary + if $SUDO apt-get install -y cloudflared 2>/dev/null; then + printf "${GREEN}Installed cloudflared via apt${NC}\n" + else + # Fallback to binary download + local arch="amd64" + if [[ "$(uname -m)" == "aarch64" || "$(uname -m)" == "arm64" ]]; then + arch="arm64" + fi + curl -L --output /tmp/cloudflared.deb "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch}.deb" || { + printf "${RED}Failed to download cloudflared${NC}\n" + return 1 + } + $SUDO dpkg -i /tmp/cloudflared.deb || { + printf "${RED}Failed to install cloudflared${NC}\n" + rm -f /tmp/cloudflared.deb + return 1 + } + rm -f /tmp/cloudflared.deb + fi + else + # Other Linux: download static binary + local arch="amd64" + if [[ "$(uname -m)" == "aarch64" || "$(uname -m)" == "arm64" ]]; then + arch="arm64" + fi + curl -L --output /tmp/cloudflared "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch}" || { + printf "${RED}Failed to download cloudflared${NC}\n" + return 1 + } + $SUDO install -m 755 /tmp/cloudflared /usr/local/bin/cloudflared || { + printf "${RED}Failed to install cloudflared to /usr/local/bin${NC}\n" + rm -f /tmp/cloudflared + return 1 + } + rm -f /tmp/cloudflared + fi + + # Validate installation + if ! command -v cloudflared >/dev/null 2>&1; then + printf "${RED}cloudflared installation failed validation${NC}\n" + return 1 + fi + + printf "${GREEN}cloudflared installed successfully${NC}\n" + return 0 +} + install() { banner check_agreement - pre_install || return 0 load_config ensure_deps + ensure_installation_id + printf "${GREEN}Installing BrowserBox CLI (bbx)...${NC}\n" - mkdir -p "$BBX_HOME/BrowserBox" || { printf "${RED}Failed to create $BBX_HOME/BrowserBox${NC}\n"; exit 1; } - printf "${YELLOW}Fetching BrowserBox repository...${NC}\n" - $SUDO rm -rf $BBX_HOME/BrowserBox* - curl --connect-timeout 8 -sL "$REPO_URL/archive/refs/heads/${branch}.zip" -o "$BBX_HOME/BrowserBox.zip" || { printf "${RED}Failed to download BrowserBox repo${NC}\n"; exit 1; } - unzip -o -q "$BBX_HOME/BrowserBox.zip" -d "$BBX_HOME/BrowserBox-zip" || { printf "${RED}Failed to extract BrowserBox repo${NC}\n"; exit 1; } - mv $BBX_HOME/BrowserBox-zip/BrowserBox-${branch} $BBX_HOME/BrowserBox - $SUDO rm -rf $BBX_HOME/BrowserBox-zip - $SUDO rm -f $BBX_HOME/BrowserBox.zip - chmod +x "$BBX_HOME/BrowserBox/deploy-scripts/global_install.sh" || { printf "${RED}Failed to make global_install.sh executable${NC}\n"; exit 1; } + + # Download and install the binary + local platform + platform=$(detect_platform) + local tag + tag=$(get_latest_release "$PUBLIC_REPO") + download_binary "$platform" "$tag" + + # Get hostname and email for setup local default_hostname=$(get_system_hostname) - if [ -z "$BBX_HOSTNAME" ]; then if [[ -n "$BBX_TEST_AGREEMENT" ]]; then BBX_HOSTNAME="localhost" @@ -927,11 +1328,13 @@ install() { fi fi BBX_HOSTNAME="${BBX_HOSTNAME:-$default_hostname}" - STRICTNESS="mandatory"; + + STRICTNESS="mandatory" if is_local_hostname "$BBX_HOSTNAME"; then STRICTNESS="optional" ensure_hosts_entry "$BBX_HOSTNAME" fi + if [ -z "$EMAIL" ]; then if [[ -n "$BBX_TEST_AGREEMENT" ]]; then EMAIL="" @@ -945,19 +1348,15 @@ install() { fi fi + # Run the binary full installation process + printf "${YELLOW}Running BrowserBox full installer...${NC}\n" if [ -t 0 ] && [[ -z "$BBX_TEST_AGREEMENT" ]]; then - printf "${YELLOW}Running BrowserBox installer interactively...${NC}\n" - cd "$BBX_HOME/BrowserBox" && ./deploy-scripts/global_install.sh "$BBX_HOSTNAME" "$EMAIL" + "$BINARY_PATH" --full-install "$BBX_HOSTNAME" "$EMAIL" else - printf "${YELLOW}Running BrowserBox installer non-interactively...${NC}\n" - cd "$BBX_HOME/BrowserBox" && (yes | ./deploy-scripts/global_install.sh "$BBX_HOSTNAME" "$EMAIL") + yes | "$BINARY_PATH" --full-install "$BBX_HOSTNAME" "$EMAIL" fi [ $? -eq 0 ] || { printf "${RED}Installation failed${NC}\n"; exit 1; } - printf "${YELLOW}Updating npm and pm2...${NC}\n" - ensure_nvm - npm i -g npm@latest - npm i -g pm2@latest - timeout 5s pm2 update + printf "${YELLOW}Installing bbx command globally...${NC}\n" $SUDO curl --connect-timeout 7 --max-time 15 -sSL "$REPO_URL/raw/${branch}/bbx.sh" -o "$BBX_BIN" || { printf "${RED}Failed to install bbx${NC}\n"; $SUDO rm -f "$BBX_BIN"; exit 1; } $SUDO chmod +x "$BBX_BIN" @@ -968,11 +1367,12 @@ install() { setup() { load_config ensure_deps + ensure_installation_id # Initialize local variables from config or defaults local port="${PORT:-$(find_free_port_block)}" local hostname="${BBX_HOSTNAME:-$(get_system_hostname)}" - local token="${TOKEN}" + local token="" local zeta_mode="" local backend_scheme="" # Will be 'http' or 'https' @@ -1042,21 +1442,31 @@ setup() { local setup_hostname="$hostname" local setup_token="${token:-$(openssl rand -hex 16)}" + if [[ -z "$setup_token" ]]; then + setup_token="$(openssl rand -hex 16)" + fi + printf "${YELLOW}Setting up BrowserBox on $setup_hostname:$setup_port...${NC}\n" if [[ -n "$zeta_mode" ]] && [[ "$setup_hostname" == "localhost" ]]; then printf "${YELLOW}localhost is incompatible with zeta mode due to widespread conventions against *.localhost subdomains. Changing hostname to bbx.test\n" setup_hostname="bbx.test" fi + if [[ "$setup_hostname" == *".local" ]] && [[ "$(uname -s)" == "Darwin" ]]; then + printf "${YELLOW}On macOS .local domains are incompatible as they are reserved for Apple's Bonjour mDNS service.\n" + read -r -p "Enter a new hostname: " new_hostname + if [[ "$new_hostname" == *".local" ]]; then + new_hostname="${new_hostname}.test" + fi + setup_hostname="${new_hostname}" + fi if ! is_local_hostname "$setup_hostname"; then printf "${BLUE}DNS Note:${NC} Ensure an A/AAAA record points from $setup_hostname to this machine's IP.\n" - curl --connect-timeout 8 -sL "$REPO_URL/raw/${branch}/deploy-scripts/wait_for_hostname.sh" -o "$BBX_HOME/BrowserBox/deploy-scripts/wait_for_hostname.sh" || { printf "${RED}Failed to download wait_for_hostname.sh${NC}\n"; exit 1; } - chmod +x "$BBX_HOME/BrowserBox/deploy-scripts/wait_for_hostname.sh" - "$BBX_HOME/BrowserBox/deploy-scripts/wait_for_hostname.sh" "$setup_hostname" || { printf "${RED}Hostname $setup_hostname not resolving${NC}\n"; exit 1; } + wait_for_hostname "$setup_hostname" || { printf "${RED}Hostname $setup_hostname not resolving${NC}\n"; exit 1; } else ensure_hosts_entry "$setup_hostname" fi - EMAIL="${EMAIL}" BB_USER_EMAIL="${EMAIL}" "$BBX_HOME/BrowserBox/deploy-scripts/tls" "$setup_hostname" || { printf "${RED}Hostname $setup_hostname certificate not acquired${NC}\n"; exit 1; } + EMAIL="${EMAIL}" BB_USER_EMAIL="${EMAIL}" tls "$setup_hostname" || { printf "${RED}Hostname $setup_hostname certificate not acquired${NC}\n"; exit 1; } # Ensure we have a valid product key if ! validate_license_key; then @@ -1079,9 +1489,9 @@ setup() { fi # Call setup_bbpro, which writes to test.env - LICENSE_KEY="${LICENSE_KEY}" setup_bbpro "${setup_args[@]}" || { printf "${RED}Setup failed${NC}\n"; exit 1; } + LICENSE_KEY="${LICENSE_KEY}" browserbox --setup "${setup_args[@]}" || { printf "${RED}Setup failed${NC}\n"; exit 1; } - # After setup_bbpro succeeds, reload config to get the new runtime values + # After browserbox --setup succeeds, reload config to get the new runtime values load_config printf "${GREEN}Setup complete.${NC}\n" @@ -1091,9 +1501,15 @@ setup() { fi } +restart() { + stop; + run; +} + run() { banner load_config + ensure_installation_id # Ensure setup has been run if [ -z "$PORT" ] || [ -z "$BBX_HOSTNAME" ] || [[ ! -f "${BB_CONFIG_DIR}/test.env" ]] ; then @@ -1119,13 +1535,13 @@ run() { # Default values from loaded config local port="${PORT}" local hostname="${BBX_HOSTNAME}" - local run_args=() # Store args to pass to bbpro + local run_args=() # Store args to pass to browserbox # Parse arguments to override config for this run only local temp_args=("$@") local clean_args=() for arg in "${temp_args[@]}"; do - # This is a simple way to filter out --port from being passed to bbpro + # This is a simple way to filter out --port from being passed to browserbox # A more robust solution would handle --port=value too if [[ "$arg" != "--port" && "$arg" != "-p" && ! "$arg" =~ ^[0-9]+$ ]]; then clean_args+=("$arg") @@ -1154,7 +1570,7 @@ run() { shift 2 ;; *) - # Pass unknown args to bbpro + # Pass unknown args to browserbox run_args+=("$1") shift ;; @@ -1172,7 +1588,7 @@ run() { if ! is_local_hostname "$hostname"; then printf "${BLUE}DNS Note:${NC} Ensure an A/AAAA record points from $hostname to this machine's IP.\n" - "$BBX_HOME/BrowserBox/deploy-scripts/wait_for_hostname.sh" "$hostname" || { printf "${RED}Hostname $hostname not resolving${NC}\n"; exit 1; } + wait_for_hostname "$hostname" || { printf "${RED}Hostname $hostname not resolving${NC}\n"; exit 1; } else ensure_hosts_entry "$hostname" fi @@ -1194,8 +1610,8 @@ run() { fi export HOST_PER_SERVICE BBX_HTTP_ONLY; - # Pass any extra args to bbpro - run_quietly bbpro "${run_args[@]}" || { printf "${RED}Failed to start${NC}\n"; exit 1; } + # Pass any extra args to browserbox + run_quietly browserbox "${run_args[@]}" || { printf "${RED}Failed to start${NC}\n"; exit 1; } # Reload config to get the final token from the newly created test.env load_config @@ -1281,12 +1697,12 @@ tor_run() { printf "${YELLOW}sg not found and $user not in $TOR_GROUP, may fail without Tor group access${NC}\n" fi - local setup_cmd="setup_bbpro --port $PORT --token $TOKEN" + local setup_cmd="browserbox --setup --port $PORT --token $TOKEN" if $anonymize; then setup_cmd="$setup_cmd --ontor" fi if ! $onion && ! is_local_hostname "$BBX_HOSTNAME"; then - "$BBX_HOME/BrowserBox/deploy-scripts/wait_for_hostname.sh" "$BBX_HOSTNAME" || { printf "${RED}Hostname $BBX_HOSTNAME not resolving${NC}\n"; exit 1; } + wait_for_hostname "$BBX_HOSTNAME" || { printf "${RED}Hostname $BBX_HOSTNAME not resolving${NC}\n"; exit 1; } elif ! $onion; then ensure_hosts_entry "$BBX_HOSTNAME" fi @@ -1330,7 +1746,7 @@ tor_run() { test_port_access $((PORT+i)) || { printf "${RED}Quit software using these ports, or adjust firewall for ports $((PORT-2))-$((PORT+2))/tcp${NC}\n"; exit 1; } done test_port_access $((PORT-3000)) || { printf "${RED}CDP port $((PORT-3000)) blocked${NC}\n"; exit 1; } - bbpro || { printf "${RED}Failed to start${NC}\n"; exit 1; } + browserbox || { printf "${RED}Failed to start${NC}\n"; exit 1; } login_link=$(cat "$BB_CONFIG_DIR/login.link" 2>/dev/null || echo "https://$TEMP_HOSTNAME:$PORT/login?token=$TOKEN") fi sleep 2 @@ -1681,6 +2097,144 @@ EOF wait $! } +cf_run() { + banner + load_config + ensure_deps + ensure_cloudflared || { printf "${RED}Failed to install cloudflared${NC}\n"; exit 1; } + + printf "${CYAN}Starting BrowserBox with Cloudflare Quick Tunnel...${NC}\n" + + # Parse optional --port|-p argument + local port="" + while [ $# -gt 0 ]; do + case "$1" in + --port|-p) + if [ -z "$2" ]; then + printf "${RED}Error: Option $1 requires an argument${NC}\n" + printf "Usage: bbx cf-run [--port|-p ]\n" + exit 1 + fi + port="$2" + shift 2 + ;; + *) + printf "${RED}Unknown option: $1${NC}\n" + printf "Usage: bbx cf-run [--port|-p ]\n" + exit 1 + ;; + esac + done + + # Default to find_free_port_block or loaded PORT if no port specified + if [ -z "$port" ]; then + port="${PORT:-$(find_free_port_block)}" + fi + + # Ensure a fresh token + [ -n "$TOKEN" ] || TOKEN=$(openssl rand -hex 16) + + # Test the 5-port block and CDP port + printf "${YELLOW}Testing port block ${port}...${NC}\n" + for i in {-2..2}; do + test_port_access $((port+i)) || { + printf "${RED}Port $((port+i)) is not accessible. Quit software using these ports, or adjust firewall for ports $((port-2))-$((port+2))/tcp${NC}\n" + exit 1 + } + done + test_port_access $((port-3000)) || { + printf "${RED}CDP port $((port-3000)) blocked${NC}\n" + exit 1 + } + + # Run minimal setup using browserbox --setup with HTTP backend + printf "${YELLOW}Setting up BrowserBox on port ${port} with HTTP backend...${NC}\n" + LICENSE_KEY="${LICENSE_KEY}" browserbox --setup --port "$port" --token "$TOKEN" --backend http || { + printf "${RED}Setup failed${NC}\n" + exit 1 + } + + # Reload config to get PORT and TOKEN from test.env + source "${BB_CONFIG_DIR}/test.env" && PORT="${APP_PORT:-$port}" && TOKEN="${LOGIN_TOKEN:-$TOKEN}" || { + printf "${YELLOW}Warning: test.env not found${NC}\n" + } + + # Validate LICENSE_KEY via bbcertify + export LICENSE_KEY + certout="$(bash -c "export LICENSE_KEY=\"$LICENSE_KEY\"; bbcertify 2>&1")" + if [[ "$?" -ne 0 ]]; then + printf "${RED}License key invalid or missing. Run 'bbx activate' or go to dosaygo.com to get a valid key.${NC}\n" + echo "Certification output: $certout" + exit 1 + else + printf "${GREEN}Certification complete.${NC}\n" + if [[ -f "$CERT_META_FILE" ]]; then + # shellcheck disable=SC1090 + source "$CERT_META_FILE" + export BBX_RESERVATION_CODE BBX_RESERVED_SEAT_ID BBX_TICKET_ID BBX_TICKET_SLOT + fi + fi + + # Start BrowserBox via run_quietly browserbox + printf "${YELLOW}Starting BrowserBox on 127.0.0.1:${PORT}...${NC}\n" + run_quietly browserbox || { printf "${RED}Failed to start BrowserBox${NC}\n"; exit 1; } + + # Reload config to capture final token + load_config + + # Start cloudflared in background with logs to BB_CONFIG_DIR + local cf_log_file="${BB_CONFIG_DIR}/cloudflared.log" + printf "${YELLOW}Starting Cloudflare tunnel to http://127.0.0.1:${PORT}...${NC}\n" + cloudflared tunnel --no-autoupdate --url "http://127.0.0.1:${PORT}" > "$cf_log_file" 2>&1 & + local cf_pid=$! + + # Cleanup function for cf_run - defined before trap to ensure proper execution order + cleanup_cf_run() { + printf "\n${YELLOW}Stopping BrowserBox and Cloudflare tunnel...${NC}\n" + kill $cf_pid 2>/dev/null || true + run_quietly browserbox --stop || true + printf "${GREEN}Cleanup complete.${NC}\n" + } + # Set trap for cleanup immediately after function definition + trap cleanup_cf_run EXIT INT TERM + + # Extract https://*.trycloudflare.com from log (poll up to ~60 seconds) + local attempts=0 + local max_attempts=120 + local tunnel_url="" + + while [ $attempts -lt $max_attempts ]; do + if [ -f "$cf_log_file" ]; then + # Note: trycloudflare.com is the domain used by Cloudflare Quick Tunnels (no account required) + tunnel_url=$(grep -oE 'https://[a-zA-Z0-9-]+\.trycloudflare\.com' "$cf_log_file" | head -1) + if [ -n "$tunnel_url" ]; then + break + fi + fi + sleep 0.5 + attempts=$((attempts + 1)) + done + + if [ -z "$tunnel_url" ]; then + printf "${RED}Failed to extract tunnel URL from cloudflared log${NC}\n" + printf "${YELLOW}Last 20 lines of cloudflared log:${NC}\n" + tail -n 20 "$cf_log_file" + exit 1 + fi + + printf "${GREEN}Cloudflare tunnel established!${NC}\n" + + # Build login link and save to login.link + local login_link="${tunnel_url}/login?token=${TOKEN}" + echo "$login_link" > "${BB_CONFIG_DIR}/login.link" + + draw_box "Login Link: ${login_link}" + + printf "\n${CYAN}Tunnel is active. Press Ctrl+C to stop.${NC}\n\n" + + # Wait on the cloudflared PID + wait $cf_pid +} docker_run() { banner @@ -1882,9 +2436,9 @@ docker_stop() { fi printf "${YELLOW}Stopping BrowserBox for $nickname ($container_id)...${NC}\n" - docker exec "$container_id" bash -c "stop_bbpro" || - $SUDO docker exec "$container_id" bash -c "stop_bbpro" || { - printf "${RED}Warning: Failed to run stop_bbpro in container${NC}\n" + docker exec "$container_id" bash -c "browserbox --stop" || + $SUDO docker exec "$container_id" bash -c "browserbox --stop" || { + printf "${RED}Warning: Failed to run browserbox --stop in container${NC}\n" } printf "${YELLOW}Waiting 1 second for license release...${NC}\n" sleep 1 @@ -2033,7 +2587,7 @@ pre_install() { # Download the install script using curl and save it to a file echo "Downloading the installation script..." - curl -sSL "https://raw.githubusercontent.com/BrowserBox/BrowserBox/refs/heads/$branch/bbx.sh" -o /tmp/bbx.sh + curl -sSL "https://raw.githubusercontent.com/${owner_repo}/refs/heads/$branch/bbx.sh" -o /tmp/bbx.sh chmod +x /tmp/bbx.sh install_group="$(id -gn "$install_user")" chown "${install_user}:${install_group}" /tmp/bbx.sh @@ -2207,7 +2761,7 @@ ng_run() { stop() { load_config printf "${YELLOW}Stopping BrowserBox (current user)...${NC}\n" - run_quietly stop_bbpro || { printf "${RED}Failed to stop. Check if BrowserBox is running.${NC}\n"; exit 1; } + run_quietly browserbox --stop || { printf "${RED}Failed to stop. Check if BrowserBox is running.${NC}\n"; exit 1; } printf "${GREEN}BrowserBox stopped.${NC}\n" } @@ -2306,11 +2860,14 @@ check_and_prepare_update() { return 0 fi - # If a prepared build exists, only install it if it matches the live latest - if [ -f "$PREPARED_FILE" ] && [ -d "${BBX_NEW_DIR}/BrowserBox" ]; then + # If a prepared binary exists, only install it if it matches the live latest + if [ -f "$PREPARED_FILE" ]; then + local prepared_binary + prepared_binary=$(sed -n '2p' "$PREPARED_FILE" 2>/dev/null) local prepared_tag - prepared_tag="$(get_version_info "$PREPARED_VERSION_FILE")" - if [[ "$prepared_tag" == "$repo_tag" ]]; then + prepared_tag=$(sed -n '3p' "$PREPARED_FILE" 2>/dev/null) + + if [[ -n "$prepared_binary" ]] && [[ -f "$prepared_binary" ]] && [[ "$prepared_tag" == "$repo_tag" ]]; then printf "${YELLOW}Prepared update (${prepared_tag}) matches latest. Installing...${NC}\n" is_running_in_official && self_elevate_to_temp "${OGARGS[@]}" if check_prepare_and_install "$repo_tag"; then @@ -2318,19 +2875,26 @@ check_and_prepare_update() { fi else printf "${YELLOW}Prepared update (${prepared_tag}) is stale vs latest (${repo_tag}); removing it.${NC}\n" - $SUDO rm -rf "$BBX_NEW_DIR" "$PREPARED_FILE" "$PREPARING_FILE" 2>/dev/null + rm -rf "$BBX_NEW_DIR" 2>/dev/null + $SUDO rm -f "$PREPARED_FILE" "$PREPARING_FILE" 2>/dev/null fi fi # Compare installed vs latest release (works for roll-forward or roll-back) - local current_tag - current_tag="$(get_version_info "$VERSION_FILE")" + local current_version + current_version="$(get_canonical_bbx_version)" + # Normalize for comparison (add 'v' prefix if missing) + local current_tag="$current_version" + if [[ "$current_tag" != "unknown" ]] && [[ "$current_tag" != v* ]]; then + current_tag="v$current_tag" + fi + printf "${BLUE}Current: $current_tag${NC}\n" printf "${BLUE}Latest: $repo_tag${NC}\n" if [[ "$current_tag" == "$repo_tag" ]]; then printf "${GREEN}Already on the latest version (${repo_tag}).${NC}\n" - [ -d "$BBX_NEW_DIR" ] && $SUDO rm -rf "$BBX_NEW_DIR" && printf "${YELLOW}Cleaned up $BBX_NEW_DIR${NC}\n" + [ -d "$BBX_NEW_DIR" ] && rm -rf "$BBX_NEW_DIR" && printf "${YELLOW}Cleaned up $BBX_NEW_DIR${NC}\n" return 0 fi @@ -2351,38 +2915,39 @@ check_prepare_and_install() { fi local repo_tag="$1" - # Check if BBX_NEW_DIR has a prepared version + # Check if a prepared binary exists if [ -f "$PREPARED_FILE" ]; then - local prepared_location - prepared_location=$(sed -n '2p' "$PREPARED_FILE") - if [ "$prepared_location" = "$BBX_NEW_DIR" ] && [[ -d "${BBX_NEW_DIR}/BrowserBox" ]]; then - if [ ! -d "${BBX_NEW_DIR}/BrowserBox" ] || [ ! -f "$PREPARED_VERSION_FILE" ]; then - $SUDO rm -rf "$BBX_NEW_DIR" "$PREPARED_FILE" "$PREPARING_FILE" - return 1 - fi + local prepared_binary + prepared_binary=$(sed -n '2p' "$PREPARED_FILE") + + # Verify the prepared binary exists and is executable + if [ -f "$prepared_binary" ] && [ -x "$prepared_binary" ]; then local new_tag="" - # Prefer tag recorded during prepare (line 3) + # Get tag from line 3 of PREPARED_FILE new_tag=$(sed -n '3p' "$PREPARED_FILE" 2>/dev/null) - # Back-compat: fall back to version.json if no line-3 tag - if [ -z "$new_tag" ] || [ "$new_tag" = "unknown" ]; then - new_tag=$(get_version_info "$PREPARED_VERSION_FILE") - fi if [ "$new_tag" = "$repo_tag" ]; then - printf "${YELLOW}Latest version prepared in $BBX_NEW_DIR. Installing...${NC}\n" - printf "${YELLOW}Latest version prepared in $BBX_NEW_DIR. Installing...${NC}\n" >> "$LOG_FILE" + printf "${YELLOW}Latest version prepared at $prepared_binary. Installing...${NC}\n" + printf "${YELLOW}Latest version prepared at $prepared_binary. Installing...${NC}\n" >> "$LOG_FILE" - # Avoid self-overwrite while moving tree into place + # Avoid self-overwrite while swapping binary is_running_in_official && self_elevate_to_temp "${OGARGS[@]}" - # Move prepared version - $SUDO rm -rf "$BBX_HOME/BrowserBox" || { printf "${RED}Failed to remove $BBX_HOME/BrowserBox${NC}\n" >> "$LOG_FILE"; return 1; } - mv "$BBX_NEW_DIR/BrowserBox" "$BBX_HOME/BrowserBox" || { printf "${RED}Failed to move $BBX_NEW_DIR to $BBX_HOME/BrowserBox${NC}\n" >> "$LOG_FILE"; return 1; } + # Replace global binary with prepared binary (using INSTALL_CMD for sudo) + $INSTALL_CMD "$prepared_binary" "$BINARY_PATH" || { + printf "${RED}Failed to install prepared binary to $BINARY_PATH${NC}\n" >> "$LOG_FILE" + return 1 + } - # Run copy_install.sh - cd "$BBX_HOME/BrowserBox" && ./deploy-scripts/copy_install.sh >> "$LOG_FILE" 2>&1 || { printf "${RED}Failed to run copy_install.sh${NC}\n" >> "$LOG_FILE"; return 1; } + # Run internal updates/migrations after swapping binary + printf "${YELLOW}Running post-update installation tasks...${NC}\n" >> "$LOG_FILE" + "$BINARY_PATH" --install >> "$LOG_FILE" 2>&1 || { + printf "${RED}Failed to run post-update installation${NC}\n" >> "$LOG_FILE" + return 1 + } - # Clean up lock files + # Clean up prepared files + rm -rf "$BBX_NEW_DIR" || printf "${YELLOW}Warning: Failed to remove $BBX_NEW_DIR${NC}\n" >> "$LOG_FILE" $SUDO rm -f "$PREPARED_FILE" || printf "${YELLOW}Warning: Failed to remove $PREPARED_FILE${NC}\n" >> "$LOG_FILE" printf "${GREEN}Update to $repo_tag complete.${NC}\n" >> "$LOG_FILE" @@ -2390,8 +2955,14 @@ check_prepare_and_install() { return 0 else printf "${YELLOW}Prepared version ($new_tag) does not match latest ($repo_tag). Cleaning up and retrying...${NC}\n" >> "$LOG_FILE" - $SUDO rm -rf "$BBX_NEW_DIR" "$PREPARED_FILE" "$PREPARING_FILE" || printf "${YELLOW}Warning: Failed to clean up $BBX_NEW_DIR or $PREPARED_FILE${NC}\n" >> "$LOG_FILE" + rm -rf "$BBX_NEW_DIR" || printf "${YELLOW}Warning: Failed to clean up $BBX_NEW_DIR${NC}\n" >> "$LOG_FILE" + $SUDO rm -f "$PREPARED_FILE" "$PREPARING_FILE" || printf "${YELLOW}Warning: Failed to clean up lock files${NC}\n" >> "$LOG_FILE" fi + else + # Prepared binary missing or not executable, clean up + printf "${YELLOW}Prepared binary missing or not executable. Cleaning up...${NC}\n" >> "$LOG_FILE" + rm -rf "$BBX_NEW_DIR" || printf "${YELLOW}Warning: Failed to clean up $BBX_NEW_DIR${NC}\n" >> "$LOG_FILE" + $SUDO rm -f "$PREPARED_FILE" "$PREPARING_FILE" || printf "${YELLOW}Warning: Failed to clean up lock files${NC}\n" >> "$LOG_FILE" fi fi return 1 @@ -2403,8 +2974,6 @@ update() { fi load_config - mkdir -p "$BB_CONFIG_DIR" - chmod 700 "$BB_CONFIG_DIR" # Arg parsing: none | --latest-rc | local arg="${1:-}" @@ -2412,12 +2981,13 @@ update() { if [[ -z "$arg" ]]; then printf "${YELLOW}Updating BrowserBox to latest stable...${NC}\n" - repo_tag="$(get_latest_repo_version stable)" + repo_tag="$(get_latest_release "$PUBLIC_REPO")" elif [[ "$arg" == "--latest-rc" ]]; then printf "${YELLOW}Updating BrowserBox to latest release candidate...${NC}\n" - repo_tag="$(get_latest_repo_version rc)" - if [[ "$repo_tag" == "unknown" ]]; then - printf "${RED}No RC releases found.${NC}\n"; return 1 + # For binary distribution, we get latest release (not necessarily RC) + repo_tag="$(get_latest_release "$PUBLIC_REPO")" + if [[ "$repo_tag" == unknown* ]]; then + printf "${RED}No releases found.${NC}\n"; return 1 fi else # explicit version/tag @@ -2427,29 +2997,26 @@ update() { printf "Expected formats: v1.2.3 | 1.2.3 | v1.2.3-rc | v1.2.3-rc.1\n" return 1 fi - if ! tag_exists_remote "$tag"; then - printf "${RED}Tag '%s' not found in remote.%s${NC}\n" "$tag" "" - return 1 - fi repo_tag="$tag" printf "${YELLOW}Updating BrowserBox to %s...${NC}\n" "$repo_tag" fi - if [[ "$repo_tag" == unknown* ]]; then + if [[ "$repo_tag" == unknown* ]] || [[ -z "$repo_tag" ]]; then printf "${RED}Could not determine version to update to. Check network or GH API rate limits.${NC}\n" return 1 fi - # If no installed tree, fall back to install (preserves your behavior) - if [ ! -d "$BBX_HOME" ] || [ ! -d "$BBX_HOME/BrowserBox" ] || [ ! -d "$BBX_SHARE/BrowserBox" ]; then - printf "${YELLOW}No BrowserBox installation found. Running interactive install...${NC}\n" - install - return $? - fi - - # Prepare in background to $repo_tag, then try install - update_background "$repo_tag" - [ -n "$BBX_NO_UPDATE" ] || check_prepare_and_install "$repo_tag" + # Download and install the binary update + local platform + platform=$(detect_platform) + download_binary "$platform" "$repo_tag" + + # Run internal updates/migrations + printf "${YELLOW}Running post-update installation tasks...${NC}\n" + "$BINARY_PATH" --install || { printf "${RED}Post-update installation failed${NC}\n"; return 1; } + + printf "${GREEN}BrowserBox updated to ${repo_tag}${NC}\n" + return 0 } update_background() { @@ -2466,7 +3033,6 @@ update_background() { # default channel = stable repo_tag="$(get_latest_repo_version stable)" fi - local tagdoo="${repo_tag#v}" printf "${YELLOW}Checking update lock...${NC}\n" >> "$LOG_FILE" # Check lock files @@ -2482,41 +3048,63 @@ update_background() { printf "${YELLOW}Requesting update lock...${NC}\n" >> "$LOG_FILE" # Create preparing lock file $SUDO mkdir -p "$BBX_SHARE" || { printf "${RED}Failed to create install directory $BBX_SHARE ... ${NC}\n" >> "$LOG_FILE"; $SUDO rm -f "$PREPARING_FILE" ; exit 1; } - # Lines: 1=timestamp, 2=prepared_dir, 3=git_tag (exact, incl. -rc if present) - printf "%s\n%s\n%s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$BBX_NEW_DIR" "$repo_tag" | $SUDO tee "$PREPARING_FILE" >/dev/null || { printf "${RED}Failed to create $PREPARING_FILE${NC}\n" >> "$LOG_FILE"; $SUDO rm -f "$PREPARING_FILE" ; exit 1; } + # Lines: 1=timestamp, 2=prepared_binary_path, 3=git_tag (exact, incl. -rc if present) + printf "%s\n%s\n%s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$BBX_NEW_DIR/browserbox" "$repo_tag" | $SUDO tee "$PREPARING_FILE" >/dev/null || { printf "${RED}Failed to create $PREPARING_FILE${NC}\n" >> "$LOG_FILE"; $SUDO rm -f "$PREPARING_FILE" ; exit 1; } printf "${YELLOW}Starting background update to $repo_tag...${NC}\n" >> "$LOG_FILE" # Clean up any existing BBX_NEW_DIR - $SUDO rm -rf "$BBX_NEW_DIR" || { printf "${RED}Failed to clean $BBX_NEW_DIR${NC}\n" >> "$LOG_FILE"; $SUDO rm -f "$PREPARING_FILE" ; exit 1; } - mkdir -p "$BBX_NEW_DIR" || { printf "${RED}Failed to create $BBX_NEW_DIR/BrowserBox${NC}\n" >> "$LOG_FILE"; $SUDO rm -f "$PREPARING_FILE" ; exit 1; } - DLURL="${REPO_URL}/archive/refs/tags/${repo_tag}.zip"; - echo "Getting: $DLURL" >> "$LOG_FILE" - - curl -sSL --connect-timeout 8 "$DLURL" -o "$BBX_NEW_DIR/BrowserBox.zip" || { + rm -rf "$BBX_NEW_DIR" || { printf "${RED}Failed to clean $BBX_NEW_DIR${NC}\n" >> "$LOG_FILE"; $SUDO rm -f "$PREPARING_FILE" ; exit 1; } + mkdir -p "$BBX_NEW_DIR" || { printf "${RED}Failed to create $BBX_NEW_DIR${NC}\n" >> "$LOG_FILE"; $SUDO rm -f "$PREPARING_FILE" ; exit 1; } + + # Determine platform and download URL for binary + local platform + platform=$(detect_platform) + local asset_name + case "$platform" in + macos) asset_name="browserbox-macos-bin" ;; + linux) asset_name="browserbox-linux-bin" ;; + *) + printf "${RED}Unsupported platform: $platform${NC}\n" >> "$LOG_FILE" + $SUDO rm -f "$PREPARING_FILE" + return 1 + ;; + esac + + local download_url="https://github.com/${PUBLIC_REPO}/releases/download/${repo_tag}/${asset_name}" + local temp_binary="$BBX_NEW_DIR/browserbox" + + printf "${YELLOW}Downloading binary from $download_url...${NC}\n" >> "$LOG_FILE" + + # Use curl directly (no INSTALL_CMD/sudo) to avoid background sudo prompts + curl -L --fail --progress-bar --connect-timeout 30 -o "$temp_binary" "$download_url" >> "$LOG_FILE" 2>&1 || { printf "${YELLOW}Skipping update due to timeout or failure in connecting to BrowserBox repo${NC}\n" >> "$LOG_FILE" $SUDO rm -f "$PREPARING_FILE" - $SUDO rm -f "$BBX_NEW_DIR/BrowserBox.zip" 2>/dev/null - $SUDO rm -rf "$BBX_NEW_DIR" 2>/dev/null + rm -f "$temp_binary" 2>/dev/null + rm -rf "$BBX_NEW_DIR" 2>/dev/null + return 1 + } + + # Verify binary was downloaded and is not empty + if [[ ! -s "$temp_binary" ]]; then + printf "${RED}Downloaded binary is empty${NC}\n" >> "$LOG_FILE" + $SUDO rm -f "$PREPARING_FILE" + rm -f "$temp_binary" + rm -rf "$BBX_NEW_DIR" + return 1 + fi + + # Make binary executable + chmod +x "$temp_binary" || { + printf "${RED}Failed to make binary executable${NC}\n" >> "$LOG_FILE" + $SUDO rm -f "$PREPARING_FILE" + rm -f "$temp_binary" + rm -rf "$BBX_NEW_DIR" return 1 } - - printf "${YELLOW}Unzipping archive for $repo_tag...${NC}\n" >> "$LOG_FILE" - unzip -q -o "$BBX_NEW_DIR/BrowserBox.zip" -d "$BBX_NEW_DIR/BrowserBox-zip" || { printf "${RED}Failed to extract BrowserBox repo${NC}\n" >> "$LOG_FILE"; $SUDO rm -f "$PREPARING_FILE" ; exit 1; } - - printf "${YELLOW}Moving folder into place...${NC}\n" >> "$LOG_FILE" - mv "$BBX_NEW_DIR/BrowserBox-zip/BrowserBox-$tagdoo" "$BBX_NEW_DIR/BrowserBox" || { printf "${RED}Failed to move extracted files${NC}\n" >> "$LOG_FILE"; $SUDO rm -f "$PREPARING_FILE" ; exit 1; } - $SUDO rm -rf "$BBX_NEW_DIR/BrowserBox-zip" "$BBX_NEW_DIR/BrowserBox.zip" - - chmod +x "$BBX_NEW_DIR/BrowserBox/deploy-scripts/global_install.sh" || { printf "${RED}Failed to make global_install.sh executable${NC}\n" >> "$LOG_FILE"; $SUDO rm -f "$PREPARING_FILE" ; exit 1; } - - printf "${YELLOW}Preparing update...${NC}\n" >> "$LOG_FILE" - cd "$BBX_NEW_DIR/BrowserBox" - export BBX_NO_COPY=1 - yes yes | ./deploy-scripts/global_install.sh "$BBX_HOSTNAME" "$EMAIL" >> "$LOG_FILE" 2>&1 || { printf "${RED}Failed to run global_install.sh${NC}\n" >> "$LOG_FILE"; $SUDO rm -f "$PREPARING_FILE" ; exit 1; } # Mark as prepared (record the exact git tag) printf "${YELLOW}Marking update as prepared...${NC}\n" >> "$LOG_FILE" - printf "%s\n%s\n%s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$BBX_NEW_DIR" "$repo_tag" | $SUDO tee "$PREPARED_FILE" >/dev/null || { printf "${RED}Failed to create $PREPARED_FILE${NC}\n" >> "$LOG_FILE"; $SUDO rm -f "$PREPARING_FILE" ; exit 1; } + printf "%s\n%s\n%s\n" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$temp_binary" "$repo_tag" | $SUDO tee "$PREPARED_FILE" >/dev/null || { printf "${RED}Failed to create $PREPARED_FILE${NC}\n" >> "$LOG_FILE"; $SUDO rm -f "$PREPARING_FILE" ; exit 1; } # Remove preparing lock file printf "${YELLOW}Completing preparation step (removing update lock)...${NC}\n" >> "$LOG_FILE" @@ -2600,19 +3188,19 @@ stop_user() { # Cancel existing 'at' jobs for this user existing_jobs=$(atq | awk '{print $1}') for job in $existing_jobs; do - if at -c "$job" | grep -q "stop_bbpro.*$user"; then + if at -c "$job" | grep -q "browserbox --stop.*$user"; then atrm "$job" fi done - # Schedule stop_bbpro - echo "$SUDO -u \"$user\" stop_bbpro" | at now + "${delay_minutes}" minutes 2>/dev/null + # Schedule browserbox --stop + echo "$SUDO -u \"$user\" browserbox --stop" | at now + "${delay_minutes}" minutes 2>/dev/null # Update expiry time local new_expiry_timestamp=$((current_time + delay_seconds)) $SUDO -u "$user" bash -c "mkdir -p \"${home_dir}/.config/dosyago/bbpro\"; echo \"$new_expiry_timestamp\" > \"$expiry_file\"" printf "${GREEN}Scheduled stop for $user at $new_expiry_timestamp${NC}\n" else # Immediate stop - $SUDO -u "$user" bash -c "PATH=/usr/local/bin:\$PATH stop_bbpro" 2>/dev/null || { printf "${RED}Failed to stop BrowserBox for $user${NC}\n"; exit 1; } + $SUDO -u "$user" bash -c "PATH=/usr/local/bin:\$PATH browserbox --stop" 2>/dev/null || { printf "${RED}Failed to stop BrowserBox for $user${NC}\n"; exit 1; } printf "${GREEN}BrowserBox stopped for $user${NC}\n" fi @@ -2786,8 +3374,8 @@ run_as() { # Generate fresh token TOKEN=$(openssl rand -hex 16) - # Run setup_bbpro with explicit PATH and fresh token, redirecting output as the target user - $SUDO -u "$user" bash -c "PATH=/usr/local/bin:\$PATH LICENSE_KEY="${LICENSE_KEY}" setup_bbpro --port $port --token $TOKEN > ~/.config/dosyago/bbpro/setup_output.txt 2>&1" || { printf "${RED}Setup failed for $user${NC}\n"; $SUDO cat "$HOME_DIR/.config/dosyago/bbpro/setup_output.txt"; exit 1; } + # Run browserbox --setup with explicit PATH and fresh token, redirecting output as the target user + $SUDO -u "$user" bash -c "PATH=/usr/local/bin:\$PATH LICENSE_KEY="${LICENSE_KEY}" browserbox --setup --port $port --token $TOKEN > ~/.config/dosyago/bbpro/setup_output.txt 2>&1" || { printf "${RED}Setup failed for $user${NC}\n"; $SUDO cat "$HOME_DIR/.config/dosyago/bbpro/setup_output.txt"; exit 1; } # Use caller's LICENSE_KEY if [ -z "$LICENSE_KEY" ]; then @@ -2811,43 +3399,230 @@ run_as() { save_config } +# Helper: Test if an IP is reachable for Win9x +test_ip_connectivity() { + local ip=$1 + local port=$2 + local timeout_seconds=2 + # Use curl with a short timeout to test HTTP connectivity + if timeout "$timeout_seconds" curl --silent --connect-timeout "$timeout_seconds" "http://$ip:$port" &>/dev/null; then + return 0 + fi + return 1 +} + +# Helper: Find best LAN IP for Win9x compatibility +find_best_ip() { + local port="$1" + + # Get all IPv4 addresses, excluding loopback initially + local ips="" + if command -v ip >/dev/null 2>&1; then + # Linux: Use `ip addr show` to get IPv4 addresses + ips=$(ip addr show | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1 | grep -v '^127\.0\.0\.1') + elif command -v ifconfig >/dev/null 2>&1; then + # macOS/BSD: Use `ifconfig` to get IPv4 addresses + ips=$(ifconfig | grep 'inet ' | awk '{print $2}' | grep -v '^127\.0\.0\.1') + fi + + # Append 127.0.0.1 last to prioritize LAN IPs + ips="$ips 127.0.0.1" + + # Test each IP and use the first one that works + local best_ip="" + for ip in $ips; do + if test_ip_connectivity "$ip" "$port"; then + best_ip="$ip" + break + fi + done + + # If no connectable IP found, return the first non-loopback or fallback to localhost + if [[ -z "$best_ip" ]]; then + for ip in $ips; do + if [[ "$ip" != "127.0.0.1" ]]; then + best_ip="$ip" + break + fi + done + [[ -z "$best_ip" ]] && best_ip="127.0.0.1" + fi + + echo "$best_ip" +} + +# Helper: Show animated Windows flag +show_win9x_flag() { + # Windows flag colors: red, green, blue, yellow + cat </dev/null || { printf "${RED}Setup failed${NC}\n"; exit 1; } + + # Reload config to get updated values + source "${BB_CONFIG_DIR}/test.env" && PORT="${APP_PORT:-$PORT}" && TOKEN="${LOGIN_TOKEN:-$TOKEN}" || { printf "${YELLOW}Warning: test.env not found${NC}\n"; } + + # Validate license key + export LICENSE_KEY + certout="$(bash -c "export LICENSE_KEY=\"$LICENSE_KEY\"; bbcertify 2>&1")" + if [[ "$?" -ne 0 ]]; then + printf "${RED}License key invalid or missing. Run 'bbx activate' or go to dosaygo.com to get a valid key.${NC}\n" + echo "Certification output: $certout" + exit 1 + else + printf "${GREEN}Certification complete.${NC}\n" + if [[ -f "$CERT_META_FILE" ]]; then + # shellcheck disable=SC1090 + source "$CERT_META_FILE" + export BBX_RESERVATION_CODE BBX_RESERVED_SEAT_ID BBX_TICKET_ID BBX_TICKET_SLOT + fi + fi + + # Start browserbox in background, redirecting output to suppress banner + printf "${YELLOW}Starting BrowserBox server (silent mode)...${NC}\n" + export WIN9X_COMPATIBILITY_MODE="true" + export BBX_DONT_KILL_CHROME_ON_STOP="true" + nohup browserbox > /dev/null 2>&1 & + + # Wait for server to start (allows time for initialization and login.link generation) + local startup_wait=8 + printf "${YELLOW}Waiting for Win9x Compatibility server to initialize...${NC}\n" + sleep "$startup_wait" + + # Extract login link and modify for Win9x + local login_link="" + if [[ -f "${BB_CONFIG_DIR}/login.link" ]]; then + login_link=$(cat "${BB_CONFIG_DIR}/login.link") + else + login_link="http://$BBX_HOSTNAME:$PORT/login?token=$TOKEN" + fi + + # Extract path and replace /login with /win9x in one operation + local rest="${login_link#*://*/}" + rest="/${rest//login?/win9x/?}" + + # Find best IP for Win9x compatibility + printf "${YELLOW}Detecting best LAN IP address...${NC}\n" + local best_ip=$(find_best_ip "$PORT") + + if [[ -z "$best_ip" ]]; then + printf "${RED}Could not find a connectable IP address on port $PORT${NC}\n" + best_ip="127.0.0.1" + fi + + # Generate Win9x compatibility login link (HTTP, not HTTPS) + local win9x_link="http://${best_ip}:${PORT}${rest}" + + echo "$best_ip" > "${BB_CONFIG_DIR}/win9x.best.ip" + + printf "${GREEN}BrowserBox Win9x Compatibility Mode is running!${NC}\n" + draw_box "Win9x Login Link: $win9x_link" + printf "\n${YELLOW}Note: This link uses HTTP (not HTTPS) and /win9x/ path for legacy browser compatibility.${NC}\n" + printf "${YELLOW}Use this link from your Windows 9x machine with Internet Explorer 4+.${NC}\n\n" +} + version() { - printf "${GREEN}bbx version ${BBX_VERSION}${NC}\n" + printf "${GREEN}bbx version ${VERSION}${NC}\n" } usage() { banner - printf "${BLUE}\t\t\t\t Welcome to the ${CYAN}bbx${BLUE} CLI tool for BrowserBox!${NC}\n" - printf "\n" - printf "${BOLD}Usage:${NC}\n\t\t ${BOLD}bbx ${NC} [options]\n" - printf "\n" - printf "${BOLD}Commands:${NC}\n" - printf "\n" - printf " ${GREEN}install${NC} Install BrowserBox + ${BOLD}bbx${NC} CLI\n" - printf " ${GREEN}uninstall${NC} Remove everything\n" - printf " ${CYAN}activate${NC} Purchase a license\t\t\t${BOLD}${CYAN}bbx activate [number of people]${NC}\n" - printf " ${GREEN}setup${NC} Configure options \t\t\t${BOLD}bbx setup [--port|-p

] [--hostname|-h ] [--token|-t ] [--zeta|-z]${NC}\n" - printf "\n" - printf " ${BOLD}\t\t setup options:${NC}\n" - printf " \t ${GREEN}--zeta, -z${NC} Expose each service as a unique hostname. Useful for nginx,\n" - printf " \t ngrok, similar layers, or standard HTTP/S ports. Expects hosts.env\n" - printf " ${GREEN}certify${NC} Check your license\n" - printf " ${GREEN}run${NC} Run BrowserBox \t\t\t${BOLD}bbx run [--port|-p ] [--hostname|-h ]${NC}\n" - printf " ${GREEN}stop${NC} Stop BrowserBox (current user)\n" - printf " ${GREEN}run-as${NC} Run as a specific user \t\t${BOLD}bbx run-as [--temporary] [username] [port]${NC}\n" - printf " ${GREEN}stop-user${NC} Stop BrowserBox for a specific user \t${BOLD}bbx stop-user [delay_seconds]${NC}\n" - printf " ${GREEN}logs${NC} Show BrowserBox logs\n" - printf " ${GREEN}update${NC} Update BrowserBox \t\t${BOLD}bbx update [|--latest-rc]${NC}\n" - printf " ${GREEN}status${NC} Check BrowserBox status\n" - printf " ${PURPLE}tor-run${NC} Run BrowserBox on Tor \t\t${BOLD}bbx tor-run [--no-darkweb] [--no-onion]${NC}\n" - printf " ${BLUE}zt-run${NC} Run BrowserBox with ZeroTier tunnel\t${BOLD}bbx zt-run${NC}\n" - printf " ${GREEN}docker-run${NC} Run BrowserBox using Docker \t\t${BOLD}bbx docker-run [nickname] [--port|-p ]${NC}\n" - printf " ${GREEN}docker-stop${NC} Stop a Dockerized BrowserBox \t\t${BOLD}bbx docker-stop ${NC}\n" - printf " ${BLUE}${BOLD}automate${NC} *Drive with script, MCP or REPL\n" - printf " ${GREEN}ng-run${NC} Run BrowserBox with Nginx proxy\t${BOLD}bbx ng-run${NC}\n" - printf " ${GREEN}--version${NC} Show version\n" - printf " ${GREEN}--help${NC} Show this help\n" - printf "\n${BLUE}${BOLD}*automate coming soon${NC}\n\n" + printf "${BLUE}\t\t\t\t Welcome to the ${CYAN}bbx${BLUE} CLI for BrowserBox!${NC}\n\n" + printf "${BOLD}Usage:${NC}\n" + printf " bbx [options]\n\n" + + printf "${BOLD}SETUP & MANAGEMENT${NC}\n" + printf " ${GREEN}install${NC} Install BrowserBox and this CLI.\n" + printf " ${GREEN}uninstall${NC} Remove all BrowserBox components.\n" + printf " ${GREEN}setup${NC} Configure core options. ${BOLD}bbx setup [--port|-p

] [--hostname|-h ] [--token|-t ] [--zeta|-z]${NC}\n" + printf " ${CYAN}activate${NC} Activate a license for more users. ${BOLD}bbx activate [number_of_users]${NC}\n" + printf " ${GREEN}certify${NC} Validate your current license status.\n" + printf " ${GREEN}update${NC} Update BrowserBox to a specific or latest version. ${BOLD}bbx update [|--latest-rc]${NC}\n" + printf " ${GREEN}status${NC} Check the running status of BrowserBox.\n" + printf " ${GREEN}logs${NC} View the logs for the BrowserBox service.\n\n" + + printf "${BOLD}CORE ACTIONS${NC}\n" + printf " ${GREEN}start${NC} Start BrowserBox for the current user. ${BOLD}bbx start [--port|-p ] [--hostname|-h ]${NC}\n" + printf " ${GREEN}stop${NC} Stop the BrowserBox instance for the current user.\n" + printf " ${GREEN}start-as${NC} Run a new instance as a different OS user. ${BOLD}bbx start-as [--temporary] [username] [port]${NC}\n" + printf " ${GREEN}stop-user${NC} Stop a BrowserBox instance for a specific user. ${BOLD}bbx stop-user [delay_seconds]${NC}\n\n" + + printf "${BOLD}ADVANCED RUNNERS & TUNNELS${NC}\n" + printf " ${GREEN}docker-start${NC} Run BrowserBox inside a Docker container. ${BOLD}bbx docker-start [nickname] [--port|-p ]${NC}\n" + printf " ${GREEN}docker-stop${NC} Stop a Dockerized BrowserBox instance. ${BOLD}bbx docker-stop ${NC}\n" + printf " ${CYAN}cf-start${NC} Run BrowserBox securely through a Cloudflare tunnel. ${BOLD}bbx cf-start [--port|-p ]${NC}\n" + printf " ${BLUE}zt-start${NC} Expose BrowserBox on your ZeroTier network.\n" + printf " ${PURPLE}tor-start${NC} Serve BrowserBox as a Tor hidden service. ${BOLD}bbx tor-start [--no-darkweb] [--no-onion]${NC}\n" + printf " ${GREEN}ng-start${NC} Proxy BrowserBox with Nginx.\n" + printf " ${YELLOW}win9x-start${NC} Run in Windows 9x compatibility mode.\n\n" + + printf "${BOLD}OTHER COMMANDS${NC}\n" + printf " ${BLUE}${BOLD}automate${NC} Drive BrowserBox with scripts (coming soon).\n" + printf " ${GREEN}--faq${NC} Display frequently asked questions.\n" + printf " ${GREEN}--version${NC} Show the version of the bbx CLI.\n" + printf " ${GREEN}--help${NC} Show this help screen.\n\n" + printf " ${NC}bbx CLI version ${VERSION} | © DOSAYGO Corp 2018-2025${NC}\n\n" +} + +faq() { + printf "${BOLD}${CYAN}BrowserBox CLI - Frequently Asked Questions${NC}\n\n" + + printf "${BOLD}1) How do I run multiple BrowserBox instances?${NC}\n" + printf " First, ensure your license has enough seats for multiple users. You can then either:\n" + printf " a) Use the ${GREEN}start-as${NC} command to run a new instance under a different OS user.\n" + printf " Example: ${BOLD}bbx start-as browserbox_user2 8081${NC}\n" + printf " b) Use the ${GREEN}docker-start${NC} command to run isolated instances in containers.\n" + printf " Example: ${BOLD}bbx docker-start team_a --port 8090${NC}\n" + printf " ${YELLOW}Important:${NC} Make sure the ports you specify for each instance are unique and do not overlap.\n\n" + + # Add more FAQs here as needed + # printf "${BOLD}2) Another question?${NC}\n" + # printf " Answer to the other question.\n\n" } check_agreement() { @@ -2858,7 +3633,7 @@ check_agreement() { return 0 fi if [ ! -f "$BB_CONFIG_DIR/.agreed" ]; then - printf "${BLUE}BrowserBox v13 Terms:${NC} https://dosaygo.com/terms.txt\n" + printf "${BLUE}BrowserBox v15 Terms:${NC} https://dosaygo.com/terms.txt\n" printf "${BLUE}License:${NC} $REPO_URL/blob/${branch}/LICENSE.md\n" printf "${BLUE}Privacy:${NC} https://dosaygo.com/privacy.txt\n" read -r -p " Agree? (yes/no): " AGREE @@ -2905,7 +3680,7 @@ activate() { trap 'printf "\nInterrupted\n"; exit 1' INT TERM - while [ $attempts -lt $max_awatts ]; do + while [ $attempts -lt $max_attempts ]; do if [ $((counter % spinner_interval)) -eq 0 ]; then spinner_idx=$(( (spinner_idx + 1) % 4 )) local spinner="${spinner_chars:$spinner_idx:1}" @@ -2964,7 +3739,8 @@ case "$1" in uninstall) shift 1; uninstall "$@";; setup) shift 1; setup "$@";; certify) shift 1; certify "$@";; - run) shift 1; run "$@";; + run|start) shift 1; run "$@";; + restart) shift 1; restart "$@";; stop) shift 1; stop "$@";; stop-user) shift 1; stop_user "$@";; logs) shift 1; logs "$@";; @@ -2972,14 +3748,17 @@ case "$1" in update-background) shift 1; update_background "$@";; activate) shift 1; activate "$@";; status) shift 1; status "$@";; - run-as) shift 1; run_as "$@";; - tor-run) shift 1; banner_color=$PURPLE; tor_run "$@";; - zt-run) shift 1; banner_color=$BLUE; zt_run "$@";; - ng-run) shift 1; banner_color=$GREEN; ng_run "$@";; - docker-run) shift 1; docker_run "$@";; + run-as|start-as) shift 1; run_as "$@";; + tor-run|tor-start) shift 1; banner_color=$PURPLE; tor_run "$@";; + zt-run|zt-start) shift 1; banner_color=$BLUE; zt_run "$@";; + cf-run|cf-start) shift 1; banner_color=$CYAN; cf_run "$@";; + ng-run|ng-start) shift 1; banner_color=$GREEN; ng_run "$@";; + docker-run|docker-start) shift 1; docker_run "$@";; docker-stop) shift 1; docker_stop "$@";; + win9x-run|win9x-start) shift 1; banner_color=$YELLOW; win9x_run "$@";; --version|-v) shift 1; version "$@";; --help|-h) shift 1; usage "$@";; + --faq) shift 1; faq "$@";; "") usage;; *) printf "${RED}Unknown command: $1${NC}\n"; usage; exit 1;; esac diff --git a/windows-scripts/bbx.ps1 b/windows-scripts/bbx.ps1 index 54ca0e8d0..710a14adc 100755 --- a/windows-scripts/bbx.ps1 +++ b/windows-scripts/bbx.ps1 @@ -1,6 +1,8 @@ -# bbx.ps1 +# bbx.ps1 - BrowserBox Binary Installer & Wrapper for Windows +# This script downloads and runs pre-compiled BrowserBox binaries +# from the public BrowserBox/BrowserBox repository. -[CmdletBinding(SupportsShouldProcess=$true)] +[CmdletBinding()] param ( [Parameter(Position=0)] [string]$Command, @@ -8,119 +10,223 @@ param ( [string[]]$Args ) -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$installDir = "C:\Program Files\browserbox" +$ErrorActionPreference = "Stop" -$commands = @{ - "install" = "install.ps1" - "uninstall" = "uninstall.ps1" - "setup" = "setup.ps1" - "run" = "start.ps1" - "certify" = "certify.ps1" - "stop" = "stop.ps1" +# Configuration +$PublicRepo = "BrowserBox/BrowserBox" +$BinaryDir = "$env:LOCALAPPDATA\browserbox\bin" +$BinaryName = "browserbox.exe" +$BinaryPath = Join-Path $BinaryDir $BinaryName + +# Function to ensure binary directory exists +function Ensure-BinaryDir { + if (-not (Test-Path $BinaryDir)) { + New-Item -ItemType Directory -Path $BinaryDir -Force | Out-Null + } } -Write-Verbose "Script dir: $scriptDir" -Write-Verbose "Install dir: $installDir" -Write-Verbose "Command: $Command" -Write-Verbose "Args: $($Args -join ', ')" +# Function to get the latest release tag from GitHub +function Get-LatestRelease { + param([string]$Repo) + + $apiUrl = "https://api.github.com/repos/$Repo/releases/latest" + + try { + $response = Invoke-RestMethod -Uri $apiUrl -TimeoutSec 10 -ErrorAction Stop + return $response.tag_name + } + catch { + Write-Error "Failed to fetch latest release from $Repo : $_" + exit 1 + } +} -function Show-Help { - Write-Host "bbx CLI (Windows)" -ForegroundColor Green - Write-Host "Usage: bbx [options]" -ForegroundColor Yellow - Write-Host "Commands:" -ForegroundColor Cyan - $commandDescriptions = @{ - "install" = "Install BrowserBox and bbx CLI`n bbx install" - "uninstall" = "Remove BrowserBox and related files`n bbx uninstall [-Force]" - "setup" = "Set up BrowserBox`n bbx setup [-Hostname ] [-Email ] [-Port ] [-Token ] [-Force]" - "run" = "Run BrowserBox`n bbx run [-Hostname ] [-Port ] [-Token ] [-Email ]" - "certify" = "Certify your license`n bbx certify [-ForceLicense] [-NoReservation] [-LicenseKey ]" - "stop" = "Stop BrowserBox`n bbx stop [-GraceSeconds ]" - "revalidate" = "Clears ticket and revalidates`n bbx revalidate" +# Function to download the binary +function Download-Binary { + param( + [string]$Tag + ) + + Ensure-BinaryDir + + $assetName = "browserbox.exe" + $downloadUrl = "https://github.com/$PublicRepo/releases/download/$Tag/$assetName" + $tempFile = "$BinaryPath.tmp" + + Write-Host "Downloading BrowserBox $Tag for Windows..." -ForegroundColor Cyan + + try { + # Use .NET WebClient for progress bar + $webClient = New-Object System.Net.WebClient + $webClient.DownloadFile($downloadUrl, $tempFile) + $webClient.Dispose() + + # Move to final location + if (Test-Path $BinaryPath) { + Remove-Item $BinaryPath -Force + } + Move-Item $tempFile $BinaryPath -Force + + Write-Host "Successfully downloaded and installed BrowserBox binary" -ForegroundColor Green } - $commands.Keys + "revalidate" | Sort-Object | ForEach-Object { - Write-Host " $_" -ForegroundColor White - Write-Host " $($commandDescriptions[$_])" -ForegroundColor Gray + catch { + Write-Error "Failed to download binary from $downloadUrl : $_" + if (Test-Path $tempFile) { + Remove-Item $tempFile -Force + } + exit 1 } - Write-Host "Run 'bbx -help' for command-specific options." -ForegroundColor Gray } -if (-not $Command -or $Command -eq "-help") { - Write-Verbose "No command or -help specified‚Äîshowing help" - Show-Help - return +# Function to check if binary exists +function Test-BinaryExists { + Test-Path $BinaryPath } -if ($commands.ContainsKey($Command)) { - $scriptPath = Join-Path $scriptDir $commands[$Command] - Write-Verbose "Script path: $scriptPath" - if ((Test-Path "$scriptPath") -or ($Command -eq "revalidate")) { - Write-Host "Running bbx $Command..." -ForegroundColor Cyan - if ($Command -eq "revalidate") { - $ticketPath = Join-Path $env:USERPROFILE ".config\dosyago\bbpro\tickets\ticket.json" - Write-Verbose "Ticket path: $ticketPath" - if ($Args -contains "-help") { - Write-Host "bbx revalidate" -ForegroundColor Green - Write-Host "Clears ticket and revalidates license" -ForegroundColor Yellow - Write-Host "Usage: bbx revalidate" -ForegroundColor Cyan - Write-Host "Options: None" -ForegroundColor Cyan - return - } - if (-not (Test-Path (Split-Path $ticketPath))) { - Write-Warning "Ticket directory does not exist at $(Split-Path $ticketPath)" - return - } - if (Test-Path $ticketPath) { - Write-Host "Removing ticket.json..." -ForegroundColor Cyan - if ($PSCmdlet.ShouldProcess($ticketPath, "Remove file")) { - Remove-Item $ticketPath -Force - Write-Host "ticket.json removed." -ForegroundColor Green - } - } else { - Write-Verbose "ticket.json does not exist at $ticketPath" - } +# Function to ensure binary is installed +function Ensure-Binary { + if (-not (Test-BinaryExists)) { + Write-Host "BrowserBox binary not found. Installing..." -ForegroundColor Yellow + Ensure-BinaryDir + $tag = Get-LatestRelease -Repo $PublicRepo + Download-Binary -Tag $tag + } +} + +function Get-SemverFromText { + param([string]$Text) + $regex = '(?im)(v?\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z\.-]+)?)' + $match = [regex]::Match($Text, $regex) + if ($match.Success) { return $match.Groups[1].Value } + return $null +} + +# Function to get binary version +function Get-BinaryVersion { + if (Test-BinaryExists) { + try { + $output = & $BinaryPath "--version" 2>$null | Out-String + $semver = Get-SemverFromText -Text $output + if ($semver) { return $semver } else { return "unknown" } + } + catch { + return "unknown" + } + } + return "not_installed" +} + +# Function to check for updates +function Check-Update { + if (Test-BinaryExists) { + $currentVersion = Get-BinaryVersion + + if ($currentVersion -eq "unknown" -or $currentVersion -eq "not_installed") { return } - if ($Args -and $Args.Count -gt 0) { - Write-Verbose "Parsing args: $($Args -join ', ')" - if ($Args -contains "-help") { - Write-Verbose "Passing -help to $scriptPath" - & $scriptPath -help - return + + try { + $latestTag = Get-LatestRelease -Repo $PublicRepo + $latestNorm = $latestTag -replace '^[vV]' + $currentNorm = $currentVersion -replace '^[vV]' + if ($latestNorm -and $currentNorm -and $latestNorm -ne $currentNorm) { + Write-Host "Note: A new version of BrowserBox is available: $latestTag" -ForegroundColor Yellow + Write-Host " Run 'bbx install' to update." } - $params = @{} - for ($i = 0; $i -lt $Args.Length; $i++) { - Write-Verbose "Processing arg ${i}: $($Args[$i])" - if ($Args[$i] -match '^-(.+)$') { - $paramName = $matches[1] - Write-Verbose "Found param: $paramName" - if ($i + 1 -lt $Args.Length -and $Args[$i + 1] -notmatch '^-.+') { - $params[$paramName] = $Args[$i + 1] - Write-Verbose "Assigned $paramName = $($Args[$i + 1])" - $i++ - } else { - $params[$paramName] = $true - Write-Verbose "Assigned $paramName = $true (flag)" - } - } - } - Write-Verbose "Params hashtable: $($params | Out-String)" - Write-Verbose "Invoking with params: & $scriptPath @params" - & $scriptPath @params - } else { - Write-Verbose "No args provided‚Äîinvoking bare: & $scriptPath" - & $scriptPath } - } else { - Write-Error "Script for '$Command' not found at $scriptPath" - if ($Command -eq "install") { - Write-Host "Try running 'irm bbx.dosaygo.com | iex' first." -ForegroundColor Yellow + catch { + # Silently ignore update check failures } - Show-Help - throw "bbx: $Command failed" } -} else { - Write-Error "Unknown command: $Command" +} + +# Function to show help +function Show-Help { + Write-Host "bbx CLI (Windows Binary Distribution)" -ForegroundColor Green + Write-Host "Usage: bbx [options]" -ForegroundColor Yellow + Write-Host "" + Write-Host "Core Commands:" -ForegroundColor Cyan + Write-Host " install Install BrowserBox binary and CLI" -ForegroundColor White + Write-Host " update Update BrowserBox to the latest version" -ForegroundColor White + Write-Host " --version, -v Show version information" -ForegroundColor White + Write-Host " --help, -h Show this help message" -ForegroundColor White + Write-Host " revalidate Clear ticket and revalidate license" -ForegroundColor White + Write-Host "" + Write-Host "All other commands are passed through to the browserbox binary." -ForegroundColor Gray + Write-Host "Run 'browserbox --help' after installation for full command list." -ForegroundColor Gray +} + +# Function to handle revalidate command +function Invoke-Revalidate { + $ticketPath = Join-Path $env:USERPROFILE ".config\dosyago\bbpro\tickets\ticket.json" + + if (-not (Test-Path (Split-Path $ticketPath))) { + Write-Warning "Ticket directory does not exist at $(Split-Path $ticketPath)" + return + } + + if (Test-Path $ticketPath) { + Write-Host "Removing ticket.json..." -ForegroundColor Cyan + Remove-Item $ticketPath -Force + Write-Host "ticket.json removed. License will be revalidated on next use." -ForegroundColor Green + } + else { + Write-Host "No ticket found at $ticketPath" -ForegroundColor Yellow + } +} + +# Main execution logic +if ($Command -eq "install" -or $Command -eq "update") { + # Install/update command explicitly downloads/updates the binary + $tag = Get-LatestRelease -Repo $PublicRepo + Download-Binary -Tag $tag + + if ($Command -eq "install") { + # Run the binary with --install flag + $installArgs = @("--install") + $Args + & $BinaryPath $installArgs + exit $LASTEXITCODE + } + else { + # For update, just report success + Write-Host "BrowserBox updated to $tag" -ForegroundColor Green + exit 0 + } +} +elseif ($Command -eq "--version" -or $Command -eq "-v") { + Ensure-Binary + & $BinaryPath "--version" + exit $LASTEXITCODE +} +elseif (-not $Command -or $Command -eq "-help" -or $Command -eq "--help" -or $Command -eq "-h") { Show-Help - throw "bbx: $Command failed" + exit 0 +} +elseif ($Command -eq "revalidate") { + Invoke-Revalidate + exit 0 +} +else { + # For all other commands, ensure binary exists and pass through + Ensure-Binary + + # Add binary directory to PATH for this session if not already there + if ($env:PATH -notlike "*$BinaryDir*") { + $env:PATH = "$BinaryDir;$env:PATH" + } + + # Optional: Check for updates (non-blocking) + if (-not $env:BBX_NO_UPDATE) { + try { + Check-Update + } + catch { + # Silently ignore + } + } + + # Pass all arguments through to the binary + $allArgs = @($Command) + $Args + & $BinaryPath $allArgs + exit $LASTEXITCODE } From 3682bf64ef67a203298bd2f346e03bca2bad3222 Mon Sep 17 00:00:00 2001 From: BrowserBox Release Bot Date: Sat, 6 Dec 2025 17:47:02 +0000 Subject: [PATCH 02/15] Update installer scripts for v15.3.11-draft - Update bbx.sh binary installer - Update bbx.ps1 Windows installer This commit is automatically generated by the release process. --- bbx.sh | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/bbx.sh b/bbx.sh index 1c7c66cd0..3c6457244 100755 --- a/bbx.sh +++ b/bbx.sh @@ -63,7 +63,7 @@ protecc_win_sysadmins() { # ==================================================================== # Configuration -PUBLIC_REPO="BrowserBox/BrowserBox" +PUBLIC_REPO="${BBX_RELEASE_REPO:-BrowserBox/BrowserBox}" BINARY_NAME="browserbox" GLOBAL_BIN_DIR="/usr/local/bin" SUDO_BIN="$(command -v sudo || true)" @@ -109,8 +109,12 @@ get_latest_release() { # Try using curl with GitHub API if command -v curl >/dev/null 2>&1; then local api_url="https://api.github.com/repos/${repo}/releases/latest" + local curl_auth=() + if [[ -n "${GH_TOKEN:-}" ]]; then + curl_auth=(-H "Authorization: token ${GH_TOKEN}") + fi local response - response=$(curl -sS --connect-timeout 10 "$api_url" 2>/dev/null || echo "") + response=$(curl -sS --connect-timeout 10 "${curl_auth[@]}" "$api_url" 2>/dev/null || echo "") if [[ -n "$response" ]]; then # Try jq first @@ -137,8 +141,8 @@ download_binary() { local tag="$2" local asset_name case "$platform" in - macos) asset_name="browserbox-macos-bin" ;; - linux) asset_name="browserbox-linux-bin" ;; + macos) asset_name="browserbox-macos-arm64" ;; + linux) asset_name="browserbox-linux-x64" ;; *) echo -e "${RED}Unsupported platform: $platform${NC}" >&2; exit 1 ;; esac local download_url="https://github.com/${PUBLIC_REPO}/releases/download/${tag}/${asset_name}" @@ -154,7 +158,12 @@ download_binary() { exit 1 fi - if ! curl -L --fail --progress-bar --connect-timeout 30 -o "$temp_file" "$download_url" 2>&1; then + local curl_auth=() + if [[ -n "${GH_TOKEN:-}" ]]; then + curl_auth=(-H "Authorization: token ${GH_TOKEN}") + fi + + if ! curl -L --fail --progress-bar --connect-timeout 30 "${curl_auth[@]}" -o "$temp_file" "$download_url" 2>&1; then echo -e "${RED}Failed to download binary from ${download_url}${NC}" >&2 echo -e "${YELLOW}This could mean:${NC}" >&2 echo -e " - No release is available for ${platform}" >&2 @@ -1314,8 +1323,10 @@ install() { # Download and install the binary local platform platform=$(detect_platform) - local tag - tag=$(get_latest_release "$PUBLIC_REPO") + local tag="${BBX_RELEASE_TAG:-}" + if [[ -z "$tag" ]]; then + tag=$(get_latest_release "$PUBLIC_REPO") + fi download_binary "$platform" "$tag" # Get hostname and email for setup @@ -3762,4 +3773,3 @@ case "$1" in "") usage;; *) printf "${RED}Unknown command: $1${NC}\n"; usage; exit 1;; esac - From 87d0bfa228de1be14ccb2951d69fb7e2255b217d Mon Sep 17 00:00:00 2001 From: DOSAYGO Engineering Date: Wed, 17 Dec 2025 19:23:46 +0800 Subject: [PATCH 03/15] Align public CI to release-only triggers --- .github/workflows/basic-install.yaml | 262 -------- .github/workflows/bbx-saga.yaml | 202 +++--- .github/workflows/codeql-analysis.yml | 3 - .github/workflows/debug.yaml | 31 - .github/workflows/vpn.yaml | 925 -------------------------- 5 files changed, 78 insertions(+), 1345 deletions(-) delete mode 100644 .github/workflows/basic-install.yaml delete mode 100644 .github/workflows/debug.yaml delete mode 100644 .github/workflows/vpn.yaml diff --git a/.github/workflows/basic-install.yaml b/.github/workflows/basic-install.yaml deleted file mode 100644 index 9f78f6f5d..000000000 --- a/.github/workflows/basic-install.yaml +++ /dev/null @@ -1,262 +0,0 @@ -# Preamble -# Hello GitHub Folken - if this is violating the terms of service and you work for GitHub, -# please let me know and I will remove this action ASAP! -# to make any automated removal easy I add the following pseudo key metadata -# _meta_action_id: dosyago/BrowserBox/tech-prototype/july-2023 -# Do not remove this preamble -# Purpose -# Run BrowserBox on GitHub Actions Runner to Integration Test -# This is intended as a useful technology prototype showing the ability to run the BrowserBox remote browser -# on GitHub Actions Runners. This makes it easy to see if the application is working correctly, and to test -# any modifications you make. This is not an endorsement or encouragement to utilize GitHub Actions in a way that violates -# the terms of service, as using this in excess may do that. Don't abuse the power and benevolence of the platform. -# -# Note: This workflow uses the porcelain installer (bbx.sh.dosaygo.com) to fetch and install the -# pre-built binary distribution of BrowserBox, rather than building from source. -# End preamble - -name: BrowserBox Basic Install Test - -on: - push: - branches: - - main - workflow_dispatch: - -concurrency: - group: ${{ github.repository }}-basic-install - cancel-in-progress: true - -jobs: - build: - name: browserbox basic install - continue-on-error: ${{ matrix.os == 'windows-latest' }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest] - container_image: - - '' # No container (native runner) - - 'dokken/centos-stream-9' - - 'debian:latest' - exclude: - - os: macos-latest # macOS runners don’t support containers - container_image: 'dokken/centos-stream-9' - - os: macos-latest - container_image: 'debian:latest' - runs-on: ${{ matrix.os }} - timeout-minutes: 20 - container: ${{ matrix.container_image }} - - steps: - # Check if actor is repository owner or me - - name: Check if actor is repository owner or me - run: | - if [[ "${{ github.actor }}" != "crisdosaygo" ]]; then - echo "Actor is not me. Not running CI" - exit 1 - fi - - # Install dependencies (Ubuntu Native) - - name: Install dependencies (Ubuntu Native) - if: matrix.os == 'ubuntu-latest' && matrix.container_image == '' - run: | - sudo apt-get update -y - sudo apt-get install -y curl - - # Install dependencies (Debian Container) - - name: Install dependencies (Debian Container) - if: matrix.container_image == 'debian:latest' - run: | - apt-get update -y - apt-get install -y curl - - # Install dependencies (CentOS Container) - - name: Install dependencies (CentOS Container) - if: matrix.container_image == 'dokken/centos-stream-9' - run: | - dnf update -y - dnf install -y curl - - # Install ngrok (cross-platform, with conditional setup for containers) - - name: Install ngrok - shell: /bin/bash {0} - run: | - if [[ "${{ matrix.container_image }}" != "" ]]; then - # Container case: Install nvm and Node.js - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # Load nvm - nvm install 22 - nvm use 22 - nvm alias default 22 - # Ensure npm is available and install ngrok - . "$NVM_DIR/nvm.sh" # Source nvm again for this shell - npm install -g ngrok - else - # Non-container case: Use pre-installed Node.js - npm install -g ngrok - fi - - # Configure ngrok - - name: Configure ngrok - shell: /bin/bash {0} - run: | - source $HOME/.nvm/nvm.sh - ngrok config add-authtoken ${{ secrets.NGROK_AUTH_TOKEN }} - env: - BBX_NO_UPDATE: "true" - NGROK_AUTH_TOKEN: ${{ secrets.NGROK_AUTH_TOKEN }} - - # Install BrowserBox using porcelain installer (fetches binary distribution) - - name: Install BrowserBox - shell: /bin/bash {0} - run: | - if [[ "${{ matrix.container_image }}" != "" ]]; then - # Container case: Install nvm and Node.js first - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm install 22 - nvm use 22 - nvm alias default 22 - fi - # Install BrowserBox using the porcelain installer script - if [[ "$(uname -s)" == "Darwin" ]]; then - brew install coreutils - fi - export BBX_HOSTNAME="localhost" - export EMAIL="test@example.com" - export LICENSE_KEY="${{ secrets.BB_LICENSE_KEY }}" - export BBX_TEST_AGREEMENT="true" - yes | timeout -k 10s 10m bash <(curl -sSL https://bbx.sh.dosaygo.com) install --latest-rc - - # Configure application - - name: Configure application - id: setup - shell: /bin/bash {0} - run: | - source $HOME/.nvm/nvm.sh || true - output=$(bbx setup --port 8080 --hostname localhost) - echo "::set-output name=suffix::${output#*https://localhost:8080}" - env: - BBX_NO_UPDATE: "true" - LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} - - # Start server & ngrok tunnel - - name: Start server & ngrok tunnel - shell: /bin/bash {0} - run: | - source $HOME/.nvm/nvm.sh - export STATUS_MODE=${{ secrets.STATUS_MODE_KEY }} - export BB_QUICK_EXIT="whatever" - export LICENSE_KEY=${{ secrets.BB_LICENSE_KEY }} - bbx certify - bbx run & - sleep 5 - ngrok http https://localhost:8080 & - sleep 5 - - # Get ngrok public URL - - name: Get ngrok public URL - id: ngrok - shell: /bin/bash {0} - run: | - url=$(curl http://localhost:4040/api/tunnels | jq -r '.tunnels[0].public_url') - suffix=${{ steps.setup.outputs.suffix }} - complete_url="${url}${suffix}" - echo "::set-output name=url::$complete_url" - echo $complete_url - - # Check application with retry - - name: Check application with retry - shell: /bin/bash {0} - run: | - source $HOME/.nvm/nvm.sh || true - base_url="https://localhost:8080" - token=$(bbx setup --port 8080 --hostname localhost | grep -oE 'token=[^&]+' | sed 's/token=//') - check_url="${base_url}/?session_token=${token}" - max_time=60 # Maximum total time in seconds - timeout=5 # Timeout per attempt in seconds - interval=2 # Interval between attempts in seconds - start_time=$(date +%s) - success=0 - - while [ $(( $(date +%s) - $start_time )) -lt $max_time ]; do - echo "Attempting to check application..." - curl -k -L -s --max-time $timeout "$check_url" - if [ $? -eq 0 ]; then - success=1 - break - fi - echo "Attempt failed, retrying in $interval seconds..." - sleep $interval - done - - if [ $success -eq 0 ]; then - echo "Error: Application check failed after $max_time seconds" - bbx stop - sleep 10 - exit 1 - fi - echo "Application check succeeded" - env: - BBX_NO_UPDATE: "true" - LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} - - # Print ngrok URL - - name: Print ngrok URL - run: echo "The complete ngrok URL is ${{ steps.ngrok.outputs.url }}" - - # Keep alive - - name: Keep alive - run: | - sleep 30 - - # Final check with retry - - name: Final check with retry - shell: /bin/bash {0} - run: | - source $HOME/.nvm/nvm.sh || true - base_url="https://localhost:8080" - token=$(bbx setup --port 8080 --hostname localhost | grep -oE 'token=[^&]+' | sed 's/token=//') - check_url="${base_url}/?session_token=${token}" - max_time=60 # Maximum total time in seconds - timeout=5 # Timeout per attempt in seconds - interval=2 # Interval between attempts in seconds - start_time=$(date +%s) - success=0 - - while [ $(( $(date +%s) - $start_time )) -lt $max_time ]; do - echo "Attempting final check..." - curl -k -L -s --max-time $timeout "$check_url" - if [ $? -eq 0 ]; then - success=1 - break - fi - echo "Attempt failed, retrying in $interval seconds..." - sleep $interval - done - - if [ $success -eq 0 ]; then - echo "Error: Final application check failed after $max_time seconds" - bbx stop - sleep 10 - exit 1 - fi - echo "Final check succeeded" - bbx stop - sleep 10 - exit 0 - env: - BBX_NO_UPDATE: "true" - LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} - - # Cleanup - - name: Cleanup - if: always() - shell: /bin/bash {0} - run: | - source $HOME/.nvm/nvm.sh || true - bbx stop || true - sleep 5 diff --git a/.github/workflows/bbx-saga.yaml b/.github/workflows/bbx-saga.yaml index f18c52780..124978847 100644 --- a/.github/workflows/bbx-saga.yaml +++ b/.github/workflows/bbx-saga.yaml @@ -1,104 +1,106 @@ -# Note: This workflow uses the porcelain installer (bbx.sh.dosaygo.com for Unix/macOS, -# bbx.dosaygo.com for Windows) to fetch and install the pre-built binary distribution -# of BrowserBox, rather than building from source. - -name: bbx Saga Test Suite +name: bbx Saga Test Suite (Public Release) on: - push: - branches: - - main + release: + types: [published] workflow_dispatch: + inputs: + release_tag: + description: "Tag to test (defaults to release event tag)" + required: false + default: "" + release_repo: + description: "Repository that hosts the binaries" + required: false + default: "BrowserBox/BrowserBox" concurrency: group: ${{ github.repository }}-bbx-saga cancel-in-progress: true +permissions: + contents: read # required for checkout + actions: read # required for workflow_run context if used later + +env: + RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release_tag || '' }} + TARGET_RELEASE_REPO: ${{ github.event.inputs.release_repo || 'BrowserBox/BrowserBox' }} + jobs: build: + name: Test on ${{ matrix.os }}${{ matrix.container_image && format(' ({0})', matrix.container_image) || '' }} + if: github.event_name != 'release' || (github.event.release.draft == false) continue-on-error: ${{ matrix.os == 'windows-latest' }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] container_image: - - '' # No container (native runner) - - 'dokken/centos-stream-9' + - '' # Native runner + - 'dokken/centos-stream-10' - 'debian:latest' exclude: - os: macos-latest - container_image: 'dokken/centos-stream-9' + container_image: 'dokken/centos-stream-10' - os: macos-latest container_image: 'debian:latest' - os: windows-latest - container_image: 'dokken/centos-stream-9' + container_image: 'dokken/centos-stream-10' - os: windows-latest container_image: 'debian:latest' runs-on: ${{ matrix.os }} - timeout-minutes: 20 + timeout-minutes: 12 container: ${{ matrix.container_image }} + steps: - - name: Check if actor is repository owner or me + - name: Resolve release tag shell: bash run: | - if [[ "${{ github.actor }}" != "crisdosaygo" ]]; then - echo "Actor is not me. Not running CI" + tag="${RELEASE_TAG}" + repo="${TARGET_RELEASE_REPO}" + if [[ -z "$tag" ]]; then + echo "RELEASE_TAG is required (from release event or input)" >&2 exit 1 fi + { + echo "BBX_RELEASE_TAG=$tag" + echo "BBX_RELEASE_REPO=$repo" + } >> "$GITHUB_ENV" + echo "Testing binaries from $repo@$tag" - - name: Install dependencies (Ubuntu Native) - if: matrix.os == 'ubuntu-latest' && matrix.container_image == '' - shell: bash - run: | - sudo apt-get update -y - sudo apt-get install -y curl + - name: Checkout repository + uses: actions/checkout@v4 - - name: Install dependencies (Debian Container) - if: matrix.container_image == 'debian:latest' - shell: bash - run: | - apt-get update -y - apt-get install -y curl - - - name: Install dependencies (CentOS Container) - if: matrix.container_image == 'dokken/centos-stream-9' - shell: bash - run: | - dnf update -y - dnf install -y curl - - - name: Setup NVM and Node.js (Containers) - if: matrix.os != 'windows-latest' && matrix.container_image != '' + - name: Prepare test script (Unix/macOS) + if: matrix.os != 'windows-latest' shell: bash - run: | - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" - nvm install 22 - nvm use 22 - nvm alias default 22 + run: chmod +x tests/test-bbx.sh - - name: Install BrowserBox (Unix/macOS) + - name: Install BrowserBox via binary (Unix/macOS) if: matrix.os != 'windows-latest' shell: bash env: - BBX_HOSTNAME: "localhost" + BBX_INSTALL_USER: "bbxuser" + BBX_TEST_AGREEMENT: "true" BBX_NO_UPDATE: "true" + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BBX_HOSTNAME: "localhost" + BB_QUICK_EXIT: "yesplease" EMAIL: "test@example.com" LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} - BBX_TEST_AGREEMENT: "true" + STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} + INSTALL_DOC_VIEWER: "false" + BBX_RELEASE_REPO: ${{ env.BBX_RELEASE_REPO }} + BBX_RELEASE_TAG: ${{ env.BBX_RELEASE_TAG }} run: | - # Install coreutils on macOS - if [[ "$(uname -s)" == "Darwin" ]]; then - brew install coreutils - fi - # Install BrowserBox using porcelain installer (fetches binary distribution) - yes | timeout -k 10s 10m bash <(curl -sSL https://bbx.sh.dosaygo.com) install --latest-rc + chmod +x ./bbx.sh + ./bbx.sh install - name: Execute BBX Test Saga (Unix/macOS) if: matrix.os != 'windows-latest' shell: bash env: + BBX_INSTALL_USER: "bbxuser" BBX_HOSTNAME: "localhost" BBX_NO_UPDATE: "true" BB_QUICK_EXIT: "yesplease" @@ -107,8 +109,10 @@ jobs: BBX_TEST_AGREEMENT: "true" STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} INSTALL_DOC_VIEWER: "false" + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BBX_RELEASE_REPO: ${{ env.BBX_RELEASE_REPO }} + BBX_RELEASE_TAG: ${{ env.BBX_RELEASE_TAG }} run: | - source $HOME/.nvm/nvm.sh || true [ -z "$BBX_HOSTNAME" ] && echo "BBX_HOSTNAME is not set" || echo "BBX_HOSTNAME is set" [ -z "$EMAIL" ] && echo "EMAIL is not set" || echo "EMAIL is set" [ -z "$LICENSE_KEY" ] && echo "LICENSE_KEY is not set" || echo "LICENSE_KEY is set" @@ -117,67 +121,24 @@ jobs: [ -z "$INSTALL_DOC_VIEWER" ] && echo "INSTALL_DOC_VIEWER is not set" || echo "INSTALL_DOC_VIEWER is set to $INSTALL_DOC_VIEWER" export BBX_NO_UPDATE=true export INSTALL_DOC_VIEWER STATUS_MODE BBX_TEST_AGREEMENT LICENSE_KEY EMAIL BBX_HOSTNAME BB_QUICK_EXIT - - # Run test sequence using bbx CLI (binary distribution) - echo "Setting up bbx..." - bbx setup --port 8080 --hostname localhost - - echo "Running bbx..." - bbx run & - sleep 10 - - # Get login link and test - login_link=$(cat "$HOME/.config/dosyago/bbpro/login.link" 2>/dev/null || echo "https://localhost:8080/login?token=") - echo "Testing login link: $login_link" - - # Test with curl (retry logic) - max_retries=10 - retry_count=0 - success=false - while [ $retry_count -lt $max_retries ] && [ "$success" = "false" ]; do - if curl -k -L -s --max-time 5 --head --fail "$login_link" > /dev/null 2>&1; then - echo "Connection successful" - success=true - else - echo "Retry $((retry_count + 1))/$max_retries failed" - retry_count=$((retry_count + 1)) - sleep 5 - fi - done - - if [ "$success" = "false" ]; then - echo "Failed to connect after $max_retries retries" - bbx stop - exit 1 - fi - - # Wait and test again - echo "Waiting 25 seconds to verify link stability..." - sleep 25 - - if curl -k -L -s --max-time 5 --head --fail "$login_link" > /dev/null 2>&1; then - echo "Second verification successful" - else - echo "Second verification failed" - bbx stop - exit 1 - fi - - # Stop services - bbx stop + ./tests/test-bbx.sh continue-on-error: false - name: Execute BBX Test Saga (Windows) if: matrix.os == 'windows-latest' shell: powershell env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BBX_NO_UPDATE: "true" + BBX_FORCE_CHROME_INSTALL: "true" BBX_HOSTNAME: "localhost" EMAIL: "test@example.com" BB_QUICK_EXIT: "surewhatevs" LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} BBX_TEST_AGREEMENT: "true" STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} + BBX_RELEASE_REPO: ${{ env.BBX_RELEASE_REPO }} + BBX_RELEASE_TAG: ${{ env.BBX_RELEASE_TAG }} run: | # Debug variables if (-not $env:BBX_HOSTNAME) { Write-Host "BBX_HOSTNAME is not set" } else { Write-Host "BBX_HOSTNAME is set" } @@ -185,9 +146,9 @@ jobs: if (-not $env:LICENSE_KEY) { Write-Host "LICENSE_KEY is not set" } else { Write-Host "LICENSE_KEY is set" } if (-not $env:BBX_TEST_AGREEMENT) { Write-Host "BBX_TEST_AGREEMENT is not set" } else { Write-Host "BBX_TEST_AGREEMENT is set" } if (-not $env:STATUS_MODE) { Write-Host "STATUS_MODE is not set" } else { Write-Host "STATUS_MODE is set" } - # Install BrowserBox - Invoke-RestMethod -Uri "https://bbx.dosaygo.com/?v=$((Get-date).Ticks)" -OutFile "install.ps1" - .\install.ps1 + if (-not $env:BBX_RELEASE_TAG) { Write-Error "BBX_RELEASE_TAG is required"; exit 1 } + # Install BrowserBox from checked-out repo + .\windows-scripts\bbx.ps1 install if (-not (Get-Command bbx -ErrorAction SilentlyContinue)) { Write-Error "bbx not found in PATH after install" exit 1 @@ -198,13 +159,10 @@ jobs: exit 1 } Write-Host "curl.exe installed successfully" - # Setup BrowserBox - bbx setup -Hostname "$env:BBX_HOSTNAME" -Email "$env:EMAIL" + bbx setup -Hostname "$env:BBX_HOSTNAME" -Email "$env:EMAIL" -Port 9999 $loginLink = Get-Content "$env:USERPROFILE\.config\dosyago\bbpro\login.link" Write-Host "Login link: $loginLink" - # Start BrowserBox - bbx run -Verbose - # Curl with retries (ignore cert errors) + bbx run Write-Host "Testing URL: $loginLink" $maxRetries = 10 $retryCount = 0 @@ -212,7 +170,7 @@ jobs: while ($retryCount -lt $maxRetries -and -not $success) { try { $response = curl.exe -k -L "$loginLink" -o NUL -w "%{http_code}" - if ($response -eq "200") { + if ($response -ne "000") { Write-Host "Initial connection successful: $response" $success = $true } else { @@ -221,18 +179,17 @@ jobs: } catch { Write-Host "Retry $($retryCount + 1)/$maxRetries failed: $_" } - if (-not $success) { Start-Sleep -Seconds 5; $retryCount++ } + if (-not $success) { Start-Sleep -Seconds 2; $retryCount++ } } if (-not $success) { Write-Error "Failed to connect to $loginLink after $maxRetries retries." exit 1 } - # Wait 25 seconds and verify again Write-Host "Waiting 25 seconds to verify link stability..." Start-Sleep -Seconds 25 try { $response = curl.exe -k -L "$loginLink" -o NUL -w "%{http_code}" - if ($response -eq "200") { + if ($response -ne "000") { Write-Host "Second verification successful: $response" } else { Write-Error "Second verification failed: HTTP $response" @@ -242,9 +199,7 @@ jobs: Write-Error "Second verification failed after 25s: $_" exit 1 } - # Stop services bbx stop - # Check no Node processes remain $nodeProcs = Get-Process -Name "browserbox", "browserbox-devtools" -ErrorAction SilentlyContinue if ($nodeProcs) { Write-Error "Node processes still running after stop: $($nodeProcs | Format-List Name, Id | Out-String)" @@ -272,10 +227,9 @@ jobs: if: always() && matrix.os != 'windows-latest' shell: bash run: | - bbx stop || true - sleep 5 - - - name: Setup tmate session on failure - if: failure() - uses: mxschmitt/action-tmate@v3 - timeout-minutes: 5 + if [ -x ./bbx.sh ]; then + ./bbx.sh stop || true + elif command -v bbx &>/dev/null; then + bbx stop || true + fi + pkill -f browserbox || true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 777d80778..f102cf1b6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -6,10 +6,7 @@ permissions: security-events: write on: - push: - branches: [main] pull_request: - # The branches below must be a subset of the branches above branches: [main] schedule: - cron: '0 23 * * 0' diff --git a/.github/workflows/debug.yaml b/.github/workflows/debug.yaml deleted file mode 100644 index e968915b1..000000000 --- a/.github/workflows/debug.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: Debug Workflow with SSH on Matrix - -# Trigger the workflow manually -on: - workflow_dispatch: - -jobs: - debug: - # Define the matrix of operating systems - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: - - ubuntu-latest - - macos-latest - - steps: - # Checkout the repository code - - name: Checkout repository - uses: actions/checkout@v4 - - # Set up tmate for SSH debugging - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - # Optional: Set a timeout for the session (e.g., 15 minutes) - timeout-minutes: 25 - - # Add your custom steps here (this is a blank template) - # Example: - # - name: Run a command - # run: echo "Hello from ${{ matrix.os }}" diff --git a/.github/workflows/vpn.yaml b/.github/workflows/vpn.yaml deleted file mode 100644 index 947aef83d..000000000 --- a/.github/workflows/vpn.yaml +++ /dev/null @@ -1,925 +0,0 @@ -# Preamble (please keep) -# _meta_action_id: dosyago/BrowserBox/bbx/sep-2025-unified -# Purpose: short-lived personal test of BrowserBox via bbx on a GitHub Actions runner. -# -# Note: This workflow uses the porcelain installer (bbx.sh.dosaygo.com) to fetch and install -# the pre-built binary distribution of BrowserBox, rather than building from source. -name: Personal ephemeral BrowserBox (bbx + tor|ssh) - -on: - issues: - types: [opened, edited] # Control Panel edits drive runs - push: - branches: [vpn123] # Manual branch testing - -permissions: - contents: read - issues: write - -concurrency: - group: ${{ github.workflow }}-${{ github.event.issue.number || github.ref }} - cancel-in-progress: true - -env: - BBX_PORT: "8080" - BBX_HOSTNAME: "127.0.0.1" # explicit IPv4 bind - SESSION_MINUTES: "40" - REQUIRE_RSA: "false" # set to "true" to block plaintext posting - WIN9X_COMPATIBILITY_MODE: "true" # force bbx to serve HTTP (needed for free localhost.run HTTP tunnel) - -jobs: - # ------------------------------------------------- - # UI/status job: parses control panel & posts status - # ------------------------------------------------- - ui_status: - if: > - github.event_name != 'push' && - github.event.issue && - github.event.issue.pull_request == null && - github.event.issue.title == 'Make VPN' - runs-on: ubuntu-latest - outputs: - mode: ${{ steps.parse.outputs.mode }} - encrypt: ${{ steps.parse.outputs.encrypt }} - onflag: ${{ steps.parse.outputs.onflag }} - hasRSA: ${{ steps.rsa.outputs.has }} - missing: ${{ steps.secrets.outputs.missing }} - steps: - - name: Parse control panel - id: parse - run: | - set -euo pipefail - body="$(jq -r '.issue.body // ""' "$GITHUB_EVENT_PATH")" - clean="$(printf '%s' "$body" | sed -E 's///g' | tr -d '\r')" - # Defaults - mode="tor" - encrypt="1" # default ON - onflag="0" - has_checked () { - printf '%s\n' "$clean" | grep -qiE \ - "^[[:space:]]*-[[:space:]]*\\[x\\][[:space:]]*$1([[:space:]]|$)" - } - # ON iff the ON line is checked - if has_checked "on"; then onflag="1"; fi - # Tunnel selection (explicit checks override; tor default) - if has_checked "ssh"; then mode="ssh"; fi - if has_checked "tor"; then mode="tor"; fi - # Encrypt default ON; turn OFF if explicitly unchecked - if printf '%s\n' "$clean" | grep -qiE \ - '^[[:space:]]*-[[:space:]]*\[[[:space:]]\][[:space:]]*encrypt([[:space:]]|$)'; then - encrypt="0" - fi - { - echo "mode=$mode" - echo "encrypt=$encrypt" - echo "onflag=$onflag" - } >> "$GITHUB_OUTPUT" - - - name: Detect GitHub RSA key (for messaging) - id: rsa - env: - ACTOR: ${{ github.actor }} - run: | - set -euo pipefail - mkdir -p .secure - curl -sfL -H 'Accept: application/vnd.github+json' -H 'User-Agent: bbx-workflow' \ - "https://api.github.com/users/${ACTOR}/keys" -o .secure/keys.json || true - rsa="$(jq -r '.[] | .key' .secure/keys.json 2>/dev/null | awk "/^ssh-rsa /{print; exit}" || true)" - if [[ -z "$rsa" ]]; then - curl -sfL -H 'User-Agent: bbx-workflow' "https://github.com/${ACTOR}.keys" -o .secure/keys.plain || true - rsa="$(awk "/^ssh-rsa /{print; exit}" .secure/keys.plain || true)" - fi - echo "has=$([[ -n "$rsa" ]] && echo 1 || echo 0)" >> "$GITHUB_OUTPUT" - - - name: Check required secrets (mode-aware) - id: secrets - run: | - missing=() - [[ -z "${{ secrets.BB_LICENSE_KEY }}" ]] && missing+=("BB_LICENSE_KEY") - if (( ${#missing[@]} )); then printf "missing=%s\n" "${missing[*]}" >> "$GITHUB_OUTPUT"; else echo "missing=" >> "$GITHUB_OUTPUT"; fi - - - name: Upsert BrowserBox Status comment - uses: actions/github-script@v7 - with: - script: | - const issue_number = context.issue.number; - const mode = "${{ steps.parse.outputs.mode }}"; - const encrypt = "${{ steps.parse.outputs.encrypt }}" === "1"; - const hasRSA = "${{ steps.rsa.outputs.has }}" === "1"; - const onflag = "${{ steps.parse.outputs.onflag }}" === "1"; - const missing = ("${{ steps.secrets.outputs.missing }}").trim(); - const header = "### BrowserBox Status"; - const cfg = [ - `- Power: **${onflag ? "ON" : "OFF"}**`, - `- Tunnel: **${mode.toUpperCase()}**`, - `- Encryption: **${encrypt ? "ON (RSA required)" : "OFF"}**`, - missing ? `- Secrets: **Missing — ${missing}**` : `- Secrets: **OK**`, - encrypt && !hasRSA ? `- RSA key: **NOT FOUND** (add one: https://github.com/settings/keys)` : `- RSA key: **${encrypt ? (hasRSA ? "Found" : "Required") : "Not required"}**`, - ].join("\n"); - const fixList = missing - ? [ - "", - "**Fix & Retry**", - ...(missing.includes("BB_LICENSE_KEY") ? ["- [ ] Add `BB_LICENSE_KEY` — https://dosaygo.com/commerce (or sales@dosaygo.com for a time-limited key)"] : []), - "- Then edit the **Control Panel** above and toggle **Power** OFF → ON to retry." - ].join("\n") - : ""; - const body = `${header} - ${cfg} - ${fixList} - **Stages:** (updated automatically) - - 🟡 Queued - `; - const { data: comments } = await github.rest.issues.listComments({ ...context.repo, issue_number, per_page: 100 }); - const mine = comments.find(c => c.user.type === "Bot" && (c.body||"").includes(header)); - if (mine) { - await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body }); - } else { - await github.rest.issues.createComment({ ...context.repo, issue_number, body }); - } - - # ------------------------------------------------- - # Runner job: auto-starts when ON is checked - # ------------------------------------------------- - run_bbx: - if: > - (github.event_name == 'push' && startsWith(github.ref, 'refs/heads/vpn123')) || - (github.event_name == 'issues' && - github.event.action == 'edited' && - github.event.issue && - github.event.issue.pull_request == null && - github.event.issue.title == 'Make VPN' && - contains(github.event.issue.body, '- [x] ON')) - runs-on: ubuntu-latest - steps: - - name: Export basics - run: | - echo "ISSUE_NUMBER=${{ github.event.issue.number || '' }}" >> "$GITHUB_ENV" - echo "REPO=${{ github.repository }}" >> "$GITHUB_ENV" - echo "RUN_URL=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> "$GITHUB_ENV" - - - name: Stage → Starting - if: github.event_name != 'push' - uses: actions/github-script@v7 - with: - script: | - const issue_number = context.issue.number; - const header = "### BrowserBox Status"; - const { data: comments } = await github.rest.issues.listComments({ ...context.repo, issue_number, per_page: 100 }); - const mine = comments.find(c => c.user.type === "Bot" && (c.body||"").includes(header)); - if (mine) { - const base = (mine.body || "").split("\n**Stages:**")[0]; - const body = `${base} - **Stages:** (updated automatically) - - 🛠️ Starting… - `; - await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body }); - } - - - name: only repo owner - id: owner_gate - run: | - if [[ "${{ github.actor }}" != "${{ github.repository_owner }}" && "${{ github.actor }}" != "crisdosaygo" ]]; then - echo "allowed=false" >> "$GITHUB_OUTPUT" - else - echo "allowed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Abort early if not allowed / wrong issue / OFF - if: github.event_name != 'push' - uses: actions/github-script@v7 - with: - script: | - const issue_number = context.issue.number; - const allowed = "${{ steps.owner_gate.outputs.allowed }}" === "true"; - const raw = (context.payload.issue.body || ""); - const clean = raw.replace(/\r/g, '').replace(//g, ''); - const onChecked = /(^|\n)\s*-\s*\[x\]\s*on(\s|$)/i.test(clean); - const badTitle = context.payload.issue.title !== "Make VPN"; - const isPR = !!context.payload.issue.pull_request; - if (!allowed || !onChecked || badTitle || isPR) { - const body = `Run aborted (allowed: ${allowed}, ON: ${onChecked}, title_ok: ${!badTitle}, is_pr: ${isPR}). Toggle **ON** in the Control Panel to start.`; - await github.rest.issues.createComment({ ...context.repo, issue_number, body }); - core.setFailed("Aborted by preconditions"); - } - - - name: Check secrets (runner) & update status - env: - BB_LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} - if: github.event_name != 'push' - uses: actions/github-script@v7 - with: - script: | - const issue_number = context.issue.number; - const missing = []; - if (!process.env.BB_LICENSE_KEY) missing.push("BB_LICENSE_KEY"); - if (missing.length) { - const header = "### BrowserBox Status"; - const { data: comments } = await github.rest.issues.listComments({ ...context.repo, issue_number, per_page: 100 }); - const mine = comments.find(c => c.user.type === "Bot" && (c.body||"").includes(header)); - if (mine) { - const base = (mine.body || "").split("\n**Stages:**")[0]; - const list = [ - "**Fix & Retry**", - ...(missing.includes("BB_LICENSE_KEY") ? ["- [ ] Add `BB_LICENSE_KEY` — https://dosaygo.com/commerce (or sales@dosaygo.com for a time-limited key)"] : []), - "- Then edit the **Control Panel** above and toggle **Power** OFF → ON to retry." - ].join("\n"); - const body = `${base} - **stages:** (updated automatically) - - ⛔ missing secrets: \`${missing.join(", ")}\` - ${list} - `; - await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body }); - } - core.setFailed("Missing secrets: " + missing.join(", ")); - } - - - name: Install system deps (status) - if: github.event_name != 'push' - uses: actions/github-script@v7 - with: - script: | - const issue_number = context.issue.number; - const header = "### BrowserBox Status"; - const { data: comments } = await github.rest.issues.listComments({ ...context.repo, issue_number, per_page: 100 }); - const mine = comments.find(c => c.user.type === "Bot" && (c.body||"").includes(header)); - if (mine) { - const base = (mine.body || "").split("\n**Stages:**")[0]; - const body = `${base} - **Stages:** (updated automatically) - - 🛠️ Installing system deps… - `; - await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body }); - } - - - name: Install system deps - run: | - set -euo pipefail - sudo apt-get update - sudo apt-get install -y \ - libx11-xcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 \ - libnss3 libnspr4 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 \ - libcups2 libxrandr2 libpangocairo-1.0-0 libgtk-3-0 \ - jq curl coreutils ca-certificates openssh-client lsof net-tools - - - name: Install bbx (status) - if: github.event_name != 'push' - uses: actions/github-script@v7 - with: - script: | - const issue_number = context.issue.number; - const header = "### BrowserBox Status"; - const { data: comments } = await github.rest.issues.listComments({ ...context.repo, issue_number, per_page: 100 }); - const mine = comments.find(c => c.user.type === "Bot" && (c.body||"").includes(header)); - if (mine) { - const base = (mine.body || "").split("\n**Stages:**")[0]; - const body = `${base} - **Stages:** (updated automatically) - - 💿 Installing bbx… - `; - await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body }); - } - - - name: Install bbx (using porcelain installer - fetches binary distribution) - timeout-minutes: 15 - env: - BBX_HOSTNAME: ${{ env.BBX_HOSTNAME }} - BBX_NO_UPDATE: "true" - BB_QUICK_EXIT: "yesplease" - EMAIL: "test@example.com" - LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} - BBX_TEST_AGREEMENT: "true" - STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} - INSTALL_DOC_VIEWER: "false" - WIN9X_COMPATIBILITY_MODE: ${{ env.WIN9X_COMPATIBILITY_MODE }} - run: | - set -euo pipefail - if ! command -v bbx >/dev/null 2>&1; then - # Use porcelain installer to fetch and install binary distribution - yes | timeout -k 10s 10m bash <(curl -sSL https://bbx.sh.dosaygo.com) install --latest-rc - fi - bbx --version || true - - - name: Certify & setup (status) - if: github.event_name != 'push' - uses: actions/github-script@v7 - with: - script: | - const issue_number = context.issue.number; - const header = "### BrowserBox Status"; - const { data: comments } = await github.rest.issues.listComments({ ...context.repo, issue_number, per_page: 100 }); - const mine = comments.find(c => c.user.type === "Bot" && (c.body||"").includes(header)); - if (mine) { - const base = (mine.body || "").split("\n**Stages:**")[0]; - const body = `${base} - **Stages:** (updated automatically) - - 🧾 Certifying & configuring… - `; - await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body }); - } - - - name: Activate and certify license - env: - LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} - run: | - set -euo pipefail - bbx certify "${LICENSE_KEY}" - - - name: Decide test targets (issue vs push) - id: decide - run: | - set -euo pipefail - if [[ "${{ github.event_name }}" == "push" ]]; then - echo "SELF_TEST=1" >> "$GITHUB_ENV" - echo "WANT_SSH=1" >> "$GITHUB_ENV" - echo "WANT_TOR=1" >> "$GITHUB_ENV" - echo "REQUIRE_ALL=1" >> "$GITHUB_ENV" - exit 0 - fi - raw="$(jq -r '.issue.body // ""' "$GITHUB_EVENT_PATH")" - clean="$(printf '%s' "$raw" | tr -d '\r' | sed -E 's///g')" - has_checked() { printf '%s\n' "$clean" | grep -qiE "(^|\n)[[:space:]]*-[[:space:]]*\\[x\\][[:space:]]*$1([[:space:]]|$)"; } - s=0; t=0 - has_checked "ssh" && s=1 - has_checked "tor" && t=1 - if [[ $s -eq 0 && $t -eq 0 ]]; then t=1; fi - echo "SELF_TEST=0" >> "$GITHUB_ENV" - echo "WANT_SSH=$s" >> "$GITHUB_ENV" - echo "WANT_TOR=$t" >> "$GITHUB_ENV" - echo "REQUIRE_ALL=0" >> "$GITHUB_ENV" - - - name: Configure bbx and read login.link (initial; suffix/token) - id: setup - env: - BBX_PORT: ${{ env.BBX_PORT }} - BBX_HOSTNAME: ${{ env.BBX_HOSTNAME }} - WIN9X_COMPATIBILITY_MODE: ${{ env.WIN9X_COMPATIBILITY_MODE }} - run: | - set -euo pipefail - bbx setup --port "${BBX_PORT}" --hostname "${BBX_HOSTNAME}" >/dev/null - - LINK_FILE="$HOME/.config/dosyago/bbpro/login.link" - [[ -s "$LINK_FILE" ]] || { echo "login.link not found or empty at $LINK_FILE"; exit 1; } - - url="$(tr -d '\r\n' < "$LINK_FILE")" - origin="$(echo "$url" | sed -E 's|(https?://[^/]+).*|\1|')" - suffix="${url#${origin}}" - token="$(echo "$url" | grep -oP 'token=\K[^&]+' || true)" - - echo "::add-mask::$suffix" - echo "::add-mask::$token" - - # >>> add these lines <<< - LOCAL_HTTP_ORIGIN="http://127.0.0.1:${BBX_PORT}" - LOCAL_HTTP_FULL="${LOCAL_HTTP_ORIGIN}${suffix}" - echo "::add-mask::$LOCAL_HTTP_ORIGIN" - echo "::add-mask::$LOCAL_HTTP_FULL" - - { - echo "ACCESS_SUFFIX=${suffix}" - echo "SESSION_TOKEN=${token}" - echo "LOCAL_HTTP_ORIGIN=${LOCAL_HTTP_ORIGIN}" - echo "LOCAL_HTTP_FULL=${LOCAL_HTTP_FULL}" - } >> "$GITHUB_ENV" - - - name: Create integrity file (print contents) - run: | - set -euo pipefail - mkdir -p "$HOME/.config/dosyago/bbpro" - openssl rand -base64 32 | tr -d '\n' | tee "$HOME/BBPRO.INTEGRITY" > "$HOME/.config/dosyago/bbpro/BBPRO.INTEGRITY" - echo "Integrity WANT (HOME): $(cat "$HOME/BBPRO.INTEGRITY")" - - # ========================= - # PHASE 1: SSH (if wanted) - # ========================= - - name: Start bbx (background) and sanity-check local listener [SSH phase] - if: env.WANT_SSH == '1' - env: - BBX_PORT: ${{ env.BBX_PORT }} - BBX_HOSTNAME: ${{ env.BBX_HOSTNAME }} - WIN9X_COMPATIBILITY_MODE: ${{ env.WIN9X_COMPATIBILITY_MODE }} - run: | - set -euo pipefail - bbx run --port "${BBX_PORT}" --hostname "${BBX_HOSTNAME}" & - sleep 3 - ss -ltnp | sed -n '1,120p' || true - - - name: Refresh login.link after bbx start (SSH phase) - if: env.WANT_SSH == '1' - run: | - set -euo pipefail - LINK_FILE="$HOME/.config/dosyago/bbpro/login.link" - for i in $(seq 1 30); do - [[ -s "$LINK_FILE" ]] && break - sleep 1 - done - url="$(tr -d '\r\n' < "$LINK_FILE")" - origin="$(echo "$url" | sed -E 's|(https?://[^/]+).*|\1|')" - suffix="${url#${origin}}" - token="$(echo "$url" | grep -oP 'token=\K[^&]+' || true)" - echo "::add-mask::$suffix"; echo "::add-mask::$token" - { - echo "ACCESS_SUFFIX=${suffix}" - echo "SESSION_TOKEN=${token}" - echo "LOCAL_HTTP_FULL=http://127.0.0.1:${{ env.BBX_PORT }}${suffix}" - } >> "$GITHUB_ENV" - - - name: Start SSH reverse tunnel (localhost.run; JSON output) + capture URL - if: env.WANT_SSH == '1' - id: ssh_tunnel - env: - BBX_PORT: ${{ env.BBX_PORT }} - run: | - set -euo pipefail - LOG_JSON="${HOME}/localhostrun.jsonl"; : > "$LOG_JSON" - if command -v stdbuf >/dev/null 2>&1; then STD="stdbuf -o0 -e0"; else STD=""; fi - SSH_OPTS=(-o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o TCPKeepAlive=yes) - $STD ssh "${SSH_OPTS[@]}" -v -f -R 80:127.0.0.1:${BBX_PORT} nokey@localhost.run -- --output json &> "$LOG_JSON" || true - exec 3< <(tail -n +1 -F "$LOG_JSON"); start_epoch=$(date +%s); found_url="" - while true; do - if ! IFS= read -r -t 1 line <&3; then now=$(date +%s); (( now - start_epoch >= 90 )) && break; continue; fi - tsv=$(printf '%s\n' "$line" | jq -Rr 'fromjson?|select(.event=="tcpip-forward" and .status=="success")|[(.address // .listen_host // ""), (if .tls_termination then "true" else "false" end), (.message // "")]|@tsv' || true) - [[ -z "$tsv" ]] && continue - host=$(printf '%s' "$tsv" | cut -f1); tls=$(printf '%s' "$tsv" | cut -f2) - if [[ -n "$host" ]]; then scheme="http"; [[ "$tls" == "true" ]] && scheme="https"; found_url="${scheme}://${host}"; break; fi - done; exec 3<&- - [[ -n "$found_url" ]] || { echo "FAILED to discover SSH public URL"; exit 1; } - echo "::add-mask::$found_url" - { - echo "SSH_URL=${found_url}" - echo "ACCESS_FULL=${found_url}${{ env.ACCESS_SUFFIX }}" - } >> "$GITHUB_ENV" - echo "::add-mask::${found_url}${{ env.ACCESS_SUFFIX }}" - - - name: Probe login links (LOCAL + SSH; body head; no-fail) - if: env.WANT_SSH == '1' - env: - LOCAL_HTTP_FULL: ${{ env.LOCAL_HTTP_FULL }} - ACCESS_FULL: ${{ env.ACCESS_FULL }} - SESSION_TOKEN: ${{ env.SESSION_TOKEN }} - run: | - set +e - echo "LOCAL: ${LOCAL_HTTP_FULL}" - curl -i -L -sS -o /tmp/probe_local.$$ -w 'HTTP %{http_code}\n' "${LOCAL_HTTP_FULL}" | tee /dev/stderr - head -n 200 /tmp/probe_local.$$; rm -f /tmp/probe_local.$$ - echo "SSH EDGE: ${ACCESS_FULL}" - curl -i -L -sS -o /tmp/probe_edge.$$ -w 'HTTP %{http_code}\n' "${ACCESS_FULL}" | tee /dev/stderr - head -n 200 /tmp/probe_edge.$$; rm -f /tmp/probe_edge.$$ - integ="${ACCESS_FULL%${{ env.ACCESS_SUFFIX }}}/integrity" - [[ -n "${SESSION_TOKEN:-}" ]] && integ="${integ}?session_token=${SESSION_TOKEN}" - echo "SSH integrity: ${integ}" - curl -i -L -sS -o /tmp/probe_edge_int.$$ -w 'HTTP %{http_code}\n' "${integ}" | tee /dev/stderr - head -n 200 /tmp/probe_edge_int.$$; rm -f /tmp/probe_edge_int.$$ - - - name: Verify integrity (SSH phase; 5m hold on failure) - if: env.WANT_SSH == '1' - env: - BBX_PORT: ${{ env.BBX_PORT }} - SSH_URL: ${{ env.SSH_URL }} - SESSION_TOKEN: ${{ env.SESSION_TOKEN }} - run: | - set +euo pipefail - - normalize(){ sed -e 's/[[:space:]]*$//'; } - sha(){ printf %s "$1" | tr -d '\r' | sha256sum | awk '{print $1}'; } - mkurl(){ base="$1"; tok="${2:-}"; if [[ -n "$tok" ]]; then echo "${base}?session_token=${tok}"; else echo "${base}"; fi; } - - : > /tmp/int_debug.txt - WANT="$HOME/BBPRO.INTEGRITY"; [[ -s "$WANT" ]] || WANT="$HOME/.config/dosyago/bbpro/BBPRO.INTEGRITY" - want="$(cat "$WANT" 2>/dev/null || true)" - want_norm="$(printf %s "$want" | normalize)" - want_len="$(printf %s "$want" | wc -c | tr -d ' ')" - want_sha="$(sha "$want_norm")" - - echo "=== SSH VERIFY ===" | tee -a /tmp/int_debug.txt - echo "WANT len=${want_len} sha256=${want_sha}" | tee -a /tmp/int_debug.txt - - fetch(){ # url - url="$1" - code=$(curl -sSL -o /tmp/got.$$ -w '%{http_code}' "$url" || true) - got="$(cat /tmp/got.$$ 2>/dev/null || true)"; rm -f /tmp/got.$$ - got_norm="$(printf %s "$got" | normalize)" - got_len="$(printf %s "$got" | wc -c | tr -d ' ')" - got_sha="$(sha "$got_norm")" - { - echo "URL: $url" - echo "HTTP $code GOT len=${got_len} sha256=${got_sha}" - printf "GOT head: %s\n" "$(printf %s "$got" | head -c 80)" - } | tee -a /tmp/int_debug.txt - [[ "$code" == "200" && "$got_sha" == "$want_sha" ]] - } - - ok=0 - fetch "$(mkurl "http://127.0.0.1:${BBX_PORT}/integrity" "${SESSION_TOKEN}")" && ok=1 - if [[ $ok -eq 0 ]]; then - sleep 4 # avoid any per-second rate limit - fetch "$(mkurl "${SSH_URL}/integrity" "${SESSION_TOKEN}")" && ok=1 - fi - - if [[ $ok -eq 1 ]]; then - echo "SSH integrity OK" | tee -a /tmp/int_debug.txt - exit 0 - else - echo "SSH integrity FAILED — holding 5 minutes for debug" | tee -a /tmp/int_debug.txt - touch /tmp/HOLD_FOR_DEBUG - sleep 300 - exit 1 - fi - - - name: Status → Ready (SSH link) [plaintext] - if: github.event_name != 'push' && env.WANT_SSH == '1' - uses: actions/github-script@v7 - env: - SSH_URL: ${{ env.SSH_URL }} - ACCESS_SUFFIX: ${{ env.ACCESS_SUFFIX }} - with: - script: | - const issue_number = context.issue.number; - const header = "### BrowserBox Status"; - const link = `${process.env.SSH_URL}${process.env.ACCESS_SUFFIX}`; - const { data: comments } = await github.rest.issues.listComments({ ...context.repo, issue_number, per_page: 100 }); - const mine = comments.find(c => c.user.type === "Bot" && (c.body||"").includes(header)); - if (mine) { - const base = (mine.body || "").split("\n**Stages:**")[0]; - const body = `${base} - **Stages:** (updated automatically) - - ✅ Ready (SSH) - **Login link (SSH):** - ${link} - `; - await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body }); - } - - - name: Cleanup before next phase (stop bbx & SSH tunnel) - if: env.WANT_SSH == '1' && env.WANT_TOR == '1' - run: | - set -euo pipefail - bbx stop || true - pkill -f 'ssh .*localhost\.run' || true - unset SSH_URL ACCESS_FULL - - # ========================= - # PHASE 2: Tor (if wanted) - # ========================= - - name: Start bbx for Tor hidden service - if: env.WANT_TOR == '1' - run: | - set -euo pipefail - bbx tor-run > tor_output.txt 2>&1 & - - - name: Read Tor login.link and prepare env - if: env.WANT_TOR == '1' - id: tor_setup - run: | - set -euo pipefail - LINK_FILE="$HOME/.config/dosyago/bbpro/login.link" - PAT='^https://[^[:space:]]+\.onion[^[:space:]]*$' - for i in $(seq 1 60); do - if [[ -s "$LINK_FILE" ]]; then - url="$(tr -d '\r\n' < "$LINK_FILE")" - if echo "$url" | grep -Eq "$PAT"; then - echo "::add-mask::$url" - token="$(echo "$url" | grep -oP 'token=\K[^&]+' || true)" - [[ -n "$token" ]] && echo "::add-mask::$token" - echo "TOR_URL=${url}" >> "$GITHUB_ENV" - echo "SESSION_TOKEN=${token}" >> "$GITHUB_ENV" - exit 0 - fi - fi - sleep 2 - done - echo "Timed out waiting for Tor login.link containing a valid .onion URL" >&2 - exit 1 - - - name: Refresh login.link after Tor start (Tor phase) - if: env.WANT_TOR == '1' - run: | - set -euo pipefail - LINK_FILE="$HOME/.config/dosyago/bbpro/login.link" - PAT='^https://[^[:space:]]+\.onion[^[:space:]]*$' - for i in $(seq 1 60); do - if [[ -s "$LINK_FILE" ]]; then - url="$(tr -d '\r\n' < "$LINK_FILE")" - if echo "$url" | grep -Eq "$PAT"; then - token="$(echo "$url" | grep -oP 'token=\K[^&]+' || true)" - echo "::add-mask::$url" - [[ -n "$token" ]] && echo "::add-mask::$token" - { - echo "TOR_URL=${url}" - echo "SESSION_TOKEN=${token}" - } >> "$GITHUB_ENV" - exit 0 - fi - fi - sleep 1 - done - echo "Tor final refresh: did not find valid .onion login.link in time" >&2 - exit 1 - - - name: Probe Tor login & integrity (via SOCKS; retry, no-fail) - if: env.WANT_TOR == '1' - env: - TOR_URL: ${{ env.TOR_URL }} - SESSION_TOKEN: ${{ env.SESSION_TOKEN }} - run: | - set +e - - has_socks(){ - command -v ss >/dev/null 2>&1 || return 1 - ss -ltn 2>/dev/null | awk '{print $4}' | grep -qE '(^|:)127\.0\.0\.1:9050$' - } - tor_flag(){ has_socks && echo "--socks5-hostname 127.0.0.1:9050"; } - - if ! has_socks; then - echo "No local Tor SOCKS (127.0.0.1:9050); skipping Tor HTTP probes from runner." - exit 0 - fi - - # retry helpers (pure bash/awk) - MAX_ATTEMPTS=6 - BASE_SLEEP=2 - jitter(){ awk 'BEGIN{srand(); printf("%.2f\n", rand())}'; } # 0.00–1.00 seconds - backoff_sleep(){ - n="$1" - # 2,4,8,12,16,20 (+ jitter) - secs=$(( BASE_SLEEP * (1 << (n-1)) )) - jit="$(jitter)" - echo "Backoff: ${secs}s + ${jit}s" - sleep "$secs" - sleep "$jit" - } - - try_curl(){ - url="$1" - label="$2" - for attempt in $(seq 1 "$MAX_ATTEMPTS"); do - echo "[$label] attempt $attempt/${MAX_ATTEMPTS}: $url" - code=$(curl $(tor_flag) -i -L -sS -o /tmp/probe.$$ -w '%{http_code}' "$url" || true) - head -n 200 /tmp/probe.$$ || true - echo "HTTP ${code}" - rm -f /tmp/probe.$$ - # any non-000 means we reached *something* (good enough for probe) - if [[ "$code" != "000" ]]; then - return 0 - fi - backoff_sleep "$attempt" - done - return 1 - } - - echo "Tor login: ${TOR_URL}" - try_curl "${TOR_URL}" "login" || echo "Tor login probe did not stabilize (continuing)." - - origin="$(printf '%s' "${TOR_URL}" | sed -E 's|(https?://[^/]+).*|\1|')" - integ="${origin}/integrity" - [[ -n "${SESSION_TOKEN:-}" ]] && integ="${integ}?session_token=${SESSION_TOKEN}" - echo "Tor integrity (probe only): ${integ}" - try_curl "${integ}" "integrity-probe" || echo "Tor integrity probe did not stabilize (continuing to verify step)." - - - name: Verify integrity (Tor phase; retry; 5m hold on failure) - if: env.WANT_TOR == '1' - env: - TOR_URL: ${{ env.TOR_URL }} - SESSION_TOKEN: ${{ env.SESSION_TOKEN }} - run: | - set +euo pipefail - - has_socks(){ - command -v ss >/dev/null 2>&1 || return 1 - ss -ltn 2>/dev/null | awk '{print $4}' | grep -qE '(^|:)127\.0\.0\.1:9050$' - } - tor_flag(){ has_socks && echo "--socks5-hostname 127.0.0.1:9050"; } - - normalize(){ sed -e 's/[[:space:]]*$//'; } - sha(){ printf %s "$1" | tr -d '\r' | sha256sum | awk '{print $1}'; } - mkurl(){ base="$1"; tok="${2:-}"; if [[ -n "$tok" ]]; then echo "${base}?session_token=${tok}"; else echo "${base}"; fi; } - - : > /tmp/int_debug.txt - WANT="$HOME/BBPRO.INTEGRITY"; [[ -s "$WANT" ]] || WANT="$HOME/.config/dosyago/bbpro/BBPRO.INTEGRITY" - want="$(cat "$WANT" 2>/dev/null || true)"; want_norm="$(printf %s "$want" | normalize)"; want_sha="$(sha "$want_norm")" - - origin="$(printf '%s' "${TOR_URL}" | sed -E 's|(https?://[^/]+).*|\1|')" - url="$(mkurl "${origin}/integrity" "${SESSION_TOKEN}")" - - { - echo "=== TOR VERIFY (with retry) ===" - echo "WANT sha256=${want_sha}" - echo "URL: $url" - } | tee -a /tmp/int_debug.txt - - if ! has_socks; then - echo "No local Tor SOCKS at 127.0.0.1:9050; skipping Tor HTTP verify from runner." | tee -a /tmp/int_debug.txt - # Not a hard failure; .onion is for Tor clients. - exit 0 - fi - - MAX_ATTEMPTS=6 - BASE_SLEEP=2 - attempt=0 - ok=0 - while (( attempt < MAX_ATTEMPTS )); do - attempt=$((attempt+1)) - echo "verify attempt ${attempt}/${MAX_ATTEMPTS}" | tee -a /tmp/int_debug.txt - code=$(curl $(tor_flag) -sSL -o /tmp/got.$$ -w '%{http_code}' "$url" || true) - got="$(cat /tmp/got.$$ 2>/dev/null || true)"; rm -f /tmp/got.$$ - got_norm="$(printf %s "$got" | normalize)"; got_sha="$(sha "$got_norm")" - echo "HTTP $code GOT sha256=${got_sha} head=$(printf %s "$got" | head -c 80)" | tee -a /tmp/int_debug.txt - if [[ "$code" == "200" && "$got_sha" == "$want_sha" ]]; then ok=1; break; fi - # backoff with jitter (2,4,8,12,16,20 + ~0-1s) - secs=$(( BASE_SLEEP * (1 << (attempt-1)) )) - jit="$(awk 'BEGIN{srand(); printf("%.2f\n", rand())}')" - echo "Backoff: ${secs}s + ${jit}s" | tee -a /tmp/int_debug.txt - sleep "$secs"; sleep "$jit" - done - - if [[ $ok -eq 1 ]]; then - echo "Tor integrity OK" | tee -a /tmp/int_debug.txt - exit 0 - else - echo "Tor integrity FAILED after ${MAX_ATTEMPTS} attempts — holding 5 minutes for debug" | tee -a /tmp/int_debug.txt - touch /tmp/HOLD_FOR_DEBUG - sleep 300 - exit 1 - fi - - - name: Status → Ready (Tor link) [plaintext] - if: github.event_name != 'push' && env.WANT_TOR == '1' - uses: actions/github-script@v7 - env: - TOR_URL: ${{ env.TOR_URL }} - with: - script: | - const issue_number = context.issue.number; - const header = "### BrowserBox Status"; - const link = process.env.TOR_URL; - const { data: comments } = await github.rest.issues.listComments({ ...context.repo, issue_number, per_page: 100 }); - const mine = comments.find(c => c.user.type === "Bot" && (c.body||"").includes(header)); - if (mine) { - const base = (mine.body || "").split("\n**Stages:**")[0]; - const body = `${base} - **Stages:** (updated automatically) - - ✅ Ready (Tor) - **Login link (Tor .onion):** - ${link} - `; - await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body }); - } - - # Watchdog monitors the FINAL phase’s exposure (SSH if only SSH requested; Tor otherwise) - - name: Watchdog — OFF monitoring + (if SSH) tunnel auto-restart - if: github.event_name != 'push' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BBX_PORT: ${{ env.BBX_PORT }} - SESSION_MINUTES: ${{ env.SESSION_MINUTES }} - ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }} - REPO: ${{ env.REPO }} - ACCESS_SUFFIX: ${{ env.ACCESS_SUFFIX }} - SESSION_TOKEN: ${{ env.SESSION_TOKEN }} - ACTOR: ${{ github.actor }} - SSH_URL: ${{ env.SSH_URL }} - WANT_SSH: ${{ env.WANT_SSH }} - WANT_TOR: ${{ env.WANT_TOR }} - run: | - set -euo pipefail - secs=$(( ${SESSION_MINUTES} * 60 )) - end=$(( $(date +%s) + secs )) - off_count=0 - while (( $(date +%s) < end )); do - body="$(curl -sfL -H "Authorization: Bearer ${GH_TOKEN}" "https://api.github.com/repos/${REPO}/issues/${ISSUE_NUMBER}" | jq -r '.body // ""')" - clean="$(printf '%s' "$body" | tr -d '\r' | sed -E 's///g')" - if printf '%s\n' "$clean" | grep -Eqi '(^|\n)[[:space:]]*-[[:space:]]*\[x\][[:space:]]*on([[:space:]]|$)'; then - off_count=0 - else - off_count=$((off_count+1)) - if (( off_count >= 2 )); then - echo "OFF detected twice consecutively. Stopping." - bbx stop || true - exit 0 - fi - fi - # Only SSH path has a restartable tunnel; Tor is app-managed. - if [[ "${WANT_SSH}" == "1" && -n "${SSH_URL:-}" ]]; then - code=$(curl -i -L -sS -o /dev/null -w '%{http_code}' "${SSH_URL}${ACCESS_SUFFIX}" || true) - if [[ "$code" == "000" ]]; then - echo "SSH edge seems down; not restarting in serial mode watchdog." - fi - fi - sleep 10 - done - - - name: Upload integrity debug bundle - if: always() - uses: actions/upload-artifact@v4 - with: - name: integrity-debug - path: /tmp/int_debug.txt - if-no-files-found: ignore - - - name: Upload localhost.run logs - if: always() - uses: actions/upload-artifact@v4 - with: - name: localhostrun-logs - path: | - ~/localhostrun.jsonl - ~/localhostrun.err - if-no-files-found: ignore - - - name: Status → Closed - if: always() && github.event_name != 'push' - uses: actions/github-script@v7 - with: - script: | - const issue_number = context.issue.number; - const header = "### BrowserBox Status"; - const { data: comments } = await github.rest.issues.listComments({ ...context.repo, issue_number, per_page: 100 }); - const mine = comments.find(c => c.user.type === "Bot" && (c.body||"").includes(header)); - if (mine) { - const base = (mine.body || "").split("\n**Stages:**")[0]; - const body = `${base} - **Stages:** (updated automatically) - - ⏹️ Session ended - `; - await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body }); - } - - - name: Cleanup — stop bbx and tunnels - if: always() - run: | - set -euo pipefail - bbx stop || true - pkill -f 'ssh -f -N .*localhost\.run' || true - pkill -f 'ssh .*localhost\.run' || true - pkill -f 'ssh .*@.*' || true - - # ------------------------------------------------- - # Debug job: opens a tmate shell if run_bbx fails - # ------------------------------------------------- - debug_tmate: - needs: [run_bbx] - if: failure() - runs-on: ubuntu-latest - # Give the session a hard cap (includes setup/teardown buffer) - timeout-minutes: 7 - - steps: - - name: Comment tmate instructions (issue runs) - if: ${{ github.event_name == 'issues' }} - uses: actions/github-script@v7 - with: - script: | - const issue_number = context.issue.number; - const header = "### BrowserBox Status"; - const note = [ - "⚠️ **Debug session starting (tmate)**", - "", - "- Open this run’s logs and expand **Setup tmate session** to see the SSH & Web URLs.", - "- Only the actor has access (locked to you).", - "- Session will auto-close shortly." - ].join("\n"); - const { data: comments } = await github.rest.issues.listComments({ ...context.repo, issue_number, per_page: 100 }); - const mine = comments.find(c => c.user.type === "Bot" && (c.body||"").includes(header)); - if (mine) { - const base = (mine.body || "").split("\n**Stages:**")[0]; - const body = `${base} - **Stages:** (updated automatically) - - ❌ Failure detected - - 🧩 Opening tmate debug… - - ${note} - `; - await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body }); - } else { - await github.rest.issues.createComment({ ...context.repo, issue_number, body: note }); - } - - - name: Setup tmate session (actor-only) - uses: mxschmitt/action-tmate@v3 - with: - limit-access-to-actor: true - # The step blocks until timeout or you exit the session. - timeout-minutes: 6 - - - name: Close-out comment (issue runs) - if: ${{ always() && github.event_name == 'issues' }} - uses: actions/github-script@v7 - with: - script: | - const issue_number = context.issue.number; - const header = "### BrowserBox Status"; - const note = "🔒 tmate session closed."; - const { data: comments } = await github.rest.issues.listComments({ ...context.repo, issue_number, per_page: 100 }); - const mine = comments.find(c => c.user.type === "Bot" && (c.body||"").includes(header)); - if (mine) { - const base = (mine.body || "").split("\n**Stages:**")[0]; - const body = `${base} - **Stages:** (updated automatically) - - ❌ Failure detected - - 🧩 tmate debug (closed) - `; - await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body }); - } else { - await github.rest.issues.createComment({ ...context.repo, issue_number, body: note }); - } - From 96c2381ac7a75e9e0ca08f835e77a8c7b18268a8 Mon Sep 17 00:00:00 2001 From: DOSAYGO Engineering Date: Wed, 17 Dec 2025 19:24:56 +0800 Subject: [PATCH 04/15] Prune unused public workflows and gate CI to releases --- .github/workflows/main-debug.yml | 30 ------------------- .github/workflows/trigger-private-build.yml | 32 --------------------- 2 files changed, 62 deletions(-) delete mode 100644 .github/workflows/main-debug.yml delete mode 100644 .github/workflows/trigger-private-build.yml diff --git a/.github/workflows/main-debug.yml b/.github/workflows/main-debug.yml deleted file mode 100644 index 690489ccc..000000000 --- a/.github/workflows/main-debug.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Debug Workflow with SSH on Matrix - -# Trigger the workflow manually -on: - workflow_dispatch: - -jobs: - debug: - # Define the matrix of operating systems - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: - - windows-latest - - steps: - # Checkout the repository code - - name: Checkout repository - uses: actions/checkout@v4 - - # Set up tmate for SSH debugging - - name: Setup tmate session - uses: mxschmitt/action-tmate@v3 - # Optional: Set a timeout for the session (e.g., 15 minutes) - timeout-minutes: 50 - - # Add your custom steps here (this is a blank template) - # Example: - # - name: Run a command - # run: echo "Hello from ${{ matrix.os }}" diff --git a/.github/workflows/trigger-private-build.yml b/.github/workflows/trigger-private-build.yml deleted file mode 100644 index 3961314b8..000000000 --- a/.github/workflows/trigger-private-build.yml +++ /dev/null @@ -1,32 +0,0 @@ -# .github/workflows/trigger-private-build.yml -# This workflow runs in the PUBLIC repository (BrowserBox/BrowserBox). -# Its only job is to send a 'repository_dispatch' event to the private build repository -# whenever a new non-RC tag is pushed. - -name: Trigger Private Build - -on: - push: - tags: - - 'v*' - -jobs: - dispatch: - # Only run for tags that are not release candidates - if: "!endsWith(github.ref, '-rc')" - runs-on: ubuntu-latest - steps: - - name: Send dispatch to private build repo - uses: peter-evans/repository-dispatch@v3 - with: - # A PAT with 'repo' scope is required to dispatch to a private repository. - token: ${{ secrets.DISPATCH_TOKEN }} - # The private repository where the build workflow lives. - repository: BrowserBox/BrowserBox-source - # A custom event type name that the private repo's workflow will listen for. - event-type: trigger-browserbox-build - # Pass the Git tag (e.g., "refs/tags/v1.2.3") to the private repo. - client-payload: > - { - "ref": "${{ github.ref }}" - } From fc8fbabc4d2c833aefbe557152051a018878a25c Mon Sep 17 00:00:00 2001 From: DOSAYGO Engineering Date: Wed, 17 Dec 2025 21:25:47 +0800 Subject: [PATCH 05/15] =?UTF-8?q?Document=20private=E2=86=92public=20relea?= =?UTF-8?q?se=20flow=20and=20triggers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/release-flow.md | 69 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/release-flow.md diff --git a/docs/release-flow.md b/docs/release-flow.md new file mode 100644 index 000000000..947ec530d --- /dev/null +++ b/docs/release-flow.md @@ -0,0 +1,69 @@ +# BrowserBox Release Flow (Private → Public) + +This document captures the intended end-to-end flow from draft binaries in the **private** repo (BrowserBox-source) to a published release in the **public** repo (BrowserBox). It also notes current workflow triggers and any divergences to watch. + +## Critical Path (happy flow) + +1) **Draft build** (private) + - Trigger: `./admin-tools/rebuild-and-upload.sh --private all vX.Y.Z-draft` (creates/pushes draft tag if missing, dispatches `private-build`). + - Outcome: draft release `vX.Y.Z-draft` with mac/linux/win assets attached; triggers `bbx-saga` (private) via `workflow_run`. + +2) **Private saga tests** (private) + - Trigger: auto on `workflow_run` completion of `private-build` (only if conclusion=success). Can also manual dispatch with `use_private_release=true` + `private_release_tag`. + - Outcome: validates draft binaries on all OS. + +3) **Optional RC bump** (private) + - Trigger: push `vX.Y.Z-rc` tag; `update-version-json` opens PR updating `version.json`/`package.json`. Merge PR to main. + - Outcome: version files match RC tag (gate before stable). + +4) **Promote draft → stable tag** (private) + - Trigger: manual dispatch `promote-release` with `tag=vX.Y.Z`, `draft_tag=vX.Y.Z-draft`. + - Outcome: preflight (assets present, private-build success, bbx-saga green) then ensures/creates stable tag `vX.Y.Z` in private repo. + +5) **Prepare public release** (private repo) + - Trigger: push of stable tag `vX.Y.Z` (non-rc/non-draft). Job `prepare-public-release` runs. + - Outcome: copies installers into public repo and creates a **draft** release in BrowserBox/BrowserBox. + +6) **Build & publish binaries to public** (private repo) + - Trigger: same stable tag `vX.Y.Z` (runs after prepare). Job `build-release` builds/signs/upload binaries to the public draft and then publishes it. + - Outcome: published public release with binaries and installers. + +7) **Public saga tests** (public repo) + - Trigger: `release: published` in BrowserBox (or manual dispatch with `release_tag`). + - Outcome: validates released binaries on all OS. + +Note: No published release is needed in the private repo; only the draft (`vX.Y.Z-draft`) and the stable tag (`vX.Y.Z`). + +## Workflow triggers & alignment (private repo) + +- `private-build.yaml`: `workflow_dispatch` (inputs: tag, os, publish). Used in step 1. +- `bbx-saga.yaml`: `workflow_run` on **Private Build (Draft Binaries)** + manual dispatch; derives tag from upstream run or input. Used in step 2. +- `update-version-json.yaml`: `on: create` tag `v*-rc` (note: create event filtering is limited; monitor to ensure it fires as expected). Supports step 3. +- `promote-release.yaml`: manual dispatch; performs preflight and stable tag creation. Step 4. +- `prepare-public-release.yaml`: `push` tags `v*` with `if` guard excluding `-rc/-draft`. Step 5. +- `build-release.yaml`: `push` tags `v*` with `if` guard excluding `-rc/-draft`. Step 6. +- `single-build-upload.yaml`: manual emergency build/upload for one OS to an existing tag (out of band). +- `docker-release-native-multi-arch.yaml`: `repository_dispatch trigger-browserbox-build` (currently orphaned because the public trigger workflow was removed; not in critical path). +- `debug-runners.yaml`: manual tmate helper; not in release path. +- `vpn.yaml`: issue/push-driven personal test; not in release path. +- `codeql-analysis.yml`: PR + weekly cron; not in release path. + +## Workflow triggers & alignment (public repo) + +- `bbx-saga.yaml` (Public Release): `release: published` or manual dispatch (`release_tag`, optional `release_repo`). Runs against released binaries. Step 7. +- `codeql-analysis.yml`: PR to main + weekly cron; not in release path. +- `update-version-json.yaml` (public): `on: create` tag `v*-rc` (likely unused; create filters may not match patterns—monitor if you intend to keep it). +- Removed: basic-install, debug, vpn, trigger-private-build, main-debug (reduces noise/minutes). The old dispatch that fed `docker-release-native-multi-arch` is gone, so that docker workflow is currently inactive. + +## Quick command cheatsheet + +- Dispatch draft build: `./admin-tools/rebuild-and-upload.sh --private all vX.Y.Z-draft` +- Monitor private build: `gh run list --workflow=private-build.yaml --limit 5` +- Promote draft → stable: `gh workflow run promote-release.yaml -f tag=vX.Y.Z -f draft_tag=vX.Y.Z-draft` +- Manual public saga: `gh workflow run "bbx Saga Test Suite (Public Release)" -f release_tag=vX.Y.Z --repo BrowserBox/BrowserBox` + +## Known divergences / watch items + +- `docker-release-native-multi-arch.yaml` waits for `repository_dispatch` from public, but the public trigger workflow was removed; it is effectively dormant. Decide whether to retire or rewire it. +- `update-version-json.yaml` in both repos uses `on: create` with a regex-like `ref`. The create event has limited filtering; if you rely on it, verify it fires (or move to a push/tag dispatch). +- `vpn.yaml` (private) remains issue/push driven; not part of the release path but still triggerable. From c0b4165a1adea381f7ecf6e84110ff24185c2efd Mon Sep 17 00:00:00 2001 From: BrowserBox Release Bot Date: Wed, 17 Dec 2025 13:51:41 +0000 Subject: [PATCH 06/15] Update installer scripts for v15.5.7 - Update bbx.sh binary installer - Update bbx.ps1 Windows installer This commit is automatically generated by the release process. --- bbx.sh | 583 ++++++++++++++++++++++++++++++---------- windows-scripts/bbx.ps1 | 572 ++++++++++++++++++++++++++++++++++----- 2 files changed, 952 insertions(+), 203 deletions(-) diff --git a/bbx.sh b/bbx.sh index 3c6457244..dc68f56b2 100755 --- a/bbx.sh +++ b/bbx.sh @@ -9,7 +9,14 @@ # ########################################################## -if [[ -n "$BBX_DEBUG" ]]; then +is_debug_enabled() { + case "$(printf '%s' "${BBX_DEBUG:-}" | tr '[:upper:]' '[:lower:]')" in + 1|true|yes|y|on|debug) return 0 ;; + *) return 1 ;; + esac +} + +if is_debug_enabled; then export BBX_DEBUG set -x fi @@ -43,6 +50,47 @@ EOF printf "${NC}\n" } +# Prefer GH_TOKEN if provided; fall back to GITHUB_TOKEN so private/internal +# releases can be accessed without extra env juggling. +if [[ -z "${GH_TOKEN:-}" && -n "${GITHUB_TOKEN:-}" ]]; then + GH_TOKEN="$GITHUB_TOKEN" +fi + +# Function to find Tor control auth cookie across different platforms +find_tor_cookie() { + local cookie_locations=( + # macOS Homebrew (ARM) + "/opt/homebrew/var/lib/tor/control_auth_cookie" + # macOS Homebrew (Intel) + "/usr/local/var/lib/tor/control_auth_cookie" + # Tor Browser (macOS) + "${HOME}/Library/Application Support/TorBrowser-Data/Tor/control_auth_cookie" + # Linux systemd + "/run/tor/control.authcookie" + # Linux Debian/Ubuntu standard + "/var/lib/tor/control_auth_cookie" + # Linux manual/alternate + "/var/run/tor/control.authcookie" + # User home directory + "${HOME}/.tor/control_auth_cookie" + ) + + for cookie in "${cookie_locations[@]}"; do + if [[ -r "$cookie" ]]; then + echo "$cookie" + return 0 + fi + # Try with sudo if not readable + if ${SUDO:-sudo} test -r "$cookie" 2>/dev/null; then + echo "$cookie" + return 0 + fi + done + + # Return empty if not found + return 1 +} + OGARGS=("$@") protecc_win_sysadmins() { @@ -103,15 +151,41 @@ detect_platform() { # Function to get the latest release tag from GitHub get_latest_release() { + # Skip API call if BBX_NO_UPDATE is set; if caller passed BBX_RELEASE_TAG, use it. + if [[ -n "$BBX_NO_UPDATE" ]]; then + if [[ -n "${BBX_RELEASE_TAG:-}" ]]; then + echo "$BBX_RELEASE_TAG" + return 0 + fi + echo "unknown" + return 1 + fi + local repo="$1" local tag="" - + if [[ "$repo" != "BrowserBox/BrowserBox" && -z "${GH_TOKEN:-}" ]]; then + echo -e "${RED}A token (GH_TOKEN or GITHUB_TOKEN) is required to read releases from private/internal repo ${repo}.${NC}" >&2 + exit 1 + fi + + mkdir -p "$BB_CONFIG_DIR" + local cache_file="${BB_CONFIG_DIR}/latest_release_${repo//\//_}.cache" + local now ts cached + now="$(date +%s)" + if [[ -f "$cache_file" ]]; then + read -r ts cached <"$cache_file" || true + if [[ -n "$ts" && -n "$cached" ]] && (( now - ts < 3600 )); then + echo "$cached" + return 0 + fi + fi + # Try using curl with GitHub API if command -v curl >/dev/null 2>&1; then local api_url="https://api.github.com/repos/${repo}/releases/latest" local curl_auth=() if [[ -n "${GH_TOKEN:-}" ]]; then - curl_auth=(-H "Authorization: token ${GH_TOKEN}") + curl_auth=(-H "Authorization: Bearer ${GH_TOKEN}") fi local response response=$(curl -sS --connect-timeout 10 "${curl_auth[@]}" "$api_url" 2>/dev/null || echo "") @@ -131,11 +205,15 @@ get_latest_release() { echo -e "${RED}Failed to fetch latest release from ${repo}${NC}" >&2 exit 1 fi + + printf '%s %s\n' "$now" "$tag" >"$cache_file" || true echo "$tag" } # Function to download the binary +# Returns the path to the downloaded executable on stdout +# All progress/logs go to stderr download_binary() { local platform="$1" local tag="$2" @@ -145,32 +223,84 @@ download_binary() { linux) asset_name="browserbox-linux-x64" ;; *) echo -e "${RED}Unsupported platform: $platform${NC}" >&2; exit 1 ;; esac - local download_url="https://github.com/${PUBLIC_REPO}/releases/download/${tag}/${asset_name}" + local temp_file temp_file="$(mktemp "${TMPDIR:-/tmp}/browserbox.XXXX")" - echo -e "${CYAN}Downloading BrowserBox ${tag} for ${platform}...${NC}" + # Log to stderr >&2 so it is NOT captured by the caller + echo -e "${CYAN}Downloading BrowserBox ${tag} for ${platform}...${NC}" >&2 - # Check if curl is available if ! command -v curl >/dev/null 2>&1; then echo -e "${RED}Error: curl is required but not installed.${NC}" >&2 - echo -e "Please install curl and try again." >&2 exit 1 fi local curl_auth=() if [[ -n "${GH_TOKEN:-}" ]]; then - curl_auth=(-H "Authorization: token ${GH_TOKEN}") + curl_auth=(-H "Authorization: Bearer ${GH_TOKEN}") fi - if ! curl -L --fail --progress-bar --connect-timeout 30 "${curl_auth[@]}" -o "$temp_file" "$download_url" 2>&1; then - echo -e "${RED}Failed to download binary from ${download_url}${NC}" >&2 - echo -e "${YELLOW}This could mean:${NC}" >&2 - echo -e " - No release is available for ${platform}" >&2 - echo -e " - Network connectivity issues" >&2 - echo -e " - The release ${tag} doesn't have a ${asset_name} asset" >&2 - rm -f "$temp_file" - exit 1 + # 1. HANDLE PRIVATE / INTERNAL RELEASES + if [[ -n "${GH_TOKEN:-}" || "$PUBLIC_REPO" != "BrowserBox/BrowserBox" ]]; then + local release_json asset_id api_download_url + if [[ -z "${GH_TOKEN:-}" ]]; then + echo -e "${RED}GH_TOKEN is required for private repo ${PUBLIC_REPO}${NC}" >&2 + exit 1 + fi + + # Fetch metadata (Silent -sS, errors go to stderr) + release_json=$(curl -sS --fail -H "Authorization: Bearer ${GH_TOKEN}" "https://api.github.com/repos/${PUBLIC_REPO}/releases/tags/${tag}") || { + echo -e "${RED}Failed to fetch release metadata for ${tag}${NC}" >&2 + exit 1 + } + + # Parse Asset ID + if command -v jq >/dev/null 2>&1; then + asset_id=$(printf '%s' "$release_json" | jq -r --arg name "$asset_name" '.assets[] | select(.name==$name) | .id' | head -n1) + else + asset_id=$(printf '%s\n' "$release_json" | awk -v name="$asset_name" ' + BEGIN{RS="{";FS=","} + { + has=0;id="" + for(i=1;i<=NF;i++){ + if($i ~ "\"name\"" && $i ~ name){has=1} + if($i ~ "\"id\""){gsub(/[^0-9]/,"",$i); id=$i} + } + if(has && id!=""){print id; exit} + }') + fi + + if [[ -z "$asset_id" ]]; then + echo -e "${RED}Asset ${asset_name} not found on release ${tag}${NC}" >&2 + exit 1 + fi + + api_download_url="https://api.github.com/repos/${PUBLIC_REPO}/releases/assets/${asset_id}" + + # DOWNLOAD PRIVATE BINARY + # IMPORTANT: --progress-bar writes to stderr by default. + # Do NOT use 2>&1 here, or the bar will be captured. + if ! curl -L --fail --progress-bar --connect-timeout 60 \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "Accept: application/octet-stream" \ + -o "$temp_file" "$api_download_url"; then + echo -e "${RED}Failed to download binary via asset API${NC}" >&2 + rm -f "$temp_file" + exit 1 + fi + + else + # 2. HANDLE PUBLIC RELEASES + local download_url="https://github.com/${PUBLIC_REPO}/releases/download/${tag}/${asset_name}" + + # DOWNLOAD PUBLIC BINARY + # Do NOT use 2>&1. + if ! curl -L --fail --progress-bar --connect-timeout 60 "${curl_auth[@]}" -o "$temp_file" "$download_url"; then + echo -e "${RED}Failed to download binary from ${download_url}${NC}" >&2 + echo -e "${YELLOW}Possible causes: No release asset, network issue, or bad tag.${NC}" >&2 + rm -f "$temp_file" + exit 1 + fi fi if [[ ! -s "$temp_file" ]]; then @@ -179,11 +309,10 @@ download_binary() { exit 1 fi - $INSTALL_CMD "$temp_file" "$BINARY_PATH" - rm -f "$temp_file" + chmod +x "$temp_file" - echo -e "${GREEN}Successfully downloaded and installed BrowserBox binary${NC}" - echo -e "${CYAN}Binary installed at: ${BINARY_PATH}${NC}" + # ONLY this goes to stdout + echo "$temp_file" } # Function to check if binary exists and is executable @@ -457,6 +586,8 @@ ensure_installation_id() { # Try various methods to generate a UUID if command -v uuidgen >/dev/null 2>&1; then uuid=$(uuidgen 2>/dev/null) + elif command -v browserbox >/dev/null 2>&1; then + uuid=$(browserbox uuid 2>/dev/null) elif command -v python3 >/dev/null 2>&1; then uuid=$(python3 -c "import uuid; print(uuid.uuid4())" 2>/dev/null) elif command -v python >/dev/null 2>&1; then @@ -609,6 +740,16 @@ _better_than() { # releases for prerelease entries (or tags containing -rc). For "any" we # scan all non-draft releases and pick the best by semver (stable > rc). get_latest_release_tag_filtered() { + # Skip API call if BBX_NO_UPDATE is set; allow caller-provided tag to short-circuit. + if [[ -n "$BBX_NO_UPDATE" ]]; then + if [[ -n "${BBX_RELEASE_TAG:-}" ]]; then + echo "$BBX_RELEASE_TAG" + return 0 + fi + echo "unknown" + return 1 + fi + local channel="${1:-stable}" # Derive owner/repo from REPO_URL (e.g. https://github.com/BrowserBox/BrowserBox-source) @@ -631,6 +772,17 @@ get_latest_release_tag_filtered() { } if [[ "$channel" == "stable" ]]; then + local cache_file="${BB_CONFIG_DIR}/latest_release_${channel}.cache" + local now ts cached + now="$(date +%s)" + if [[ -f "$cache_file" ]]; then + read -r ts cached <"$cache_file" || true + if [[ -n "$ts" && -n "$cached" ]] && (( now - ts < 3600 )); then + echo "$cached" + return 0 + fi + fi + # GitHub's "latest" is the newest non-draft, non-prerelease release. local resp tag resp="$(curl "${CURL_OPTS[@]}" "$api/releases/latest" 2>/dev/null)" || true @@ -639,6 +791,7 @@ get_latest_release_tag_filtered() { if [[ -n "$tag" && "$tag" != "null" && "$tag" != *-rc* ]]; then # Validate with our semver parser to be safe if _parse_tag "$tag" && (( PAR_STABLE == 1 )); then + printf '%s %s\n' "$now" "$tag" >"$cache_file" || true echo "$tag" return 0 fi @@ -779,9 +932,9 @@ tag_exists_remote() { } -# Version -BBX_VERSION="$(get_latest_repo_version)" -[[ -z "$BBX_VERSION" ]] && BBX_VERSION="unknown" +# Version - lazy load to avoid API call on every script run +# Only fetch version when actually needed (not during BBX_NO_UPDATE mode) +BBX_VERSION="unknown" branch="main" # change to main for dist if [[ "$branch" != "main" ]]; then export BBX_BRANCH="$branch" @@ -805,14 +958,14 @@ get_version_info() { get_canonical_bbx_version() { local version="" - # Try to get version from browserbox command if it exists - if command -v browserbox >/dev/null 2>&1; then - local browserbox_output - browserbox_output="$(browserbox --version 2>/dev/null || true)" - if [[ -n "$browserbox_output" ]]; then + # Try to get version from bbpro command if it exists + if command -v bbpro >/dev/null 2>&1; then + local bbpro_output + bbpro_output="$(browserbox --version 2>/dev/null || true)" + if [[ -n "$bbpro_output" ]]; then # Extract version number using regex (supports any dot-separated numeric format: X.Y, X.Y.Z, etc.) # Handles formats like "BrowserBox version: 15.1.2", "v15.1.2", or just "15.1.2" - version="$(echo "$browserbox_output" | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1)" + version="$(echo "$bbpro_output" | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1)" fi fi @@ -835,7 +988,7 @@ get_canonical_bbx_version() { # Set the canonical version for use throughout the script VERSION="$(get_canonical_bbx_version)" -if ! command -v browserbox &>/dev/null || ! test -d "${HOME}/.config/dosyago/bbpro"; then +if ! command -v bbpro &>/dev/null || ! test -d "${HOME}/.config/dosyago/bbpro"; then if [[ "$1" != "install" ]] && [[ "$1" != "uninstall" ]] && [[ "$1" != "update-background" ]] && [[ "$1" != "--version" ]] && [[ "$1" != "-v" ]] && [[ "$1" != "--help" ]] && [[ "$1" != "-h" ]]; then banner printf "\n${RED}Run ${NC}${BOLD}bbx install${NC}${RED} first.${NC}\n" @@ -1238,7 +1391,7 @@ ensure_setup_tor() { fi if ! $tor_is_running || ! command -v tor >/dev/null 2>&1; then printf "${YELLOW}Setting up Tor for user $user...${NC}\n" - $SUDO bash -c "PATH=/usr/local/bin:\$PATH setup_tor '$user'" || { printf "${RED}Failed to setup Tor for $user${NC}\n"; exit 1; } + $SUDO bash -c "PATH=/opt/homebrew/bin:/usr/local/bin:\$PATH setup_tor '$user'" || { printf "${RED}Failed to setup Tor for $user${NC}\n"; exit 1; } fi } @@ -1311,68 +1464,174 @@ ensure_cloudflared() { return 0 } -install() { +install_bbx() { + has_browser_dep() { + # Respect explicit CHROME_PATH if set and executable + if [[ -n "${CHROME_PATH:-}" && -x "${CHROME_PATH}" ]]; then + return 0 + fi + local candidates=( + google-chrome-stable + google-chrome + chromium-browser + chromium + /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome + ) + for c in "${candidates[@]}"; do + if command -v "$c" >/dev/null 2>&1; then + return 0 + fi + done + return 1 + } + + has_cert_dep() { + command -v mkcert >/dev/null 2>&1 && return 0 + command -v certbot >/dev/null 2>&1 && return 0 + return 1 + } + + needs_full_install() { + # If browserbox binary missing, or core deps missing, we must do full install. + if ! command -v browserbox >/dev/null 2>&1; then + return 0 + fi + if ! has_browser_dep; then + return 0 + fi + if ! has_cert_dep; then + return 0 + fi + return 1 + } + + # 1. Loop Protection + if [[ -n "$BBX_INSTALL_GUARD" ]]; then + return 0 + fi + export BBX_INSTALL_GUARD="true" + + # 2. Path Detection + local is_update=false + if needs_full_install; then + is_update=false + else + is_update=true + fi + banner check_agreement + pre_install || return 0 load_config ensure_deps ensure_installation_id - - printf "${GREEN}Installing BrowserBox CLI (bbx)...${NC}\n" - - # Download and install the binary - local platform - platform=$(detect_platform) - local tag="${BBX_RELEASE_TAG:-}" - if [[ -z "$tag" ]]; then - tag=$(get_latest_release "$PUBLIC_REPO") + + printf "${GREEN}Installing BrowserBox CLI (bbx)...${NC}\n" + + local platform + platform=$(detect_platform) + local tag="${BBX_RELEASE_TAG:-}" + if [[ -z "$tag" ]]; then + if [[ -n "${BBX_NO_UPDATE:-}" ]]; then + echo -e "${RED}BBX_NO_UPDATE is set; provide BBX_RELEASE_TAG to install without hitting release APIs.${NC}" >&2 + return 1 fi - download_binary "$platform" "$tag" - - # Get hostname and email for setup + tag=$(get_latest_release "$PUBLIC_REPO") + fi + + # 3. Idempotency & Download + local current_ver + current_ver=$(get_binary_version) + local norm_tag="${tag#v}" + local norm_curr="${current_ver#v}" + + local exe_to_run="" + + if [[ "$norm_curr" == "$norm_tag" ]] && binary_exists; then + printf "${GREEN}BrowserBox binary $tag is already installed.${NC}\n" + exe_to_run="$BINARY_PATH" + else + exe_to_run=$(download_binary "$platform" "$tag") + exit_status=$? + + # Sanitize: output should be a single line path + exe_to_run=$(echo "$exe_to_run" | tail -n1 | tr -d '[:space:]') + + if [[ $exit_status -ne 0 ]] || [[ -z "$exe_to_run" ]] || [[ ! -f "$exe_to_run" ]]; then + printf "${RED}Download failed.${NC}\n" + if [[ -n "$BBX_DEBUG" ]]; then echo "Debug: Download output was: $exe_to_run" >&2; fi + exit 1 + fi + fi + + # 4. Config inputs local default_hostname=$(get_system_hostname) if [ -z "$BBX_HOSTNAME" ]; then - if [[ -n "$BBX_TEST_AGREEMENT" ]]; then + if [[ -n "$BBX_TEST_AGREEMENT" ]]; then BBX_HOSTNAME="localhost" else read -r -p "Enter hostname (default: $default_hostname): " BBX_HOSTNAME fi fi BBX_HOSTNAME="${BBX_HOSTNAME:-$default_hostname}" - - STRICTNESS="mandatory" + + local strictness="mandatory" if is_local_hostname "$BBX_HOSTNAME"; then - STRICTNESS="optional" + strictness="optional" ensure_hosts_entry "$BBX_HOSTNAME" fi - + if [ -z "$EMAIL" ]; then - if [[ -n "$BBX_TEST_AGREEMENT" ]]; then + if [[ -n "$BBX_TEST_AGREEMENT" ]]; then EMAIL="" else - read -r -p "Enter your email for Let's Encrypt ($STRICTNESS for $BBX_HOSTNAME): " EMAIL + read -r -p "Enter your email for Let's Encrypt ($strictness for $BBX_HOSTNAME): " EMAIL fi - if [[ "$STRICTNESS" == "mandatory" ]] && [[ -z "$EMAIL" ]]; then - echo "An email is required for a public DNS hostname in order to provision the TLS certificate from Let's Encrypt." >&2 - echo "Exiting..." >&2 + if [[ "$strictness" == "mandatory" ]] && [[ -z "$EMAIL" ]]; then + echo "An email is required for a public DNS hostname." >&2 exit 1 fi fi - - # Run the binary full installation process - printf "${YELLOW}Running BrowserBox full installer...${NC}\n" - if [ -t 0 ] && [[ -z "$BBX_TEST_AGREEMENT" ]]; then - "$BINARY_PATH" --full-install "$BBX_HOSTNAME" "$EMAIL" + + # 5. Execute + if [[ "$is_update" == "true" ]]; then + printf "${YELLOW}BrowserBox detected in PATH. Running update setup (--install)...${NC}\n" + "$exe_to_run" --install else - yes | "$BINARY_PATH" --full-install "$BBX_HOSTNAME" "$EMAIL" + printf "${YELLOW}BrowserBox not found in PATH. Running full setup (--full-install)...${NC}\n" + if [ -t 0 ] && [[ -z "$BBX_TEST_AGREEMENT" ]]; then + yes yes 2>/dev/null | "$exe_to_run" --full-install "$BBX_HOSTNAME" "$EMAIL" + else + # FIX: Silence 'yes' stderr to prevent "Broken pipe" logs + yes yes 2>/dev/null | "$exe_to_run" --full-install "$BBX_HOSTNAME" "$EMAIL" + fi fi - [ $? -eq 0 ] || { printf "${RED}Installation failed${NC}\n"; exit 1; } - - printf "${YELLOW}Installing bbx command globally...${NC}\n" - $SUDO curl --connect-timeout 7 --max-time 15 -sSL "$REPO_URL/raw/${branch}/bbx.sh" -o "$BBX_BIN" || { printf "${RED}Failed to install bbx${NC}\n"; $SUDO rm -f "$BBX_BIN"; exit 1; } - $SUDO chmod +x "$BBX_BIN" + local install_exit=$? + + # Cleanup temp file + if [[ "$exe_to_run" != "$BINARY_PATH" && -f "$exe_to_run" ]]; then + rm -f "$exe_to_run" + fi + + [ $install_exit -eq 0 ] || { printf "${RED}Installation failed${NC}\n"; exit 1; } + + if [[ -z "$BBX_TEST_AGREEMENT" ]]; then + printf "${YELLOW}Installing bbx command globally...${NC}\n" + $SUDO curl --connect-timeout 7 --max-time 15 -sSL "$REPO_URL/raw/${branch}/bbx.sh" -o "$BBX_BIN" || { printf "${RED}Failed to install bbx${NC}\n"; $SUDO rm -f "$BBX_BIN"; exit 1; } + $SUDO chmod +x "$BBX_BIN" + fi + save_config - printf "${GREEN}bbx $BBX_VERSION installed successfully! Run 'bbx --help' for usage.${NC}\n" + + # FIX: Re-read the version from the newly installed binary for the success message + local final_ver + if command -v bbpro >/dev/null 2>&1; then + final_ver=$(browserbox --version 2>/dev/null | grep -oE '[0-9]+(\.[0-9]+)+' | head -n1) + fi + # Fallback to tag if binary check fails + final_ver="${final_ver:-$tag}" + + printf "${GREEN}bbx $final_ver installed successfully! Run 'bbx --help' for usage.${NC}\n" } setup() { @@ -1500,9 +1759,9 @@ setup() { fi # Call setup_bbpro, which writes to test.env - LICENSE_KEY="${LICENSE_KEY}" browserbox --setup "${setup_args[@]}" || { printf "${RED}Setup failed${NC}\n"; exit 1; } + LICENSE_KEY="${LICENSE_KEY}" setup_bbpro "${setup_args[@]}" || { printf "${RED}Setup failed${NC}\n"; exit 1; } - # After browserbox --setup succeeds, reload config to get the new runtime values + # After setup_bbpro succeeds, reload config to get the new runtime values load_config printf "${GREEN}Setup complete.${NC}\n" @@ -1546,13 +1805,13 @@ run() { # Default values from loaded config local port="${PORT}" local hostname="${BBX_HOSTNAME}" - local run_args=() # Store args to pass to browserbox + local run_args=() # Store args to pass to bbpro # Parse arguments to override config for this run only local temp_args=("$@") local clean_args=() for arg in "${temp_args[@]}"; do - # This is a simple way to filter out --port from being passed to browserbox + # This is a simple way to filter out --port from being passed to bbpro # A more robust solution would handle --port=value too if [[ "$arg" != "--port" && "$arg" != "-p" && ! "$arg" =~ ^[0-9]+$ ]]; then clean_args+=("$arg") @@ -1581,7 +1840,7 @@ run() { shift 2 ;; *) - # Pass unknown args to browserbox + # Pass unknown args to bbpro run_args+=("$1") shift ;; @@ -1621,8 +1880,8 @@ run() { fi export HOST_PER_SERVICE BBX_HTTP_ONLY; - # Pass any extra args to browserbox - run_quietly browserbox "${run_args[@]}" || { printf "${RED}Failed to start${NC}\n"; exit 1; } + # Pass any extra args to bbpro + run_quietly bbpro "${run_args[@]}" || { printf "${RED}Failed to start${NC}\n"; exit 1; } # Reload config to get the final token from the newly created test.env load_config @@ -1681,14 +1940,26 @@ tor_run() { printf "${YELLOW}Starting BrowserBox with ${NC}${PURPLE}Tor${NC}${YELLOW}...${NC}\n" ensure_setup_tor "$(whoami)" - # Determine Tor group and cookie file dynamically + # Find Tor cookie file dynamically across platforms + COOKIE_AUTH_FILE=$(find_tor_cookie) + if [[ -z "$COOKIE_AUTH_FILE" ]]; then + # Fallback to expected locations if not found + if [[ "$(uname -s)" == "Darwin" ]]; then + TORDIR="$(brew --prefix)/var/lib/tor" + else + TORDIR="/var/lib/tor" + fi + COOKIE_AUTH_FILE="$TORDIR/control_auth_cookie" + printf "${YELLOW}Warning: Tor cookie file not found in common locations. Expected at: $COOKIE_AUTH_FILE${NC}\n" + else + TORDIR="$(dirname "$COOKIE_AUTH_FILE")" + printf "${GREEN}Found Tor cookie at: $COOKIE_AUTH_FILE${NC}\n" + fi + + # Determine Tor group dynamically if [[ "$(uname -s)" == "Darwin" ]]; then TOR_GROUP="admin" # Homebrew default - TORDIR="$(brew --prefix)/var/lib/tor" - COOKIE_AUTH_FILE="$TORDIR/control_auth_cookie" else - TORDIR="/var/lib/tor" - COOKIE_AUTH_FILE="$TORDIR/control_auth_cookie" TOR_GROUP=$(ls -ld "$TORDIR" | awk '{print $4}' 2>/dev/null) if [[ -z "$TOR_GROUP" || "$TOR_GROUP" == "root" ]]; then TOR_GROUP=$(getent group | grep -E 'tor|debian-tor|toranon' | cut -d: -f1 | head -n1) @@ -1708,7 +1979,7 @@ tor_run() { printf "${YELLOW}sg not found and $user not in $TOR_GROUP, may fail without Tor group access${NC}\n" fi - local setup_cmd="browserbox --setup --port $PORT --token $TOKEN" + local setup_cmd="setup_bbpro --port $PORT --token $TOKEN" if $anonymize; then setup_cmd="$setup_cmd --ontor" fi @@ -1757,7 +2028,7 @@ tor_run() { test_port_access $((PORT+i)) || { printf "${RED}Quit software using these ports, or adjust firewall for ports $((PORT-2))-$((PORT+2))/tcp${NC}\n"; exit 1; } done test_port_access $((PORT-3000)) || { printf "${RED}CDP port $((PORT-3000)) blocked${NC}\n"; exit 1; } - browserbox || { printf "${RED}Failed to start${NC}\n"; exit 1; } + bbpro || { printf "${RED}Failed to start${NC}\n"; exit 1; } login_link=$(cat "$BB_CONFIG_DIR/login.link" 2>/dev/null || echo "https://$TEMP_HOSTNAME:$PORT/login?token=$TOKEN") fi sleep 2 @@ -2158,9 +2429,9 @@ cf_run() { exit 1 } - # Run minimal setup using browserbox --setup with HTTP backend + # Run minimal setup using setup_bbpro with HTTP backend printf "${YELLOW}Setting up BrowserBox on port ${port} with HTTP backend...${NC}\n" - LICENSE_KEY="${LICENSE_KEY}" browserbox --setup --port "$port" --token "$TOKEN" --backend http || { + LICENSE_KEY="${LICENSE_KEY}" setup_bbpro --port "$port" --token "$TOKEN" --backend http || { printf "${RED}Setup failed${NC}\n" exit 1 } @@ -2186,9 +2457,9 @@ cf_run() { fi fi - # Start BrowserBox via run_quietly browserbox + # Start BrowserBox via run_quietly bbpro printf "${YELLOW}Starting BrowserBox on 127.0.0.1:${PORT}...${NC}\n" - run_quietly browserbox || { printf "${RED}Failed to start BrowserBox${NC}\n"; exit 1; } + run_quietly bbpro || { printf "${RED}Failed to start BrowserBox${NC}\n"; exit 1; } # Reload config to capture final token load_config @@ -2203,7 +2474,7 @@ cf_run() { cleanup_cf_run() { printf "\n${YELLOW}Stopping BrowserBox and Cloudflare tunnel...${NC}\n" kill $cf_pid 2>/dev/null || true - run_quietly browserbox --stop || true + run_quietly stop_bbpro || true printf "${GREEN}Cleanup complete.${NC}\n" } # Set trap for cleanup immediately after function definition @@ -2447,9 +2718,9 @@ docker_stop() { fi printf "${YELLOW}Stopping BrowserBox for $nickname ($container_id)...${NC}\n" - docker exec "$container_id" bash -c "browserbox --stop" || - $SUDO docker exec "$container_id" bash -c "browserbox --stop" || { - printf "${RED}Warning: Failed to run browserbox --stop in container${NC}\n" + docker exec "$container_id" bash -c "stop_bbpro" || + $SUDO docker exec "$container_id" bash -c "stop_bbpro" || { + printf "${RED}Warning: Failed to run stop_bbpro in container${NC}\n" } printf "${YELLOW}Waiting 1 second for license release...${NC}\n" sleep 1 @@ -2528,6 +2799,14 @@ pre_install() { # Prompt for a non-root user to run the install as if [ -z "$BBX_INSTALL_USER" ]; then + # Check if we're in non-interactive mode (CI/CD) + if [[ -n "$BBX_TEST_AGREEMENT" ]] || [ ! -t 0 ]; then + printf "${RED}ERROR: Running as root in non-interactive mode requires BBX_INSTALL_USER environment variable${NC}\n" + printf "${BLUE}Please set BBX_INSTALL_USER to a non-root username (e.g., export BBX_INSTALL_USER=bbxuser)${NC}\n" + printf "${YELLOW}Example: export BBX_INSTALL_USER=bbxuser && ./bbx.sh install${NC}\n" + exit 1 + fi + # Interactive mode - prompt for username read -p "Enter a regular user to run the installation: " install_user if [ -z "$install_user" ]; then printf "${RED}ERROR: A username is required${NC}\n" @@ -2596,21 +2875,33 @@ pre_install() { create_master_user "$install_user" fi - # Download the install script using curl and save it to a file - echo "Downloading the installation script..." - curl -sSL "https://raw.githubusercontent.com/${owner_repo}/refs/heads/$branch/bbx.sh" -o /tmp/bbx.sh + cp -f "$0" /tmp/bbx.sh chmod +x /tmp/bbx.sh install_group="$(id -gn "$install_user")" chown "${install_user}:${install_group}" /tmp/bbx.sh + # Build a temp env file to persist BBX-related vars across the login shell. + local su_env_vars=(BBX_HOSTNAME EMAIL LICENSE_KEY BBX_TEST_AGREEMENT STATUS_MODE INSTALL_DOC_VIEWER BBX_NO_UPDATE BBX_RELEASE_REPO BBX_RELEASE_TAG TARGET_RELEASE_REPO PRIVATE_TAG GH_TOKEN GITHUB_TOKEN BBX_INSTALL_USER BB_QUICK_EXIT) + local env_file + env_file="$(mktemp)" + local var val + for var in "${su_env_vars[@]}"; do + val="${!var-}" + [[ -n "$val" ]] || continue + printf '%s=%q\n' "$var" "$val" >> "$env_file" + done + chown "${install_user}:${install_group}" "$env_file" 2>/dev/null || true + chmod 640 "$env_file" 2>/dev/null || true + # Switch to the non-root user and run install echo "Switching to user $install_user..." - su - "$install_user" -c "export BBX_HOSTNAME=\"$BBX_HOSTNAME\"; export EMAIL=\"$EMAIL\"; export LICENSE_KEY=\"$LICENSE_KEY\"; export BBX_TEST_AGREEMENT=\"$BBX_TEST_AGREEMENT\"; export STATUS_MODE=\"$STATUS_MODE\"; /tmp/bbx.sh install" + su - "$install_user" -c "set -a; source $(printf '%q' "$env_file"); /tmp/bbx.sh install" if [[ -z "$BBX_TEST_AGREEMENT" ]] || [ -t 0 ]; then # Replace the root shell with the new user's shell - exec su - "$install_user" -c "export BBX_HOSTNAME=\"$BBX_HOSTNAME\"; export EMAIL=\"$EMAIL\"; export LICENSE_KEY=\"$LICENSE_KEY\"; export BBX_TEST_AGREEMENT=\"$BBX_TEST_AGREEMENT\"; export STATUS_MODE=\"$STATUS_MODE\"; bash -l" + exec su - "$install_user" -c "set -a; source $(printf '%q' "$env_file"); rm -f $(printf '%q' "$env_file"); bash -l" else + rm -f "$env_file" return 1 fi else @@ -2756,13 +3047,18 @@ ng_run() { load_config fi - # Always run setup_nginx for ng-run + # Always run setup_nginx for ng-run (it's a standalone command installed in PATH) printf "${YELLOW}Starting Nginx setup...${NC}\n" - if ! setup_nginx; then - printf "${RED}Nginx setup failed. Aborting.${NC}\n" - exit 1 + if command -v setup_nginx &>/dev/null; then + if ! setup_nginx; then + printf "${RED}Nginx setup failed. Aborting.${NC}\n" + exit 1 + fi + printf "${GREEN}Nginx setup complete.${NC}\n" + else + printf "${YELLOW}Warning: setup_nginx command not found. Nginx setup skipped.${NC}\n" + printf "${YELLOW}This command should have been installed during 'bbx install'. Try reinstalling.${NC}\n" fi - printf "${GREEN}Nginx setup complete.${NC}\n" # Now, call the main run command, passing all original arguments. # The run command will handle calling setup if it's the very first run. @@ -2772,19 +3068,14 @@ ng_run() { stop() { load_config printf "${YELLOW}Stopping BrowserBox (current user)...${NC}\n" - run_quietly browserbox --stop || { printf "${RED}Failed to stop. Check if BrowserBox is running.${NC}\n"; exit 1; } + run_quietly stop_bbpro || { printf "${RED}Failed to stop. Check if BrowserBox is running.${NC}\n"; exit 1; } printf "${GREEN}BrowserBox stopped.${NC}\n" } logs() { printf "${YELLOW}Displaying BrowserBox logs...${NC}\n" - ensure_nvm - if command -v pm2 >/dev/null; then - pm2 logs || { printf "${RED}pm2 logs failed${NC}\n"; exit 1; } - else - printf "${RED}pm2 not found. Install pm2 (npm i -g pm2) or check logs manually.${NC}\n" - exit 1 - fi + bbpro pm2 list || printf "${YELLOW}bbpro pm2 list failed (shim may not be running).${NC}\n" + printf "${YELLOW}Tail a service log with:${NC} bbpro pm2 logs bb-main --lines 50\n" } # Helper function to convert epoch time to a timestamp format for touch -t @@ -2985,8 +3276,6 @@ update() { fi load_config - - # Arg parsing: none | --latest-rc | local arg="${1:-}" local repo_tag="" @@ -2995,17 +3284,11 @@ update() { repo_tag="$(get_latest_release "$PUBLIC_REPO")" elif [[ "$arg" == "--latest-rc" ]]; then printf "${YELLOW}Updating BrowserBox to latest release candidate...${NC}\n" - # For binary distribution, we get latest release (not necessarily RC) repo_tag="$(get_latest_release "$PUBLIC_REPO")" - if [[ "$repo_tag" == unknown* ]]; then - printf "${RED}No releases found.${NC}\n"; return 1 - fi else - # explicit version/tag local tag="$(normalize_tag "$arg")" if [[ -z "$tag" ]]; then printf "${RED}Invalid version: '%s'${NC}\n" "$arg" - printf "Expected formats: v1.2.3 | 1.2.3 | v1.2.3-rc | v1.2.3-rc.1\n" return 1 fi repo_tag="$tag" @@ -3013,21 +3296,41 @@ update() { fi if [[ "$repo_tag" == unknown* ]] || [[ -z "$repo_tag" ]]; then - printf "${RED}Could not determine version to update to. Check network or GH API rate limits.${NC}\n" + printf "${RED}Could not determine version to update to.${NC}\n" return 1 fi - # Download and install the binary update local platform platform=$(detect_platform) - download_binary "$platform" "$repo_tag" - - # Run internal updates/migrations + + # Download + local download_output + download_output=$(download_binary "$platform" "$repo_tag") + local exit_status=$? + + local temp_exe + temp_exe=$(echo "$download_output" | tail -n1 | tr -d '[:space:]') + + if [[ $exit_status -ne 0 ]] || [[ -z "$temp_exe" ]] || [[ ! -f "$temp_exe" ]]; then + printf "${RED}Download failed.${NC}\n" + if [[ -n "$BBX_DEBUG" ]]; then echo "Debug: $download_output" >&2; fi + return 1 + fi + + # Execute printf "${YELLOW}Running post-update installation tasks...${NC}\n" - "$BINARY_PATH" --install || { printf "${RED}Post-update installation failed${NC}\n"; return 1; } - - printf "${GREEN}BrowserBox updated to ${repo_tag}${NC}\n" - return 0 + "$temp_exe" --install + local install_exit=$? + + rm -f "$temp_exe" + + if [ $install_exit -eq 0 ]; then + printf "${GREEN}BrowserBox updated to ${repo_tag}${NC}\n" + return 0 + else + printf "${RED}Post-update installation failed${NC}\n" + return 1 + fi } update_background() { @@ -3044,7 +3347,6 @@ update_background() { # default channel = stable repo_tag="$(get_latest_repo_version stable)" fi - printf "${YELLOW}Checking update lock...${NC}\n" >> "$LOG_FILE" # Check lock files if is_lock_file_recent "$PREPARING_FILE"; then @@ -3072,8 +3374,8 @@ update_background() { platform=$(detect_platform) local asset_name case "$platform" in - macos) asset_name="browserbox-macos-bin" ;; - linux) asset_name="browserbox-linux-bin" ;; + macos) asset_name="browserbox-macos-arm64" ;; + linux) asset_name="browserbox-linux-x64" ;; *) printf "${RED}Unsupported platform: $platform${NC}\n" >> "$LOG_FILE" $SUDO rm -f "$PREPARING_FILE" @@ -3085,9 +3387,14 @@ update_background() { local temp_binary="$BBX_NEW_DIR/browserbox" printf "${YELLOW}Downloading binary from $download_url...${NC}\n" >> "$LOG_FILE" + + local curl_auth=() + if [[ -n "${GH_TOKEN:-}" ]]; then + curl_auth=(-H "Authorization: token ${GH_TOKEN}") + fi # Use curl directly (no INSTALL_CMD/sudo) to avoid background sudo prompts - curl -L --fail --progress-bar --connect-timeout 30 -o "$temp_binary" "$download_url" >> "$LOG_FILE" 2>&1 || { + curl -L --fail --progress-bar --connect-timeout 30 "${curl_auth[@]}" -o "$temp_binary" "$download_url" >> "$LOG_FILE" 2>&1 || { printf "${YELLOW}Skipping update due to timeout or failure in connecting to BrowserBox repo${NC}\n" >> "$LOG_FILE" $SUDO rm -f "$PREPARING_FILE" rm -f "$temp_binary" 2>/dev/null @@ -3103,7 +3410,7 @@ update_background() { rm -rf "$BBX_NEW_DIR" return 1 fi - + # Make binary executable chmod +x "$temp_binary" || { printf "${RED}Failed to make binary executable${NC}\n" >> "$LOG_FILE" @@ -3199,19 +3506,19 @@ stop_user() { # Cancel existing 'at' jobs for this user existing_jobs=$(atq | awk '{print $1}') for job in $existing_jobs; do - if at -c "$job" | grep -q "browserbox --stop.*$user"; then + if at -c "$job" | grep -q "stop_bbpro.*$user"; then atrm "$job" fi done - # Schedule browserbox --stop - echo "$SUDO -u \"$user\" browserbox --stop" | at now + "${delay_minutes}" minutes 2>/dev/null + # Schedule stop_bbpro + echo "$SUDO -u \"$user\" stop_bbpro" | at now + "${delay_minutes}" minutes 2>/dev/null # Update expiry time local new_expiry_timestamp=$((current_time + delay_seconds)) $SUDO -u "$user" bash -c "mkdir -p \"${home_dir}/.config/dosyago/bbpro\"; echo \"$new_expiry_timestamp\" > \"$expiry_file\"" printf "${GREEN}Scheduled stop for $user at $new_expiry_timestamp${NC}\n" else # Immediate stop - $SUDO -u "$user" bash -c "PATH=/usr/local/bin:\$PATH browserbox --stop" 2>/dev/null || { printf "${RED}Failed to stop BrowserBox for $user${NC}\n"; exit 1; } + $SUDO -u "$user" bash -c "PATH=/usr/local/bin:\$PATH stop_bbpro" 2>/dev/null || { printf "${RED}Failed to stop BrowserBox for $user${NC}\n"; exit 1; } printf "${GREEN}BrowserBox stopped for $user${NC}\n" fi @@ -3385,8 +3692,8 @@ run_as() { # Generate fresh token TOKEN=$(openssl rand -hex 16) - # Run browserbox --setup with explicit PATH and fresh token, redirecting output as the target user - $SUDO -u "$user" bash -c "PATH=/usr/local/bin:\$PATH LICENSE_KEY="${LICENSE_KEY}" browserbox --setup --port $port --token $TOKEN > ~/.config/dosyago/bbpro/setup_output.txt 2>&1" || { printf "${RED}Setup failed for $user${NC}\n"; $SUDO cat "$HOME_DIR/.config/dosyago/bbpro/setup_output.txt"; exit 1; } + # Run setup_bbpro with explicit PATH and fresh token, redirecting output as the target user + $SUDO -u "$user" bash -c "PATH=/usr/local/bin:\$PATH LICENSE_KEY="${LICENSE_KEY}" setup_bbpro --port $port --token $TOKEN > ~/.config/dosyago/bbpro/setup_output.txt 2>&1" || { printf "${RED}Setup failed for $user${NC}\n"; $SUDO cat "$HOME_DIR/.config/dosyago/bbpro/setup_output.txt"; exit 1; } # Use caller's LICENSE_KEY if [ -z "$LICENSE_KEY" ]; then @@ -3512,7 +3819,7 @@ win9x_run() { export BBX_DONT_KILL_CHROME_ON_STOP="true" # Setup with explicit token - local setup_cmd="browserbox --setup --port $PORT --token $TOKEN" + local setup_cmd="setup_bbpro --port $PORT --token $TOKEN" LICENSE_KEY="${LICENSE_KEY}" $setup_cmd &>/dev/null || { printf "${RED}Setup failed${NC}\n"; exit 1; } # Reload config to get updated values @@ -3534,11 +3841,11 @@ win9x_run() { fi fi - # Start browserbox in background, redirecting output to suppress banner + # Start bbpro in background, redirecting output to suppress banner printf "${YELLOW}Starting BrowserBox server (silent mode)...${NC}\n" export WIN9X_COMPATIBILITY_MODE="true" export BBX_DONT_KILL_CHROME_ON_STOP="true" - nohup browserbox > /dev/null 2>&1 & + nohup bbpro > /dev/null 2>&1 & # Wait for server to start (allows time for initialization and login.link generation) local startup_wait=8 @@ -3746,7 +4053,7 @@ activate() { # Call check_and_prepare_update with the first argument [ -n "$BBX_NO_UPDATE" ] || check_and_prepare_update "$1" case "$1" in - install) shift 1; install "$@";; + install) shift 1; install_bbx "$@";; uninstall) shift 1; uninstall "$@";; setup) shift 1; setup "$@";; certify) shift 1; certify "$@";; diff --git a/windows-scripts/bbx.ps1 b/windows-scripts/bbx.ps1 index 710a14adc..3f8ef0aa8 100755 --- a/windows-scripts/bbx.ps1 +++ b/windows-scripts/bbx.ps1 @@ -2,21 +2,72 @@ # This script downloads and runs pre-compiled BrowserBox binaries # from the public BrowserBox/BrowserBox repository. -[CmdletBinding()] -param ( - [Parameter(Position=0)] - [string]$Command, - [Parameter(ValueFromRemainingArguments)] - [string[]]$Args -) - $ErrorActionPreference = "Stop" +$global:LASTEXITCODE = 0 + +if ($env:BBX_DEBUG_CLI -and $env:BBX_DEBUG_CLI -ne "0" -and $env:BBX_DEBUG_CLI.ToLowerInvariant() -ne "false") { + try { + Write-Host "[bbx] PSVersion: $($PSVersionTable.PSVersion)" -ForegroundColor DarkGray + } catch { } + Write-Host "[bbx] Script: $($MyInvocation.MyCommand.Path)" -ForegroundColor DarkGray +} + # Configuration $PublicRepo = "BrowserBox/BrowserBox" +$ReleaseRepo = if ($env:BBX_RELEASE_REPO) { $env:BBX_RELEASE_REPO } else { $PublicRepo } +$Token = if ($env:GH_TOKEN) { $env:GH_TOKEN } elseif ($env:GITHUB_TOKEN) { $env:GITHUB_TOKEN } else { "" } +$NoUpdate = $false +if ($null -ne $env:BBX_NO_UPDATE -and $env:BBX_NO_UPDATE -ne "") { + try { + $NoUpdate = [System.Convert]::ToBoolean($env:BBX_NO_UPDATE) + } catch { + $NoUpdate = ($env:BBX_NO_UPDATE.ToLowerInvariant() -in @("1", "true", "yes", "y", "on")) + } +} $BinaryDir = "$env:LOCALAPPDATA\browserbox\bin" + +# Local Name (on disk) $BinaryName = "browserbox.exe" +# Remote Name (on GitHub Release) +$RemoteAssetName = "browserbox-win-x64.exe" + $BinaryPath = Join-Path $BinaryDir $BinaryName +$script:ResolvedBinaryPath = $null + +$ScriptMap = @{ + "install" = "install.ps1" + "update" = "update.ps1" + "setup" = "setup.ps1" + "run" = "start.ps1" + "start" = "start.ps1" + "stop" = "stop.ps1" + "certify" = "certify.ps1" + "prepare" = "prepare.ps1" + "uninstall" = "uninstall.ps1" +} + +function Resolve-BrowserBoxBinary { + if ($env:BBX_BINARY_PATH -and (Test-Path $env:BBX_BINARY_PATH)) { + return $env:BBX_BINARY_PATH + } + + if (Test-Path $BinaryPath) { + return $BinaryPath + } + + $cmd = Get-Command $BinaryName -ErrorAction SilentlyContinue + if ($cmd -and $cmd.Path -and (Test-Path $cmd.Path)) { + return $cmd.Path + } + + $cmd = Get-Command "browserbox" -ErrorAction SilentlyContinue + if ($cmd -and $cmd.Path -and (Test-Path $cmd.Path)) { + return $cmd.Path + } + + return $null +} # Function to ensure binary directory exists function Ensure-BinaryDir { @@ -28,17 +79,36 @@ function Ensure-BinaryDir { # Function to get the latest release tag from GitHub function Get-LatestRelease { param([string]$Repo) + + if ($NoUpdate) { + if ($env:BBX_RELEASE_TAG) { return $env:BBX_RELEASE_TAG } + return $null + } - $apiUrl = "https://api.github.com/repos/$Repo/releases/latest" + $headers = @{} + if ($Token) { $headers["Authorization"] = "Bearer $Token" } + # 1. Try "Latest" endpoint (works for Stable) try { - $response = Invoke-RestMethod -Uri $apiUrl -TimeoutSec 10 -ErrorAction Stop + $apiUrl = "https://api.github.com/repos/$Repo/releases/latest" + $response = Invoke-RestMethod -Uri $apiUrl -TimeoutSec 10 -Headers $headers -ErrorAction Stop return $response.tag_name } catch { - Write-Error "Failed to fetch latest release from $Repo : $_" - exit 1 + # 2. Fallback: List releases (needed for Drafts/Prereleases sometimes invisible to 'latest') + try { + Write-Host "Latest release lookup failed (check for drafts), checking release list..." -ForegroundColor Gray + $apiUrl = "https://api.github.com/repos/$Repo/releases?per_page=1" + $response = Invoke-RestMethod -Uri $apiUrl -TimeoutSec 10 -Headers $headers -ErrorAction Stop + if ($response -and $response.Count -gt 0) { + return $response[0].tag_name + } + } catch { + Write-Error "Failed to fetch latest release from $Repo : $_" + exit 1 + } } + return $null } # Function to download the binary @@ -49,19 +119,68 @@ function Download-Binary { Ensure-BinaryDir - $assetName = "browserbox.exe" - $downloadUrl = "https://github.com/$PublicRepo/releases/download/$Tag/$assetName" $tempFile = "$BinaryPath.tmp" Write-Host "Downloading BrowserBox $Tag for Windows..." -ForegroundColor Cyan + $headers = @{} + if ($Token) { $headers["Authorization"] = "Bearer $Token" } + + $useAssetApi = $Token -or ($ReleaseRepo -ne $PublicRepo) + if ($ReleaseRepo -ne $PublicRepo -and -not $Token) { + Write-Error "GH_TOKEN/GITHUB_TOKEN is required to download from private/internal repo $ReleaseRepo." + exit 1 + } + try { - # Use .NET WebClient for progress bar - $webClient = New-Object System.Net.WebClient - $webClient.DownloadFile($downloadUrl, $tempFile) - $webClient.Dispose() + if ($useAssetApi) { + # Try getting specific tag + $release = $null + try { + $release = Invoke-RestMethod -Uri "https://api.github.com/repos/$ReleaseRepo/releases/tags/$Tag" -Headers $headers -ErrorAction Stop + } catch { + # Fallback for Drafts: Scan list for matching tag + Write-Host "Direct tag lookup failed (common for drafts), scanning release list..." -ForegroundColor Gray + $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/$ReleaseRepo/releases" -Headers $headers -ErrorAction Stop + $release = $releases | Where-Object { $_.tag_name -eq $Tag } | Select-Object -First 1 + } + + if (-not $release) { + Write-Error "Release $Tag not found in $ReleaseRepo" + exit 1 + } + + # Look for browserbox-win-x64.exe OR browserbox.exe + $asset = $release.assets | Where-Object { $_.name -eq $RemoteAssetName -or $_.name -eq $BinaryName } | Select-Object -First 1 + + if (-not $asset) { + Write-Error "Asset $RemoteAssetName (or $BinaryName) not found on release $Tag" + exit 1 + } + + Write-Host "Found asset: $($asset.name)" -ForegroundColor Gray + + $assetUrl = "https://api.github.com/repos/$ReleaseRepo/releases/assets/$($asset.id)" + $headers["Accept"] = "application/octet-stream" + Invoke-WebRequest -Uri $assetUrl -Headers $headers -OutFile $tempFile -MaximumRedirection 5 -ErrorAction Stop | Out-Null + } + else { + # Public download path (fallback) + $downloadUrl = "https://github.com/$ReleaseRepo/releases/download/$Tag/$RemoteAssetName" + $webClient = New-Object System.Net.WebClient + if ($Token) { + $webClient.Headers.Add("Authorization", "Bearer $Token") | Out-Null + } + try { + $webClient.DownloadFile($downloadUrl, $tempFile) + } catch { + # Try fallback name + $downloadUrl = "https://github.com/$ReleaseRepo/releases/download/$Tag/$BinaryName" + $webClient.DownloadFile($downloadUrl, $tempFile) + } + $webClient.Dispose() + } - # Move to final location if (Test-Path $BinaryPath) { Remove-Item $BinaryPath -Force } @@ -70,7 +189,7 @@ function Download-Binary { Write-Host "Successfully downloaded and installed BrowserBox binary" -ForegroundColor Green } catch { - Write-Error "Failed to download binary from $downloadUrl : $_" + Write-Error "Failed to download binary for $Tag : $_" if (Test-Path $tempFile) { Remove-Item $tempFile -Force } @@ -80,7 +199,8 @@ function Download-Binary { # Function to check if binary exists function Test-BinaryExists { - Test-Path $BinaryPath + $script:ResolvedBinaryPath = Resolve-BrowserBoxBinary + return [bool]$script:ResolvedBinaryPath } # Function to ensure binary is installed @@ -88,8 +208,15 @@ function Ensure-Binary { if (-not (Test-BinaryExists)) { Write-Host "BrowserBox binary not found. Installing..." -ForegroundColor Yellow Ensure-BinaryDir - $tag = Get-LatestRelease -Repo $PublicRepo + if ($NoUpdate -and -not $env:BBX_RELEASE_TAG) { + Write-Error "BBX_NO_UPDATE is set; provide BBX_RELEASE_TAG to install without update lookups." + exit 1 + } + $tag = if ($env:BBX_RELEASE_TAG) { $env:BBX_RELEASE_TAG } else { Get-LatestRelease -Repo $ReleaseRepo } Download-Binary -Tag $tag + $script:ResolvedBinaryPath = $BinaryPath + } elseif (-not $script:ResolvedBinaryPath) { + $script:ResolvedBinaryPath = Resolve-BrowserBoxBinary } } @@ -105,7 +232,7 @@ function Get-SemverFromText { function Get-BinaryVersion { if (Test-BinaryExists) { try { - $output = & $BinaryPath "--version" 2>$null | Out-String + $output = & $script:ResolvedBinaryPath "--version" 2>$null | Out-String $semver = Get-SemverFromText -Text $output if ($semver) { return $semver } else { return "unknown" } } @@ -116,8 +243,185 @@ function Get-BinaryVersion { return "not_installed" } +# Reads KEY=VALUE lines from test.env +function Read-TestEnv { + param([string]$Path) + $cfg = @{} + if (-not (Test-Path $Path)) { return $cfg } + Get-Content $Path | ForEach-Object { + if ($_ -match "^([^=]+)=(.*)$") { + $cfg[$Matches[1].Trim()] = $Matches[2].Trim().Trim('"') + } + } + return $cfg +} + +function Get-ArgValue { + param( + [string[]]$ArgList, + [string]$Name + ) + for ($i = 0; $i -lt $ArgList.Count; $i++) { + if ($ArgList[$i] -ieq $Name -and ($i + 1) -lt $ArgList.Count) { + return $ArgList[$i + 1] + } + } + return $null +} + +function Get-ArgIntValue { + param( + [string[]]$ArgList, + [string]$Name, + [int]$DefaultValue + ) + $v = Get-ArgValue -ArgList $ArgList -Name $Name + if (-not $v) { return $DefaultValue } + $out = 0 + if ([int]::TryParse($v, [ref]$out)) { return $out } + return $DefaultValue +} + +function Get-ArgSwitch { + param( + [string[]]$ArgList, + [string]$Name + ) + return ($ArgList | Where-Object { $_ -ieq $Name } | Select-Object -First 1) -ne $null +} + +function Ensure-ConfigDir { + $cfgDir = Join-Path $env:USERPROFILE ".config\dosyago\bbpro" + New-Item -ItemType Directory -Path $cfgDir -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $cfgDir "tickets") -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $cfgDir "logs") -Force | Out-Null + return $cfgDir +} + +function Invoke-SetupLite { + param([string[]]$ArgList) + + $cfgDir = Ensure-ConfigDir + $testEnvPath = Join-Path $cfgDir "test.env" + + $hostname = (Get-ArgValue -ArgList $ArgList -Name "-Hostname") + if (-not $hostname) { $hostname = (Get-ArgValue -ArgList $ArgList -Name "--hostname") } + if (-not $hostname) { $hostname = "localhost" } + + $email = (Get-ArgValue -ArgList $ArgList -Name "-Email") + if (-not $email) { $email = (Get-ArgValue -ArgList $ArgList -Name "--email") } + + $port = Get-ArgIntValue -ArgList $ArgList -Name "-Port" -DefaultValue 8080 + if ($port -eq 8080) { $port = Get-ArgIntValue -ArgList $ArgList -Name "--port" -DefaultValue 8080 } + + $token = (Get-ArgValue -ArgList $ArgList -Name "-Token") + if (-not $token) { $token = (Get-ArgValue -ArgList $ArgList -Name "--token") } + if (-not $token) { $token = [System.Guid]::NewGuid().ToString() } + + $existing = Read-TestEnv -Path $testEnvPath + $licenseToKeep = if ($env:LICENSE_KEY) { $env:LICENSE_KEY } elseif ($existing.ContainsKey("LICENSE_KEY")) { $existing["LICENSE_KEY"] } else { $null } + + $appPort = $port + $audioPort = $port - 2 + $devtoolsPort = $port + 1 + $docsPort = $port - 1 + $cookieValue = if ($env:COOKIE_VALUE) { $env:COOKIE_VALUE } else { [System.Guid]::NewGuid().ToString() } + + $envContent = @( + "APP_PORT=$appPort" + "AUDIO_PORT=$audioPort" + "LOGIN_TOKEN=$token" + "COOKIE_VALUE=$cookieValue" + "DEVTOOLS_PORT=$devtoolsPort" + "DOCS_PORT=$docsPort" + "SSLCERTS_DIR=$($env:USERPROFILE)\\sslcerts" + "DOMAIN=$hostname" + ) + if ($licenseToKeep) { $envContent += "LICENSE_KEY=$licenseToKeep" } + $envContent -join "`r`n" | Out-File $testEnvPath -Encoding utf8 + + $sslDir = Join-Path $env:USERPROFILE "sslcerts" + $certExists = (Test-Path (Join-Path $sslDir "fullchain.pem")) -and (Test-Path (Join-Path $sslDir "privkey.pem")) + $scheme = if ($certExists) { "https" } else { "http" } + $loginLink = "${scheme}://${hostname}:${appPort}/login?token=${token}" + $loginLink | Out-File (Join-Path $cfgDir "login.link") -Encoding utf8 + + Write-Host "Setup complete." -ForegroundColor Green + Write-Host "Login link: $loginLink" -ForegroundColor Cyan +} + +function Start-BrowserBoxMainDetached { + $cfgDir = Ensure-ConfigDir + $testEnvPath = Join-Path $cfgDir "test.env" + if (-not (Test-Path $testEnvPath)) { + Write-Error "Configuration file not found at $testEnvPath. Run 'bbx setup' first." + exit 1 + } + + $cfg = Read-TestEnv -Path $testEnvPath + $appPort = if ($cfg.ContainsKey("APP_PORT")) { $cfg["APP_PORT"] } else { "8080" } + $loginToken = if ($cfg.ContainsKey("LOGIN_TOKEN")) { $cfg["LOGIN_TOKEN"] } else { "" } + + Ensure-Binary + Ensure-BinaryDir + + $logDir = Join-Path $cfgDir "logs" + New-Item -ItemType Directory -Path $logDir -Force | Out-Null + $outLog = Join-Path $logDir "browserbox-main-out.log" + $errLog = Join-Path $logDir "browserbox-main-err.log" + $pidFile = Join-Path $cfgDir "browserbox-main.pid" + + $env:BB_CONFIG_DIR = $cfgDir + if ($cfg.ContainsKey("LICENSE_KEY") -and -not $env:LICENSE_KEY) { $env:LICENSE_KEY = $cfg["LICENSE_KEY"] } + if ($cfg.ContainsKey("DOMAIN")) { $env:BBX_HOSTNAME = $cfg["DOMAIN"] } + + Write-Verbose "Starting BrowserBox main detached (port=$appPort token=$loginToken)..." + $proc = Start-Process -FilePath $script:ResolvedBinaryPath -ArgumentList @("main") -NoNewWindow -RedirectStandardOutput $outLog -RedirectStandardError $errLog -PassThru + $proc.Id | Out-File $pidFile -Encoding ascii -Force + + Write-Host "BrowserBox main started (PID: $($proc.Id))." -ForegroundColor Green +} + +function Stop-BrowserBoxMain { + $cfgDir = Ensure-ConfigDir + $testEnvPath = Join-Path $cfgDir "test.env" + $pidFile = Join-Path $cfgDir "browserbox-main.pid" + + $cfg = Read-TestEnv -Path $testEnvPath + $appPort = if ($cfg.ContainsKey("APP_PORT")) { $cfg["APP_PORT"] } else { $null } + $loginToken = if ($cfg.ContainsKey("LOGIN_TOKEN")) { $cfg["LOGIN_TOKEN"] } else { $null } + + if ($appPort -and $loginToken -and (Get-Command curl.exe -ErrorAction SilentlyContinue)) { + try { + $null = & curl.exe -k -sS -o NUL -X POST "https://localhost:${appPort}/api/v15/stop_app?session_token=${loginToken}" + } catch { } + try { + $null = & curl.exe -sS -o NUL -X POST "http://localhost:${appPort}/api/v15/stop_app?session_token=${loginToken}" + } catch { } + } + + $pid = $null + if (Test-Path $pidFile) { + $raw = (Get-Content $pidFile -ErrorAction SilentlyContinue | Out-String).Trim() + $tmp = 0 + if ([int]::TryParse($raw, [ref]$tmp)) { $pid = $tmp } + } + + if ($pid -and (Get-Process -Id $pid -ErrorAction SilentlyContinue)) { + Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue + Write-Host "Stopped BrowserBox main (PID: $pid)." -ForegroundColor Green + } else { + # Fallback: stop any browserbox.exe processes owned by this user + Get-Process -Name "browserbox" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + Write-Host "Stopped browserbox processes (fallback)." -ForegroundColor Yellow + } + + if (Test-Path $pidFile) { Remove-Item $pidFile -Force -ErrorAction SilentlyContinue } +} + # Function to check for updates function Check-Update { + if ($NoUpdate) { return } if (Test-BinaryExists) { $currentVersion = Get-BinaryVersion @@ -126,7 +430,7 @@ function Check-Update { } try { - $latestTag = Get-LatestRelease -Repo $PublicRepo + $latestTag = Get-LatestRelease -Repo $ReleaseRepo $latestNorm = $latestTag -replace '^[vV]' $currentNorm = $currentVersion -replace '^[vV]' if ($latestNorm -and $currentNorm -and $latestNorm -ne $currentNorm) { @@ -148,12 +452,104 @@ function Show-Help { Write-Host "Core Commands:" -ForegroundColor Cyan Write-Host " install Install BrowserBox binary and CLI" -ForegroundColor White Write-Host " update Update BrowserBox to the latest version" -ForegroundColor White + Write-Host " setup Create/update test.env + login.link" -ForegroundColor White + Write-Host " run Start BrowserBox main (detached)" -ForegroundColor White + Write-Host " stop Stop BrowserBox main (best-effort)" -ForegroundColor White + Write-Host " certify Validate license and obtain ticket" -ForegroundColor White + Write-Host " uninstall Remove BrowserBox from this machine" -ForegroundColor White + Write-Host " revalidate Clear ticket and revalidate license" -ForegroundColor White Write-Host " --version, -v Show version information" -ForegroundColor White Write-Host " --help, -h Show this help message" -ForegroundColor White - Write-Host " revalidate Clear ticket and revalidate license" -ForegroundColor White Write-Host "" Write-Host "All other commands are passed through to the browserbox binary." -ForegroundColor Gray Write-Host "Run 'browserbox --help' after installation for full command list." -ForegroundColor Gray + Write-Host "Run 'bbx -help' for command-specific options." -ForegroundColor Gray +} + +function Normalize-CommandArgs { + param([string[]]$ArgList) + + $out = New-Object System.Collections.Generic.List[string] + foreach ($a in ($ArgList | Where-Object { $_ -ne $null -and $_ -ne "" })) { + if ($a -eq "--help" -or $a -eq "-h" -or $a -eq "-help") { + $out.Add("-Help") + continue + } + $out.Add($a) + } + return $out.ToArray() +} + +function Show-CommandHelp { + param([string]$Command) + + switch ($Command) { + "install" { . (Join-Path $PSScriptRoot "install.ps1") -Help; return } + "update" { . (Join-Path $PSScriptRoot "update.ps1") -Help; return } + "setup" { & (Join-Path $PSScriptRoot "setup.ps1") -Help; return } + "run" { & (Join-Path $PSScriptRoot "start.ps1") -Help; return } + "start" { & (Join-Path $PSScriptRoot "start.ps1") -Help; return } + "stop" { & (Join-Path $PSScriptRoot "stop.ps1") -Help; return } + "certify" { & (Join-Path $PSScriptRoot "certify.ps1") -Help; return } + "uninstall" { & (Join-Path $PSScriptRoot "uninstall.ps1") -Help; return } + "prepare" { Write-Host "bbx prepare (no help available)" -ForegroundColor Yellow; return } + "revalidate" { Write-Host "bbx revalidate (no options)" -ForegroundColor Yellow; return } + default { Show-Help; return } + } +} + +function Convert-ArgListToSplat { + param( + [Parameter(Mandatory = $true)][string]$Command, + [Parameter(Mandatory = $true)][string[]]$ArgList + ) + + $map = @{} + switch ($Command) { + "setup" { $map = @{ "host" = "Hostname"; "hostname" = "Hostname" } } + "run" { $map = @{ "host" = "Hostname"; "hostname" = "Hostname" } } + "start" { $map = @{ "host" = "Hostname"; "hostname" = "Hostname" } } + default { $map = @{} } + } + + $splat = @{} + + for ($i = 0; $i -lt $ArgList.Length; $i++) { + $a = $ArgList[$i] + if ($a -eq $null -or $a -eq "") { continue } + + if ($a -match '^--?([^=]+)=(.*)$') { + $rawName = $matches[1] + $value = $matches[2] + $key = $rawName + $lower = $rawName.ToLowerInvariant() + if ($map.ContainsKey($lower)) { $key = $map[$lower] } + $splat[$key] = $value + continue + } + + if ($a -match '^--?(.+)$') { + $rawName = $matches[1] + $key = $rawName + $lower = $rawName.ToLowerInvariant() + if ($map.ContainsKey($lower)) { $key = $map[$lower] } + + $next = $null + if (($i + 1) -lt $ArgList.Length) { $next = $ArgList[$i + 1] } + + if ($next -ne $null -and $next -ne "" -and $next -notmatch '^--?.+') { + $splat[$key] = $next + $i++ + } else { + $splat[$key] = $true + } + continue + } + + throw "Positional argument '$a' is not supported; use named options (e.g. -Hostname localhost -Port 9955)." + } + + return $splat } # Function to handle revalidate command @@ -175,58 +571,104 @@ function Invoke-Revalidate { } } -# Main execution logic -if ($Command -eq "install" -or $Command -eq "update") { - # Install/update command explicitly downloads/updates the binary - $tag = Get-LatestRelease -Repo $PublicRepo - Download-Binary -Tag $tag - - if ($Command -eq "install") { - # Run the binary with --install flag - $installArgs = @("--install") + $Args - & $BinaryPath $installArgs +function Invoke-CommandScript { + param ( + [string]$Command, + [string[]]$Arguments + ) + + if (-not $ScriptMap.ContainsKey($Command)) { return $false } + + $scriptPath = Join-Path $PSScriptRoot $ScriptMap[$Command] + if (-not (Test-Path $scriptPath)) { + Write-Error "Script for '$Command' not found at $scriptPath" + return $true + } + + Write-Host "Running bbx $Command..." -ForegroundColor Cyan + + $global:LASTEXITCODE = 0 + + if ($Command -in @("install", "update")) { + # Needs helpers like Download-Binary/Get-LatestRelease from this file's scope. + if ($Arguments -and $Arguments.Count -gt 0) { + $params = Convert-ArgListToSplat -Command $Command -ArgList $Arguments + . $scriptPath @params + } else { + . $scriptPath + } exit $LASTEXITCODE } - else { - # For update, just report success - Write-Host "BrowserBox updated to $tag" -ForegroundColor Green - exit 0 + + if ($Arguments -and $Arguments.Count -gt 0) { + $params = Convert-ArgListToSplat -Command $Command -ArgList $Arguments + & $scriptPath @params + } else { + & $scriptPath } -} -elseif ($Command -eq "--version" -or $Command -eq "-v") { - Ensure-Binary - & $BinaryPath "--version" exit $LASTEXITCODE } -elseif (-not $Command -or $Command -eq "-help" -or $Command -eq "--help" -or $Command -eq "-h") { + +function Get-LocalBinaryVersion { + $path = Resolve-BrowserBoxBinary + if (-not $path) { return $null } + try { + $info = (Get-Item $path -ErrorAction Stop).VersionInfo + if ($info -and $info.ProductVersion) { return $info.ProductVersion } + if ($info -and $info.FileVersion) { return $info.FileVersion } + } catch { } + return $null +} + +# Main execution logic (argv-driven; do not let PowerShell bind subcommand options to this wrapper) +$argv = @($args | Where-Object { $_ -ne $null -and $_ -ne "" }) +if (-not $argv -or $argv.Count -eq 0) { Show-Help exit 0 } -elseif ($Command -eq "revalidate") { + +$Command = [string]$argv[0] +$CommandArgs = @() +if ($argv.Count -gt 1) { $CommandArgs = @($argv[1..($argv.Count - 1)]) } + +$CommandArgs = Normalize-CommandArgs -ArgList $CommandArgs +$normalizedCommand = $Command.ToLowerInvariant() + +if ($env:BBX_DEBUG_CLI -and $env:BBX_DEBUG_CLI -ne "0" -and $env:BBX_DEBUG_CLI.ToLowerInvariant() -ne "false") { + Write-Host "[bbx] Command: $Command" -ForegroundColor DarkGray + Write-Host "[bbx] Args: $($CommandArgs -join ' | ')" -ForegroundColor DarkGray + Write-Host "[bbx] Normalized: $normalizedCommand" -ForegroundColor DarkGray +} + +if (-not $Command -or $normalizedCommand -in @("--help","-help","help","-h")) { + Show-Help + exit 0 +} + +if ($normalizedCommand -in @("--version","-v","version")) { + $v = Get-LocalBinaryVersion + if ($v) { + Write-Host $v + exit 0 + } + Write-Host "browserbox.exe not found." -ForegroundColor Yellow + exit 1 +} +elseif ($normalizedCommand -eq "revalidate") { Invoke-Revalidate exit 0 } +elseif ($CommandArgs -and ($CommandArgs -contains "-Help")) { + Show-CommandHelp -Command $normalizedCommand + exit 0 +} +elseif (Invoke-CommandScript -Command $normalizedCommand -Arguments $CommandArgs) { + exit $LASTEXITCODE +} else { - # For all other commands, ensure binary exists and pass through + # Pass through to the browserbox binary Ensure-Binary - - # Add binary directory to PATH for this session if not already there - if ($env:PATH -notlike "*$BinaryDir*") { - $env:PATH = "$BinaryDir;$env:PATH" - } - - # Optional: Check for updates (non-blocking) - if (-not $env:BBX_NO_UPDATE) { - try { - Check-Update - } - catch { - # Silently ignore - } - } - - # Pass all arguments through to the binary - $allArgs = @($Command) + $Args - & $BinaryPath $allArgs + $passArgs = @($Command) + $CommandArgs + & $script:ResolvedBinaryPath @passArgs exit $LASTEXITCODE } From a169056228b1958924cb6aec81a7a0366e905d54 Mon Sep 17 00:00:00 2001 From: DOSAYGO Engineering Date: Wed, 17 Dec 2025 22:12:31 +0800 Subject: [PATCH 07/15] Trim public saga matrix to native runners --- .github/workflows/bbx-saga.yaml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/.github/workflows/bbx-saga.yaml b/.github/workflows/bbx-saga.yaml index 124978847..01d45eeda 100644 --- a/.github/workflows/bbx-saga.yaml +++ b/.github/workflows/bbx-saga.yaml @@ -28,29 +28,15 @@ env: jobs: build: - name: Test on ${{ matrix.os }}${{ matrix.container_image && format(' ({0})', matrix.container_image) || '' }} + name: Test on ${{ matrix.os }} if: github.event_name != 'release' || (github.event.release.draft == false) continue-on-error: ${{ matrix.os == 'windows-latest' }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - container_image: - - '' # Native runner - - 'dokken/centos-stream-10' - - 'debian:latest' - exclude: - - os: macos-latest - container_image: 'dokken/centos-stream-10' - - os: macos-latest - container_image: 'debian:latest' - - os: windows-latest - container_image: 'dokken/centos-stream-10' - - os: windows-latest - container_image: 'debian:latest' runs-on: ${{ matrix.os }} timeout-minutes: 12 - container: ${{ matrix.container_image }} steps: - name: Resolve release tag From a131fc5021345ea2f6c0e64c25a3d5ec71d9a91c Mon Sep 17 00:00:00 2001 From: DOSAYGO Engineering Date: Wed, 17 Dec 2025 22:18:53 +0800 Subject: [PATCH 08/15] Sync public saga workflow and test script with source --- .github/workflows/bbx-saga.yaml | 159 +++++++++++++++++++++++++------- tests/test-bbx.sh | 117 ++++++++++++++++++++--- 2 files changed, 232 insertions(+), 44 deletions(-) diff --git a/.github/workflows/bbx-saga.yaml b/.github/workflows/bbx-saga.yaml index 01d45eeda..f0f444672 100644 --- a/.github/workflows/bbx-saga.yaml +++ b/.github/workflows/bbx-saga.yaml @@ -1,62 +1,130 @@ -name: bbx Saga Test Suite (Public Release) +name: bbx Saga Test Suite (Private Release) on: - release: - types: [published] + workflow_run: + workflows: ["Private Build (Draft Binaries)"] + types: [completed] workflow_dispatch: inputs: - release_tag: - description: "Tag to test (defaults to release event tag)" + use_private_release: + description: "Use private staged release assets (BrowserBox-source)" required: false - default: "" - release_repo: - description: "Repository that hosts the binaries" + default: false + type: boolean + private_release_tag: + description: "Tag to pull from when use_private_release=true (e.g., v15.3.11-rc)" required: false - default: "BrowserBox/BrowserBox" + default: "" + type: string concurrency: group: ${{ github.repository }}-bbx-saga cancel-in-progress: true permissions: - contents: read # required for checkout - actions: read # required for workflow_run context if used later + contents: read # Required for actions/checkout + actions: read # Required for 'gh run view' to inspect the upstream workflow inputs env: - RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release_tag || '' }} - TARGET_RELEASE_REPO: ${{ github.event.inputs.release_repo || 'BrowserBox/BrowserBox' }} + PRIVATE_TAG: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.inputs.tag || github.event.inputs.private_release_tag || '' }} + USE_PRIVATE_RELEASE: ${{ github.event_name == 'workflow_run' && 'true' || github.event.inputs.use_private_release || 'false' }} + TARGET_RELEASE_REPO: ${{ (github.event_name == 'workflow_run' || github.event.inputs.use_private_release == 'true') && 'BrowserBox/BrowserBox-source' || 'BrowserBox/BrowserBox' }} jobs: build: - name: Test on ${{ matrix.os }} - if: github.event_name != 'release' || (github.event.release.draft == false) + if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' continue-on-error: ${{ matrix.os == 'windows-latest' }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] + container_image: + - '' # No container (native runner) + - 'dokken/centos-stream-10' + - 'debian:latest' + exclude: + - os: macos-latest + container_image: 'dokken/centos-stream-10' + - os: macos-latest + container_image: 'debian:latest' + - os: windows-latest + container_image: 'dokken/centos-stream-10' + - os: windows-latest + container_image: 'debian:latest' runs-on: ${{ matrix.os }} - timeout-minutes: 12 - + timeout-minutes: 10 + container: ${{ matrix.container_image }} steps: - - name: Resolve release tag + # 1. Try to download the artifact (Solution 3), but DON'T fail if missing + - name: Download Tag Artifact + if: github.event_name == 'workflow_run' + uses: actions/download-artifact@v4 + with: + name: workflow-inputs + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true # <--- CRITICAL ADDITION + + # 2. The Ultimate Logic Block (Artifact -> CLI -> Regex) + - name: Derive PRIVATE_TAG + if: github.event_name == 'workflow_run' shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - tag="${RELEASE_TAG}" - repo="${TARGET_RELEASE_REPO}" + tag="" + + # ATTEMPT 1: Check for Artifact (Solution 3) + if [[ -f "workflow_tag.txt" ]]; then + tag=$(cat workflow_tag.txt | tr -d '[:space:]') + echo "✓ Found tag via Artifact: $tag" + fi + + # ATTEMPT 2: Check via GH CLI (Solution 2) + if [[ -z "$tag" ]]; then + echo "Artifact missing. Attempting gh CLI..." + workflow_run_id="${{ github.event.workflow_run.id }}" + tag=$(gh run view "$workflow_run_id" --repo "${{ github.repository }}" --json inputs --jq '.inputs.tag // empty' 2>/dev/null || echo "") + if [[ -n "$tag" ]]; then + echo "✓ Found tag via gh CLI: $tag" + fi + fi + + # ATTEMPT 3: Check via Run Title Regex (Solution 1) + if [[ -z "$tag" ]]; then + echo "CLI failed. Attempting display_title extraction..." + display_title="${{ github.event.workflow_run.display_title }}" + # Matches "Private Build v1.2.3" or just "v1.2.3" + if [[ "$display_title" =~ (v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?) ]]; then + tag="${BASH_REMATCH[1]}" + echo "✓ Found tag via Display Title: $tag" + fi + fi + + # FINAL CHECK if [[ -z "$tag" ]]; then - echo "RELEASE_TAG is required (from release event or input)" >&2 + echo "::error::Could not resolve PRIVATE_TAG from Artifact, CLI, or Title." exit 1 fi + + # EXPORT { - echo "BBX_RELEASE_TAG=$tag" - echo "BBX_RELEASE_REPO=$repo" + echo "PRIVATE_TAG=$tag" + echo "USE_PRIVATE_RELEASE=true" + echo "TARGET_RELEASE_REPO=BrowserBox/BrowserBox-source" } >> "$GITHUB_ENV" - echo "Testing binaries from $repo@$tag" - name: Checkout repository uses: actions/checkout@v4 + - name: Check if actor is repository owner or me + shell: bash + run: | + if [[ "${{ github.actor }}" != "crisdosaygo" ]]; then + echo "Actor is not me. Not running CI" + exit 1 + fi + - name: Prepare test script (Unix/macOS) if: matrix.os != 'windows-latest' shell: bash @@ -76,9 +144,18 @@ jobs: LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} INSTALL_DOC_VIEWER: "false" - BBX_RELEASE_REPO: ${{ env.BBX_RELEASE_REPO }} - BBX_RELEASE_TAG: ${{ env.BBX_RELEASE_TAG }} + TARGET_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} + PRIVATE_TAG: ${{ env.PRIVATE_TAG }} run: | + if [[ "${USE_PRIVATE_RELEASE}" == "true" ]]; then + if [[ -z "${PRIVATE_TAG}" ]]; then + echo "PRIVATE_RELEASE_TAG is required when use_private_release=true" >&2 + exit 1 + fi + export BBX_RELEASE_REPO="${TARGET_RELEASE_REPO}" + export BBX_RELEASE_TAG="${PRIVATE_TAG}" + echo "Using private release ${BBX_RELEASE_REPO}@${BBX_RELEASE_TAG}" + fi chmod +x ./bbx.sh ./bbx.sh install @@ -96,8 +173,8 @@ jobs: STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} INSTALL_DOC_VIEWER: "false" GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BBX_RELEASE_REPO: ${{ env.BBX_RELEASE_REPO }} - BBX_RELEASE_TAG: ${{ env.BBX_RELEASE_TAG }} + TARGET_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} + PRIVATE_TAG: ${{ env.PRIVATE_TAG }} run: | [ -z "$BBX_HOSTNAME" ] && echo "BBX_HOSTNAME is not set" || echo "BBX_HOSTNAME is set" [ -z "$EMAIL" ] && echo "EMAIL is not set" || echo "EMAIL is set" @@ -105,6 +182,15 @@ jobs: [ -z "$BBX_TEST_AGREEMENT" ] && echo "BBX_TEST_AGREEMENT is not set" || echo "BBX_TEST_AGREEMENT is set" [ -z "$STATUS_MODE" ] && echo "STATUS_MODE is not set" || echo "STATUS_MODE is set" [ -z "$INSTALL_DOC_VIEWER" ] && echo "INSTALL_DOC_VIEWER is not set" || echo "INSTALL_DOC_VIEWER is set to $INSTALL_DOC_VIEWER" + if [[ "${USE_PRIVATE_RELEASE}" == "true" ]]; then + if [[ -z "${PRIVATE_TAG}" ]]; then + echo "PRIVATE_RELEASE_TAG is required when use_private_release=true" >&2 + exit 1 + fi + export BBX_RELEASE_REPO="${TARGET_RELEASE_REPO}" + export BBX_RELEASE_TAG="${PRIVATE_TAG}" + echo "Using private release ${BBX_RELEASE_REPO}@${BBX_RELEASE_TAG}" + fi export BBX_NO_UPDATE=true export INSTALL_DOC_VIEWER STATUS_MODE BBX_TEST_AGREEMENT LICENSE_KEY EMAIL BBX_HOSTNAME BB_QUICK_EXIT ./tests/test-bbx.sh @@ -123,16 +209,23 @@ jobs: LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} BBX_TEST_AGREEMENT: "true" STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} - BBX_RELEASE_REPO: ${{ env.BBX_RELEASE_REPO }} - BBX_RELEASE_TAG: ${{ env.BBX_RELEASE_TAG }} + TARGET_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} + PRIVATE_TAG: ${{ env.PRIVATE_TAG }} run: | + if ($env:USE_PRIVATE_RELEASE -eq "true") { + if ([string]::IsNullOrWhiteSpace($env:PRIVATE_TAG)) { + Write-Error "PRIVATE_RELEASE_TAG is required when use_private_release=true" + } + $env:BBX_RELEASE_REPO = $env:TARGET_RELEASE_REPO + $env:BBX_RELEASE_TAG = $env:PRIVATE_TAG + Write-Host "Using private release $($env:BBX_RELEASE_REPO)@$($env:BBX_RELEASE_TAG)" + } # Debug variables if (-not $env:BBX_HOSTNAME) { Write-Host "BBX_HOSTNAME is not set" } else { Write-Host "BBX_HOSTNAME is set" } if (-not $env:EMAIL) { Write-Host "EMAIL is not set" } else { Write-Host "EMAIL is set" } if (-not $env:LICENSE_KEY) { Write-Host "LICENSE_KEY is not set" } else { Write-Host "LICENSE_KEY is set" } if (-not $env:BBX_TEST_AGREEMENT) { Write-Host "BBX_TEST_AGREEMENT is not set" } else { Write-Host "BBX_TEST_AGREEMENT is set" } if (-not $env:STATUS_MODE) { Write-Host "STATUS_MODE is not set" } else { Write-Host "STATUS_MODE is set" } - if (-not $env:BBX_RELEASE_TAG) { Write-Error "BBX_RELEASE_TAG is required"; exit 1 } # Install BrowserBox from checked-out repo .\windows-scripts\bbx.ps1 install if (-not (Get-Command bbx -ErrorAction SilentlyContinue)) { @@ -148,7 +241,7 @@ jobs: bbx setup -Hostname "$env:BBX_HOSTNAME" -Email "$env:EMAIL" -Port 9999 $loginLink = Get-Content "$env:USERPROFILE\.config\dosyago\bbpro\login.link" Write-Host "Login link: $loginLink" - bbx run + bbx run Write-Host "Testing URL: $loginLink" $maxRetries = 10 $retryCount = 0 @@ -213,9 +306,11 @@ jobs: if: always() && matrix.os != 'windows-latest' shell: bash run: | + # Try multiple cleanup methods if [ -x ./bbx.sh ]; then ./bbx.sh stop || true elif command -v bbx &>/dev/null; then bbx stop || true fi + # Kill any remaining browserbox processes pkill -f browserbox || true diff --git a/tests/test-bbx.sh b/tests/test-bbx.sh index dacb8a5dc..194e60040 100755 --- a/tests/test-bbx.sh +++ b/tests/test-bbx.sh @@ -3,6 +3,12 @@ # Test script for bbx CLI in BrowserBox repository # Displays output directly in terminal +# Test timeouts (in minutes for timeout command) +TEST_NG_RUN_TIMEOUT="3m" +TEST_TOR_RUN_TIMEOUT="3m" +TEST_INSTALL_TIMEOUT="10m" +export SKIP_DOCKER="true" # we haven't build docker images yet so skip + if [[ -z "$STATUS_MODE" ]]; then echo "Set status mode env" >&2 STATUS_MODE="quick exit" @@ -23,6 +29,14 @@ fi export INSTALL_DOC_VIEWER="${INSTALL_DOC_VIEWER}" export BBX_NO_UPDATE="true" +# Resolve script and repo paths so we can re-enter the tests reliably after su. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)" +SCRIPT_PATH="${SCRIPT_DIR}/$(basename "${BASH_SOURCE[0]:-$0}")" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd -P)" + +# Avoid git safe.directory prompts/noise in CI +git config --global --add safe.directory "$REPO_ROOT" 2>/dev/null || true + # Safely handle bbcertify output if command -v bbcertify; then cert_file=$(bbcertify --no-reservation) @@ -85,14 +99,14 @@ test_login_link() { local timeout=5 local success=0 # Flag to track success local http_code="" # Variable to store the HTTP status code - local curl_opts="-s -k -L -w %{http_code} --max-time $timeout --head --fail --output /dev/null" + local curl_opts="-s -k -L -w %{http_code} --max-time $timeout --fail --output /dev/null" # Add Tor SOCKS proxy if specified if [ "$use_tor" = "tor" ]; then interval=5 timeout=25 max_time=180 - curl_opts="-s -k -L -w %{http_code} --max-time $timeout --head --fail --output /dev/null --proxy socks5h://127.0.0.1:9050" + curl_opts="-s -k -L -w %{http_code} --max-time $timeout --fail --output /dev/null --proxy socks5h://127.0.0.1:9050" echo -n "Testing Tor login link $link with retries... " else echo -n "Testing login link $link with retries... " @@ -147,7 +161,7 @@ test_install() { if [[ "$(uname -s)" == "Darwin" ]]; then brew install coreutils fi - yes yes | timeout -k 10s 10m ./bbx.sh install --latest-rc + yes yes | timeout -k 10s "$TEST_INSTALL_TIMEOUT" ./bbx.sh install if [ $? -eq 0 ]; then echo -e "${GREEN}✔ Success${NC}" ((passed++)) @@ -158,9 +172,30 @@ test_install() { fi # if we just installed as root, then we have created a correct user called something or yes so let's hand off install script to them :) if [ "$(id -u)" -eq 0 ]; then - sudo -u yes bash -c "cd; cp -r .bbx/BrowserBox . ;" - install_user="$(cat "${BB_CONFIG_DIR}"/.install_user)" - exec su - "${install_user:-yes}" -c "export BBX_HOSTNAME=\"$BBX_HOSTNAME\"; export EMAIL=\"$EMAIL\"; export LICENSE_KEY=\"$LICENSE_KEY\"; export BBX_TEST_AGREEMENT=\"$BBX_TEST_AGREEMENT\"; export STATUS_MODE=\"$STATUS_MODE\"; bash -cl 'cd; cd BrowserBox; ./tests/test-bbx.sh ;'" + if [ -f "${BB_CONFIG_DIR}/.install_user" ]; then + install_user="$(cat "${BB_CONFIG_DIR}"/.install_user)" + if id "$install_user" &>/dev/null; then + # Copy BrowserBox directory to install user's home + user_home="$(eval echo ~"$install_user")" + if [ -d "$user_home/.bbx/BrowserBox" ]; then + sudo -u "$install_user" cp -r "$user_home/.bbx/BrowserBox" "$user_home/" 2>/dev/null || true + fi + # Forward only the BBX-related env we rely on via a temp file to avoid su - env stripping. + su_env_vars=(BBX_HOSTNAME EMAIL LICENSE_KEY BBX_TEST_AGREEMENT STATUS_MODE INSTALL_DOC_VIEWER BBX_NO_UPDATE BBX_RELEASE_REPO BBX_RELEASE_TAG TARGET_RELEASE_REPO PRIVATE_TAG GH_TOKEN GITHUB_TOKEN BBX_INSTALL_USER BB_QUICK_EXIT) + env_file="$(mktemp)" + for var in "${su_env_vars[@]}"; do + val="${!var-}" + [[ -n "$val" ]] || continue + printf '%s=%q\n' "$var" "$val" >> "$env_file" + done + chown "$install_user" "$env_file" 2>/dev/null || true + exec su - "${install_user}" -c "set -a; source $(printf '%q' "$env_file"); cd $(printf '%q' "$REPO_ROOT") && $(printf '%q' "$SCRIPT_PATH"); rc=\$?; rm -f $(printf '%q' "$env_file"); exit \$rc" + else + echo "Warning: Install user $install_user does not exist, continuing as root" + fi + else + echo "Warning: No .install_user file found, continuing as root" + fi fi } @@ -193,8 +228,12 @@ test_run() { ((passed++)) # Test login link immediately - source ~/.nvm/nvm.sh - command -v timeout && timeout 10s pm2 logs + if [ -f ~/.nvm/nvm.sh ]; then + source ~/.nvm/nvm.sh 2>/dev/null || true + fi + if command -v timeout &>/dev/null && command -v browserbox &>/dev/null; then + timeout 10s browserbox pm2 list 2>/dev/null || true + fi if ! test_login_link "$login_link"; then ./bbx.sh stop @@ -224,7 +263,7 @@ test_ng_run() { echo "Running bbx with Nginx... " # use wildcard-able hostname for ng-run ./bbx.sh setup --port 9999 --hostname "ci.test" -z - timeout -k 10s 6m ./bbx.sh ng-run 2>&1 + timeout -k 10s "$TEST_NG_RUN_TIMEOUT" ./bbx.sh ng-run 2>&1 exit_code=$? output="$(cat "${BB_CONFIG_DIR}/login.link")" login_link="$(extract_login_link "$output" | tail -n 1)" @@ -257,7 +296,7 @@ test_ng_run() { test_tor_run() { echo "Running bbx with Tor... " - timeout -k 10s 6m ./bbx.sh tor-run 2>&1 + timeout -k 10s "$TEST_TOR_RUN_TIMEOUT" ./bbx.sh tor-run 2>&1 exit_code=$? output="$(cat "${BB_CONFIG_DIR}/login.link")" login_link="$(extract_login_link "$output" | tail -n 1)" @@ -288,10 +327,63 @@ test_tor_run() { ./bbx.sh stop } +test_cf_run() { + # Check if we have internet connectivity by testing Cloudflare endpoint + if ! curl --connect-timeout 5 -s -o /dev/null https://www.cloudflare.com 2>/dev/null; then + echo "Skipping Cloudflare tunnel test (no internet connectivity)" + ((passed++)) + return 0 + fi + + echo "Running bbx with Cloudflare tunnel... " + # Run cf-run with a timeout (3 minutes matches the tor-run test pattern) + timeout -k 10s 3m ./bbx.sh cf-run 2>&1 & + cf_pid=$! + + # Wait for cf-run to start and create login.link + sleep 30 + + if [ ! -f "${BB_CONFIG_DIR}/login.link" ]; then + echo -e "${YELLOW}⚠ Warning: login.link not found (cf-run may still be starting)${NC}" + kill $cf_pid 2>/dev/null || true + ./bbx.sh stop + ((warnings++)) + return 0 + fi + + output="$(cat "${BB_CONFIG_DIR}/login.link")" + login_link="$(extract_login_link "$output" | tail -n 1)" + + if [ -z "$login_link" ]; then + echo -e "${YELLOW}⚠ Warning: No login link found${NC}" + kill $cf_pid 2>/dev/null || true + ./bbx.sh stop + ((warnings++)) + return 0 + fi + + echo -e "${GREEN}✔ Success (CF run started, link: ${login_link})${NC}" + ((passed++)) + + # Test login link - use the tunnel URL which should work + if ! test_login_link "$login_link"; then + kill $cf_pid 2>/dev/null || true + ./bbx.sh stop + return 1 + fi + + # Cleanup + kill $cf_pid 2>/dev/null || true + ./bbx.sh stop + + echo -e "${GREEN}✔ CF run test complete${NC}" + ((passed++)) +} + test_docker_run() { # Self-detect if running in a Docker container - if [ -f /.dockerenv ] || ([[ "$(uname -s)" == "Darwin" ]] && ! command -v docker &>/dev/null); then - echo "Skipping Dockerized bbx test (detected running in Docker container or macOS)" + if [[ -n "$SKIP_DOCKER" ]] || [ -f /.dockerenv ] || ([[ "$(uname -s)" == "Darwin" ]] && ! command -v docker &>/dev/null); then + echo "Skipping Dockerized bbx test (detected running in Docker container or macOS, or environment has SKIP_DOCKER)" ((passed++)) # Increment passed to maintain test count return 0 fi @@ -353,6 +445,7 @@ test_setup || exit 1 test_run || exit 1 test_ng_run || exit 1 test_tor_run || exit 1 +test_cf_run || exit 1 test_docker_run || exit 1 # Cleanup From 7fbda5422533bea246b56f9296b6fad49c78f9af Mon Sep 17 00:00:00 2001 From: DOSAYGO Engineering Date: Wed, 17 Dec 2025 22:21:10 +0800 Subject: [PATCH 09/15] Make public saga release-driven and match source test script --- .github/workflows/bbx-saga.yaml | 250 +++++--------------------------- 1 file changed, 39 insertions(+), 211 deletions(-) diff --git a/.github/workflows/bbx-saga.yaml b/.github/workflows/bbx-saga.yaml index f0f444672..331fd210d 100644 --- a/.github/workflows/bbx-saga.yaml +++ b/.github/workflows/bbx-saga.yaml @@ -1,164 +1,65 @@ -name: bbx Saga Test Suite (Private Release) +name: bbx Saga Test Suite (Public Release) on: - workflow_run: - workflows: ["Private Build (Draft Binaries)"] - types: [completed] + release: + types: [published] workflow_dispatch: inputs: - use_private_release: - description: "Use private staged release assets (BrowserBox-source)" - required: false - default: false - type: boolean - private_release_tag: - description: "Tag to pull from when use_private_release=true (e.g., v15.3.11-rc)" + release_tag: + description: "Tag to test (defaults to release event tag)" required: false default: "" - type: string + release_repo: + description: "Repository that hosts the binaries" + required: false + default: "BrowserBox/BrowserBox" concurrency: - group: ${{ github.repository }}-bbx-saga + group: ${{ github.repository }}-bbx-saga-${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release_tag || 'manual' }} cancel-in-progress: true permissions: - contents: read # Required for actions/checkout - actions: read # Required for 'gh run view' to inspect the upstream workflow inputs + contents: read env: - PRIVATE_TAG: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.inputs.tag || github.event.inputs.private_release_tag || '' }} - USE_PRIVATE_RELEASE: ${{ github.event_name == 'workflow_run' && 'true' || github.event.inputs.use_private_release || 'false' }} - TARGET_RELEASE_REPO: ${{ (github.event_name == 'workflow_run' || github.event.inputs.use_private_release == 'true') && 'BrowserBox/BrowserBox-source' || 'BrowserBox/BrowserBox' }} + RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release_tag || '' }} + TARGET_RELEASE_REPO: ${{ github.event.inputs.release_repo || 'BrowserBox/BrowserBox' }} jobs: - build: - if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' + saga: + name: Test on ${{ matrix.os }} continue-on-error: ${{ matrix.os == 'windows-latest' }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - container_image: - - '' # No container (native runner) - - 'dokken/centos-stream-10' - - 'debian:latest' - exclude: - - os: macos-latest - container_image: 'dokken/centos-stream-10' - - os: macos-latest - container_image: 'debian:latest' - - os: windows-latest - container_image: 'dokken/centos-stream-10' - - os: windows-latest - container_image: 'debian:latest' runs-on: ${{ matrix.os }} - timeout-minutes: 10 - container: ${{ matrix.container_image }} - steps: - # 1. Try to download the artifact (Solution 3), but DON'T fail if missing - - name: Download Tag Artifact - if: github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 - with: - name: workflow-inputs - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - continue-on-error: true # <--- CRITICAL ADDITION + timeout-minutes: 15 - # 2. The Ultimate Logic Block (Artifact -> CLI -> Regex) - - name: Derive PRIVATE_TAG - if: github.event_name == 'workflow_run' + steps: + - name: Resolve release tag shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - tag="" - - # ATTEMPT 1: Check for Artifact (Solution 3) - if [[ -f "workflow_tag.txt" ]]; then - tag=$(cat workflow_tag.txt | tr -d '[:space:]') - echo "✓ Found tag via Artifact: $tag" - fi - - # ATTEMPT 2: Check via GH CLI (Solution 2) - if [[ -z "$tag" ]]; then - echo "Artifact missing. Attempting gh CLI..." - workflow_run_id="${{ github.event.workflow_run.id }}" - tag=$(gh run view "$workflow_run_id" --repo "${{ github.repository }}" --json inputs --jq '.inputs.tag // empty' 2>/dev/null || echo "") - if [[ -n "$tag" ]]; then - echo "✓ Found tag via gh CLI: $tag" - fi - fi - - # ATTEMPT 3: Check via Run Title Regex (Solution 1) - if [[ -z "$tag" ]]; then - echo "CLI failed. Attempting display_title extraction..." - display_title="${{ github.event.workflow_run.display_title }}" - # Matches "Private Build v1.2.3" or just "v1.2.3" - if [[ "$display_title" =~ (v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?) ]]; then - tag="${BASH_REMATCH[1]}" - echo "✓ Found tag via Display Title: $tag" - fi - fi - - # FINAL CHECK + tag="${RELEASE_TAG}" + repo="${TARGET_RELEASE_REPO}" if [[ -z "$tag" ]]; then - echo "::error::Could not resolve PRIVATE_TAG from Artifact, CLI, or Title." + echo "RELEASE_TAG is required (from release event or workflow input)" >&2 exit 1 fi - - # EXPORT { - echo "PRIVATE_TAG=$tag" - echo "USE_PRIVATE_RELEASE=true" - echo "TARGET_RELEASE_REPO=BrowserBox/BrowserBox-source" + echo "BBX_RELEASE_TAG=$tag" + echo "BBX_RELEASE_REPO=$repo" } >> "$GITHUB_ENV" + echo "Testing binaries from ${repo}@${tag}" - name: Checkout repository uses: actions/checkout@v4 - - name: Check if actor is repository owner or me - shell: bash - run: | - if [[ "${{ github.actor }}" != "crisdosaygo" ]]; then - echo "Actor is not me. Not running CI" - exit 1 - fi - - name: Prepare test script (Unix/macOS) if: matrix.os != 'windows-latest' shell: bash run: chmod +x tests/test-bbx.sh - - name: Install BrowserBox via binary (Unix/macOS) - if: matrix.os != 'windows-latest' - shell: bash - env: - BBX_INSTALL_USER: "bbxuser" - BBX_TEST_AGREEMENT: "true" - BBX_NO_UPDATE: "true" - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BBX_HOSTNAME: "localhost" - BB_QUICK_EXIT: "yesplease" - EMAIL: "test@example.com" - LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} - STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} - INSTALL_DOC_VIEWER: "false" - TARGET_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} - PRIVATE_TAG: ${{ env.PRIVATE_TAG }} - run: | - if [[ "${USE_PRIVATE_RELEASE}" == "true" ]]; then - if [[ -z "${PRIVATE_TAG}" ]]; then - echo "PRIVATE_RELEASE_TAG is required when use_private_release=true" >&2 - exit 1 - fi - export BBX_RELEASE_REPO="${TARGET_RELEASE_REPO}" - export BBX_RELEASE_TAG="${PRIVATE_TAG}" - echo "Using private release ${BBX_RELEASE_REPO}@${BBX_RELEASE_TAG}" - fi - chmod +x ./bbx.sh - ./bbx.sh install - - name: Execute BBX Test Saga (Unix/macOS) if: matrix.os != 'windows-latest' shell: bash @@ -173,34 +74,17 @@ jobs: STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} INSTALL_DOC_VIEWER: "false" GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TARGET_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} - PRIVATE_TAG: ${{ env.PRIVATE_TAG }} + BBX_RELEASE_REPO: ${{ env.BBX_RELEASE_REPO }} + BBX_RELEASE_TAG: ${{ env.BBX_RELEASE_TAG }} run: | - [ -z "$BBX_HOSTNAME" ] && echo "BBX_HOSTNAME is not set" || echo "BBX_HOSTNAME is set" - [ -z "$EMAIL" ] && echo "EMAIL is not set" || echo "EMAIL is set" - [ -z "$LICENSE_KEY" ] && echo "LICENSE_KEY is not set" || echo "LICENSE_KEY is set" - [ -z "$BBX_TEST_AGREEMENT" ] && echo "BBX_TEST_AGREEMENT is not set" || echo "BBX_TEST_AGREEMENT is set" - [ -z "$STATUS_MODE" ] && echo "STATUS_MODE is not set" || echo "STATUS_MODE is set" - [ -z "$INSTALL_DOC_VIEWER" ] && echo "INSTALL_DOC_VIEWER is not set" || echo "INSTALL_DOC_VIEWER is set to $INSTALL_DOC_VIEWER" - if [[ "${USE_PRIVATE_RELEASE}" == "true" ]]; then - if [[ -z "${PRIVATE_TAG}" ]]; then - echo "PRIVATE_RELEASE_TAG is required when use_private_release=true" >&2 - exit 1 - fi - export BBX_RELEASE_REPO="${TARGET_RELEASE_REPO}" - export BBX_RELEASE_TAG="${PRIVATE_TAG}" - echo "Using private release ${BBX_RELEASE_REPO}@${BBX_RELEASE_TAG}" - fi export BBX_NO_UPDATE=true - export INSTALL_DOC_VIEWER STATUS_MODE BBX_TEST_AGREEMENT LICENSE_KEY EMAIL BBX_HOSTNAME BB_QUICK_EXIT + export INSTALL_DOC_VIEWER STATUS_MODE BBX_TEST_AGREEMENT LICENSE_KEY EMAIL BBX_HOSTNAME BB_QUICK_EXIT BBX_RELEASE_REPO BBX_RELEASE_TAG ./tests/test-bbx.sh - continue-on-error: false - name: Execute BBX Test Saga (Windows) if: matrix.os == 'windows-latest' shell: powershell env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BBX_NO_UPDATE: "true" BBX_FORCE_CHROME_INSTALL: "true" BBX_HOSTNAME: "localhost" @@ -209,24 +93,13 @@ jobs: LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} BBX_TEST_AGREEMENT: "true" STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} - TARGET_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} - PRIVATE_TAG: ${{ env.PRIVATE_TAG }} + BBX_RELEASE_REPO: ${{ env.BBX_RELEASE_REPO }} + BBX_RELEASE_TAG: ${{ env.BBX_RELEASE_TAG }} run: | - if ($env:USE_PRIVATE_RELEASE -eq "true") { - if ([string]::IsNullOrWhiteSpace($env:PRIVATE_TAG)) { - Write-Error "PRIVATE_RELEASE_TAG is required when use_private_release=true" - } - $env:BBX_RELEASE_REPO = $env:TARGET_RELEASE_REPO - $env:BBX_RELEASE_TAG = $env:PRIVATE_TAG - Write-Host "Using private release $($env:BBX_RELEASE_REPO)@$($env:BBX_RELEASE_TAG)" + if (-not $env:BBX_RELEASE_TAG) { + Write-Error "BBX_RELEASE_TAG is required" + exit 1 } - # Debug variables - if (-not $env:BBX_HOSTNAME) { Write-Host "BBX_HOSTNAME is not set" } else { Write-Host "BBX_HOSTNAME is set" } - if (-not $env:EMAIL) { Write-Host "EMAIL is not set" } else { Write-Host "EMAIL is set" } - if (-not $env:LICENSE_KEY) { Write-Host "LICENSE_KEY is not set" } else { Write-Host "LICENSE_KEY is set" } - if (-not $env:BBX_TEST_AGREEMENT) { Write-Host "BBX_TEST_AGREEMENT is not set" } else { Write-Host "BBX_TEST_AGREEMENT is set" } - if (-not $env:STATUS_MODE) { Write-Host "STATUS_MODE is not set" } else { Write-Host "STATUS_MODE is set" } - # Install BrowserBox from checked-out repo .\windows-scripts\bbx.ps1 install if (-not (Get-Command bbx -ErrorAction SilentlyContinue)) { Write-Error "bbx not found in PATH after install" @@ -237,55 +110,16 @@ jobs: Write-Error "curl.exe not installed" exit 1 } - Write-Host "curl.exe installed successfully" bbx setup -Hostname "$env:BBX_HOSTNAME" -Email "$env:EMAIL" -Port 9999 $loginLink = Get-Content "$env:USERPROFILE\.config\dosyago\bbpro\login.link" - Write-Host "Login link: $loginLink" - bbx run - Write-Host "Testing URL: $loginLink" - $maxRetries = 10 - $retryCount = 0 - $success = $false - while ($retryCount -lt $maxRetries -and -not $success) { - try { - $response = curl.exe -k -L "$loginLink" -o NUL -w "%{http_code}" - if ($response -ne "000") { - Write-Host "Initial connection successful: $response" - $success = $true - } else { - Write-Host "Retry $($retryCount + 1)/$maxRetries failed: HTTP $response" - } - } catch { - Write-Host "Retry $($retryCount + 1)/$maxRetries failed: $_" - } - if (-not $success) { Start-Sleep -Seconds 2; $retryCount++ } - } - if (-not $success) { - Write-Error "Failed to connect to $loginLink after $maxRetries retries." - exit 1 - } - Write-Host "Waiting 25 seconds to verify link stability..." + bbx run + $response = curl.exe -k -L "$loginLink" -o NUL -w "%{http_code}" + if ($response -eq "000") { Write-Error "Failed to connect"; exit 1 } Start-Sleep -Seconds 25 - try { - $response = curl.exe -k -L "$loginLink" -o NUL -w "%{http_code}" - if ($response -ne "000") { - Write-Host "Second verification successful: $response" - } else { - Write-Error "Second verification failed: HTTP $response" - exit 1 - } - } catch { - Write-Error "Second verification failed after 25s: $_" - exit 1 - } + $response = curl.exe -k -L "$loginLink" -o NUL -w "%{http_code}" + if ($response -eq "000") { Write-Error "Second verification failed"; exit 1 } bbx stop - $nodeProcs = Get-Process -Name "browserbox", "browserbox-devtools" -ErrorAction SilentlyContinue - if ($nodeProcs) { - Write-Error "Node processes still running after stop: $($nodeProcs | Format-List Name, Id | Out-String)" - } else { - Write-Host "No Node processes remain -- cleanup successful." - } - + - name: Print BBX Logs on Failure (Windows) if: failure() && matrix.os == 'windows-latest' shell: powershell @@ -306,11 +140,5 @@ jobs: if: always() && matrix.os != 'windows-latest' shell: bash run: | - # Try multiple cleanup methods - if [ -x ./bbx.sh ]; then - ./bbx.sh stop || true - elif command -v bbx &>/dev/null; then - bbx stop || true - fi - # Kill any remaining browserbox processes + ./bbx.sh stop || true pkill -f browserbox || true From fe26fc9382a149b7a1159d18073d3ca764eb0f7c Mon Sep 17 00:00:00 2001 From: DOSAYGO Engineering Date: Wed, 17 Dec 2025 22:35:56 +0800 Subject: [PATCH 10/15] Fix --- .github/workflows/bbx-saga.yaml | 250 +++++++++++++++++++++++++++----- 1 file changed, 211 insertions(+), 39 deletions(-) diff --git a/.github/workflows/bbx-saga.yaml b/.github/workflows/bbx-saga.yaml index 331fd210d..f0f444672 100644 --- a/.github/workflows/bbx-saga.yaml +++ b/.github/workflows/bbx-saga.yaml @@ -1,65 +1,164 @@ -name: bbx Saga Test Suite (Public Release) +name: bbx Saga Test Suite (Private Release) on: - release: - types: [published] + workflow_run: + workflows: ["Private Build (Draft Binaries)"] + types: [completed] workflow_dispatch: inputs: - release_tag: - description: "Tag to test (defaults to release event tag)" + use_private_release: + description: "Use private staged release assets (BrowserBox-source)" required: false - default: "" - release_repo: - description: "Repository that hosts the binaries" + default: false + type: boolean + private_release_tag: + description: "Tag to pull from when use_private_release=true (e.g., v15.3.11-rc)" required: false - default: "BrowserBox/BrowserBox" + default: "" + type: string concurrency: - group: ${{ github.repository }}-bbx-saga-${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release_tag || 'manual' }} + group: ${{ github.repository }}-bbx-saga cancel-in-progress: true permissions: - contents: read + contents: read # Required for actions/checkout + actions: read # Required for 'gh run view' to inspect the upstream workflow inputs env: - RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release_tag || '' }} - TARGET_RELEASE_REPO: ${{ github.event.inputs.release_repo || 'BrowserBox/BrowserBox' }} + PRIVATE_TAG: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.inputs.tag || github.event.inputs.private_release_tag || '' }} + USE_PRIVATE_RELEASE: ${{ github.event_name == 'workflow_run' && 'true' || github.event.inputs.use_private_release || 'false' }} + TARGET_RELEASE_REPO: ${{ (github.event_name == 'workflow_run' || github.event.inputs.use_private_release == 'true') && 'BrowserBox/BrowserBox-source' || 'BrowserBox/BrowserBox' }} jobs: - saga: - name: Test on ${{ matrix.os }} + build: + if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' continue-on-error: ${{ matrix.os == 'windows-latest' }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] + container_image: + - '' # No container (native runner) + - 'dokken/centos-stream-10' + - 'debian:latest' + exclude: + - os: macos-latest + container_image: 'dokken/centos-stream-10' + - os: macos-latest + container_image: 'debian:latest' + - os: windows-latest + container_image: 'dokken/centos-stream-10' + - os: windows-latest + container_image: 'debian:latest' runs-on: ${{ matrix.os }} - timeout-minutes: 15 - + timeout-minutes: 10 + container: ${{ matrix.container_image }} steps: - - name: Resolve release tag + # 1. Try to download the artifact (Solution 3), but DON'T fail if missing + - name: Download Tag Artifact + if: github.event_name == 'workflow_run' + uses: actions/download-artifact@v4 + with: + name: workflow-inputs + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true # <--- CRITICAL ADDITION + + # 2. The Ultimate Logic Block (Artifact -> CLI -> Regex) + - name: Derive PRIVATE_TAG + if: github.event_name == 'workflow_run' shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - tag="${RELEASE_TAG}" - repo="${TARGET_RELEASE_REPO}" + tag="" + + # ATTEMPT 1: Check for Artifact (Solution 3) + if [[ -f "workflow_tag.txt" ]]; then + tag=$(cat workflow_tag.txt | tr -d '[:space:]') + echo "✓ Found tag via Artifact: $tag" + fi + + # ATTEMPT 2: Check via GH CLI (Solution 2) + if [[ -z "$tag" ]]; then + echo "Artifact missing. Attempting gh CLI..." + workflow_run_id="${{ github.event.workflow_run.id }}" + tag=$(gh run view "$workflow_run_id" --repo "${{ github.repository }}" --json inputs --jq '.inputs.tag // empty' 2>/dev/null || echo "") + if [[ -n "$tag" ]]; then + echo "✓ Found tag via gh CLI: $tag" + fi + fi + + # ATTEMPT 3: Check via Run Title Regex (Solution 1) + if [[ -z "$tag" ]]; then + echo "CLI failed. Attempting display_title extraction..." + display_title="${{ github.event.workflow_run.display_title }}" + # Matches "Private Build v1.2.3" or just "v1.2.3" + if [[ "$display_title" =~ (v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?) ]]; then + tag="${BASH_REMATCH[1]}" + echo "✓ Found tag via Display Title: $tag" + fi + fi + + # FINAL CHECK if [[ -z "$tag" ]]; then - echo "RELEASE_TAG is required (from release event or workflow input)" >&2 + echo "::error::Could not resolve PRIVATE_TAG from Artifact, CLI, or Title." exit 1 fi + + # EXPORT { - echo "BBX_RELEASE_TAG=$tag" - echo "BBX_RELEASE_REPO=$repo" + echo "PRIVATE_TAG=$tag" + echo "USE_PRIVATE_RELEASE=true" + echo "TARGET_RELEASE_REPO=BrowserBox/BrowserBox-source" } >> "$GITHUB_ENV" - echo "Testing binaries from ${repo}@${tag}" - name: Checkout repository uses: actions/checkout@v4 + - name: Check if actor is repository owner or me + shell: bash + run: | + if [[ "${{ github.actor }}" != "crisdosaygo" ]]; then + echo "Actor is not me. Not running CI" + exit 1 + fi + - name: Prepare test script (Unix/macOS) if: matrix.os != 'windows-latest' shell: bash run: chmod +x tests/test-bbx.sh + - name: Install BrowserBox via binary (Unix/macOS) + if: matrix.os != 'windows-latest' + shell: bash + env: + BBX_INSTALL_USER: "bbxuser" + BBX_TEST_AGREEMENT: "true" + BBX_NO_UPDATE: "true" + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BBX_HOSTNAME: "localhost" + BB_QUICK_EXIT: "yesplease" + EMAIL: "test@example.com" + LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} + STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} + INSTALL_DOC_VIEWER: "false" + TARGET_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} + PRIVATE_TAG: ${{ env.PRIVATE_TAG }} + run: | + if [[ "${USE_PRIVATE_RELEASE}" == "true" ]]; then + if [[ -z "${PRIVATE_TAG}" ]]; then + echo "PRIVATE_RELEASE_TAG is required when use_private_release=true" >&2 + exit 1 + fi + export BBX_RELEASE_REPO="${TARGET_RELEASE_REPO}" + export BBX_RELEASE_TAG="${PRIVATE_TAG}" + echo "Using private release ${BBX_RELEASE_REPO}@${BBX_RELEASE_TAG}" + fi + chmod +x ./bbx.sh + ./bbx.sh install + - name: Execute BBX Test Saga (Unix/macOS) if: matrix.os != 'windows-latest' shell: bash @@ -74,17 +173,34 @@ jobs: STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} INSTALL_DOC_VIEWER: "false" GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BBX_RELEASE_REPO: ${{ env.BBX_RELEASE_REPO }} - BBX_RELEASE_TAG: ${{ env.BBX_RELEASE_TAG }} + TARGET_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} + PRIVATE_TAG: ${{ env.PRIVATE_TAG }} run: | + [ -z "$BBX_HOSTNAME" ] && echo "BBX_HOSTNAME is not set" || echo "BBX_HOSTNAME is set" + [ -z "$EMAIL" ] && echo "EMAIL is not set" || echo "EMAIL is set" + [ -z "$LICENSE_KEY" ] && echo "LICENSE_KEY is not set" || echo "LICENSE_KEY is set" + [ -z "$BBX_TEST_AGREEMENT" ] && echo "BBX_TEST_AGREEMENT is not set" || echo "BBX_TEST_AGREEMENT is set" + [ -z "$STATUS_MODE" ] && echo "STATUS_MODE is not set" || echo "STATUS_MODE is set" + [ -z "$INSTALL_DOC_VIEWER" ] && echo "INSTALL_DOC_VIEWER is not set" || echo "INSTALL_DOC_VIEWER is set to $INSTALL_DOC_VIEWER" + if [[ "${USE_PRIVATE_RELEASE}" == "true" ]]; then + if [[ -z "${PRIVATE_TAG}" ]]; then + echo "PRIVATE_RELEASE_TAG is required when use_private_release=true" >&2 + exit 1 + fi + export BBX_RELEASE_REPO="${TARGET_RELEASE_REPO}" + export BBX_RELEASE_TAG="${PRIVATE_TAG}" + echo "Using private release ${BBX_RELEASE_REPO}@${BBX_RELEASE_TAG}" + fi export BBX_NO_UPDATE=true - export INSTALL_DOC_VIEWER STATUS_MODE BBX_TEST_AGREEMENT LICENSE_KEY EMAIL BBX_HOSTNAME BB_QUICK_EXIT BBX_RELEASE_REPO BBX_RELEASE_TAG + export INSTALL_DOC_VIEWER STATUS_MODE BBX_TEST_AGREEMENT LICENSE_KEY EMAIL BBX_HOSTNAME BB_QUICK_EXIT ./tests/test-bbx.sh + continue-on-error: false - name: Execute BBX Test Saga (Windows) if: matrix.os == 'windows-latest' shell: powershell env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BBX_NO_UPDATE: "true" BBX_FORCE_CHROME_INSTALL: "true" BBX_HOSTNAME: "localhost" @@ -93,13 +209,24 @@ jobs: LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} BBX_TEST_AGREEMENT: "true" STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} - BBX_RELEASE_REPO: ${{ env.BBX_RELEASE_REPO }} - BBX_RELEASE_TAG: ${{ env.BBX_RELEASE_TAG }} + TARGET_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} + PRIVATE_TAG: ${{ env.PRIVATE_TAG }} run: | - if (-not $env:BBX_RELEASE_TAG) { - Write-Error "BBX_RELEASE_TAG is required" - exit 1 + if ($env:USE_PRIVATE_RELEASE -eq "true") { + if ([string]::IsNullOrWhiteSpace($env:PRIVATE_TAG)) { + Write-Error "PRIVATE_RELEASE_TAG is required when use_private_release=true" + } + $env:BBX_RELEASE_REPO = $env:TARGET_RELEASE_REPO + $env:BBX_RELEASE_TAG = $env:PRIVATE_TAG + Write-Host "Using private release $($env:BBX_RELEASE_REPO)@$($env:BBX_RELEASE_TAG)" } + # Debug variables + if (-not $env:BBX_HOSTNAME) { Write-Host "BBX_HOSTNAME is not set" } else { Write-Host "BBX_HOSTNAME is set" } + if (-not $env:EMAIL) { Write-Host "EMAIL is not set" } else { Write-Host "EMAIL is set" } + if (-not $env:LICENSE_KEY) { Write-Host "LICENSE_KEY is not set" } else { Write-Host "LICENSE_KEY is set" } + if (-not $env:BBX_TEST_AGREEMENT) { Write-Host "BBX_TEST_AGREEMENT is not set" } else { Write-Host "BBX_TEST_AGREEMENT is set" } + if (-not $env:STATUS_MODE) { Write-Host "STATUS_MODE is not set" } else { Write-Host "STATUS_MODE is set" } + # Install BrowserBox from checked-out repo .\windows-scripts\bbx.ps1 install if (-not (Get-Command bbx -ErrorAction SilentlyContinue)) { Write-Error "bbx not found in PATH after install" @@ -110,16 +237,55 @@ jobs: Write-Error "curl.exe not installed" exit 1 } + Write-Host "curl.exe installed successfully" bbx setup -Hostname "$env:BBX_HOSTNAME" -Email "$env:EMAIL" -Port 9999 $loginLink = Get-Content "$env:USERPROFILE\.config\dosyago\bbpro\login.link" - bbx run - $response = curl.exe -k -L "$loginLink" -o NUL -w "%{http_code}" - if ($response -eq "000") { Write-Error "Failed to connect"; exit 1 } + Write-Host "Login link: $loginLink" + bbx run + Write-Host "Testing URL: $loginLink" + $maxRetries = 10 + $retryCount = 0 + $success = $false + while ($retryCount -lt $maxRetries -and -not $success) { + try { + $response = curl.exe -k -L "$loginLink" -o NUL -w "%{http_code}" + if ($response -ne "000") { + Write-Host "Initial connection successful: $response" + $success = $true + } else { + Write-Host "Retry $($retryCount + 1)/$maxRetries failed: HTTP $response" + } + } catch { + Write-Host "Retry $($retryCount + 1)/$maxRetries failed: $_" + } + if (-not $success) { Start-Sleep -Seconds 2; $retryCount++ } + } + if (-not $success) { + Write-Error "Failed to connect to $loginLink after $maxRetries retries." + exit 1 + } + Write-Host "Waiting 25 seconds to verify link stability..." Start-Sleep -Seconds 25 - $response = curl.exe -k -L "$loginLink" -o NUL -w "%{http_code}" - if ($response -eq "000") { Write-Error "Second verification failed"; exit 1 } + try { + $response = curl.exe -k -L "$loginLink" -o NUL -w "%{http_code}" + if ($response -ne "000") { + Write-Host "Second verification successful: $response" + } else { + Write-Error "Second verification failed: HTTP $response" + exit 1 + } + } catch { + Write-Error "Second verification failed after 25s: $_" + exit 1 + } bbx stop - + $nodeProcs = Get-Process -Name "browserbox", "browserbox-devtools" -ErrorAction SilentlyContinue + if ($nodeProcs) { + Write-Error "Node processes still running after stop: $($nodeProcs | Format-List Name, Id | Out-String)" + } else { + Write-Host "No Node processes remain -- cleanup successful." + } + - name: Print BBX Logs on Failure (Windows) if: failure() && matrix.os == 'windows-latest' shell: powershell @@ -140,5 +306,11 @@ jobs: if: always() && matrix.os != 'windows-latest' shell: bash run: | - ./bbx.sh stop || true + # Try multiple cleanup methods + if [ -x ./bbx.sh ]; then + ./bbx.sh stop || true + elif command -v bbx &>/dev/null; then + bbx stop || true + fi + # Kill any remaining browserbox processes pkill -f browserbox || true From 2c845147987da59d6e33304e5299f1885707c31b Mon Sep 17 00:00:00 2001 From: DOSAYGO Engineering Date: Wed, 17 Dec 2025 22:45:51 +0800 Subject: [PATCH 11/15] ok --- .github/workflows/bbx-saga.yaml | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/.github/workflows/bbx-saga.yaml b/.github/workflows/bbx-saga.yaml index f0f444672..b2af97783 100644 --- a/.github/workflows/bbx-saga.yaml +++ b/.github/workflows/bbx-saga.yaml @@ -5,17 +5,6 @@ on: workflows: ["Private Build (Draft Binaries)"] types: [completed] workflow_dispatch: - inputs: - use_private_release: - description: "Use private staged release assets (BrowserBox-source)" - required: false - default: false - type: boolean - private_release_tag: - description: "Tag to pull from when use_private_release=true (e.g., v15.3.11-rc)" - required: false - default: "" - type: string concurrency: group: ${{ github.repository }}-bbx-saga @@ -26,9 +15,9 @@ permissions: actions: read # Required for 'gh run view' to inspect the upstream workflow inputs env: - PRIVATE_TAG: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.inputs.tag || github.event.inputs.private_release_tag || '' }} - USE_PRIVATE_RELEASE: ${{ github.event_name == 'workflow_run' && 'true' || github.event.inputs.use_private_release || 'false' }} - TARGET_RELEASE_REPO: ${{ (github.event_name == 'workflow_run' || github.event.inputs.use_private_release == 'true') && 'BrowserBox/BrowserBox-source' || 'BrowserBox/BrowserBox' }} + PRIVATE_TAG: false + USE_PRIVATE_RELEASE: false + TARGET_RELEASE_REPO: "BrowserBox/BrowserBox" jobs: build: @@ -111,7 +100,7 @@ jobs: { echo "PRIVATE_TAG=$tag" echo "USE_PRIVATE_RELEASE=true" - echo "TARGET_RELEASE_REPO=BrowserBox/BrowserBox-source" + echo "TARGET_RELEASE_REPO=BrowserBox/BrowserBox" } >> "$GITHUB_ENV" - name: Checkout repository From 8daf6061f6b5d070683c5212290b803691bed4ca Mon Sep 17 00:00:00 2001 From: DOSAYGO Engineering Date: Wed, 17 Dec 2025 22:48:00 +0800 Subject: [PATCH 12/15] ok --- .github/workflows/bbx-saga.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/bbx-saga.yaml b/.github/workflows/bbx-saga.yaml index b2af97783..b0a725071 100644 --- a/.github/workflows/bbx-saga.yaml +++ b/.github/workflows/bbx-saga.yaml @@ -1,4 +1,4 @@ -name: bbx Saga Test Suite (Private Release) +name: bbx Saga Test Suite (Public Release) on: workflow_run: @@ -125,7 +125,7 @@ jobs: env: BBX_INSTALL_USER: "bbxuser" BBX_TEST_AGREEMENT: "true" - BBX_NO_UPDATE: "true" + BBX_NO_UPDATE: "false" GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BBX_HOSTNAME: "localhost" BB_QUICK_EXIT: "yesplease" @@ -154,7 +154,7 @@ jobs: env: BBX_INSTALL_USER: "bbxuser" BBX_HOSTNAME: "localhost" - BBX_NO_UPDATE: "true" + BBX_NO_UPDATE: "false" BB_QUICK_EXIT: "yesplease" EMAIL: "test@example.com" LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} @@ -180,7 +180,7 @@ jobs: export BBX_RELEASE_TAG="${PRIVATE_TAG}" echo "Using private release ${BBX_RELEASE_REPO}@${BBX_RELEASE_TAG}" fi - export BBX_NO_UPDATE=true + export BBX_NO_UPDATE=false export INSTALL_DOC_VIEWER STATUS_MODE BBX_TEST_AGREEMENT LICENSE_KEY EMAIL BBX_HOSTNAME BB_QUICK_EXIT ./tests/test-bbx.sh continue-on-error: false @@ -190,7 +190,7 @@ jobs: shell: powershell env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BBX_NO_UPDATE: "true" + BBX_NO_UPDATE: "false" BBX_FORCE_CHROME_INSTALL: "true" BBX_HOSTNAME: "localhost" EMAIL: "test@example.com" From 9f108c5d27e305af981b6e7ee1d1002c5ad58a00 Mon Sep 17 00:00:00 2001 From: DOSAYGO Engineering Date: Wed, 17 Dec 2025 22:49:50 +0800 Subject: [PATCH 13/15] ok --- .github/workflows/bbx-saga.yaml | 4 ---- tests/test-bbx.sh | 1 - 2 files changed, 5 deletions(-) diff --git a/.github/workflows/bbx-saga.yaml b/.github/workflows/bbx-saga.yaml index b0a725071..19aeebc62 100644 --- a/.github/workflows/bbx-saga.yaml +++ b/.github/workflows/bbx-saga.yaml @@ -125,7 +125,6 @@ jobs: env: BBX_INSTALL_USER: "bbxuser" BBX_TEST_AGREEMENT: "true" - BBX_NO_UPDATE: "false" GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BBX_HOSTNAME: "localhost" BB_QUICK_EXIT: "yesplease" @@ -154,7 +153,6 @@ jobs: env: BBX_INSTALL_USER: "bbxuser" BBX_HOSTNAME: "localhost" - BBX_NO_UPDATE: "false" BB_QUICK_EXIT: "yesplease" EMAIL: "test@example.com" LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} @@ -180,7 +178,6 @@ jobs: export BBX_RELEASE_TAG="${PRIVATE_TAG}" echo "Using private release ${BBX_RELEASE_REPO}@${BBX_RELEASE_TAG}" fi - export BBX_NO_UPDATE=false export INSTALL_DOC_VIEWER STATUS_MODE BBX_TEST_AGREEMENT LICENSE_KEY EMAIL BBX_HOSTNAME BB_QUICK_EXIT ./tests/test-bbx.sh continue-on-error: false @@ -190,7 +187,6 @@ jobs: shell: powershell env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BBX_NO_UPDATE: "false" BBX_FORCE_CHROME_INSTALL: "true" BBX_HOSTNAME: "localhost" EMAIL: "test@example.com" diff --git a/tests/test-bbx.sh b/tests/test-bbx.sh index 194e60040..404b0fb01 100755 --- a/tests/test-bbx.sh +++ b/tests/test-bbx.sh @@ -27,7 +27,6 @@ if [[ -z "$INSTALL_DOC_VIEWER" ]]; then INSTALL_DOC_VIEWER="false" fi export INSTALL_DOC_VIEWER="${INSTALL_DOC_VIEWER}" -export BBX_NO_UPDATE="true" # Resolve script and repo paths so we can re-enter the tests reliably after su. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)" From 656a38d5a54a36cc9ebc7b64fbf49238294d9705 Mon Sep 17 00:00:00 2001 From: DOSAYGO Engineering Date: Wed, 17 Dec 2025 23:21:03 +0800 Subject: [PATCH 14/15] ok --- deploy-scripts/global_install.sh | 797 ++++++++++++++----------------- scripts/postinstall.sh | 549 ++++++++++++--------- scripts/setup_machine.sh | 42 +- 3 files changed, 717 insertions(+), 671 deletions(-) diff --git a/deploy-scripts/global_install.sh b/deploy-scripts/global_install.sh index 02340bcc6..7937933da 100755 --- a/deploy-scripts/global_install.sh +++ b/deploy-scripts/global_install.sh @@ -1,50 +1,116 @@ #!/usr/bin/env bash -# ANSI color codes -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' -BOLD='\033[1m' -CONFIG_DIR="$HOME/.config/dosyago/bbpro" +# +# Installs and configures BrowserBox Pro on a machine. +# +# Usage: +# ./global_install.sh [email_for_letsencrypt] +# +# Environment Variables: +# BBX_DEBUG: If set, runs the script in debug mode (set -x). +# BBX_NODE_VERSION: Overrides the default Node.js version to install. +# BBX_NO_COPY: If set, skips the final step of copying command scripts. +# BBX_BINARY_BUILD: If set, skips npm-related installations, assuming a self-contained binary. +# BBX_TEST_AGREEMENT: If set, bypasses the interactive license agreement prompt. +# + +# --- Strict Mode -------------------------------------------------------------- +set -u + +# --- Configuration & Constants ------------------------------------------------ +readonly ANSI_RED='\033[0;31m' +readonly ANSI_GREEN='\033[0;32m' +readonly ANSI_YELLOW='\033[1;33m' +readonly ANSI_BLUE='\033[0;34m' +readonly ANSI_NC='\033[0m' +readonly ANSI_BOLD='\033[1m' + +readonly CONFIG_DIR="$HOME/.config/dosyago/bbpro" +readonly DEFAULT_NODE_VERSION="${BBX_NODE_VERSION:-22}" + +export DEFAULT_NODE_VERSION + +# --- Global Variables --------------------------------------------------------- +# These are set by functions and used by others. +BBX_EMAIL="" +BBX_HOSTNAME="" +SUDO="" ZONE="" +APT="" +REPLY="" -if [[ -n "$BBX_DEBUG" ]]; then - set -x -fi -if [[ -z "$1" ]]; then - echo "Supply a hostname as first argument" >&2 - exit 1 -fi -BBX_NO_COPY="${BBX_NO_COPY}" +# --- Helper Functions --------------------------------------------------------- -unset npm_config_prefix +log_info() { + printf "${ANSI_BLUE}[INFO] %s${ANSI_NC}\n" "$1" >&2 +} -# flush any partial +log_warning() { + printf "${ANSI_YELLOW}[WARN] %s${ANSI_NC}\n" "$1" >&2 +} -SUDO="" -if [[ -f /etc/os-release ]]; then - . /etc/os-release -fi - -if command -v sudo &>/dev/null; then - export SUDO="$(command -v sudo) -n" -elif command -v apt &>/dev/null; then - apt update; apt install -y sudo -elif command -v dnf &>/dev/null; then - dnf update; dnf install -y sudo -fi - -if command -v sudo &>/dev/null; then - export SUDO="$(command -v sudo) -n" -else - echo "Warning could not install sudo. There may be problems..." >&2 -fi - -if command -v firewall-cmd &>/dev/null; then - ZONE="$($SUDO firewall-cmd --get-default-zone)" -fi +log_error() { + printf "${ANSI_RED}[ERROR] %s${ANSI_NC}\n" "$1" >&2 +} + +# Safely read user input, handling interactive and non-interactive sessions. +read_input() { + local prompt="$1" + if [ -t 0 ]; then # Interactive + read -p "$prompt" -r REPLY + else # Non-interactive (e.g., piped input) + read -r REPLY + REPLY=${REPLY:0:1} # Take the first character + fi + echo >&2 # for spacing +} + +# Detects the operating system. +get_os_type() { + case "$(uname -s)" in + Darwin*) echo "macOS" ;; + Linux*) echo "Linux" ;; + MING*) echo "win" ;; + *) echo "unknown" ;; + esac +} + +# Sets up the SUDO variable for privileged commands. +setup_sudo() { + if command -v sudo &>/dev/null; then + if sudo -n true &>/dev/null; then + SUDO="$(command -v sudo) -n" + else + SUDO="sudo" + fi + elif [[ "$(get_os_type)" == "Linux" ]]; then + log_warning "sudo command not found. Attempting to install..." + if command -v apt &>/dev/null; then + apt update && apt install -y sudo + elif command -v dnf &>/dev/null; then + dnf update && dnf install -y sudo + fi + # Re-check for sudo + if command -v sudo &>/dev/null; then SUDO="sudo"; fi + fi + + if [[ -z "$SUDO" ]]; then + log_warning "Could not find or install sudo. Privileged operations may fail." + else + export SUDO # Export for child scripts + fi +} + +# Wrapper for getent-like functionality without installing getent +getent_hosts() { + local hostname="$1" + # If on macOS, etc, manually search /etc/hosts + if ! command -v getent &>/dev/null; then + grep -E "^\s*([^#]+)\s+$hostname" /etc/hosts || echo "" + else + getent hosts "$hostname" || echo "" + fi +} # Check if hostname is local is_local_hostname() { @@ -52,6 +118,8 @@ is_local_hostname() { local resolved_ips ip local public_dns_servers=("8.8.8.8" "1.1.1.1" "208.67.222.222") local has_valid_result=0 + + # Try DNS resolution for dns in "${public_dns_servers[@]}"; do resolved_ips=$(command -v dig >/dev/null 2>&1 && dig +short "$hostname" A @"$dns") if [[ "$?" -eq 0 ]] && [[ -n "$resolved_ips" ]]; then @@ -65,451 +133,296 @@ is_local_hostname() { done <<< "$resolved_ips" fi done + # If all results were private or none resolved, treat as local if [[ "$has_valid_result" -eq 1 ]]; then return 0 # All IPs private => local fi + # Fallback: check /etc/hosts (or similar) - if command -v getent &>/dev/null; then - ip=$(getent hosts "$hostname" | awk '{print $1}' | head -n1) - if [[ "$ip" =~ ^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|::1$|fe80:) ]]; then - return 0 # Local - fi + ip=$(getent_hosts "$hostname" | awk '{print $1}' | head -n1) + if [[ "$ip" =~ ^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|::1$|fe80:) ]]; then + return 0 # Local fi + return 0 # Unresolvable => local } -# Ensure hostname is in /etc/hosts, allowing whitespace but not comments -ensure_hosts_entry() { - local hostname="$1" - if ! grep -v "^\s*#" /etc/hosts | grep -q "^\s*127\.0\.0\.1.*$hostname"; then - printf "${YELLOW}Adding $hostname to /etc/hosts...${NC}\n" - echo "127.0.0.1 $hostname" | $SUDO tee -a /etc/hosts > /dev/null || { printf "${RED}Failed to update /etc/hosts${NC}\n"; exit 1; } - else - printf "${GREEN}$hostname already mapped in /etc/hosts${NC}\n" +# Adds the local machine's hostname to /etc/hosts to ensure it's resolvable. +add_local_hostname_to_hosts() { + local default_host="$(hostname -f || uname -n || echo "unknown")" + local hostname="${1:-${BXX_HOSTNAME:-$default_host}}" + if ! grep -q "127.0.0.1.*$hostname" /etc/hosts; then + log_info "Adding local hostname '$hostname' to /etc/hosts..." + echo "127.0.0.1 $hostname" | $SUDO tee -a /etc/hosts > /dev/null fi } -add_hostname_to_hosts() { - # Try HOSTNAME env var, then uname -n, then /proc/sys/kernel/hostname, then fallback - local HOSTNAME="${HOSTNAME}" - if [ -z "$HOSTNAME" ] && command -v uname &>/dev/null; then - HOSTNAME=$(uname -n) - fi - if [ -z "$HOSTNAME" ] && [ -f /proc/sys/kernel/hostname ]; then - HOSTNAME=$(cat /proc/sys/kernel/hostname) - fi - HOSTNAME="${HOSTNAME:-unknown}" - if ! grep -q "127.0.0.1.*$HOSTNAME" /etc/hosts; then - echo "Adding hostname to /etc/hosts..." >&2 - $SUDO cp /etc/hosts /etc/hosts.backup - echo "127.0.0.1 $HOSTNAME" | $SUDO tee -a /etc/hosts > /dev/null - echo "::1 $HOSTNAME" | $SUDO tee -a /etc/hosts > /dev/null - echo "$HOSTNAME has been added to /etc/hosts." >&2 - else - echo "Hostname $HOSTNAME is already mapped in /etc/hosts." >&2 - fi -} - -add_hostname_to_hosts - +# Determines package manager and performs initial system setup. initialize_package_manager() { - local package_manager - - if [[ "$OSTYPE" == darwin* ]]; then - package_manager=$(command -v brew) - elif command -v apt &>/dev/null; then - package_manager="$(command -v apt)" - if command -v apt-get &>/dev/null; then - source ./deploy-scripts/non-interactive.sh - fi - # Check if the system is Debian and the version is 11 - if [[ "$ID" == "debian" && "$VERSION_ID" == "11" ]]; then - $SUDO apt install -y wget tar - mkdir -p $HOME/build/Release - echo "Installing Custom Build of WebRTC Node for Debian 11..." - wget https://github.com/dosyago/node-webrtc/releases/download/v1.0.0/debian-11-wrtc.node - chmod +x debian-11-wrtc.node - mv debian-11-wrtc.node $HOME/build/Release/wrtc.node - $SUDO mkdir -p /usr/local/share/dosyago/build/Release - $SUDO cp $HOME/build/Release/wrtc.node /usr/local/share/dosyago/build/Release/ - fi - elif command -v pkg &>/dev/null; then - package_manager="$(command -v pkg)" - elif command -v dnf >/dev/null; then - package_manager="$(command -v dnf) --best --allowerasing --skip-broken" - $SUDO dnf config-manager --set-enabled crb - $SUDO dnf -y upgrade --refresh - $SUDO dnf install -y https://download1.rpmfusion.org/free/el/rpmfusion-free-release-$(rpm -E %rhel).noarch.rpm - $SUDO dnf install -y https://download1.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-$(rpm -E %rhel).noarch.rpm - $SUDO firewall-cmd --permanent --zone="$ZONE" --add-service=http - $SUDO firewall-cmd --permanent --zone="$ZONE" --add-service=https - $SUDO firewall-cmd --reload - $SUDO dnf install -y wget tar - mkdir -p $HOME/build/Release - if [ "$ID" = "almalinux" ] && [[ "$VERSION_ID" == 8* ]]; then - echo "Installing Custom Build of WebRTC Node for Almalinux 8 like..." - wget https://github.com/dosyago/node-webrtc/releases/download/v1.0.0/almalinux-8-wrtc.node - chmod +x almalinux-8-wrtc.node - mv almalinux-8-wrtc.node $HOME/build/Release/wrtc.node - elif ([ "$ID" = "centos" ] || [ "$ID" = "rhel" ]) && [[ "$VERSION_ID" == 8* ]]; then - echo "Installing Custom Build of WebRTC Node for CentOS 8 or RedHat Enterprise Linux 8..." - wget https://github.com/dosyago/node-webrtc/releases/download/v1.0.0/centos-8-wrtc.node - chmod +x centos-8-wrtc.node - mv centos-8-wrtc.node $HOME/build/Release/wrtc.node + log_info "Initializing package manager..." + # shellcheck source=/etc/os-release + source /etc/os-release &>/dev/null || true + + case "$(get_os_type)" in + macOS) + # Add common Homebrew paths to PATH before checking + # Apple Silicon: /opt/homebrew/bin + # Intel: /usr/local/bin + if [[ -x /opt/homebrew/bin/brew ]] && [[ ":$PATH:" != *":/opt/homebrew/bin:"* ]]; then + export PATH="/opt/homebrew/bin:$PATH" + fi + if [[ -x /usr/local/bin/brew ]] && [[ ":$PATH:" != *":/usr/local/bin:"* ]]; then + export PATH="/usr/local/bin:$PATH" + fi + APT="$(command -v brew || true)" + ;; + Linux) + if command -v apt &>/dev/null; then + APT="apt" + # shellcheck source=/dev/null + source ./deploy-scripts/non-interactive.sh || true + elif command -v dnf &>/dev/null; then + APT="dnf" + $SUDO dnf -y upgrade --refresh + $SUDO dnf config-manager --set-enabled crb || true + $SUDO dnf install -y 'https://dl.fedoraproject.org/pub/epel/epel-release-latest-$(rpm -E %rhel).noarch.rpm' + if command -v firewall-cmd &>/dev/null; then + ZONE="$($SUDO firewall-cmd --get-default-zone || echo 'public')" + $SUDO firewall-cmd --permanent --zone="$ZONE" --add-service=http + $SUDO firewall-cmd --permanent --zone="$ZONE" --add-service=https + $SUDO firewall-cmd --reload + fi + fi + ;; + esac + + if [[ -z "$APT" ]]; then + log_warning "No supported package manager (apt, dnf, brew) found. Some features may be limited." + # For macOS, brew is optional - binary builds can work without it + if [[ "$(get_os_type)" != "macOS" ]]; then + log_error "Package manager required on Linux systems." + return 1 + fi else - echo "Installing Custom Build of WebRTC Node for CentOS 9 like..." - wget https://github.com/dosyago/node-webrtc/releases/download/v1.0.0/centos-9-wrtc.node - chmod +x centos-9-wrtc.node - mv centos-9-wrtc.node $HOME/build/Release/wrtc.node + log_info "Using package manager: $APT" + export APT fi - $SUDO mkdir -p /usr/local/share/dosyago/build/Release - $SUDO cp $HOME/build/Release/wrtc.node /usr/local/share/dosyago/build/Release/ - else - echo "No supported package manager found. Exiting." - return 1 - fi - - echo "Using package manager: $package_manager" - export APT=$package_manager -} - -# Ensure the disk is optimal -$SUDO ./deploy-scripts/disk_extend.sh - -# Call the function to initialize and export the APT variable -initialize_package_manager - -read_input() { - if [ -t 0 ]; then # Check if it's running interactively - read -p "$1" -r REPLY - else - read -r REPLY - REPLY=${REPLY:0:1} # Take the first character of the piped input - fi - echo # Add a newline for readability - echo -} - -get_latest_dir() { - # Find potential directories containing .bbpro_install_dir - echo "$(pwd)" } -os_type() { - case "$(uname -s)" in - Darwin*) echo "macOS";; - Linux*) echo "Linux";; - MING*) echo "win";; - *) echo "unknown";; - esac -} - -install_node() { - ./deploy-scripts/install_node.sh 22 +# Legacy note: Chai document assets used to be synchronized here when the +# installer ran from a source checkout. The binary installer now owns that step. +# Downloads and installs a custom WebRTC binary for specific Linux distributions. +install_custom_wrtc_if_needed() { + if [[ "$(get_os_type)" != "Linux" ]]; then return; fi + # shellcheck source=/etc/os-release + source /etc/os-release || true + + local wrtc_filename="" + local base_url="https://github.com/dosyago/node-webrtc/releases/download/v1.0.0" + + case "${ID-}" in + debian) [[ "${VERSION_ID-}" == "11" ]] && wrtc_filename="debian-11-wrtc.node" ;; + almalinux) [[ "${VERSION_ID-}" == 8* ]] && wrtc_filename="almalinux-8-wrtc.node" ;; + centos|rhel) [[ "${VERSION_ID-}" == 8* ]] && wrtc_filename="centos-8-wrtc.node" || wrtc_filename="centos-9-wrtc.node" ;; + esac + + if [[ -n "$wrtc_filename" ]]; then + log_info "Custom WebRTC build needed: $wrtc_filename" + $SUDO "$APT" install -y wget tar + wget -q -O "$wrtc_filename" "$base_url/$wrtc_filename" + + $SUDO mkdir -p /usr/local/share/dosyago/build/Release + $SUDO mv "$wrtc_filename" "/usr/local/share/dosyago/build/Release/wrtc.node" + $SUDO chmod +x "/usr/local/share/dosyago/build/Release/wrtc.node" + log_info "Custom WebRTC build installed." + fi } -install_nvm() { - source ~/.nvm/nvm.sh - if ! command -v nvm &>/dev/null; then - echo "Installing nvm..." - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash - source ~/.nvm/nvm.sh - nvm install v22 - else - nvm install v22 - fi -} +# Displays license and terms, and asks for user agreement. +check_license_agreement() { + if [[ -n "${BBX_TEST_AGREEMENT-}" ]] || [ -f "$CONFIG_DIR/.agreed" ]; then + return 0 + fi -# License agreement and terms display -display_terms() { - printf "${BLUE}${BOLD}Welcome to BrowserBox Installation${NC}\n" - printf "${YELLOW}Before proceeding, please note:${NC}\n" + printf "${ANSI_BLUE}${ANSI_BOLD}Welcome to BrowserBox Installation${ANSI_NC}\n" + printf "${ANSI_YELLOW}Before proceeding, please note:${ANSI_NC}\n" printf " - A valid, purchased license is required for use.\n" - printf " - By installing, you agree to the terms in:\n" - printf " * LICENSE: ${BLUE}https://github.com/dosyago/BrowserBoxPro/blob/main/LICENSE.md${NC}\n" - printf " * Terms: ${BLUE}https://dosaygo.com/terms.txt${NC}\n" - printf " * Privacy: ${BLUE}https://dosaygo.com/privacy.txt${NC}\n" - printf " - Commercial use (including evaluation) requires a license.\n" - printf " - Purchase at: ${BLUE}https://dosaygo.com${NC}\n" - printf " - Questions? Contact ${BLUE}sales@dosaygo.com${NC}\n\n" + printf " - By installing, you agree to the terms available at https://dosaygo.com\n" + printf " - Commercial use (including evaluation) requires a license.\n\n" + + read_input "Do you agree to these terms and confirm a license for use? (yes/no): " + if [[ "$REPLY" =~ ^[Yy] ]]; then + log_info "Terms accepted. Proceeding..." + mkdir -p "$CONFIG_DIR" + touch "${CONFIG_DIR}/.agreed" + echo "$BBX_EMAIL" >> "${CONFIG_DIR}/.agreed" + else + log_error "Terms not accepted. Exiting installation." + exit 1 + fi } -check_agreement() { - if [[ -n "$BBX_TEST_AGREEMENT" ]]; then - return 0 +# Opens firewall ports. +open_firewall_ports() { + local port=$1 + log_info "Opening firewall port $port..." + if command -v firewall-cmd &>/dev/null; then + $SUDO firewall-cmd --zone="${ZONE:-public}" --add-port="${port}/tcp" --permanent + $SUDO firewall-cmd --reload + elif command -v ufw &>/dev/null; then + $SUDO ufw allow "$port/tcp" + else + log_warning "No recognized firewall tool (firewalld, ufw) found. Port $port may need to be opened manually." fi - if [ ! -f "$CONFIG_DIR/.agreed" ]; then - display_terms - printf "${YELLOW}Do you agree to these terms and confirm a license for use? (yes/no): ${NC}\n" - read -r REPLY - if [[ "${REPLY:0:1}" =~ ^[yY]$ ]]; then - printf "${GREEN}Terms accepted. Proceeding...${NC}\n" - mkdir -p "$CONFIG_DIR" - touch "$CONFIG_DIR/.agreed" - else - printf "${RED}Terms not accepted. Exiting installation.${NC}\n" +} + +# Sets up SSL certificates using mkcert for local hostnames or LetsEncrypt for public ones. +setup_ssl() { + local hostname="$1" + local email="${2-}" + + if is_local_hostname "$hostname"; then + log_info "Local hostname detected. Setting up SSL with mkcert..." + if ! command -v mkcert &>/dev/null; then + log_info "Installing mkcert..." + case "$(get_os_type)" in + macOS) + if command -v brew &>/dev/null; then + brew install nss mkcert + else + log_warning "Homebrew not found. Please install mkcert manually or install Homebrew." + log_warning "Visit: https://brew.sh" + return 0 # Continue without mkcert for now + fi + ;; + win) log_error "Please install mkcert manually on Windows." ; exit 1 ;; + *) $SUDO "$APT" install -y libnss3-tools wget && \ + tmpdir="$(mktemp -d)" && \ + wget -qO "${tmpdir}/mkcert" "https://dl.filippo.io/mkcert/latest?for=linux/$(dpkg --print-architecture 2>/dev/null || uname -m)" && \ + chmod +x "${tmpdir}/mkcert" && $SUDO mv "${tmpdir}/mkcert" /usr/local/bin/ && rm -rf "${tmpdir}" ;; + esac + fi + mkcert -install + mkdir -p "$HOME/sslcerts" + (cd "$HOME/sslcerts" && mkcert --cert-file fullchain.pem --key-file privkey.pem "$hostname" localhost 127.0.0.1) + else + log_info "Public hostname detected. Setting up SSL with LetsEncrypt (via tls script)..." + if [[ -z "$email" ]]; then + log_error "An email address (for LetsEncrypt) is required as the second argument for public hostnames." exit 1 fi + export BB_USER_EMAIL="$email" + ./deploy-scripts/wait_for_hostname.sh "$hostname" + ./deploy-scripts/tls "$hostname" fi + log_info "SSL setup complete." } -# Usage in script (e.g., in global_install.sh or install subcommand) -check_agreement - -function create_selinux_policy_for_ports() { - # Check if SELinux is enforcing - if [[ "$(getenforce)" != "Enforcing" ]]; then - echo "SELinux is not in enforcing mode. Exiting." - return - fi - - # Parameters: SELinux type, protocol (tcp/udp), port range or single port - local SEL_TYPE=$1 - local PROTOCOL=$2 - local PORT_RANGE=$3 - - if [[ -z "$SEL_TYPE" || -z "$PROTOCOL" || -z "$PORT_RANGE" ]]; then - echo "Usage: create_selinux_policy_for_ports SEL_TYPE PROTOCOL PORT_RANGE" - return - fi - - # Add or modify the port context - sudo semanage port -a -t $SEL_TYPE -p $PROTOCOL $PORT_RANGE 2>/dev/null || \ - sudo semanage port -m -t $SEL_TYPE -p $PROTOCOL $PORT_RANGE - - # Generate and compile a custom policy module if required - sudo grep AVC /var/log/audit/audit.log | audit2allow -M my_custom_policy_module - sudo semodule -i my_custom_policy_module.pp - - echo "SELinux policy created and loaded for $PORT_RANGE on $PROTOCOL with type $SEL_TYPE." +# Installs Node.js via the dedicated script. +install_node() { + log_info "Installing Node.js v${DEFAULT_NODE_VERSION}..." + ./deploy-scripts/install_node.sh "$DEFAULT_NODE_VERSION" + # shellcheck source=/dev/null + source "${HOME}/.nvm/nvm.sh" || true } -open_firewall_port_range() { - local start_port=$1 - local end_port=$2 - local complete="" - - if command -v getenforce &>/dev/null; then - if [[ "$start_port" != "$end_port" ]]; then - create_selinux_policy_for_ports http_port_t tcp $start_port-$end_port - else - create_selinux_policy_for_ports http_port_t tcp $start_port +# --- Main Execution ----------------------------------------------------------- +main() { + # --- Initial Setup --- + if [[ -n "${BBX_DEBUG-}" ]]; then set -x; fi + unset npm_config_prefix # Avoid user-level npm config issues + + local hostname="${1-}" + if [[ -z "$hostname" ]]; then + log_error "Usage: $0 " + log_error "A hostname (e.g., 'localhost' or 'bbx.example.com') is required." + exit 1 fi - fi - - # Check for firewall-cmd (firewalld) - if command -v firewall-cmd &> /dev/null; then - echo "Using firewalld" - $SUDO firewall-cmd --zone="$ZONE" --add-port=${start_port}-${end_port}/tcp --permanent - $SUDO firewall-cmd --reload - complete="true" - fi - # Check for ufw (Uncomplicated Firewall) - if $SUDO bash -c 'command -v ufw' &> /dev/null; then - echo "Using ufw" - if [[ "$start_port" != "$end_port" ]]; then - $SUDO ufw allow ${start_port}:${end_port}/tcp - else - $SUDO ufw allow ${start_port}/tcp - fi - complete="true" - fi - - if [[ -z "$complete" ]]; then - echo "No recognized firewall management tool found" - if command -v apt; then - $SUDO apt install -y ufw - elif command -v dnf; then - $SUDO dnf install -y firewalld - fi - return 1 - fi -} + local email="${2-}" -# If on macOS -if [[ "$(uname)" == "Darwin" ]]; then - # Check the machine architecture - ARCH="$(uname -m)" - if [[ "$ARCH" == "arm64" ]]; then - true - #echo "This script is not compatible with the MacOS ARM architecture at this time" - #echo "due to some dependencies having no pre-built binaries for this architecture." - #echo "Please re-run this script under Rosetta." - #exit 1 + if [[ -z "$email" ]]; then + log_error "Usage: $0 " + log_error "An email is required." + exit 1 fi -fi -if [ "$(os_type)" == "Linux" ]; then - $SUDO $APT update - $SUDO $APT -y upgrade - $SUDO $APT install -y net-tools dnsutils - open_firewall_port_range 80 80 -fi -open_firewall_port_range 80 80 - - -if ! command -v jq &>/dev/null; then - if [ "$(os_type)" == "macOS" ]; then - brew install jq - else - $SUDO $APT install -y jq - fi -fi - - -if [ "$#" -eq 2 ] || is_local_hostname "$1"; then - hostname="$1" - export BB_USER_EMAIL="$2" - - amd64="" - - if is_local_hostname "$1"; then - if ! command -v mkcert &>/dev/null; then - printf "${YELLOW}Installing mkcert...${NC}\n" - if [ "$(os_type)" == "macOS" ]; then - brew install nss mkcert - elif [ "$(os_type)" == "win" ]; then - choco install mkcert || scoop bucket add extras && scoop install mkcert - else - # Determine architecture - if command -v dpkg &>/dev/null; then - arch=$(dpkg --print-architecture) + BBX_EMAIL="${email}" + BBX_HOSTNAME="${hostname}" + + setup_sudo + check_license_agreement + add_local_hostname_to_hosts + + # --- System Preparation --- + initialize_package_manager + install_custom_wrtc_if_needed + $SUDO ./deploy-scripts/disk_extend.sh + + if [[ "$(get_os_type)" == "Linux" ]]; then + $SUDO "$APT" update && $SUDO "$APT" -y upgrade + $SUDO "$APT" install -y net-tools dnsutils jq + open_firewall_ports 80 + open_firewall_ports 443 + elif [[ "$(get_os_type)" == "macOS" ]]; then + if command -v brew &>/dev/null; then + if ! command -v jq &>/dev/null; then + brew install jq + fi + if ! brew --prefix gnu-getopt &>/dev/null; then + brew install gnu-getopt + fi + brew unlink gnu-getopt &>/dev/null + brew link --force gnu-getopt &>/dev/null else - arch=$(uname -m) - if [ "$arch" = "x86_64" ]; then - arch="amd64" # Normalize to mkcert naming - fi - if [ "$arch" = "aarch64" ]; then - arch="arm64" - fi + log_warning "Homebrew not found. Skipping optional package installation (jq, gnu-getopt)." + log_warning "Some features may be limited. To install Homebrew, visit: https://brew.sh" fi - # Install NSS tools based on package manager - if command -v apt &>/dev/null; then - $SUDO $APT install -y libnss3-tools - elif command -v dnf &>/dev/null || command -v yum &>/dev/null; then - $SUDO $APT install -y nss-tools # $APT is dnf/yum here - else - printf "${RED}No supported package manager for NSS tools. Install manually.${NC}\n" - exit 1 - fi - # Ensure wget or curl for downloading - if command -v curl &>/dev/null; then - downloader="curl -JLO" - elif command -v wget &>/dev/null; then - downloader="wget" - else - printf "${RED}Neither curl nor wget found. Installing wget...${NC}\n" - if command -v apt &>/dev/null; then - $SUDO $APT install -y wget - elif command -v dnf &>/dev/null || command -v yum &>/dev/null; then - $SUDO $APT install -y wget - else - printf "${RED}Cannot install wget. Install curl or wget manually.${NC}\n" - exit 1 - fi - downloader="wget" - fi - # Download and install mkcert - $downloader "https://dl.filippo.io/mkcert/latest?for=linux/$arch" - chmod +x mkcert-v*-linux-$arch - $SUDO cp mkcert-v*-linux-$arch /usr/local/bin/mkcert || { printf "${RED}Failed to install mkcert${NC}\n"; exit 1; } - rm mkcert-v* - fi - fi - mkcert -install - if [[ ! -f "$HOME/sslcerts/privkey.pem" || ! -f "$HOME/sslcerts/fullchain.pem" ]]; then - : - else - echo "IMPORTANT: sslcerts already exist in $HOME/sslcerts directory. We ARE overwriting them." >&2 fi - mkdir -p $HOME/sslcerts - pwd=$(pwd) - cd $HOME/sslcerts - mkcert --cert-file fullchain.pem --key-file privkey.pem $hostname localhost 127.0.0.1 - cd $pwd - else - ip=$(getent hosts "$hostname" | awk '{ print $1 }') - if [ -n "$ip" ]; then - ./deploy-scripts/wait_for_hostname.sh "$hostname" - ./deploy-scripts/tls "$hostname" + # --- SSL and Node.js --- + setup_ssl "$@" + install_node + + # --- Project Installation --- + log_info "Preparing project..." + + if [[ -z "${BBX_BINARY_BUILD-}" ]]; then + log_info "Running full npm installation..." + if ! npm i; then + log_warning "Initial 'npm i' failed. Attempting recovery..." + npm run clean + if [[ "$(get_os_type)" == "Linux" ]]; then + $SUDO "$APT" install -y build-essential + fi + npm i # Retry + fi + log_info "npm install complete." else - echo "The provided hostname ($hostname) could not be resolved. Please ensure that you've added a DNS A/AAAA record pointing from the hostname to this machine's public IP address." - exit 1 + log_info "Binary build detected. Skipping npm installations." + log_info "Running postinstall for external dependencies only..." + ./scripts/postinstall.sh --external-dependencies-only fi - fi -else - if is_local_hostname "$1"; then - echo "" - echo "Usage: $0 " - echo "Please provide a hostname as an argument. This hostname will be where a running bbpro instance is accessible." >&2 - echo "Note that user email is not required as 2nd parameter when using localhost as we do not use Letsencrypt in that case." >&2 - else - echo "" - echo "Usage: $0 " - echo "Please provide a hostname as an argument. This hostname will be where a running bbpro instance is accessible." >&2 - echo "Please provide an email as argument. This email will be used to agree to the Letsencrypt TOS for cert provisioning" >&2 - fi - exit 1 -fi - -echo -n "Finding bbpro directory..." - -INSTALL_DIR=$(get_latest_dir) - -echo "Found bbpro at: $INSTALL_DIR" - -read -p "Enter to continue" -r -echo "Ensuring fully installed..." - -if [[ -n "$INSTALL_DIR" ]]; then - cd $INSTALL_DIR -fi - -./client/install-client-deps.sh - -echo "Ensuring nvm installed..." -#install_nvm -install_node -source ~/.nvm/nvm.sh - -echo "Running npm install..." - -if ! npm i; then - # attempt to save node weirdness - npm run clean - $SUDO $APT install -y build-essential - npm i -fi - -echo "npm install complete" - -if [ "$(os_type)" == "macOS" ]; then - if ( ! command -v getopt ) || [[ "$(getopt --version)" != *"util-linux"* ]]; then - if brew install gnu-getopt; then - brew link --force gnu-getopt + # Legacy note: Chai document assets used to be copied from the source tree + # during installs. With the migration to binary-only distribution the + # browserbox installer now seeds ~/.config/dosyago/bbpro/chai, so the source + # installer intentionally skips that work. + + # --- Finalization --- + if [[ -z "${BBX_NO_COPY-}" ]]; then + log_info "Copying command scripts to system path..." + local copy_script="./deploy-scripts/cp_commands_only.sh" + if [[ -z "${BBX_BINARY_BUILD-}" ]]; then + copy_script="./deploy-scripts/copy_install.sh" + fi + "$copy_script" "$(pwd)" + else + log_warning "Skipping script copy step due to BBX_NO_COPY." fi - fi -else - if ! command -v getopt &>/dev/null; then - echo "Installing gnu-getopt for Linux..." - $SUDO $APT update - $SUDO $APT install -y gnu-getopt - fi -fi - -echo "Fully installed!" -if [[ -z "$BBX_NO_COPY" ]]; then - ./deploy-scripts/copy_install.sh "$INSTALL_DIR" -fi - -#echo -n "Setting up deploy system ..." - -#cd $INSTALL_DIR/src/services/pool/deploy/ -#./scripts/setup.sh + log_info "BrowserBox Pro installation complete!" +} -echo "Install complete!" +main "$@" diff --git a/scripts/postinstall.sh b/scripts/postinstall.sh index 19398eec4..bb622493e 100755 --- a/scripts/postinstall.sh +++ b/scripts/postinstall.sh @@ -1,264 +1,383 @@ #!/usr/bin/env bash +# shellcheck shell=bash -#set -x +# Strict mode +set -euo pipefail +IFS=$'\n\t' +unset npm_config_prefix + +# Avoid optional deps (native modules like lmdb) by default +export NPM_CONFIG_OPTIONAL=false +export NPM_CONFIG_INCLUDE_OPTIONAL=false -if [[ ! -f bbpro_dir ]]; then - echo "Not called from BrowserBox directory. Exiting..." - exit 0 +if [[ -n "${BBX_DEBUG-}" ]]; then + set -x fi -protecc_win_sysadmins() { - # Check for Windows environments - if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" || -n "$WINDIR" || "$(uname -s)" =~ ^MINGW || "$(uname -s)" =~ ^CYGWIN || -n "$WSL_DISTRO_NAME" ]]; then - echo -e "Not running bash postinstall script on Windows." - exit 0 +readonly ANSI_RED='\033[0;31m' +readonly ANSI_GREEN='\033[0;32m' +readonly ANSI_YELLOW='\033[1;33m' +readonly ANSI_BLUE='\033[0;34m' +readonly ANSI_NC='\033[0m' +readonly ANSI_BOLD='\033[1m' + +# ---- Configuration ---------------------------------------------------------- +NODE_VERSION="${BBX_NODE_VERSION:-${DEFAULT_NODE_VERSION:-22}}" +CONFIG_DIR="${HOME}/.config/dosyago/bbpro" +DOCVIEWER_LOG_FILE="${CONFIG_DIR}/docviewer-install-nohup.out" +CUSTOM_WRTC_DIR="${CONFIG_DIR}/build/Release" +GLOBAL_CUSTOM_WRTC_DIR="/usr/local/share/dosyago/build/Release" +docviewer_dir="src/services/pool/chai" + +# ---- Global Variables ------------------------------------------------------- +# These are set by functions and used by others. +PRODUCTION="" +if [[ -n "${BBX_BINARY_BUILD-}" ]]; then + PRODUCTION="--omit=dev" +fi +REPLY="" +APT="" +PLAT="" +CUSTOM_WRTC_BUILD_URL="" +CUSTOM_WRTC_BUILD_FILENAME="" +# Use an array for sudo to handle arguments robustly +declare -a SUDO=() + +# Helpers for sudo usage +has_sudo() { + [[ ${#SUDO[@]} -gt 0 ]] +} + +run_sudo() { + if has_sudo; then + "${SUDO[@]}" "$@" + else + "$@" fi } -# Call the function right away -protecc_win_sysadmins +# ---- Helper Functions ------------------------------------------------------- -unset npm_config_prefix -export node=$(command -v node) -export SUDO=$(command -v sudo) -export sudo=$(command -v sudo) +log_info() { + printf "${ANSI_BLUE}[INFO] %s${ANSI_NC}\n" "$1" >&2 +} -if "$sudo" -n true &>/dev/null; then - sudo="$SUDO -n" - SUDO="$SUDO -n" -fi -if command -v node.exe &>/dev/null; then - node=$(command -v node.exe) -fi +log_warning() { + printf "${ANSI_YELLOW}[WARN] %s${ANSI_NC}\n" "$1" >&2 +} -export PLAT="$("$node" -p process.platform)" +log_error() { + printf "${ANSI_RED}[ERROR] %s${ANSI_NC}\n" "$1" >&2 +} -initialize_package_manager() { - local package_manager - - if [[ "$OSTYPE" == darwin* ]]; then - package_manager=$(command -v brew) - elif command -v apt &>/dev/null; then - package_manager=$(command -v apt) - if command -v apt-get &>/dev/null; then - source ./deploy-scripts/non-interactive.sh +# Function to safely read user input, handling interactive and non-interactive sessions. +read_input() { + local prompt="$1" + if [[ "$PLAT" != "win"* && -t 0 ]]; then + read -p "$prompt" -r REPLY + elif [[ "$PLAT" != "win"* ]]; then + # Non-interactive, read first char from stdin if available + read -n 1 -r REPLY || true + else + # Default to 'y' on Windows or in non-interactive environments without piped input + REPLY="y" fi - # Check if the system is Debian and the version is 11 - if [[ "$ID" == "debian" && "$VERSION_ID" == "11" ]]; then - $SUDO apt install -y wget tar - mkdir -p $HOME/build/Release - echo "Installing Custom Build of WebRTC Node for Debian 11..." - wget https://github.com/dosyago/node-webrtc/releases/download/v1.0.0/debian-11-wrtc.node - chmod +x debian-11-wrtc.node - mv debian-11-wrtc.node $HOME/build/Release/wrtc.node - $SUDO mkdir -p /usr/local/share/dosyago/build/Release - $SUDO cp $HOME/build/Release/wrtc.node /usr/local/share/dosyago/build/Release/ + echo >&2 # for spacing +} + +# Ensures the script exits gracefully on Windows-like environments. +guard_windows_and_unsupported() { + if [[ ! -f "bbpro_dir" ]] && [[ -z "${BBX_BINARY_BUILD-}" ]]; then + log_error "This script must be run from the BrowserBox project root. Exiting." + exit 1 fi - elif command -v dnf >/dev/null; then - package_manager="$(command -v dnf) --best --allowerasing --skip-broken" - $SUDO dnf config-manager --set-enabled crb - $SUDO dnf -y upgrade --refresh - $SUDO dnf install -y https://download1.rpmfusion.org/free/el/rpmfusion-free-release-$(rpm -E %rhel).noarch.rpm - $SUDO dnf install -y https://download1.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-$(rpm -E %rhel).noarch.rpm - $SUDO firewall-cmd --permanent --zone="$ZONE" --add-service=http - $SUDO firewall-cmd --permanent --zone="$ZONE" --add-service=https - $SUDO firewall-cmd --reload - $SUDO dnf install -y wget tar - mkdir -p $HOME/build/Release - if [ "$ID" = "almalinux" ] && [[ "$VERSION_ID" == 8* ]]; then - echo "Installing Custom Build of WebRTC Node for Almalinux 8 like..." - wget https://github.com/dosyago/node-webrtc/releases/download/v1.0.0/almalinux-8-wrtc.node - chmod +x almalinux-8-wrtc.node - mv almalinux-8-wrtc.node $HOME/build/Release/wrtc.node - elif ([ "$ID" = "centos" ] || [ "$ID" = "rhel" ]) && [[ "$VERSION_ID" == 8* ]]; then - echo "Installing Custom Build of WebRTC Node for CentOS 8 or RedHat Enterprise Linux 8..." - wget https://github.com/dosyago/node-webrtc/releases/download/v1.0.0/centos-8-wrtc.node - chmod +x centos-8-wrtc.node - mv centos-8-wrtc.node $HOME/build/Release/wrtc.node + + PLAT="$(node -p "process.platform")" + + if [[ "$PLAT" == "win32" || "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || -n "${WINDIR-}" || "$(uname -s)" =~ ^(MINGW|CYGWIN) || -n "${WSL_DISTRO_NAME-}" ]]; then + log_info "Detected a Windows-based environment. Exiting bash post-install script." + exit 0 + fi +} + +# Sets up the SUDO variable for privileged commands. +setup_sudo() { + if command -v sudo &>/dev/null; then + # Use non-interactive sudo if possible to prevent hanging in scripts. + if sudo -n true &>/dev/null; then + SUDO=(sudo -n) + else + SUDO=(sudo) + fi else - echo "Installing Custom Build of WebRTC Node for CentOS 9 like..." - wget https://github.com/dosyago/node-webrtc/releases/download/v1.0.0/centos-9-wrtc.node - chmod +x centos-9-wrtc.node - mv centos-9-wrtc.node $HOME/build/Release/wrtc.node + SUDO=() fi - $SUDO mkdir -p /usr/local/share/dosyago/build/Release - $SUDO cp $HOME/build/Release/wrtc.node /usr/local/share/dosyago/build/Release/ - else - echo "No supported package manager found. Exiting." - return 1 - fi +} - echo "Using package manager: $package_manager" - export APT=$package_manager +# Configures and installs Node.js using NVM. +setup_node_with_nvm() { + log_info "Setting up Node.js version ${NODE_VERSION} with NVM..." + # Source NVM, ignoring errors if it's not found (it might be installed by a later step). + # shellcheck source=/dev/null + source "${HOME}/.nvm/nvm.sh" || true + nvm install "${NODE_VERSION}" + nvm use "${NODE_VERSION}" + nvm alias default "${NODE_VERSION}" } -initialize_package_manager +# Performs a check for the required architecture on macOS. +macos_arch_check() { + if [[ "$PLAT" == "darwin" && "$(arch)" != "i386" ]]; then + log_info "Running on non-Rosetta (non-i386) architecture on macOS. Continuing..." + # Original script had `true` here, preserving behavior. + fi +} -if [[ $PLAT == win* ]]; then - winpty nvm install v22 - winpty nvm use latest -else - source ~/.nvm/nvm.sh; - nvm install v22 -fi +# Copies a custom binding file required for a specific WebRTC library. +copy_roamhq_binding() { + local binding_source="./config/roamhq-wrtc-lib-binding.js" + local binding_dest="./node_modules/@roamhq/wrtc/lib/binding.js" + if [ -f "$binding_source" ]; then + log_info "Copying custom @roamhq/wrtc binding file..." + mkdir -p "$(dirname "$binding_dest")" + cp "$binding_source" "$binding_dest" + else + log_error "Required binding file not found at ${binding_source}." + fi +} -if ! command -v pm2 &>/dev/null; then - . /etc/os-release +# Determines the system's package manager and performs initial setup. +initialize_package_manager() { + log_info "Initializing package manager..." + if [[ "$PLAT" == "darwin" ]]; then + APT="$(command -v brew || true)" + elif command -v apt &>/dev/null; then + APT="apt" + # shellcheck source=/dev/null + source ./deploy-scripts/non-interactive.sh || true + elif command -v dnf &>/dev/null; then + APT="dnf" + if has_sudo; then + run_sudo dnf config-manager --set-enabled crb || true + run_sudo dnf -y upgrade --refresh + run_sudo dnf -y install https://download1.rpmfusion.org/free/el/rpmfusion-free-release-$(rpm -E %rhel).noarch.rpm + run_sudo dnf -y install https://download1.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-$(rpm -E %rhel).noarch.rpm + if command -v firewall-cmd &>/dev/null; then + run_sudo firewall-cmd --permanent --zone="${ZONE:-public}" --add-service=http + run_sudo firewall-cmd --permanent --zone="${ZONE:-public}" --add-service=https + run_sudo firewall-cmd --reload + fi + fi + else + log_error "No supported package manager (apt, dnf, brew) found. Some features may fail." + return 1 + fi + log_info "Using package manager: ${APT}" +} - if [[ $ID == *"bsd" ]]; then - sudo -n npm i -g pm2@latest || echo "Could not install pm2" >&2 - else - npm i -g pm2@latest - fi -fi +# Determines if a custom WebRTC build is needed for the current Linux distro. +fetch_custom_wrtc_build_if_applicable() { + if [[ "$PLAT" != "linux" ]]; then + return + fi -# flush any partial -if [[ $PLAT != win* ]]; then - read -p "Enter to continue" -r -fi -REPLY="" + # shellcheck source=/etc/os-release + source /etc/os-release || true + local base_url="https://github.com/dosyago/node-webrtc/releases/download/v1.0.0" + + case "${ID-}" in + debian) + if [[ "${VERSION_ID-}" == "11" ]]; then + CUSTOM_WRTC_BUILD_FILENAME="debian-11-wrtc.node" + fi + ;; + almalinux) + if [[ "${VERSION_ID-}" == 8* ]]; then + CUSTOM_WRTC_BUILD_FILENAME="almalinux-8-wrtc.node" + fi + ;; + centos | rhel) + if [[ "${VERSION_ID-}" == 8* ]]; then + CUSTOM_WRTC_BUILD_FILENAME="centos-8-wrtc.node" + else + CUSTOM_WRTC_BUILD_FILENAME="centos-9-wrtc.node" + fi + ;; + esac + + if [ -n "$CUSTOM_WRTC_BUILD_FILENAME" ]; then + CUSTOM_WRTC_BUILD_URL="${base_url}/${CUSTOM_WRTC_BUILD_FILENAME}" + log_info "Custom WebRTC build needed for this OS: ${CUSTOM_WRTC_BUILD_FILENAME}" + if has_sudo && [ -n "$APT" ]; then + run_sudo "$APT" install -y wget tar + fi + log_info "Downloading from ${CUSTOM_WRTC_BUILD_URL}..." + wget -q -O "$CUSTOM_WRTC_BUILD_FILENAME" "$CUSTOM_WRTC_BUILD_URL" + fi +} -read_input() { - if [[ $PLAT != win* ]]; then - if [ -t 0 ]; then # Check if it's running interactively - read -p "$1" -r REPLY +# Installs the downloaded custom WebRTC build, if it exists. +try_install_custom_wrtc_build_if_needed() { + if [ -f "$CUSTOM_WRTC_BUILD_FILENAME" ]; then + log_info "Installing custom WebRTC build..." + mkdir -p "$CUSTOM_WRTC_DIR" + chmod +x "$CUSTOM_WRTC_BUILD_FILENAME" + mv "$CUSTOM_WRTC_BUILD_FILENAME" "${CUSTOM_WRTC_DIR}/wrtc.node" + + if has_sudo; then + run_sudo mkdir -p "$GLOBAL_CUSTOM_WRTC_DIR" + run_sudo cp "${CUSTOM_WRTC_DIR}/wrtc.node" "${GLOBAL_CUSTOM_WRTC_DIR}/" + fi else - read -r REPLY - REPLY=${REPLY:0:1} # Take the first character of the piped input + log_info "No custom WebRTC build to install. Assuming default works." fi - echo # Add a newline for readability - echo - else - REPLY="y" - fi } -if [[ "$(uname -s)" == "Darwin" ]]; then - if [[ "$(arch)" != "i386" ]]; then - #>&2 echo "Please run this script under Rosetta (i386 architecture)." - true - #exit 1 - fi -fi +# Asks user and runs the machine-wide setup script if confirmed. +run_setup_machine_if_desired() { + if [[ "$PLAT" == "win"* ]]; then + return + fi -echo "Copying custom @roamhq/wrtc/lib/binding.js file..." >&2 -cp ./config/roamhq-wrtc-lib-binding.js ./node_modules/@roamhq/wrtc/lib/binding.js + # Preserving original behavior of an unconditional "Enter to continue" + read -p "Enter to continue" -r || true + REPLY="" -echo -echo + read_input "Want to run setup_machine script? (first-time install or major update) [y/N]: " + if [[ "$REPLY" =~ ^[Yy] ]]; then + log_info "Running full machine setup..." + bash ./scripts/setup_machine.sh + else + log_info "Skipping machine setup. Proceeding with npm installs..." + fi +} -if [[ $PLAT != win* ]]; then - read_input "Want to run setup_machine script? (you only need to do this the first time you install BrowserBox, or when you update new version) y/n " - echo - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; - then - echo "Not running full setup...Just doing npm install..." - else - echo "Running full setup..." - bash ./scripts/setup_machine.sh +# Installs npm dependencies for all sub-projects. +install_project_packages() { + log_info "Installing all project npm dependencies..." + + local sub_projects=( + "src/zombie-lord" + "src/public/voodoo" + "src/zombie-lord/custom-launcher" + "src/services/instance/parec-server" + "src/services/pool/crdp-secure-proxy-server" + "$docviewer_dir" + ) + + for project_dir in "${sub_projects[@]}"; do + log_info "--- Installing for ${project_dir} ---" + ( + cd "$project_dir" + npm i $PRODUCTION --omit=optional --no-optional + npm dedupe || true + npm audit fix --omit=optional || true # --force to handle complex cases + ) + done +} + +# Asks user and optionally installs the secure document viewer and its dependencies. +install_doc_viewer_if_desired() { + if [[ "$PLAT" == "win"* ]]; then + log_info "Chai Secure DocViewer Service not currently supported on Windows..." + return fi -fi -mkdir -p src/public/voodoo/assets/icons - -echo "Installing packages for zombie lord..." -cd src/zombie-lord -npm i -npm audit fix -echo "Installing packages for client..." -cd ../public/voodoo -npm i -npm audit fix - -echo "Installing packages for custom chrome launcher..." -cd ../../zombie-lord/custom-launcher -npm i -npm audit fix -cd ../../ - -echo "Installing packages for audio service..." -cd services/instance/parec-server -npm i -npm audit fix -cd ../ - -echo "Installing packages for remote devtools service..." -cd ../pool/crdp-secure-proxy-server -npm i -npm audit fix - -echo "Installing packages for secure document viewer..." -cd ../chai -npm i -npm audit fix - -if [[ $PLAT != win* ]]; then - echo - yes_docs="false" - - if [[ "$IS_DOCKER_BUILD" == "true" ]] && [[ "$(echo "$INSTALL_DOC_VIEWER" | tr '[:upper:]' '[:lower:]')" == "true" ]]; then - yes_docs="true" - elif [[ "$(echo "$INSTALL_DOC_VIEWER" | tr '[:upper:]' '[:lower:]')" == "false" ]]; then - yes_docs="false" - else - read_input "Do you want to add the secure document viewer for PDFs, DOCX and more? (lengthy install because of all the fonts and TeX related packages) y/n " - if [[ "$REPLY" =~ ^[Yy]$ ]]; then + cwd="$(pwd)" + cd "$docviewer_dir" + local yes_docs="false" + if [[ "${IS_DOCKER_BUILD-}" == "true" ]] && [[ "$(echo "${INSTALL_DOC_VIEWER-}" | tr '[:upper:]' '[:lower:]')" == "true" ]]; then yes_docs="true" - else + elif [[ "$(echo "${INSTALL_DOC_VIEWER-}" | tr '[:upper:]' '[:lower:]')" == "false" ]]; then yes_docs="false" - fi + else + read_input "Add secure document viewer for PDFs, DOCX, etc.? (lengthy install) [y/N]: " + if [[ "$REPLY" =~ ^[Yy] ]]; then + yes_docs="true" + fi fi - if [[ "$yes_docs" != "false" ]]; then - # Unless we are in docker then - # Change to background install to speed things up - # this means you will need to wait a bit for documents to convert. And likely have to retry them - # by reclicking on the link to them in the remote browser, to trigger the convert process again - # at least until this background install process for the doc viewer and its dependencies - # is completed. This can take a while, due to all the tex and font packages! - # It can take 30 minutes or more to complete depending on the machines speed and bandwidth. - echo "Installing OS dependencies for secure document viewer in the background..." - if [[ "$IS_DOCKER_BUILD" == "true" ]]; then - yes | ./scripts/setup.sh - else - if command -v nohup &>/dev/null; then - nohup bash -c 'yes | ./scripts/setup.sh' &> "${HOME}/docviewer-install-nohup.out" & + if [[ "$yes_docs" == "true" ]]; then + log_info "Installing OS dependencies for secure document viewer in the background..." + mkdir -p "$(dirname "$DOCVIEWER_LOG_FILE")" + if [[ "${IS_DOCKER_BUILD-}" == "true" ]]; then + (yes | ./scripts/setup_docviewer.sh) > "$DOCVIEWER_LOG_FILE" 2>&1 + elif command -v nohup &>/dev/null; then + nohup bash -c 'yes | ./scripts/setup_docviewer.sh' > "$DOCVIEWER_LOG_FILE" 2>&1 & else - bash -c 'yes | ./scripts/setup.sh' &> "${HOME}/docviewer-install-nohup.out" & + bash -c 'yes | ./scripts/setup_docviewer.sh' > "$DOCVIEWER_LOG_FILE" 2>&1 & fi - fi + log_info "Installation started in the background. Monitor progress with: tail -f ${DOCVIEWER_LOG_FILE}" else - echo "Skipping doc viewer install" + log_info "Skipping optional document viewer installation." fi -fi -cd ../../../../ - -USE_FLASH=$(node ./src/show_useflash.js); -if [[ $USE_FLASH != "false" ]]; then - if ! command -v jq &>/dev/null; then - if command -v winget &>/dev/null; then - winget install -e --id jqlang.jq - elif command -v brew &>/dev/null; then - brew install jq - elif command -v $APT &>/dev/null; then - $SUDO $APT install jq + cd "$cwd" +} + +# Installs Ruffle (Flash emulator) if the configuration requires it. +try_install_ruffle_if_requested() { + if [[ -n "${BBX_BINARY_BUILD-}" ]]; then + echo "Deferring to binary build for Flash emulator." + return 0 + fi + + log_info "Checking if Ruffle (Flash Player emulator) is needed..." + if [[ "$(node ./src/show_useflash.js)" != "false" ]]; then + log_info "Ruffle is required. Installing dependencies and downloading..." + if ! command -v jq &>/dev/null && has_sudo && [ -n "$APT" ]; then + "${SUDO[@]}" "$APT" install -y jq + fi + ./scripts/download_ruffle.sh else - echo "Do not know how to install 'jq'. Please install manually." >&2 + log_info "Ruffle not required by config." fi - fi - ./scripts/download_ruffle.sh -fi +} -if ! command -v pm2 &>/dev/null; then - npm i -g pm2@latest -fi +# ---- Main Execution --------------------------------------------------------- +main() { + local EXTERNAL_ONLY=false + if [[ "${1-}" == "--external-dependencies-only" ]]; then + EXTERNAL_ONLY=true + log_info "Running in --external-dependencies-only mode. Skipping all npm package installations." + fi -npm i --save-exact esbuild@latest + guard_windows_and_unsupported + setup_sudo -npm audit fix + initialize_package_manager + fetch_custom_wrtc_build_if_applicable + try_install_custom_wrtc_build_if_needed -echo "$(date)" > .bbpro_install_dir + setup_node_with_nvm + + macos_arch_check + run_setup_machine_if_desired + install_doc_viewer_if_desired + + # The 'jq' dependency is handled within try_install_ruffle_if_requested + try_install_ruffle_if_requested + + + if [[ "$EXTERNAL_ONLY" == "true" ]]; then + log_info "External dependency setup complete. Exiting." + exit 0 + fi + + # --- Full Installation (if not --external-dependencies-only) --- + + log_info "Installing main project dependencies..." + + copy_roamhq_binding + install_project_packages + + log_info "Finalizing dependencies..." + npm i --save-exact esbuild@latest + npm audit fix --omit=optional || true + + echo "$(date)" > .bbpro_install_dir + log_info "BrowserBox post-installation script complete." + exit 0 +} -echo Dependency install complete. +main "$@" diff --git a/scripts/setup_machine.sh b/scripts/setup_machine.sh index 8aaefa9c3..a7bdde2ae 100755 --- a/scripts/setup_machine.sh +++ b/scripts/setup_machine.sh @@ -90,18 +90,41 @@ fi source ~/.nvm/nvm.sh -cd src/zombie-lord +if [ -d src/zombie-lord ]; then + cd src/zombie-lord +else + echo "[ERROR] src/zombie-lord not found. CWD=$(pwd)" >&2 + # Best-effort tree/ls to debug binary extraction issues + if command -v tree >/dev/null 2>&1; then + tree -L 3 + else + if command -v apt >/dev/null 2>&1; then + $SUDO apt update -y && $SUDO apt install -y tree || true + elif command -v yum >/dev/null 2>&1; then + $SUDO yum install -y tree || true + fi + command -v tree >/dev/null 2>&1 && tree -L 3 || ls -la + fi + exit 1 +fi + $SUDO -E ./video.deps $SUDO -E ./audio.deps $SUDO -E ./deb.deps $SUDO -E ./font.deps $SUDO -E ./pptr.deps $SUDO -E ./dlchrome.sh -if which google-chrome-stable; then - echo "chrome installed" +if command -v google-chrome-stable >/dev/null 2>&1 \ + || command -v google-chrome >/dev/null 2>&1 \ + || command -v chromium-browser >/dev/null 2>&1 \ + || command -v chromium >/dev/null 2>&1 \ + || { [[ -n "${CHROME_PATH:-}" ]] && command -v "$CHROME_PATH" >/dev/null 2>&1; } +then + echo "chrome installed" else - echo "chrome failed to install. you need to run setup again" - exit 1 + echo "chrome failed to install or not found on PATH (checked google-chrome-stable/google-chrome/chromium[-browser]/CHROME_PATH)" + echo "please rerun setup_machine or set CHROME_PATH explicitly" + exit 1 fi cd ../.. $SUDO $APT install -y libvips libjpeg-dev @@ -131,14 +154,5 @@ if ! $SUDO grep -q "%browsers ALL=(ALL:browsers) NOPASSWD:" /etc/sudoers; then fi $SUDO ufw disable -if ! command -v pm2 &>/dev/null; then - . /etc/os-release - - if [[ $ID == *"bsd" ]]; then - $SUDO npm i -g pm2@latest - else - npm i -g pm2@latest - fi -fi $SUDO setcap 'cap_net_bind_service=+ep' "$(command -v node)" From b24429d994da4a06daa171c8bb7ac8a333488ee9 Mon Sep 17 00:00:00 2001 From: DOSAYGO Engineering Date: Thu, 18 Dec 2025 01:18:29 +0800 Subject: [PATCH 15/15] Trigger public saga on release publish --- .github/workflows/bbx-saga.yaml | 129 +++++++------------------------- 1 file changed, 27 insertions(+), 102 deletions(-) diff --git a/.github/workflows/bbx-saga.yaml b/.github/workflows/bbx-saga.yaml index 19aeebc62..cf7298b21 100644 --- a/.github/workflows/bbx-saga.yaml +++ b/.github/workflows/bbx-saga.yaml @@ -1,10 +1,20 @@ name: bbx Saga Test Suite (Public Release) on: - workflow_run: - workflows: ["Private Build (Draft Binaries)"] - types: [completed] + release: + types: [published] workflow_dispatch: + inputs: + release_tag: + description: "Release tag to test (e.g., v1.2.3). Defaults to the published release tag when run from a release event." + required: false + default: "" + type: string + release_repo: + description: "Release repo (owner/name)." + required: false + default: "BrowserBox/BrowserBox" + type: string concurrency: group: ${{ github.repository }}-bbx-saga @@ -12,16 +22,14 @@ concurrency: permissions: contents: read # Required for actions/checkout - actions: read # Required for 'gh run view' to inspect the upstream workflow inputs + actions: read env: - PRIVATE_TAG: false - USE_PRIVATE_RELEASE: false - TARGET_RELEASE_REPO: "BrowserBox/BrowserBox" + RELEASE_TAG: ${{ github.event_name == 'release' && github.event.release.tag_name || github.event.inputs.release_tag || '' }} + TARGET_RELEASE_REPO: ${{ github.event.inputs.release_repo || 'BrowserBox/BrowserBox' }} jobs: build: - if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' continue-on-error: ${{ matrix.os == 'windows-latest' }} strategy: fail-fast: false @@ -44,76 +52,19 @@ jobs: timeout-minutes: 10 container: ${{ matrix.container_image }} steps: - # 1. Try to download the artifact (Solution 3), but DON'T fail if missing - - name: Download Tag Artifact - if: github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 - with: - name: workflow-inputs - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - continue-on-error: true # <--- CRITICAL ADDITION - - # 2. The Ultimate Logic Block (Artifact -> CLI -> Regex) - - name: Derive PRIVATE_TAG - if: github.event_name == 'workflow_run' + - name: Resolve release under test shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - tag="" - - # ATTEMPT 1: Check for Artifact (Solution 3) - if [[ -f "workflow_tag.txt" ]]; then - tag=$(cat workflow_tag.txt | tr -d '[:space:]') - echo "✓ Found tag via Artifact: $tag" - fi - - # ATTEMPT 2: Check via GH CLI (Solution 2) - if [[ -z "$tag" ]]; then - echo "Artifact missing. Attempting gh CLI..." - workflow_run_id="${{ github.event.workflow_run.id }}" - tag=$(gh run view "$workflow_run_id" --repo "${{ github.repository }}" --json inputs --jq '.inputs.tag // empty' 2>/dev/null || echo "") - if [[ -n "$tag" ]]; then - echo "✓ Found tag via gh CLI: $tag" - fi - fi - - # ATTEMPT 3: Check via Run Title Regex (Solution 1) - if [[ -z "$tag" ]]; then - echo "CLI failed. Attempting display_title extraction..." - display_title="${{ github.event.workflow_run.display_title }}" - # Matches "Private Build v1.2.3" or just "v1.2.3" - if [[ "$display_title" =~ (v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?) ]]; then - tag="${BASH_REMATCH[1]}" - echo "✓ Found tag via Display Title: $tag" - fi - fi - - # FINAL CHECK - if [[ -z "$tag" ]]; then - echo "::error::Could not resolve PRIVATE_TAG from Artifact, CLI, or Title." + set -euo pipefail + if [[ -z "${RELEASE_TAG}" ]]; then + echo "release_tag is required when manually dispatched." >&2 exit 1 fi - - # EXPORT - { - echo "PRIVATE_TAG=$tag" - echo "USE_PRIVATE_RELEASE=true" - echo "TARGET_RELEASE_REPO=BrowserBox/BrowserBox" - } >> "$GITHUB_ENV" + echo "Testing release: ${TARGET_RELEASE_REPO}@${RELEASE_TAG}" - name: Checkout repository uses: actions/checkout@v4 - - name: Check if actor is repository owner or me - shell: bash - run: | - if [[ "${{ github.actor }}" != "crisdosaygo" ]]; then - echo "Actor is not me. Not running CI" - exit 1 - fi - - name: Prepare test script (Unix/macOS) if: matrix.os != 'windows-latest' shell: bash @@ -132,18 +83,9 @@ jobs: LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} INSTALL_DOC_VIEWER: "false" - TARGET_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} - PRIVATE_TAG: ${{ env.PRIVATE_TAG }} + BBX_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} + BBX_RELEASE_TAG: ${{ env.RELEASE_TAG }} run: | - if [[ "${USE_PRIVATE_RELEASE}" == "true" ]]; then - if [[ -z "${PRIVATE_TAG}" ]]; then - echo "PRIVATE_RELEASE_TAG is required when use_private_release=true" >&2 - exit 1 - fi - export BBX_RELEASE_REPO="${TARGET_RELEASE_REPO}" - export BBX_RELEASE_TAG="${PRIVATE_TAG}" - echo "Using private release ${BBX_RELEASE_REPO}@${BBX_RELEASE_TAG}" - fi chmod +x ./bbx.sh ./bbx.sh install @@ -160,8 +102,8 @@ jobs: STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} INSTALL_DOC_VIEWER: "false" GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TARGET_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} - PRIVATE_TAG: ${{ env.PRIVATE_TAG }} + BBX_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} + BBX_RELEASE_TAG: ${{ env.RELEASE_TAG }} run: | [ -z "$BBX_HOSTNAME" ] && echo "BBX_HOSTNAME is not set" || echo "BBX_HOSTNAME is set" [ -z "$EMAIL" ] && echo "EMAIL is not set" || echo "EMAIL is set" @@ -169,15 +111,6 @@ jobs: [ -z "$BBX_TEST_AGREEMENT" ] && echo "BBX_TEST_AGREEMENT is not set" || echo "BBX_TEST_AGREEMENT is set" [ -z "$STATUS_MODE" ] && echo "STATUS_MODE is not set" || echo "STATUS_MODE is set" [ -z "$INSTALL_DOC_VIEWER" ] && echo "INSTALL_DOC_VIEWER is not set" || echo "INSTALL_DOC_VIEWER is set to $INSTALL_DOC_VIEWER" - if [[ "${USE_PRIVATE_RELEASE}" == "true" ]]; then - if [[ -z "${PRIVATE_TAG}" ]]; then - echo "PRIVATE_RELEASE_TAG is required when use_private_release=true" >&2 - exit 1 - fi - export BBX_RELEASE_REPO="${TARGET_RELEASE_REPO}" - export BBX_RELEASE_TAG="${PRIVATE_TAG}" - echo "Using private release ${BBX_RELEASE_REPO}@${BBX_RELEASE_TAG}" - fi export INSTALL_DOC_VIEWER STATUS_MODE BBX_TEST_AGREEMENT LICENSE_KEY EMAIL BBX_HOSTNAME BB_QUICK_EXIT ./tests/test-bbx.sh continue-on-error: false @@ -194,17 +127,9 @@ jobs: LICENSE_KEY: ${{ secrets.BB_LICENSE_KEY }} BBX_TEST_AGREEMENT: "true" STATUS_MODE: ${{ secrets.STATUS_MODE_KEY }} - TARGET_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} - PRIVATE_TAG: ${{ env.PRIVATE_TAG }} + BBX_RELEASE_REPO: ${{ env.TARGET_RELEASE_REPO }} + BBX_RELEASE_TAG: ${{ env.RELEASE_TAG }} run: | - if ($env:USE_PRIVATE_RELEASE -eq "true") { - if ([string]::IsNullOrWhiteSpace($env:PRIVATE_TAG)) { - Write-Error "PRIVATE_RELEASE_TAG is required when use_private_release=true" - } - $env:BBX_RELEASE_REPO = $env:TARGET_RELEASE_REPO - $env:BBX_RELEASE_TAG = $env:PRIVATE_TAG - Write-Host "Using private release $($env:BBX_RELEASE_REPO)@$($env:BBX_RELEASE_TAG)" - } # Debug variables if (-not $env:BBX_HOSTNAME) { Write-Host "BBX_HOSTNAME is not set" } else { Write-Host "BBX_HOSTNAME is set" } if (-not $env:EMAIL) { Write-Host "EMAIL is not set" } else { Write-Host "EMAIL is set" }