I have several Spring Boot and Angular applications/microservices that form together a whole application. As it is tedious to start them via the terminal, I've written the following script to perform the starting of all.
I've simplified the script a bit to remove some redundant parts. Note that I usally start this script from ~/Documents/git/
and the script itself is located in ~/Documents/git/start-services
. That means, I usually do: $ ./start-services/services.sh start
to start all services.
The standard out and standard error streams of the started services are redirected to files located in ~/Documents/git/start-services/logs/
. For each service a separate log file.
#!/bin/bash
start_service_and_wait() {
is_running=$(nc -v -z localhost 3ドル 2>&1 >/dev/null)
bold="033円[1m"
reset="033円[0m"
if [[ $is_running =~ "succeeded" ]]; then
echo -e "There is already a service running on port 3ドル. Won't try to start service $bold\"2ドル\"$reset."
return
fi
waiting_time=60
cd 1ドル
git_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)"
echo -e "Start service $bold\"2ドル\"$reset on branch $bold$git_branch$reset..."
START_TIME=$SECONDS
nohup 4ドル > ../start-services/logs/1ドル.log 2>&1 &
cd ..
# TODO Maybe try to use the actuator port to check whether the service is available?
./start-services/wait-for-it.sh localhost:3ドル -q -t $waiting_time
result=$?
ELAPSED_TIME=$(($SECONDS - $START_TIME))
if [[ $result != 0 ]]; then
echo -e " \U26A0\UFE0F Service $bold\"2ドル\"$reset has been started but did not become available at port 3ドル after $waiting_time s. Result is $result. Showing the last lines of the captured log file:"
tail ./start-services/logs/1ドル.log
else
echo -e " \U2705 Service $bold\"2ドル\"$reset is available at port 3ドル after $ELAPSED_TIME s. With result $result."
fi
}
start_services() {
if [ -z 1ドル ] || [ 1ドル = "config-server" ]; then
start_service_and_wait "config-server" "Config Server" "8888" "mvn spring-boot:run"
fi
if [ -z 1ドル ] || [ 1ドル = "profile" ]; then
start_service_and_wait "profile" "Profile" "8082" "mvn spring-boot:run"
fi
if [ -z 1ドル ] || [ 1ドル = "security" ]; then
start_service_and_wait "security" "Security" "8080" "mvn spring-boot:run"
fi
if [ -z 1ドル ] || [ 1ドル = "analysis" ]; then
start_service_and_wait "analysis" "Analysis" "8092" "mvn spring-boot:run"
fi
if [ -z 1ドル ] || [ 1ドル = "frontend" ]; then
start_service_and_wait "frontend" "Frontend" "4200" "yarn start"
fi
}
stop_service() {
# check whether there is a *.pid file before trying to stop
pid=$(lsof -t -i:3ドル)
bold="033円[1m"
reset="033円[0m"
if [[ -n "${pid// /}" ]]; then
echo -e "Stopping service $bold\"2ドル\"$reset."
kill $pid
fi
}
stop_services() {
if [ -z 1ドル ] || [ 1ドル = "config-server" ]; then
stop_service "config-server" "Config Server" "8888"
fi
if [ -z 1ドル ] || [ 1ドル = "profile" ]; then
stop_service "profile" "Profile" "8082"
fi
if [ -z 1ドル ] || [ 1ドル = "security" ]; then
stop_service "security" "Security" "8080"
fi
if [ -z 1ドル ] || [ 1ドル = "analysis" ]; then
stop_service "analysis" "Analysis" "8092"
fi
if [ -z 1ドル ] || [ 1ドル = "frontend" ]; then
stop_service "frontend" "Frontend" "4200"
fi
}
status_service() {
if [ ! -d "2ドル" ]; then
return;
fi
RED="31"
REDBOLD="\e[1;${RED}m"
ENDCOLOR="\e[0m"
bold="033円[1m"
reset="033円[0m"
result=$(nc -v -z localhost 3ドル 2>&1 >/dev/null)
if [[ $result =~ "Connection refused" ]]; then
echo -e " \U274C Service $bold\"1ドル\"$reset is ${REDBOLD}not availble${ENDCOLOR} on port 3ドル."
else
echo -e " \U2705 Service $bold\"1ドル\"$reset is available on port 3ドル."
fi
}
status() {
echo "The following information is determined by net-catting on localhost and the corresponding host. Note that just because the port is used doesn't mean that the Service is running."
status_service "Config Server" "config-server" "8888"
status_service "Profile" "profile" "8082"
status_service "Security" "security" "8080"
status_service "Frontend" "frontend" "4200"
}
usage() {
echo "Commands:"
echo " services.sh start Starts all services"
echo " services.sh start <name> Starts only the service with the given name"
echo " services.sh stop Stops all services"
echo " services.sh status Shows the status of the services whether started or not"
echo ""
echo "Services that will be started and there key:"
echo " Config Server -> config-server"
echo " Profile -> profile"
echo " Security -> security"
echo " Analysis -> analysis"
echo " Frontend -> frontend"
}
case "1ドル" in
start)
start_service 2ドル
;;
stop)
stop_service 2ドル
;;
status)
status
;;
--help)
usage
;;
'')
status
;;
esac
Questions:
- Is it good to assume a certain folder from which the script is started?
- How to handle the formatting of the output better?
- Is it better to migrate to
printf
to remove usage ofecho
? - Can we store the information about the services (i.e. name, directory, port) in some kind of global array?
2 Answers 2
Give descriptive names to variables
start_service_and_wait
is a bit hard to read because to understand what are 1ドル
, 2ドル
, and so on, I have to read the callers.
It would be better to create local variables with descriptive names at the start of the function.
The same goes for the other functions, and also at the top level in the program.
Add more error handling to make the program more robust
Most notably error checking of the cd 1ドル
is strongly recommended, as failures of the cd
command can be destructive. I recommend adding the following safeguard at the top of every Bash script:
set -euo pipefail
The effect of this will be, to put it simply, when the program encounters a failed command or undefined variable, it will immediately abort.
Avoid cd "$var"; ...; cd ..
in scripts
This is fragile, and the cd ..
might not land in the original directory.
One safer way is to use pushd "$var"; ...; popd
instead.
Another safer way is to use a subshell: (cd "$var"; ...)
.
And sometimes you can write the program differently and avoid changing the directory. Instead of this:
cd 1ドル git_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" echo -e "Start service $bold\"2ドル\"$reset on branch $bold$git_branch$reset..." START_TIME=$SECONDS nohup 4ドル > ../start-services/logs/1ドル.log 2>&1 & cd ..
This is equivalent, without ever changing the directory:
git_branch="$(cd 1ドル; git rev-parse --abbrev-ref HEAD 2>/dev/null)"
echo -e "Start service $bold\"2ドル\"$reset on branch $bold$git_branch$reset..."
START_TIME=$SECONDS
nohup 4ドル > start-services/logs/1ドル.log 2>&1 &
Enclose variables in double-quotes
For example write cd "1ドル"
instead of cd 1ドル
.
Unquoted expressions are subject to globbing and word splitting by the shell,
and can lead to difficult to debug failures.
Avoid ALL_CAPS variable names
ALL_CAPS names may clash with system environment variables, therefore they are not recommended for user-defined variables.
Use arrays
Many values are used in many functions, and if something has to change, it will have to be changed in multiple places, for example the service port numbers. I think associative arrays could be practical to define these values in one place, and also reduce some duplicated logic.
Consider for example this pattern:
declare -A service_names
service_names=(
[config-server]="Config Server"
[profile]="Profile"
# ... and so on
)
declare -A ports
ports=(
[config-server]=8888
[profile]=8082
# ... and so on
)
stop_services() {
for key in "${!service_names[@]}"; do
if [ -z "1ドル" ] || [ "1ドル" = "$key" ]; then
stop_service "$key" "${service_names[$key]}" "${ports[$key]}"
fi
done
}
-
\$\begingroup\$ Thanks for your plenty comments. The last point with associative arrays is quite a game changer for my bash skills. Is there a way to preserve the ordering of the associative array while looping over it? For the first point "Give descriptive names to variables", do you mean, I should do smth like:
start_service() { \n path=1ドル \n name = 2ドル \n port = 3 \n ... }
and use, e.g.$path
later on instead of1ドル
? \$\endgroup\$jmizv– jmizv2023年12月13日 10:55:04 +00:00Commented Dec 13, 2023 at 10:55 -
1\$\begingroup\$ Good point about the ordering of members in associative arrays, I don't think it's possible to control, so to enforce consistent ordering, I'm afraid you'll need a dedicated list of the keys. As for the descriptive names, yes that's what I meant. \$\endgroup\$janos– janos2023年12月13日 13:13:58 +00:00Commented Dec 13, 2023 at 13:13
These strings are terminal-specific:
bold="033円[1m" reset="033円[0m"
You don't want those if the output stream can't do bold (perhaps it isn't a terminal), or if it uses a different escape code. Use test -t 1
to find whether standard output is a terminal, and $(tput bold)
& $(tput sgr0)
to generate the appropriate strings.
Consider making these constants global, rather than repeating the same initialisation in different functions.
-
\$\begingroup\$ Thanks for the comment. Could you give an example on how to use the
test -t 1
with anif
condition? \$\endgroup\$jmizv– jmizv2023年12月13日 10:21:27 +00:00Commented Dec 13, 2023 at 10:21 -
1\$\begingroup\$ No need for an example - just use it the way you'd use
if
with any command. \$\endgroup\$Toby Speight– Toby Speight2023年12月15日 07:50:16 +00:00Commented Dec 15, 2023 at 7:50