Newsletter
TechAnV Blog
Get updates on security engineering, Rust, eBPF, and DevSecOps. No spam, unsubscribe anytime.
Check your inbox and click the confirmation link to complete your subscription.
Start, test, then stop a localhost web server in a Bash script#
I wanted to write a bash script that would start a Datasette server running, run a request against it using curl, then stop the server again.
It should then return an exit code indicating if the curl request was succesful or not.
Research notes in this issue - here’s the final script I came up with:
1#!/bin/bash2
3# Generate certificates4python -m trustme5# This creates server.pem, server.key, client.pem6
7# Start the server in the background8datasette --memory \9 --ssl-keyfile=server.key \10 --ssl-certfile=server.pem \11 -p 8152 &12
13# Store the background process ID in a variable14server_pid=$!15
16# Wait for the server to start17sleep 218
19# Make a test request using curl20curl -f --cacert client.pem 'https://localhost:8152/_memory.json'21
22# Save curl's exit code (-f option causes it to return one on HTTP errors)23curl_exit_code=$?24
25# Shut down the server26kill $server_pid27sleep 128
29# Clean up the certificates30rm server.pem server.key client.pem31
32echo $curl_exit_code33exit $curl_exit_codeThere are a few additional tricks in this - it’s running python -m trustme to create self-signed certificates in the current directory which are used for the test - but the key parts are these:
datasette ... &- starts Datasette running as a background processserver_pid=$!- records the PID of the server we just started so we can shut it down latercurl -f ...- makes acurlrequest, but returns an exit code that indicates if the request succeeded or was a 404 or 500 error or similarcurl_exit_code=$?- records that exit code for laterkill $server_id- causes the server to exit - I then did asleep 1to provide time for it to output its shutdown text to the terminalexit $curl_exit_code- the exit code for the Bash script is now the same as the exit code returned bycurl
I’m running this script in CI in GitHub Actions.
An improved version by Jan Lehnardt#
Jan Lehnardt submitted a pull request with this improved version:
1#!/bin/bash2
3# Generate certificates4python -m trustme5# This creates server.pem, server.key, client.pem6
7cleanup () {8 rm server.pem server.key client.pem9}10
11# Start the server in the background12datasette --memory \13 --ssl-keyfile=server.key \14 --ssl-certfile=server.pem \15 -p 8152 &16
17# Store the background process ID in a variable18server_pid=$!19
20test_url='https://localhost:8152/_memory.json'21
22# Wait for the server to start23
24# h/t https://github.com/pouchdb/pouchdb/blob/25db22fb0ff025b8d2c698da30c6c409066baa0c/bin/run-test.sh#L102-L11325waiting=026until $(curl --output /dev/null --silent --insecure --head --fail --max-time 2 $test_url); do27 if [ $waiting -eq 4 ]; then28 echo "$test_url can not be reached, server failed to start"29 cleanup30 exit 131 fi32 let waiting=waiting+133 sleep 134done35
36# Make a test request using curl37curl -f --cacert client.pem $test_url38
39# Save curl's exit code (-f option causes it to return one on HTTP errors)40curl_exit_code=$?41
42# Shut down the server43kill $server_pid44waiting=045# show all pids46# | find just the $server_pid47# | | don’t match on the previous grep48# | | | we don’t need the output49# | | | |50until ( ! ps ax | grep $server_pid | grep -v grep > /dev/null ); do51 if [ $waiting -eq 4 ]; then52 echo "$server_pid does still exist, server failed to stop"53 cleanup54 exit 155 fi56 let waiting=waiting+157 sleep 158done59
60# Clean up the certificates61cleanup62
63echo $curl_exit_code64exit $curl_exit_codeThere’s not much extra commentary I can add to this - Jan’s inline comments are fantastic!
I really like the waiting=0 pattern here for retrying up to 4 times.
Worth breaking down the curl command here a bit:
1curl --output /dev/null --silent --insecure --head --fail --max-time 2 $test_urlIt’s avoiding any output at all with a combination of writing output to /dev/null and using --silent to turn off logging.
It uses --insecure because our server is running with a self-signed certificate, which will produce errors without this option - and here we just want to poll until the server is up and running.
--max-time ensures each poll waits a maximum of two seconds.
And as before, --fail causes curl to return an exit code that indicates if the request was successful or not.
Jan used this as the impetus to start a new library of shell utility functions.