forked from heroku/heroku-buildpack-php
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathheroku-php-nginx
executable file
·305 lines (272 loc) · 15.1 KB
/
heroku-php-nginx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
#!/usr/bin/env bash
# fail hard
set -o pipefail
# fail harder
set -eu
# for ${DOCUMENT_ROOT%%*(/)} pattern further down
shopt -s extglob
# for detecting when -l 'logs/*.log' matches nothing
shopt -s nullglob
verbose=
php_passthrough() {
local dir=$(dirname "$1")
local file=$(basename "$1")
local out=$(basename "$file" .php)
if [[ "$out" != "$file" ]]; then
[[ $verbose ]] && echo "Interpreting ${1#$HEROKU_APP_DIR/} to $out" >&2
out="$dir/$out"
php "$1" > "$out"
echo "$out"
else
echo "$1"
fi
}
check_exists() {
if [[ ! -f "$HEROKU_APP_DIR/$1" ]]; then
echo "Cannot read -$2 '$1' (relative to '$HEROKU_APP_DIR')" >&2
exit 1
else
echo "$HEROKU_APP_DIR/$1"
fi
}
touch_log() {
mkdir -p $(dirname "$1") && touch "$1"
}
strip_fpm_child_said() {
local sedcmd='s/^\[[^]]+\] WARNING: \[pool [^]]+\] child [0-9]+ said into std(err|out): "(.*)"$/\2/'
case $(uname) in
Darwin) sed -l -E "$sedcmd";; # mac/bsd sed: -l buffers on line boundaries
*) sed -u -E "$sedcmd";; # unix/gnu sed: -u unbuffered (arbitrary) chunks of data
esac
}
print_help() {
echo "\
${1:-Boots PHP-FPM together with Nginx on Heroku and for local development.}
Usage:
heroku-php-nginx [options] [<DOCUMENT_ROOT>]
Options:
-C <nginx.inc.conf> The path to the configuration file to include inside
the Nginx server config (see option -c below). Will
be included inside the 'server { ... }' block just
after the 'listen', 'root' etc directives.
Recommended approach when customizing Nginx's config
in most cases, unless you need to set http or
fundamental server level options.
[default: \$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/nginx/default_include.conf.php]
-c <nginx.conf> The path to the full configuration file that is
included after Heroku's (or your local) Nginx config
is loaded. It must contain an 'http { ... }' block
with a 'server { ... }' inside that contains 'listen'
and 'root' (see option -C above), but no global,
directives (globals are read from the system's default
Nginx configuration files).
[default: \$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/nginx/heroku.conf.php]
-F <php-fpm.inc.conf> The path to the configuration file to include at the
end of php-fpm.conf (see option -f below), in the
'[www]' pool section. Recommended approach when
customizing PHP-FPM's configuration in most cases,
unless you need to set global options.
-f <php-fpm.conf> The path to the full PHP-FPM configuration file.
[default: \$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/php/php-fpm.conf]
-h, --help Display this help screen and exit.
-i <php.ini> The path to the php.ini file to use.
[default: \$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/php/php.ini]
-l <tailme.log> Path to additional log file to tail to STDERR so its
contents appear in 'heroku logs'. If the file does not
exist, it will be created. Wildcards are allowed, but
must be quoted and must match already existing files.
Note: this option can be repeated multiple times.
-p <PORT> Port to listen on for HTTP traffic. If this argument
is not given, then the port number to use is read from
the \$PORT environment variable, or a random port is
chosen if that variable does not exist.
-v, --verbose Be more verbose during startup.
All file paths must be relative to '$HEROKU_APP_DIR'.
Any file name that ends in '.php' will be run through the PHP interpreter first.
You may use this for templating; this is, for instance, necessary for Nginx,
where environment variables cannot be referenced in configuration files.
If you would like to use the -C and -c or -F and -f options together, make sure
you retain the appropriate include mechanisms (see default configs for details).
" >&2
}
# we need this in configs
export HEROKU_APP_DIR=$(pwd)
export DOCUMENT_ROOT="$HEROKU_APP_DIR"
# set a default port if none is given
export PORT=${PORT:-$(( $RANDOM+1024 ))}
# our standard logs
logs=( "/tmp/heroku.php-fpm.$PORT.log" "/tmp/heroku.php-fpm.www.$PORT.log" "/tmp/heroku.nginx_access.$PORT.log" )
optstring=":-:C:c:F:f:i:l:p:vh"
# process flags first
while getopts "$optstring" opt; do
case $opt in
-)
case "$OPTARG" in
verbose)
verbose=1
;;
help)
print_help 2>&1
exit
;;
*)
echo "Invalid option: --$OPTARG" >&2
exit
;;
esac
;;
v)
verbose=1
;;
h)
print_help 2>&1
exit
;;
esac
done
OPTIND=1 # start over with options parsing
while getopts "$optstring" opt; do
case $opt in
C)
nginx_config_include=$(check_exists "$OPTARG" "C")
;;
c)
nginx_config=$(check_exists "$OPTARG" "c")
;;
F)
fpm_config_include=$(check_exists "$OPTARG" "F")
;;
f)
fpm_config=$(check_exists "$OPTARG" "f")
;;
i)
php_config=$(check_exists "$OPTARG" "i")
;;
l)
logarg=( $OPTARG ) # must not quote this or wildcards won't get expanded into individual values
if [[ ${#logarg[@]} -eq 0 ]]; then # we set nullglob to detect if a pattern matched nothing (then the array is empty)
echo "Pattern '$OPTARG' passed to option -l matched no files" >&2
exit 1
fi
for logfile in "${logarg[@]}"; do
if [[ -d "$logfile" ]]; then
echo "-l '$logfile': is a directory" >&2
exit 1
fi
touch_log "$logfile" || { echo "Could not touch '$logfile'; permissions problem?" >&2; exit 1; }
[[ $verbose ]] && echo "Tailing '$logfile' to stderr" >&2
logs+=("$logfile") # must quote here in case a wildcard matched a file with a space in the name
done
;;
p)
PORT="$OPTARG"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 2
;;
:)
echo "Option -$OPTARG requires an argument" >&2
exit 2
;;
esac
done
# clear processed arguments
shift $((OPTIND-1))
if [[ "$#" -gt "1" ]]; then
print_help "$0: too many arguments. If you're using options,
make sure to list them before any document root argument you're providing."
exit 2
fi
php() {
# the newrelic extension logs to stderr which would pollute boot output with each invocation of PHP, and each call to PHP would be logged to NR as non-web traffic, so we disable it for the next few CLI calls
`which php` -dnewrelic.enabled=0 -dnewrelic.loglevel=error -dnewrelic.daemon.dont_launch=3 -dnewrelic.daemon.loglevel=error "$@"
}
php-fpm() {
# the newrelic extension logs to stderr which would pollute boot output with each invocation of php-fpm, and each call to php-fpm would be logged to NR as non-web traffic, so we disable it for the next version check call
`which php-fpm` -dnewrelic.enabled=0 -dnewrelic.loglevel=error -dnewrelic.daemon.dont_launch=3 -dnewrelic.daemon.loglevel=error "$@"
}
php -r 'exit((int)version_compare(PHP_VERSION, "5.5.11", "<"));' || { echo "This program requires PHP 5.5.11 or newer; check your 'php' command." >&2; exit 1; }
{ php-fpm -v | php -r 'exit((int)version_compare(preg_replace("#PHP (\S+) \(fpm-fcgi\).+$#sm", "\\1", file_get_contents("php://stdin")), "5.5.11", "<"));'; } || { echo "This program requires PHP 5.5.11 or newer; check your 'php-fpm' command." >&2; exit 1; }
# make sure we run a local composer.phar if present, or global composer if not
composer() {
local composer_bin=$(which ./composer.phar composer | head -n1)
# check if we the composer binary is executable by PHP
if file --brief --dereference $composer_bin | grep "bash" > /dev/null ; then # newer versions of file return "data" for .phar
# run it directly; it's probably a bash script or similar (homebrew-php does this)
$composer_bin "$@"
else
php $composer_bin "$@"
fi
}
COMPOSER_VENDOR_DIR=$(composer config vendor-dir 2> /dev/null | tail -n 1) && export COMPOSER_VENDOR_DIR || { echo "Unable to determine Composer vendor-dir setting; is 'composer' executable on path or 'composer.phar' in current working directory?" >&2; exit 1; } # tail, as composer echos outdated version warnings to STDOUT; export after the assignment or exit status will that be of 'export
COMPOSER_BIN_DIR=$(composer config bin-dir 2> /dev/null | tail -n 1) && export COMPOSER_BIN_DIR || { echo "Unable to determine Composer vendor-dir setting; is 'composer' executable on path or 'composer.phar' in current working directory?" >&2; exit 1; } # tail, as composer echos outdated version warnings to STDOUT; export after the assignment or exit status will that be of 'export
[[ -z ${DYNO-} || $verbose ]] && echo "Booting on port $PORT..." >&2
if [[ "$#" == "1" ]]; then
DOCUMENT_ROOT="$HEROKU_APP_DIR/$1"
if [[ ! -d "$DOCUMENT_ROOT" ]]; then
echo "DOCUMENT_ROOT '$1' does not exist" >&2
exit 1
else
# strip trailing slashes if present
DOCUMENT_ROOT=${DOCUMENT_ROOT%%*(/)} # powered by extglob
if [[ $verbose ]]; then
echo "DOCUMENT_ROOT changed to '$DOCUMENT_ROOT'" >&2
else
echo "DOCUMENT_ROOT changed to '${1%%*(/)}/'" >&2
fi
fi
fi
if [[ -n ${fpm_config_include:-} ]]; then
echo "Using PHP-FPM configuration include '${fpm_config_include#$HEROKU_APP_DIR/}'" >&2
fpm_config_include=$(php_passthrough "$fpm_config_include")
export HEROKU_PHP_FPM_CONFIG_INCLUDE="$fpm_config_include"
fi
if [[ -n ${fpm_config:-} || ( ${fpm_config:="$HEROKU_APP_DIR/$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/php/php-fpm.conf"} && $verbose ) ]]; then
echo "Using PHP-FPM configuration file '${fpm_config#$HEROKU_APP_DIR/}'" >&2
fi
fpm_config=$(php_passthrough "$fpm_config")
if [[ -n ${php_config:-} || ( ${php_config:="$HEROKU_APP_DIR/$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/php/php.ini"} && $verbose ) ]]; then
echo "Using PHP configuration (php.ini) file '${php_config#$HEROKU_APP_DIR/}'" >&2
fi
php_config=$(php_passthrough "$php_config")
if [[ -n ${nginx_config_include:-} || ( ${nginx_config_include:="$HEROKU_APP_DIR/$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/nginx/default_include.conf.php"} && $verbose ) ]]; then
echo "Using Nginx server-level configuration include '${nginx_config_include#$HEROKU_APP_DIR/}'" >&2
fi
nginx_config_include=$(php_passthrough "$nginx_config_include")
export HEROKU_PHP_NGINX_CONFIG_INCLUDE="$nginx_config_include"
if [[ -n ${nginx_config:-} || ( ${nginx_config:="$HEROKU_APP_DIR/$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/nginx/heroku.conf.php"} && $verbose) ]]; then
echo "Using Nginx configuration file '${nginx_config#$HEROKU_APP_DIR/}'" >&2
fi
nginx_config=$(php_passthrough "$nginx_config")
# make a shared pipe; we'll write the name of the process that exits to it once that happens, and wait for that event below
# this particular call works on Linux and Mac OS (will create a literal ".XXXXXX" on Mac, but that doesn't matter).
wait_pipe=$(mktemp -t "heroku.waitpipe-$PORT.XXXXXX" -u)
rm -rf $wait_pipe
mkfifo $wait_pipe
# trap SIGINT/SIGQUIT (ctrl+c or ctrl+\ on the console), SIGTERM, and EXIT (upon failure of any command due to set -e, or because of the exit 1 at the very end), kill subshell child processes, then subshells
# 1) restore EXIT trap immediately, or the exit at the end of the line will trigger this trap again
# 2) kill childrens' child processes (the stuff running inside the sub-shells) using xargs because this is easier (-P expects a comma separated list); the || true prevents premature exit (set -e) if one of those doesn't have children anymore (it's likely that's why we're hitting this bit of code in the first place), and redirect all to /dev/null as usage help when no args given (because jobs -p was empty) is sometimes (Linux) printed to STDOUT
# 3) kill child processes (that's the sub-shells); it's likely that some of them have already disappeared, so xarg || true it too and suppress "no such process" complaints by sending them to /dev/null
# FIXME: this doesn't currently fire when the subshells themselves are terminated
# TODO: for extra brownie points, move to a function and curry for each given signal, passing the signal in as an arg, so we can use different exit codes or messages
trap 'trap - EXIT; echo "Going down, terminating child processes..." >&2; jobs -p | xargs -n1 pkill -TERM -P &> /dev/null || true; jobs -p | xargs -n1 kill -TERM 2> /dev/null || true; exit' SIGINT SIGQUIT SIGTERM EXIT
# launch processes. all run using || true to prevent premature exit of the subshell (from set -e) regardless of exit status
# after a subprocess terminates (because it was killed or because it crashed or because it quit voluntarily), we write the name to FD 3 (because programs could output something on FD 1 (STDOUT) or FD 2 (STDERR)) and send that to the shared pipe (mkfifo) above, and a read command further down waits for something to come in on the shared pipe
# redirect logs to STDERR; write "tail ..." to the shared pipe if it exits
[[ $verbose ]] && echo "Starting log redirection..." >&2
( touch "${logs[@]}"; tail -qF -n 0 "${logs[@]}" | strip_fpm_child_said 1>&2 || true; echo 'tail "${logs[@]}"' >&3; ) 3> $wait_pipe &
# start FPM; write "php-fpm" to the shared pipe if it exits
echo "Starting php-fpm..." >&2
unset -f php-fpm # remove the alias we made earlier that would prevent newrelic from starting on php-fpm -v
( php-fpm --nodaemonize -y "$fpm_config" -c "$php_config" || true; echo "php-fpm" >&3; ) 3> $wait_pipe &
# wait a few seconds for FPM to finish initializing; otherwise an early request might break nginx with the FastCGI pipe not being ready
# start nginx; write "nginx" to the shared pipe if it exits
echo "Starting nginx..." >&2
( sleep 2; nginx -g "daemon off; include $nginx_config;" || true; echo "nginx" >&3; ) 3> $wait_pipe &
# wait for something to come from the shared pipe, which means that the given process was killed or has failed
read exitproc < $wait_pipe
# we'll only reach this if one of the processes above has terminated
echo "Process exited unexpectedly: $exitproc" >&2
# this will trigger the trap and kill all remaining children
exit 1