forked from heroku/heroku-buildpack-php
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathheroku-hhvm-apache2
executable file
·349 lines (309 loc) · 16.3 KB
/
heroku-hhvm-apache2
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
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
#!/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"
hhvm "$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"
}
print_help() {
echo "\
${1:-Boots HHVM together with Apache2 on Heroku and for local development.}
Usage:
heroku-hhvm-apache2 [options] [<DOCUMENT_ROOT>]
Options:
-C <httpd.inc.conf> The path to the configuration file to include inside
the Apache2 VHost config (see option -c below). Will
be included inside the '<VirtualHost>' section just
after the '<Directory>' & 'ProxyPassMatch' directives.
Recommended approach when customizing Apache2's config
in most cases, unless you need to set fundamental
server level options.
[default: \$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/apache2/default_include.conf]
-c <httpd.conf> The path to the full VHost configuration file that is
included after Heroku's (or your local) Apache2 config
is loaded. Must contain a 'Listen \${PORT}' directive
and should have a '<VirtualHost>' and likely also a
'<Directory>' section (see option -C above).
[default: \$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/apache2/heroku.conf]
-h, --help Display this help screen and exit.
-I <php.extra.ini> The path to an extra php.ini to use in addition to the
default HHVM php.ini (see option -i below).
-i <php.ini> The path to the php.ini file to use. It is highly
recommended to use the -I option (see above) instead.
[default: \$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/hhvm/php.ini.php]
-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 although this is less useful than e.g. for Nginx
where unlike in Apache2, you cannot reference environment variables in config
files using a '\${VARNAME}' syntax.
If you would like to use the -C and -c 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 ))}
# init logs array here as empty before parsing options; -l could append to it, but the default list gets added later since we use $PORT in there and that can be set using -p
declare -a logs
optstring=":-:C:c:I: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)
httpd_config_include=$(check_exists "$OPTARG" "C")
;;
c)
httpd_config=$(check_exists "$OPTARG" "c")
;;
I)
hhvm_config_extra=$(check_exists "$OPTARG" "I")
;;
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
# our standard logs
logs+=( "/tmp/heroku.apache2_error.$PORT.log" "/tmp/heroku.apache2_access.$PORT.log" )
hhvm --php -r 'exit((int)version_compare(HHVM_VERSION, "3.0.1", "<"));' || { echo "This program requires HHVM 3.0.1 or newer" >&2; exit 1; }
{ { httpd -v | hhvm --php -r 'exit((int)version_compare(preg_replace("#^Server version: Apache/(\S+).+$#sm", "\\1", file_get_contents("php://stdin")), "2.4.10", "<"));'; } && { httpd -t -D DUMP_MODULES | grep 'proxy_fcgi_module' > /dev/null; }; } || { echo "This program requires Apache 2.4.10 or newer with mod_proxy and mod_proxy_fcgi enabled; check your 'httpd' 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
hhvm $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
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
function prepare_hhvm_ini() { # we have to do this twice, as the WEB_CONCURRENCY setting is evaluated using PHP code, not ${...} syntax which HHVM does not support; if a value for $1 is passed then it won't echo messages upon second invocation
if [[ ( -n ${php_config:-} && ! ${1:-} ) || ( ${php_config:="$HEROKU_APP_DIR/$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/hhvm/php.ini.php"} && $verbose && ! ${1:-} ) ]]; then
echo "Using HHVM configuration (php.ini) file '${php_config#$HEROKU_APP_DIR/}'" >&2
fi
php_configs=( "-c" "$(php_passthrough "$php_config")" )
if [[ -n ${hhvm_config_extra:-} ]]; then
[[ ${1:-} ]] || echo "Using additional HHVM configuration (php.ini) file '${hhvm_config_extra#$HEROKU_APP_DIR/}'" >&2
php_configs+=( "-c" "$(php_passthrough "$hhvm_config_extra")" )
fi
}
prepare_hhvm_ini
if [[ -n ${httpd_config_include:-} || ( ${httpd_config_include:="$HEROKU_APP_DIR/$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/apache2/default_include.conf"} && $verbose ) ]]; then
echo "Using Apache2 VirtualHost-level configuration include '${httpd_config_include#$HEROKU_APP_DIR/}'" >&2
fi
httpd_config_include=$(php_passthrough "$httpd_config_include")
export HEROKU_PHP_HTTPD_CONFIG_INCLUDE="$httpd_config_include"
if [[ -n ${httpd_config:-} || ( ${httpd_config:="$HEROKU_APP_DIR/$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/apache2/heroku.conf"} && $verbose) ]]; then
echo "Using Apache2 configuration file '${httpd_config#$HEROKU_APP_DIR/}'" >&2
fi
httpd_config=$(php_passthrough "$httpd_config")
if [[ -z ${WEB_CONCURRENCY:-} ]]; then
maxprocs=$(ulimit -u)
ram="512M"
if [[ -n ${DYNO:-} && "$maxprocs" == "32768" ]]; then
echo "Optimizing defaults for PX dyno...." >&2
ram="6G"
elif [[ -n ${DYNO:-} && "$maxprocs" == "512" ]]; then
echo "Optimizing defaults for 2X dyno..." >&2
ram="1G"
elif [[ -n ${DYNO:-} && "$maxprocs" == "256" ]]; then
echo "Optimizing defaults for 1X dyno..." >&2
elif [[ -n ${DYNO:-} || $verbose ]]; then
echo "No dyno detected; using defaults for 1X..." >&2
fi
# determine number of HHVM threads to run
read WEB_CONCURRENCY php_memory_limit <<<$(hhvm "${php_configs[@]}" $(composer config vendor-dir 2> /dev/null | tail -n 1)/heroku/heroku-buildpack-php/bin/util/autotune.php "$ram") # tail, as composer echos outdated version warnings to STDOUT
[[ $WEB_CONCURRENCY -lt 1 ]] && WEB_CONCURRENCY=1
export WEB_CONCURRENCY
echo "${WEB_CONCURRENCY} threads at ${php_memory_limit}B memory limit." >&2
else
echo "Using WEB_CONCURRENCY=${WEB_CONCURRENCY} threads." >&2
fi
# we changed WEB_CONCURRENCY; now we need to re-evaluate the configs as HHVM doesn't support ${...} syntax, so the configs contain PHP getenv() calls and are templated; we pass an argument to it to avoid it echoing "using HHVM config ..." messages again because that already happened
prepare_hhvm_ini no_messages
# 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 -f $wait_pipe
mkfifo $wait_pipe
exec 3<> $wait_pipe
pids=()
# trap SIGQUIT (ctrl+\ on the console), SIGTERM (when we get killed) and EXIT (upon failure of any command due to set -e, or because of the exit 1 at the very end), we then
# 1) restore the trap so it doesn't fire again a loop due to the exit at the end (if we're handling SIGQUIT or SIGTERM) or another signal
# 2) remove our FIFO from above
# 3) kill all the subshells we've spawned - they in turn have their own traps to kill their respective subprocesses
# 3a) send STDERR to /dev/null so we don't see "no such process" errors - after all, one of the subshells may be gone
# 3b) || true so that set -e doesn't cause a mess if the kill returns 1 on "no such process" cases (which is likely)
# 4) exit in case we're handling SIGQUIT or SIGTERM
trap 'trap - QUIT TERM EXIT; echo "Going down, terminating child processes..." >&2; rm -f ${wait_pipe} || true; kill -TERM "${pids[@]}" 2> /dev/null || true; exit' QUIT TERM EXIT
# if FD 1 is a TTY (that's the -t 1 check), trap SIGINT/Ctrl+C
# 1) restore the INT trap so it doesn't fire in a loop due to 2)
# 2) be nice to the caller and send SIGINT to ourselves (http://mywiki.wooledge.org/SignalTrap#Special_Note_On_SIGINT)
# 3) *do* exit after all to run the cleanup code from above (avoids duplication)
if [[ -t 1 ]]; then
trap 'trap - INT; kill -INT $$; exit' INT;
# if FD 1 is not a TTY (e.g. when we're run through 'foreman start'), do nothing on SIGINT; the assumption is that the parent will send us a SIGTERM or something when this happens. With the trap above, Ctrl+C-ing out of a 'foreman start' run would trigger the INT trap both in Foreman and here (because Ctrl+C sends SIGINT to the entire process group, but there is no way to tell the two cases apart), and while the trap is still doing its shutdown work triggered by the SIGTERM from the Ctrl+C, Foreman would then send a SIGTERM because that's what it does when it receives a SIGINT itself.
else
trap '' INT;
fi
# we are now launching a subshell for each of the tasks (log tail, app server, web server)
# 1) each subshell has a trap on EXIT that echos the command name to FD 3 (see the FIFO set up above)
# 1a) a 'read' at the end of the script will block on reading from that FD and then trigger the exit trap above, which does the cleanup
# 2) each subshell also has a trap on TERM that
# 2a) kills $! (the last process executed)
# 2b) ... which in turn will unblock the 'wait' in 4)
# 3) execute the command in the background
# 4) 'wait' on the command (wait is interrupted by an incoming TERM to the subshell, whereas running 3) in the foreground would wait for that 3) to finish before triggering the trap)
# 5) add the PID of the subshell to the array that the EXIT trap further above uses to clean everything up
[[ $verbose ]] && echo "Starting log redirection..." >&2
(
trap 'echo "tail" >&3;' EXIT
trap 'kill -TERM $! 2> /dev/null' TERM
touch "${logs[@]}"
tail -qF -n 0 "${logs[@]}" 1>&2 &
wait
) & pids+=($!)
echo "Starting hhvm..." >&2
(
trap 'echo "hhvm" >&3;' EXIT
trap 'kill -TERM $! 2> /dev/null' TERM
hhvm --mode server "${php_configs[@]}" &
wait
) & pids+=($!)
# wait a few seconds for HHVM to finish initializing; otherwise an early request might break Apache with the FastCGI pipe not being ready
sleep 2
echo "Starting httpd..." >&2
(
trap 'echo "httpd" >&3;' EXIT
trap 'kill -TERM $! 2> /dev/null' TERM
httpd -D NO_DETACH -c "Include $httpd_config" &
wait
) & pids+=($!)
# on Heroku, there is a "state changed from starting to up", but for local execution, we want a "ready" message
[[ -z ${DYNO:-} || $verbose ]] && echo "Application ready for connections on port $PORT." >&2
# wait for something to come from the FIFO attached to FD 3, which means that the given process was killed or has failed
# this will be interrupted by a SIGTERM or SIGINT in the traps further up
# if the pipe unblocks and this executes, then we won't read it again, so if the traps further up kill the remaining subshells above, their writing to FD 3 will have no effect
read exitproc <&3
# we'll only reach this if one of the processes above has terminated
echo "Process exited unexpectedly: $exitproc" >&2
# this will trigger the EXIT trap further up and kill all remaining children
exit 1