|
| 1 | +#!/bin/bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +if [ -f /home/search/.config/searchgov-codedeploy.env ]; then |
| 5 | + set -a |
| 6 | + # shellcheck disable=SC1090 |
| 7 | + source /home/search/.config/searchgov-codedeploy.env |
| 8 | + set +a |
| 9 | +fi |
| 10 | + |
| 11 | +log() { |
| 12 | + echo "[CODEDEPLOY][APPLICATION_START] $*" |
| 13 | +} |
| 14 | + |
| 15 | +warn() { |
| 16 | + echo "[CODEDEPLOY][APPLICATION_START][WARN] $*" |
| 17 | +} |
| 18 | + |
| 19 | +error() { |
| 20 | + echo "[CODEDEPLOY][APPLICATION_START][ERROR] $*" |
| 21 | +} |
| 22 | + |
| 23 | +service_exists() { |
| 24 | + local service_name="$1" |
| 25 | + systemctl list-unit-files --type=service --no-legend 2>/dev/null | awk '{print $1}' | grep -Fxq "${service_name}.service" || \ |
| 26 | + systemctl list-unit-files --type=service --no-legend 2>/dev/null | awk '{print $1}' | grep -Fxq "$service_name" |
| 27 | +} |
| 28 | + |
| 29 | +restart_or_start_service() { |
| 30 | + local service_name="$1" |
| 31 | + |
| 32 | + if service_exists "$service_name"; then |
| 33 | + log "Restarting service: $service_name" |
| 34 | + systemctl restart "$service_name" |
| 35 | + else |
| 36 | + log "Service not found, skipping: $service_name" |
| 37 | + fi |
| 38 | +} |
| 39 | + |
| 40 | +wait_for_pid_exit() { |
| 41 | + local pid="$1" |
| 42 | + local timeout_seconds="${2:-10}" |
| 43 | + local elapsed=0 |
| 44 | + |
| 45 | + while kill -0 "$pid" >/dev/null 2>&1; do |
| 46 | + if [ "$elapsed" -ge "$timeout_seconds" ]; then |
| 47 | + warn "PID $pid did not exit after ${timeout_seconds}s" |
| 48 | + return 1 |
| 49 | + fi |
| 50 | + sleep 1 |
| 51 | + elapsed=$((elapsed + 1)) |
| 52 | + done |
| 53 | + |
| 54 | + return 0 |
| 55 | +} |
| 56 | + |
| 57 | +start_puma_fallback() { |
| 58 | + local app_root="$1" |
| 59 | + local current_path="${app_root}/current" |
| 60 | + local puma_pidfile="${PUMA_PIDFILE:-${current_path}/tmp/pids/server.pid}" |
| 61 | + local puma_stdout_log="${PUMA_STDOUT_LOG:-${current_path}/log/puma_stdout.log}" |
| 62 | + local puma_stderr_log="${PUMA_STDERR_LOG:-${current_path}/log/puma_stderr.log}" |
| 63 | + |
| 64 | + if [ ! -d "$current_path" ]; then |
| 65 | + log "Current release path not found, skipping fallback puma start: $current_path" |
| 66 | + return 0 |
| 67 | + fi |
| 68 | + |
| 69 | + |
| 70 | + # CodeDeploy hooks run in non-login shells; make rbenv shims available if present. |
| 71 | + if [ -d "/home/search/.rbenv" ]; then |
| 72 | + export RBENV_ROOT="/home/search/.rbenv" |
| 73 | + export PATH="$RBENV_ROOT/bin:$RBENV_ROOT/shims:$PATH" |
| 74 | + if command -v rbenv >/dev/null 2>&1; then |
| 75 | + eval "$(rbenv init - bash)" || warn "rbenv init failed; continuing" |
| 76 | + fi |
| 77 | + fi |
| 78 | + |
| 79 | + # Configure shared Bundler environment (same as after_install.sh) |
| 80 | + log "Configuring shared Bundler environment for Puma" |
| 81 | + export BUNDLE_WITHOUT="development:test" |
| 82 | + export BUNDLE_PATH="${app_root}/shared/bundle" |
| 83 | + export BUNDLE_APP_CONFIG="${app_root}/shared/.bundle" |
| 84 | + export BUNDLE_DEPLOYMENT="false" |
| 85 | + export BUNDLE_FROZEN="false" |
| 86 | + |
| 87 | + # Stop existing Puma process from pidfile (preferred method) |
| 88 | + if [ -f "$puma_pidfile" ]; then |
| 89 | + local existing_pid |
| 90 | + existing_pid="$(cat "$puma_pidfile" 2>/dev/null || true)" |
| 91 | + if [[ -n "$existing_pid" && "$existing_pid" =~ ^[0-9]+$ ]]; then |
| 92 | + if kill -0 "$existing_pid" >/dev/null 2>&1; then |
| 93 | + log "Stopping existing puma process from pidfile: $existing_pid" |
| 94 | + kill "$existing_pid" || true |
| 95 | + wait_for_pid_exit "$existing_pid" 10 || kill -9 "$existing_pid" || true |
| 96 | + fi |
| 97 | + fi |
| 98 | + rm -f "$puma_pidfile" |
| 99 | + else |
| 100 | + # Fallback: If no pidfile exists, check if port 3000 is occupied by Puma |
| 101 | + log "No pidfile found, checking for processes using port 3000..." |
| 102 | + if command -v lsof >/dev/null 2>&1; then |
| 103 | + PORT_3000_PID=$(lsof -ti:3000 2>/dev/null || true) |
| 104 | + if [ -n "$PORT_3000_PID" ]; then |
| 105 | + # Verify it's a Puma process before killing |
| 106 | + if ps -p "$PORT_3000_PID" -o comm= 2>/dev/null | grep -qi puma; then |
| 107 | + log "Found Puma process on port 3000: $PORT_3000_PID" |
| 108 | + log "Killing Puma process on port 3000..." |
| 109 | + kill -TERM "$PORT_3000_PID" 2>/dev/null || true |
| 110 | + sleep 3 |
| 111 | + # Force kill if still running |
| 112 | + if lsof -ti:3000 >/dev/null 2>&1; then |
| 113 | + log "Force killing stubborn Puma process on port 3000..." |
| 114 | + kill -KILL $(lsof -ti:3000 2>/dev/null) 2>/dev/null || true |
| 115 | + sleep 1 |
| 116 | + fi |
| 117 | + log "Port 3000 cleared" |
| 118 | + else |
| 119 | + warn "Process $PORT_3000_PID on port 3000 is not Puma - leaving it alone" |
| 120 | + fi |
| 121 | + else |
| 122 | + log "Port 3000 is available" |
| 123 | + fi |
| 124 | + else |
| 125 | + warn "lsof command not found, cannot check port 3000" |
| 126 | + fi |
| 127 | + fi |
| 128 | + |
| 129 | + if ! command -v bundle >/dev/null 2>&1; then |
| 130 | + log "ERROR: bundle command not found in PATH=$PATH" |
| 131 | + return 127 |
| 132 | + fi |
| 133 | + |
| 134 | + cd "$current_path" |
| 135 | + mkdir -p "$(dirname "$puma_pidfile")" "$(dirname "$puma_stdout_log")" "$(dirname "$puma_stderr_log")" |
| 136 | + |
| 137 | + # NOTE: Puma 6+ can reject `-d` daemon mode depending on config/plugins. |
| 138 | + # Start it in background from the shell and capture logs explicitly. |
| 139 | + log "Starting puma in background (fallback, no systemd service found)" |
| 140 | + RAILS_ENV="${RAILS_ENV:-production}" bundle exec puma -C config/puma.rb >>"$puma_stdout_log" 2>>"$puma_stderr_log" & |
| 141 | + |
| 142 | + # Brief pause to surface immediate boot failures before health polling starts. |
| 143 | + sleep 2 |
| 144 | +} |
| 145 | + |
| 146 | +wait_for_http_healthy() { |
| 147 | + local url="$1" |
| 148 | + local attempts="${2:-12}" |
| 149 | + local sleep_seconds="${3:-5}" |
| 150 | + local try=1 |
| 151 | + |
| 152 | + while [ "$try" -le "$attempts" ]; do |
| 153 | + if curl --fail --silent --show-error --max-time 10 "$url" >/dev/null; then |
| 154 | + log "HTTP endpoint became healthy on attempt ${try}/${attempts}: $url" |
| 155 | + return 0 |
| 156 | + fi |
| 157 | + |
| 158 | + if [ "$try" -lt "$attempts" ]; then |
| 159 | + warn "HTTP endpoint not ready yet (attempt ${try}/${attempts}): $url" |
| 160 | + sleep "$sleep_seconds" |
| 161 | + fi |
| 162 | + |
| 163 | + try=$((try + 1)) |
| 164 | + done |
| 165 | + |
| 166 | + return 1 |
| 167 | +} |
| 168 | + |
| 169 | +dump_startup_diagnostics() { |
| 170 | + local app_root="$1" |
| 171 | + local current_path="${app_root}/current" |
| 172 | + |
| 173 | + warn "Collecting startup diagnostics" |
| 174 | + warn "PATH=$PATH" |
| 175 | + warn "pwd=$(pwd)" |
| 176 | + |
| 177 | + if command -v ss >/dev/null 2>&1; then |
| 178 | + warn "Listening sockets on :3000 (ss)" |
| 179 | + ss -ltnp 2>/dev/null | grep ':3000' || true |
| 180 | + elif command -v netstat >/dev/null 2>&1; then |
| 181 | + warn "Listening sockets on :3000 (netstat)" |
| 182 | + netstat -lntp 2>/dev/null | grep ':3000' || true |
| 183 | + fi |
| 184 | + |
| 185 | + warn "Running puma-related processes" |
| 186 | + ps -ef | grep -E 'puma|rails server' | grep -v grep || true |
| 187 | + |
| 188 | + if [ -f "${current_path}/log/puma_error.log" ]; then |
| 189 | + warn "Tail of ${current_path}/log/puma_error.log" |
| 190 | + tail -n 80 "${current_path}/log/puma_error.log" || true |
| 191 | + fi |
| 192 | + |
| 193 | + if [ -f "${current_path}/log/puma_stdout.log" ]; then |
| 194 | + warn "Tail of ${current_path}/log/puma_stdout.log" |
| 195 | + tail -n 80 "${current_path}/log/puma_stdout.log" || true |
| 196 | + fi |
| 197 | + |
| 198 | + if [ -f "${current_path}/log/puma_stderr.log" ]; then |
| 199 | + warn "Tail of ${current_path}/log/puma_stderr.log" |
| 200 | + tail -n 80 "${current_path}/log/puma_stderr.log" || true |
| 201 | + fi |
| 202 | + |
| 203 | + if [ -f "${current_path}/log/production.log" ]; then |
| 204 | + warn "Tail of ${current_path}/log/production.log" |
| 205 | + tail -n 80 "${current_path}/log/production.log" || true |
| 206 | + fi |
| 207 | +} |
| 208 | + |
| 209 | +PUMA_SERVICE="${PUMA_SERVICE:-puma}" |
| 210 | +RESQUE_WORKER_SERVICE="${RESQUE_WORKER_SERVICE:-resque-worker}" |
| 211 | +RESQUE_SCHEDULER_SERVICE="${RESQUE_SCHEDULER_SERVICE:-resque-scheduler}" |
| 212 | +SEARCHGOV_ROOT="${SEARCHGOV_ROOT:-/home/search/searchgov}" |
| 213 | +APP_HEALTHCHECK_URL="${APP_HEALTHCHECK_URL:-http://127.0.0.1:3000/}" |
| 214 | + |
| 215 | +log "Starting ApplicationStart hook" |
| 216 | +log "Host: $(hostname) | User: $(whoami)" |
| 217 | + |
| 218 | +restart_or_start_service "$PUMA_SERVICE" |
| 219 | +restart_or_start_service "$RESQUE_WORKER_SERVICE" |
| 220 | +restart_or_start_service "$RESQUE_SCHEDULER_SERVICE" |
| 221 | + |
| 222 | +if ! service_exists "$PUMA_SERVICE"; then |
| 223 | + start_puma_fallback "$SEARCHGOV_ROOT" |
| 224 | +fi |
| 225 | + |
| 226 | +if ! wait_for_http_healthy "$APP_HEALTHCHECK_URL" "${HEALTHCHECK_ATTEMPTS:-12}" "${HEALTHCHECK_SLEEP_SECONDS:-5}"; then |
| 227 | + error "ApplicationStart could not bring app online at $APP_HEALTHCHECK_URL" |
| 228 | + dump_startup_diagnostics "$SEARCHGOV_ROOT" |
| 229 | + exit 1 |
| 230 | +fi |
| 231 | + |
| 232 | +log "ApplicationStart hook completed" |
0 commit comments