3
\$\begingroup\$

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 of echo?
  • Can we store the information about the services (i.e. name, directory, port) in some kind of global array?
asked Dec 6, 2023 at 15:24
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

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
}
answered Dec 9, 2023 at 21:38
\$\endgroup\$
2
  • \$\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 of 1ドル? \$\endgroup\$ Commented 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\$ Commented Dec 13, 2023 at 13:13
2
\$\begingroup\$

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.

answered Dec 6, 2023 at 16:54
\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the comment. Could you give an example on how to use the test -t 1 with an if condition? \$\endgroup\$ Commented 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\$ Commented Dec 15, 2023 at 7:50

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.