The scripts were hardcoded to look for OpenSMTPD at a custom workspace path, but the Dockerfile installs it via apt to /usr/sbin/smtpd. This change adds fallback logic to check multiple locations: 1. Custom workspace path (for backward compatibility) 2. System path /usr/sbin/smtpd 3. Anywhere in PATH Also adds graceful handling when OpenSMTPD is not installed, logging an informative message instead of failing with "No such file or directory".
448 lines
17 KiB
Bash
Executable File
448 lines
17 KiB
Bash
Executable File
#!/bin/bash
|
|
set -e
|
|
|
|
# Source diagnostic logger if available
|
|
if [ -f "/usr/local/bin/diagnostic-logger.sh" ]; then
|
|
. /usr/local/bin/diagnostic-logger.sh
|
|
fi
|
|
|
|
# Helper function to log messages
|
|
log() {
|
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
echo "[${timestamp}] $*" >&2
|
|
}
|
|
|
|
# Sanitize environment variables by removing invisible Unicode characters
|
|
# This fixes the Portainer deployment issue where U+200E and similar characters
|
|
# cause "unexpected character" errors in variable names
|
|
sanitize_env_vars() {
|
|
log "Sanitizing environment variables..."
|
|
|
|
# Create a secure temporary file
|
|
local temp_env
|
|
temp_env=$(mktemp /tmp/sanitized_env.XXXXXX)
|
|
|
|
# Export current environment to a file, then clean it
|
|
export -p > "$temp_env"
|
|
|
|
# Remove common invisible Unicode characters in a single sed command:
|
|
# - U+200E (Left-to-Right Mark) - E2 80 8E
|
|
# - U+200F (Right-to-Left Mark) - E2 80 8F
|
|
# - U+200B (Zero Width Space) - E2 80 8B
|
|
# - U+FEFF (Zero Width No-Break Space / BOM) - EF BB BF
|
|
# - U+202A-202E (Directional formatting characters)
|
|
sed -i \
|
|
-e 's/\xE2\x80\x8E//g' \
|
|
-e 's/\xE2\x80\x8F//g' \
|
|
-e 's/\xE2\x80\x8B//g' \
|
|
-e 's/\xEF\xBB\xBF//g' \
|
|
-e 's/\xE2\x80\xAA//g' \
|
|
-e 's/\xE2\x80\xAB//g' \
|
|
-e 's/\xE2\x80\xAC//g' \
|
|
-e 's/\xE2\x80\xAD//g' \
|
|
-e 's/\xE2\x80\xAE//g' \
|
|
"$temp_env" 2>/dev/null
|
|
|
|
# Source the sanitized environment
|
|
# If sourcing fails, log a warning but continue (environment may be partially set)
|
|
if ! source "$temp_env" 2>/dev/null; then
|
|
log "WARNING: Failed to source sanitized environment. Some variables may not be set correctly."
|
|
fi
|
|
|
|
# Clean up temporary file
|
|
rm -f "$temp_env"
|
|
|
|
log "Environment variables sanitized successfully"
|
|
}
|
|
|
|
# Sanitize environment variables on startup
|
|
log "=== STARTUP INITIALIZATION ==="
|
|
log "Container ID: $(hostname 2>/dev/null || 'unknown')"
|
|
log "Timestamp: $(date '+%Y-%m-%d %H:%M:%S %Z')"
|
|
|
|
# Initialize diagnostic logging only if file exists and we're in debug mode
|
|
if [ -f "/usr/local/bin/diagnostic-logger.sh" ] && [ "${DEBUG_LOGGING:-false}" = "true" ]; then
|
|
. /usr/local/bin/diagnostic-logger.sh
|
|
log_system_info
|
|
validate_environment
|
|
rotate_logs
|
|
fi
|
|
|
|
sanitize_env_vars
|
|
|
|
log "=== ENVIRONMENT SANITIZATION COMPLETE ==="
|
|
|
|
# Repository configuration
|
|
REPO_URL="${REPO_URL:-}"
|
|
REPO_DIR="/home/web/data"
|
|
REPO_BRANCH="${REPO_BRANCH:-main}"
|
|
GITHUB_USERNAME="${GITHUB_USERNAME:-}"
|
|
GITHUB_PAT="${GITHUB_PAT:-}"
|
|
|
|
# Helper function to get authenticated repository URL
|
|
get_auth_url() {
|
|
local url="$1"
|
|
if [ -n "$GITHUB_USERNAME" ] && [ -n "$GITHUB_PAT" ]; then
|
|
# Extract the repository path from the URL
|
|
local repo_path=$(echo "$url" | sed 's|https://github.com/||')
|
|
echo "https://${GITHUB_USERNAME}:${GITHUB_PAT}@github.com/${repo_path}"
|
|
else
|
|
echo "$url"
|
|
fi
|
|
}
|
|
|
|
log "Initializing Shopify AI App Builder..."
|
|
|
|
# Log repository configuration details
|
|
log "Repository Configuration:"
|
|
log " URL: ${REPO_URL:-'NOT SET'}"
|
|
log " Branch: ${REPO_BRANCH:-'NOT SET'}"
|
|
log " Directory: ${REPO_DIR}"
|
|
log " GitHub Username: ${GITHUB_USERNAME:-'NOT SET'}"
|
|
log " GitHub PAT: ${GITHUB_PAT:+SET (hidden)}${GITHUB_PAT:-NOT SET}"
|
|
|
|
# Only clone/pull if REPO_URL is set
|
|
if [ -n "$REPO_URL" ]; then
|
|
log "Repository URL: $REPO_URL"
|
|
log "Repository directory: $REPO_DIR"
|
|
log "Default branch: $REPO_BRANCH"
|
|
|
|
# Check if authentication credentials are available
|
|
if [ -n "$GITHUB_USERNAME" ] && [ -n "$GITHUB_PAT" ]; then
|
|
log "GitHub authentication credentials found for user: $GITHUB_USERNAME"
|
|
else
|
|
log "WARNING: No GitHub authentication credentials found. Private repository access may fail."
|
|
fi
|
|
|
|
# Check if git is available
|
|
if ! command -v git &> /dev/null; then
|
|
log "ERROR: git is not available in the container"
|
|
exit 1
|
|
fi
|
|
|
|
# Check if the repository directory is empty or doesn't have .git
|
|
if [ ! -d "$REPO_DIR/.git" ]; then
|
|
# Directory doesn't exist or .git is missing - need to clone
|
|
if [ -d "$REPO_DIR" ] && [ "$(ls -A "$REPO_DIR")" ]; then
|
|
# Directory exists but is not a git repo - back it up
|
|
log "WARNING: $REPO_DIR exists but is not a git repository. Backing up to ${REPO_DIR}.backup"
|
|
mv "$REPO_DIR" "${REPO_DIR}.backup"
|
|
fi
|
|
|
|
log "Repository not found. Cloning $REPO_URL into $REPO_DIR..."
|
|
auth_url=$(get_auth_url "$REPO_URL")
|
|
git clone "$auth_url" "$REPO_DIR"
|
|
cd "$REPO_DIR"
|
|
log "Successfully cloned repository"
|
|
else
|
|
# Repository exists, pull latest changes
|
|
cd "$REPO_DIR"
|
|
log "Repository found at $REPO_DIR. Pulling latest changes from $REPO_BRANCH..."
|
|
|
|
# Update remote URL to use authentication if credentials are available
|
|
if [ -n "$GITHUB_USERNAME" ] && [ -n "$GITHUB_PAT" ]; then
|
|
auth_url=$(get_auth_url "$REPO_URL")
|
|
log "Updating remote URL with authentication credentials"
|
|
git remote set-url origin "$auth_url"
|
|
fi
|
|
|
|
# Check if we're on a detached HEAD or have uncommitted changes
|
|
if git diff --quiet && git diff --cached --quiet; then
|
|
git fetch origin
|
|
# Use plain git pull per policy (do not force origin/branch explicitly here)
|
|
git pull || {
|
|
log "WARNING: Failed to pull from $REPO_BRANCH, attempting to pull from any available branch"
|
|
git pull || log "WARNING: Pull operation failed, continuing anyway"
|
|
}
|
|
log "Successfully pulled latest changes"
|
|
else
|
|
log "WARNING: Repository has uncommitted changes. Skipping pull to avoid conflicts."
|
|
log "Run 'git status' to see changes, then 'git pull' manually if desired"
|
|
fi
|
|
fi
|
|
log "Repository is ready at $REPO_DIR"
|
|
else
|
|
log "No REPO_URL set - starting with empty workspace"
|
|
mkdir -p "$REPO_DIR"
|
|
fi
|
|
|
|
log "Setting up OpenSMTPD service..."
|
|
|
|
# Start OpenSMTPD if configuration exists
|
|
OPENSMTPD_CONFIG="/workspace/src/backend/app/opensmtpd/install/etc/smtpd.conf"
|
|
OPENSMTPD_PID_FILE="/workspace/src/backend/app/opensmtpd/install/var/run/smtpd.pid"
|
|
OPENSMTPD_CUSTOM_BINARY="/workspace/src/backend/app/opensmtpd/install/sbin/smtpd"
|
|
OPENSMTPD_SYSTEM_BINARY="/usr/sbin/smtpd"
|
|
|
|
# Determine which binary to use (prefer custom, fallback to system)
|
|
OPENSMTPD_BINARY=""
|
|
if [ -x "$OPENSMTPD_CUSTOM_BINARY" ]; then
|
|
OPENSMTPD_BINARY="$OPENSMTPD_CUSTOM_BINARY"
|
|
log "Using custom OpenSMTPD binary: $OPENSMTPD_BINARY"
|
|
elif [ -x "$OPENSMTPD_SYSTEM_BINARY" ]; then
|
|
OPENSMTPD_BINARY="$OPENSMTPD_SYSTEM_BINARY"
|
|
log "Using system OpenSMTPD binary: $OPENSMTPD_BINARY"
|
|
else
|
|
# Try to find in PATH
|
|
if command -v smtpd &>/dev/null; then
|
|
OPENSMTPD_BINARY="$(command -v smtpd)"
|
|
log "Using OpenSMTPD from PATH: $OPENSMTPD_BINARY"
|
|
fi
|
|
fi
|
|
|
|
if [ -n "$OPENSMTPD_BINARY" ] && [ -f "$OPENSMTPD_CONFIG" ]; then
|
|
log "OpenSMTPD configuration found at $OPENSMTPD_CONFIG"
|
|
|
|
# Ensure required directories exist with proper permissions
|
|
mkdir -p /workspace/src/backend/app/opensmtpd/install/var/spool/smtpd
|
|
mkdir -p /workspace/src/backend/app/opensmtpd/install/var/spool/queue
|
|
mkdir -p /workspace/src/backend/app/opensmtpd/install/var/run
|
|
chmod -R 755 /workspace/src/backend/app/opensmtpd/install/var
|
|
|
|
# Check if OpenSMTPD is already running
|
|
if [ -f "$OPENSMTPD_PID_FILE" ] && kill -0 "$(cat "$OPENSMTPD_PID_FILE")" 2>/dev/null; then
|
|
log "OpenSMTPD is already running (PID: $(cat "$OPENSMTPD_PID_FILE"))"
|
|
else
|
|
log "Starting OpenSMTPD..."
|
|
"$OPENSMTPD_BINARY" -d -f "$OPENSMTPD_CONFIG" 2>&1 &
|
|
OPENSMTPD_PID=$!
|
|
|
|
# Wait for OpenSMTPD to start
|
|
sleep 2
|
|
if kill -0 "$OPENSMTPD_PID" 2>/dev/null; then
|
|
log "OpenSMTPD started successfully (PID: $OPENSMTPD_PID)"
|
|
echo "$OPENSMTPD_PID" > "$OPENSMTPD_PID_FILE"
|
|
else
|
|
log "WARNING: OpenSMTPD failed to start"
|
|
fi
|
|
fi
|
|
else
|
|
log "OpenSMTPD not configured or binary not found, skipping SMTP setup"
|
|
if [ ! -f "$OPENSMTPD_CONFIG" ]; then
|
|
log " Config file missing: $OPENSMTPD_CONFIG"
|
|
fi
|
|
if [ -z "$OPENSMTPD_BINARY" ]; then
|
|
log " Binary not found at: $OPENSMTPD_CUSTOM_BINARY or $OPENSMTPD_SYSTEM_BINARY"
|
|
fi
|
|
fi
|
|
|
|
log "Starting Shopify AI App Builder service..."
|
|
|
|
# Use /opt/webchat directly as it contains the actual server.js with node_modules
|
|
# /opt/webchat_v2 is just a wrapper and causes module loading failures
|
|
CHAT_APP_DIR="${CHAT_APP_DIR:-/opt/webchat}"
|
|
CHAT_APP_FALLBACK="/opt/webchat_v2"
|
|
CHAT_PORT="${CHAT_PORT:-4500}"
|
|
CHAT_HOST="${CHAT_HOST:-0.0.0.0}"
|
|
ACCESS_PASSWORD="${ACCESS_PASSWORD:-}"
|
|
|
|
# Persist opencode installation & user config
|
|
# Move/Copy /root/.opencode to $REPO_DIR/.opencode if not already present
|
|
# Then symlink /root/.opencode -> $REPO_DIR/.opencode so opencode state (connections, providers)
|
|
# are persisted on container restarts while keeping the install accessible for runtime.
|
|
PERSISTED_OPENCODE_DIR="$REPO_DIR/.opencode"
|
|
OPENCODE_INSTALL_DIR="/root/.opencode"
|
|
if [ -d "$OPENCODE_INSTALL_DIR" ]; then
|
|
# If persisted dir does not exist, copy initial files so the app continues to work
|
|
if [ ! -d "$PERSISTED_OPENCODE_DIR" ] || [ -z "$(ls -A $PERSISTED_OPENCODE_DIR 2>/dev/null)" ]; then
|
|
log "Persisting opencode into $PERSISTED_OPENCODE_DIR"
|
|
mkdir -p "$PERSISTED_OPENCODE_DIR"
|
|
# Copy installed files to the persisted folder to preserve both the binary and state
|
|
cp -a "$OPENCODE_INSTALL_DIR/." "$PERSISTED_OPENCODE_DIR/" || true
|
|
chown -R root:root "$PERSISTED_OPENCODE_DIR" || true
|
|
fi
|
|
# Replace the install dir with a symlink to the persisted directory (only if not a symlink already)
|
|
if [ -e "$OPENCODE_INSTALL_DIR" ] && [ ! -L "$OPENCODE_INSTALL_DIR" ]; then
|
|
log "Symlinking $OPENCODE_INSTALL_DIR -> $PERSISTED_OPENCODE_DIR"
|
|
rm -rf "$OPENCODE_INSTALL_DIR"
|
|
ln -s "$PERSISTED_OPENCODE_DIR" "$OPENCODE_INSTALL_DIR"
|
|
elif [ -L "$OPENCODE_INSTALL_DIR" ]; then
|
|
log "$OPENCODE_INSTALL_DIR already symlinked; skipping"
|
|
fi
|
|
fi
|
|
|
|
# Only ensure opencode command exists - qwen and gemini commands should not be aliased to opencode
|
|
ensure_cli_wrappers() {
|
|
local bin_dir="$PERSISTED_OPENCODE_DIR/bin"
|
|
mkdir -p "$bin_dir"
|
|
# Only create symlink for opencode command itself
|
|
if [ ! -L "/usr/local/bin/opencode" ]; then
|
|
ln -sf "$bin_dir/opencode" "/usr/local/bin/opencode"
|
|
fi
|
|
}
|
|
ensure_cli_wrappers
|
|
|
|
# Ensure a root-level opencode.json exists so OpenCode can discover the configured
|
|
# Ollama/OpenAI-compatible model. This file lives in the persisted storage root
|
|
# (/home/web/data) and is overwritten on every container start.
|
|
ensure_root_opencode_config() {
|
|
local config_path="$REPO_DIR/opencode.json"
|
|
|
|
# Allow overrides while defaulting to the PluginCompass Ollama gateway + qwen3 model
|
|
export OPENCODE_OLLAMA_BASE_URL="${OPENCODE_OLLAMA_BASE_URL:-https://ollama.plugincompass.com}"
|
|
export OPENCODE_OLLAMA_MODEL="${OPENCODE_OLLAMA_MODEL:-qwen3:0.6b}"
|
|
|
|
# Prefer an explicit Ollama key, fall back to OPENCODE_API_KEY (existing env var)
|
|
export OPENCODE_OLLAMA_API_KEY="${OPENCODE_OLLAMA_API_KEY:-${OPENCODE_API_KEY:-}}"
|
|
|
|
mkdir -p "$(dirname "$config_path")"
|
|
|
|
log "Writing OpenCode config: ${config_path} (baseURL=${OPENCODE_OLLAMA_BASE_URL}, model=${OPENCODE_OLLAMA_MODEL})"
|
|
|
|
python3 - <<'PY' > "$config_path"
|
|
import json, os
|
|
|
|
base_url = (os.environ.get("OPENCODE_OLLAMA_BASE_URL") or "https://ollama.plugincompass.com").rstrip("/")
|
|
|
|
model_id = os.environ.get("OPENCODE_OLLAMA_MODEL") or "qwen3:0.6b"
|
|
api_key = (os.environ.get("OPENCODE_OLLAMA_API_KEY") or "").strip()
|
|
provider_name = os.environ.get("OPENCODE_OLLAMA_PROVIDER") or "ollama"
|
|
|
|
provider_cfg = {
|
|
"options": {
|
|
"baseURL": base_url,
|
|
},
|
|
"models": {
|
|
model_id: {
|
|
"id": model_id,
|
|
"name": model_id,
|
|
"tool_call": True,
|
|
"temperature": True,
|
|
}
|
|
},
|
|
}
|
|
|
|
if api_key:
|
|
provider_cfg["options"]["apiKey"] = api_key
|
|
|
|
cfg = {
|
|
"$schema": "https://opencode.ai/config.json",
|
|
"model": f"{provider_name}/{model_id}",
|
|
"small_model": f"{provider_name}/{model_id}",
|
|
"provider": {
|
|
provider_name: provider_cfg,
|
|
},
|
|
}
|
|
|
|
print(json.dumps(cfg, indent=2))
|
|
PY
|
|
|
|
chmod 600 "$config_path" 2>/dev/null || true
|
|
}
|
|
ensure_root_opencode_config
|
|
|
|
# Set up signal handlers to properly clean up background processes
|
|
cleanup() {
|
|
local signal="$1"
|
|
log "=== SIGNAL RECEIVED: ${signal} ==="
|
|
log "Initiating graceful shutdown..."
|
|
|
|
# Log final resource snapshot before shutdown
|
|
if type monitor_resources &>/dev/null; then
|
|
log "Final resource snapshot:"
|
|
monitor_resources
|
|
fi
|
|
|
|
# Stop OpenSMTPD if running
|
|
if [ -n "$OPENSMTPD_PID" ] && kill -0 "$OPENSMTPD_PID" 2>/dev/null; then
|
|
log "Terminating OpenSMTPD service (PID: $OPENSMTPD_PID)"
|
|
kill "$OPENSMTPD_PID" 2>/dev/null || true
|
|
wait "$OPENSMTPD_PID" 2>/dev/null || true
|
|
log "OpenSMTPD service terminated"
|
|
fi
|
|
|
|
if [ -n "$MONITOR_PID" ] && kill -0 "$MONITOR_PID" 2>/dev/null; then
|
|
log "Terminating monitor process (PID: $MONITOR_PID)"
|
|
kill "$MONITOR_PID" 2>/dev/null || true
|
|
wait "$MONITOR_PID" 2>/dev/null || true
|
|
log "Monitor process terminated"
|
|
fi
|
|
|
|
if [ -n "$CHAT_PID" ] && kill -0 "$CHAT_PID" 2>/dev/null; then
|
|
log "Terminating chat service (PID: $CHAT_PID) - giving it time for graceful shutdown"
|
|
kill "$CHAT_PID" 2>/dev/null || true
|
|
|
|
# Wait up to 25 seconds for graceful shutdown (Docker stop_grace_period is 30s)
|
|
for i in $(seq 1 25); do
|
|
if ! kill -0 "$CHAT_PID" 2>/dev/null; then
|
|
log "Chat service terminated gracefully (${i} seconds)"
|
|
if type diag_log &>/dev/null; then
|
|
diag_log "INFO" "Chat service shutdown complete"
|
|
fi
|
|
exit 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
log "WARNING: Chat service did not terminate gracefully, forcing exit"
|
|
kill -9 "$CHAT_PID" 2>/dev/null || true
|
|
fi
|
|
|
|
log "=== SHUTDOWN COMPLETE ==="
|
|
exit 0
|
|
}
|
|
|
|
# Set up traps for common signals
|
|
trap cleanup SIGTERM SIGINT SIGQUIT SIGHUP
|
|
|
|
if [ -f "$CHAT_APP_DIR/server.js" ]; then
|
|
log "Launching chat service on ${CHAT_HOST}:${CHAT_PORT} from $CHAT_APP_DIR"
|
|
log "Environment: CHAT_PORT=${CHAT_PORT} CHAT_HOST=${CHAT_HOST} CHAT_DATA_ROOT=${REPO_DIR} CHAT_REPO_ROOT=${REPO_DIR}"
|
|
CHAT_PORT=$CHAT_PORT CHAT_HOST=$CHAT_HOST CHAT_DATA_ROOT=$REPO_DIR CHAT_REPO_ROOT=$REPO_DIR node "$CHAT_APP_DIR/server.js" 2>&1 &
|
|
CHAT_PID=$!
|
|
log "Chat service started with PID: $CHAT_PID"
|
|
elif [ -f "$CHAT_APP_FALLBACK/server.js" ]; then
|
|
log "Primary chat service not found at $CHAT_APP_DIR, trying fallback at $CHAT_APP_FALLBACK"
|
|
log "Launching chat service on ${CHAT_HOST}:${CHAT_PORT} from $CHAT_APP_FALLBACK"
|
|
log "Environment: CHAT_PORT=${CHAT_PORT} CHAT_HOST=${CHAT_HOST} CHAT_DATA_ROOT=${REPO_DIR} CHAT_REPO_ROOT=${REPO_DIR}"
|
|
CHAT_PORT=$CHAT_PORT CHAT_HOST=$CHAT_HOST CHAT_DATA_ROOT=$REPO_DIR CHAT_REPO_ROOT=$REPO_DIR node "$CHAT_APP_FALLBACK/server.js" 2>&1 &
|
|
CHAT_PID=$!
|
|
log "Chat service started with PID: $CHAT_PID"
|
|
else
|
|
log "ERROR: Chat service not found at $CHAT_APP_DIR or $CHAT_APP_FALLBACK; skipping startup"
|
|
exit 1
|
|
fi
|
|
|
|
# Log initial service status after startup
|
|
sleep 2
|
|
if type check_service_status &>/dev/null; then
|
|
check_service_status "chat" "$CHAT_PORT" "$CHAT_PID"
|
|
fi
|
|
|
|
# Monitor chat service health
|
|
if [ -n "$CHAT_PID" ]; then
|
|
(
|
|
# Initial check
|
|
sleep 5
|
|
if type check_service_status &>/dev/null; then
|
|
check_service_status "chat" "$CHAT_PORT" "$CHAT_PID"
|
|
fi
|
|
|
|
# Periodic monitoring
|
|
check_count=0
|
|
while kill -0 "$CHAT_PID" 2>/dev/null; do
|
|
sleep 30
|
|
check_count=$((check_count + 1))
|
|
|
|
# Every 2 minutes (4 checks), do resource monitoring
|
|
if [ $((check_count % 4)) -eq 0 ] && type monitor_resources &>/dev/null; then
|
|
monitor_resources
|
|
fi
|
|
|
|
# Every 5 minutes (10 checks), do service status check
|
|
if [ $((check_count % 10)) -eq 0 ] && type check_service_status &>/dev/null; then
|
|
check_service_status "chat" "$CHAT_PORT" "$CHAT_PID"
|
|
fi
|
|
done
|
|
log "ERROR: Chat service (PID: $CHAT_PID) has exited unexpectedly"
|
|
if type diag_log &>/dev/null; then
|
|
diag_log "ERROR" "Chat service exited unexpectedly after ${check_count} checks"
|
|
fi
|
|
) &
|
|
MONITOR_PID=$!
|
|
log "Health monitor started with PID: $MONITOR_PID"
|
|
fi
|
|
|
|
# Keep the container running
|
|
log "Container started successfully. Waiting for chat service..."
|
|
wait "$CHAT_PID" || true
|